claude-brain 0.30.2 → 0.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
package/src/server/web-viewer.ts
CHANGED
|
@@ -1,1115 +1,1145 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web Viewer for Claude Brain — Phase 30
|
|
3
|
-
* Single-page dashboard served at localhost:3333/
|
|
4
|
-
* Inline HTML/CSS/JS, no build step, no external dependencies.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Hono } from 'hono'
|
|
8
|
-
import { getMemoryService, getVaultService, isServicesInitialized } from '@/server/services'
|
|
9
|
-
import type { CodeQuery } from '@/code-intelligence/query'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
display: flex;
|
|
332
|
-
align-items: center;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
padding:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
font-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
margin-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
.
|
|
532
|
-
|
|
533
|
-
.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
font-size:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
<
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
<div
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
</div>
|
|
752
|
-
<div
|
|
753
|
-
</div>
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
<
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// ───
|
|
801
|
-
async function
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
document.getElementById(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
html += '<div class="
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
html += '</div>';
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
html += '
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
.
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Web Viewer for Claude Brain — Phase 30
|
|
3
|
+
* Single-page dashboard served at localhost:3333/
|
|
4
|
+
* Inline HTML/CSS/JS, no build step, no external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Hono } from 'hono'
|
|
8
|
+
import { getMemoryService, getVaultService, isServicesInitialized } from '@/server/services'
|
|
9
|
+
import type { CodeQuery } from '@/code-intelligence/query'
|
|
10
|
+
import type { ObservationResult } from '@/memory/fts5-search'
|
|
11
|
+
|
|
12
|
+
/** Raw observation row from direct DB queries */
|
|
13
|
+
interface RawObservationRow {
|
|
14
|
+
id: string
|
|
15
|
+
project: string
|
|
16
|
+
category: string
|
|
17
|
+
content: string
|
|
18
|
+
reasoning: string | null
|
|
19
|
+
context: string | null
|
|
20
|
+
tags: string | null
|
|
21
|
+
confidence: number
|
|
22
|
+
source: string | null
|
|
23
|
+
file_paths: string | null
|
|
24
|
+
symbols: string | null
|
|
25
|
+
archived: number
|
|
26
|
+
access_count: number
|
|
27
|
+
last_accessed: string | null
|
|
28
|
+
created_at: string
|
|
29
|
+
updated_at: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Count result row */
|
|
33
|
+
interface CountRow { cnt: number }
|
|
34
|
+
|
|
35
|
+
/** Project row */
|
|
36
|
+
interface ProjectRow { project: string }
|
|
37
|
+
|
|
38
|
+
/** Category count row */
|
|
39
|
+
interface CategoryCountRow { category: string; cnt: number }
|
|
40
|
+
|
|
41
|
+
let _codeQuery: CodeQuery | null = null
|
|
42
|
+
|
|
43
|
+
export function setWebViewerCodeQuery(cq: CodeQuery): void {
|
|
44
|
+
_codeQuery = cq
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setupWebViewer(app: Hono): void {
|
|
48
|
+
// Serve the viewer HTML
|
|
49
|
+
app.get('/', (c) => {
|
|
50
|
+
c.header('Content-Type', 'text/html; charset=utf-8')
|
|
51
|
+
return c.html(viewerHTML)
|
|
52
|
+
})
|
|
53
|
+
app.get('/viewer', (c) => {
|
|
54
|
+
c.header('Content-Type', 'text/html; charset=utf-8')
|
|
55
|
+
return c.html(viewerHTML)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// --- API endpoints for the web viewer ---
|
|
59
|
+
|
|
60
|
+
// Search endpoint — queries FTS5
|
|
61
|
+
app.get('/api/memory/search', async (c) => {
|
|
62
|
+
try {
|
|
63
|
+
const query = c.req.query('query') || c.req.query('q') || ''
|
|
64
|
+
const project = c.req.query('project') || undefined
|
|
65
|
+
const category = c.req.query('category') || undefined
|
|
66
|
+
const limit = parseInt(c.req.query('limit') || '20', 10)
|
|
67
|
+
|
|
68
|
+
if (!query.trim()) {
|
|
69
|
+
return c.json({ success: true, data: [] })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isServicesInitialized()) {
|
|
73
|
+
return c.json({ success: true, data: [] })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const memoryService = getMemoryService()
|
|
77
|
+
if (!memoryService?.isInitialized()) {
|
|
78
|
+
return c.json({ success: true, data: [] })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fts5 = memoryService.fts5
|
|
82
|
+
if (fts5) {
|
|
83
|
+
let results = fts5.searchWithConfidence(query, project, limit)
|
|
84
|
+
if (category) {
|
|
85
|
+
results = results.filter(r => r.category === category)
|
|
86
|
+
}
|
|
87
|
+
return c.json({ success: true, data: results })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fallback to searchRaw
|
|
91
|
+
const results = await memoryService.searchRaw(query, { project, limit })
|
|
92
|
+
return c.json({ success: true, data: results })
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return c.json({ success: false, error: 'Search failed' }, 500)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Timeline endpoint — observations grouped by day
|
|
99
|
+
app.get('/api/memory/timeline', async (c) => {
|
|
100
|
+
try {
|
|
101
|
+
const project = c.req.query('project') || undefined
|
|
102
|
+
const days = parseInt(c.req.query('days') || '7', 10)
|
|
103
|
+
|
|
104
|
+
if (!isServicesInitialized()) {
|
|
105
|
+
return c.json({ success: true, data: {} })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const memoryService = getMemoryService()
|
|
109
|
+
if (!memoryService?.isInitialized() || !memoryService.fts5) {
|
|
110
|
+
return c.json({ success: true, data: {} })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const end = new Date()
|
|
114
|
+
const start = new Date(Date.now() - days * 86400000)
|
|
115
|
+
|
|
116
|
+
// We need a project for fetchByTimeRange; if none, list all projects and merge
|
|
117
|
+
let observations: import('@/memory/fts5-search').ObservationResult[] = []
|
|
118
|
+
if (project) {
|
|
119
|
+
observations = memoryService.fts5.fetchByTimeRange(project, start, end, 200)
|
|
120
|
+
} else {
|
|
121
|
+
// Fetch from all projects by doing a broad LIKE query on the db directly
|
|
122
|
+
try {
|
|
123
|
+
const db = memoryService.database.getDb()
|
|
124
|
+
const rows = db.prepare(`
|
|
125
|
+
SELECT * FROM observations
|
|
126
|
+
WHERE archived = 0 AND created_at >= ? AND created_at <= ?
|
|
127
|
+
ORDER BY created_at DESC
|
|
128
|
+
LIMIT 200
|
|
129
|
+
`).all(start.toISOString(), end.toISOString()) as RawObservationRow[]
|
|
130
|
+
observations = rows.map((row: RawObservationRow) => ({
|
|
131
|
+
id: row.id,
|
|
132
|
+
project: row.project,
|
|
133
|
+
category: row.category,
|
|
134
|
+
content: row.content,
|
|
135
|
+
reasoning: row.reasoning || null,
|
|
136
|
+
context: row.context || null,
|
|
137
|
+
confidence: row.confidence ?? 0.8,
|
|
138
|
+
source: row.source || 'explicit',
|
|
139
|
+
tags: row.tags ? safeParseJson(row.tags) : [],
|
|
140
|
+
file_paths: row.file_paths ? safeParseJson(row.file_paths) : [],
|
|
141
|
+
symbols: row.symbols ? safeParseJson(row.symbols) : [],
|
|
142
|
+
access_count: row.access_count ?? 0,
|
|
143
|
+
last_accessed: row.last_accessed || null,
|
|
144
|
+
created_at: row.created_at,
|
|
145
|
+
updated_at: row.updated_at,
|
|
146
|
+
archived: row.archived === 1
|
|
147
|
+
}))
|
|
148
|
+
} catch {
|
|
149
|
+
observations = []
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Group by day
|
|
154
|
+
const grouped: Record<string, ObservationResult[]> = {}
|
|
155
|
+
for (const obs of observations) {
|
|
156
|
+
const day = obs.created_at?.slice(0, 10) || 'unknown'
|
|
157
|
+
if (!grouped[day]) grouped[day] = []
|
|
158
|
+
grouped[day].push(obs)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return c.json({ success: true, data: grouped })
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return c.json({ success: false, error: 'Timeline failed' }, 500)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Stats endpoint
|
|
168
|
+
app.get('/api/stats', async (c) => {
|
|
169
|
+
try {
|
|
170
|
+
if (!isServicesInitialized()) {
|
|
171
|
+
return c.json({
|
|
172
|
+
success: true,
|
|
173
|
+
data: {
|
|
174
|
+
totalObservations: 0,
|
|
175
|
+
projects: [],
|
|
176
|
+
categories: {},
|
|
177
|
+
activityLast24h: 0,
|
|
178
|
+
codeIndex: null
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const memoryService = getMemoryService()
|
|
184
|
+
if (!memoryService?.isInitialized()) {
|
|
185
|
+
return c.json({
|
|
186
|
+
success: true,
|
|
187
|
+
data: {
|
|
188
|
+
totalObservations: 0,
|
|
189
|
+
projects: [],
|
|
190
|
+
categories: {},
|
|
191
|
+
activityLast24h: 0,
|
|
192
|
+
codeIndex: null
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const db = memoryService.database.getDb()
|
|
198
|
+
|
|
199
|
+
// Total observations
|
|
200
|
+
const totalRow = db.prepare('SELECT COUNT(*) as cnt FROM observations WHERE archived = 0').get() as CountRow | null
|
|
201
|
+
const totalObservations = totalRow?.cnt || 0
|
|
202
|
+
|
|
203
|
+
// Projects
|
|
204
|
+
const projectRows = db.prepare('SELECT DISTINCT project FROM observations WHERE archived = 0').all() as ProjectRow[]
|
|
205
|
+
const projects = projectRows.map((p) => p.project).filter(Boolean)
|
|
206
|
+
|
|
207
|
+
// Categories breakdown
|
|
208
|
+
const catRows = db.prepare(
|
|
209
|
+
'SELECT category, COUNT(*) as cnt FROM observations WHERE archived = 0 GROUP BY category'
|
|
210
|
+
).all() as CategoryCountRow[]
|
|
211
|
+
const categories: Record<string, number> = {}
|
|
212
|
+
for (const row of catRows) {
|
|
213
|
+
categories[row.category] = row.cnt
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Activity last 24h
|
|
217
|
+
const cutoff = new Date(Date.now() - 86400000).toISOString()
|
|
218
|
+
const activityRow = db.prepare(
|
|
219
|
+
'SELECT COUNT(*) as cnt FROM observations WHERE created_at > ? AND archived = 0'
|
|
220
|
+
).get(cutoff) as CountRow | null
|
|
221
|
+
const activityLast24h = activityRow?.cnt || 0
|
|
222
|
+
|
|
223
|
+
// Code index stats (if available)
|
|
224
|
+
let codeIndex = null
|
|
225
|
+
if (_codeQuery) {
|
|
226
|
+
try {
|
|
227
|
+
// Try to get stats for the first project
|
|
228
|
+
const firstProject = projects[0]
|
|
229
|
+
if (firstProject) {
|
|
230
|
+
codeIndex = _codeQuery.getStats(firstProject)
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Code intelligence may not be available
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return c.json({
|
|
238
|
+
success: true,
|
|
239
|
+
data: {
|
|
240
|
+
totalObservations,
|
|
241
|
+
projects,
|
|
242
|
+
categories,
|
|
243
|
+
activityLast24h,
|
|
244
|
+
codeIndex
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return c.json({ success: false, error: 'Stats failed' }, 500)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Projects list (for filter dropdown)
|
|
253
|
+
app.get('/api/viewer/projects', async (c) => {
|
|
254
|
+
try {
|
|
255
|
+
if (!isServicesInitialized()) {
|
|
256
|
+
return c.json({ success: true, data: [] })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const memoryService = getMemoryService()
|
|
260
|
+
if (!memoryService?.isInitialized()) {
|
|
261
|
+
return c.json({ success: true, data: [] })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const db = memoryService.database.getDb()
|
|
265
|
+
const rows = db.prepare(
|
|
266
|
+
'SELECT DISTINCT project FROM observations WHERE archived = 0 ORDER BY project'
|
|
267
|
+
).all() as ProjectRow[]
|
|
268
|
+
const projects = rows.map((r) => r.project).filter(Boolean)
|
|
269
|
+
|
|
270
|
+
return c.json({ success: true, data: projects })
|
|
271
|
+
} catch {
|
|
272
|
+
return c.json({ success: true, data: [] })
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function safeParseJson(val: string): string[] {
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(val)
|
|
280
|
+
return Array.isArray(parsed) ? parsed : []
|
|
281
|
+
} catch {
|
|
282
|
+
return val.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── The single-page HTML viewer ───────────────────────────────
|
|
287
|
+
|
|
288
|
+
const viewerHTML = `<!DOCTYPE html>
|
|
289
|
+
<html lang="en">
|
|
290
|
+
<head>
|
|
291
|
+
<meta charset="UTF-8">
|
|
292
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
293
|
+
<title>Claude Brain — Memory Viewer</title>
|
|
294
|
+
<style>
|
|
295
|
+
:root {
|
|
296
|
+
--bg: #0d1117;
|
|
297
|
+
--bg-card: #161b22;
|
|
298
|
+
--bg-input: #0d1117;
|
|
299
|
+
--bg-hover: #1c2333;
|
|
300
|
+
--border: #30363d;
|
|
301
|
+
--text: #e6edf3;
|
|
302
|
+
--text-dim: #8b949e;
|
|
303
|
+
--text-muted: #484f58;
|
|
304
|
+
--accent: #58a6ff;
|
|
305
|
+
--accent-dim: #1f6feb;
|
|
306
|
+
--green: #3fb950;
|
|
307
|
+
--orange: #d29922;
|
|
308
|
+
--red: #f85149;
|
|
309
|
+
--purple: #bc8cff;
|
|
310
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
311
|
+
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
312
|
+
--radius: 6px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
316
|
+
|
|
317
|
+
body {
|
|
318
|
+
background: var(--bg);
|
|
319
|
+
color: var(--text);
|
|
320
|
+
font-family: var(--font);
|
|
321
|
+
font-size: 14px;
|
|
322
|
+
line-height: 1.5;
|
|
323
|
+
min-height: 100vh;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Header */
|
|
327
|
+
.header {
|
|
328
|
+
background: var(--bg-card);
|
|
329
|
+
border-bottom: 1px solid var(--border);
|
|
330
|
+
padding: 16px 24px;
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: space-between;
|
|
334
|
+
position: sticky;
|
|
335
|
+
top: 0;
|
|
336
|
+
z-index: 100;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.header-left {
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
gap: 12px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.logo {
|
|
346
|
+
font-size: 18px;
|
|
347
|
+
font-weight: 600;
|
|
348
|
+
color: var(--accent);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.version {
|
|
352
|
+
font-size: 12px;
|
|
353
|
+
color: var(--text-dim);
|
|
354
|
+
background: var(--bg);
|
|
355
|
+
padding: 2px 8px;
|
|
356
|
+
border-radius: 12px;
|
|
357
|
+
border: 1px solid var(--border);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.header-right {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: 12px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.status-dot {
|
|
367
|
+
width: 8px;
|
|
368
|
+
height: 8px;
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
background: var(--green);
|
|
371
|
+
display: inline-block;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.status-dot.offline { background: var(--red); }
|
|
375
|
+
|
|
376
|
+
/* Tabs */
|
|
377
|
+
.tabs {
|
|
378
|
+
display: flex;
|
|
379
|
+
background: var(--bg-card);
|
|
380
|
+
border-bottom: 1px solid var(--border);
|
|
381
|
+
padding: 0 24px;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.tab {
|
|
385
|
+
padding: 10px 16px;
|
|
386
|
+
cursor: pointer;
|
|
387
|
+
color: var(--text-dim);
|
|
388
|
+
border-bottom: 2px solid transparent;
|
|
389
|
+
font-size: 13px;
|
|
390
|
+
font-weight: 500;
|
|
391
|
+
transition: all 0.15s;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.tab:hover { color: var(--text); }
|
|
395
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
396
|
+
|
|
397
|
+
/* Main layout */
|
|
398
|
+
.main {
|
|
399
|
+
max-width: 1200px;
|
|
400
|
+
margin: 0 auto;
|
|
401
|
+
padding: 24px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Stats bar */
|
|
405
|
+
.stats-bar {
|
|
406
|
+
display: grid;
|
|
407
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
408
|
+
gap: 16px;
|
|
409
|
+
margin-bottom: 24px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.stat-card {
|
|
413
|
+
background: var(--bg-card);
|
|
414
|
+
border: 1px solid var(--border);
|
|
415
|
+
border-radius: var(--radius);
|
|
416
|
+
padding: 16px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.stat-label {
|
|
420
|
+
color: var(--text-dim);
|
|
421
|
+
font-size: 12px;
|
|
422
|
+
text-transform: uppercase;
|
|
423
|
+
letter-spacing: 0.5px;
|
|
424
|
+
margin-bottom: 4px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.stat-value {
|
|
428
|
+
font-size: 28px;
|
|
429
|
+
font-weight: 600;
|
|
430
|
+
color: var(--text);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.stat-sub {
|
|
434
|
+
font-size: 12px;
|
|
435
|
+
color: var(--text-muted);
|
|
436
|
+
margin-top: 4px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Search section */
|
|
440
|
+
.search-section {
|
|
441
|
+
margin-bottom: 24px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.search-row {
|
|
445
|
+
display: flex;
|
|
446
|
+
gap: 12px;
|
|
447
|
+
align-items: center;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.search-input {
|
|
451
|
+
flex: 1;
|
|
452
|
+
background: var(--bg-input);
|
|
453
|
+
border: 1px solid var(--border);
|
|
454
|
+
border-radius: var(--radius);
|
|
455
|
+
padding: 10px 14px;
|
|
456
|
+
color: var(--text);
|
|
457
|
+
font-size: 14px;
|
|
458
|
+
font-family: var(--font);
|
|
459
|
+
outline: none;
|
|
460
|
+
transition: border-color 0.15s;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.search-input:focus { border-color: var(--accent); }
|
|
464
|
+
|
|
465
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
466
|
+
|
|
467
|
+
select.filter-select {
|
|
468
|
+
background: var(--bg-input);
|
|
469
|
+
border: 1px solid var(--border);
|
|
470
|
+
border-radius: var(--radius);
|
|
471
|
+
padding: 10px 14px;
|
|
472
|
+
color: var(--text);
|
|
473
|
+
font-size: 13px;
|
|
474
|
+
font-family: var(--font);
|
|
475
|
+
outline: none;
|
|
476
|
+
cursor: pointer;
|
|
477
|
+
min-width: 140px;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
select.filter-select:focus { border-color: var(--accent); }
|
|
481
|
+
|
|
482
|
+
.search-meta {
|
|
483
|
+
margin-top: 8px;
|
|
484
|
+
font-size: 12px;
|
|
485
|
+
color: var(--text-muted);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Results / Timeline */
|
|
489
|
+
.results-container {
|
|
490
|
+
display: flex;
|
|
491
|
+
flex-direction: column;
|
|
492
|
+
gap: 8px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.observation-card {
|
|
496
|
+
background: var(--bg-card);
|
|
497
|
+
border: 1px solid var(--border);
|
|
498
|
+
border-radius: var(--radius);
|
|
499
|
+
padding: 16px;
|
|
500
|
+
cursor: pointer;
|
|
501
|
+
transition: all 0.15s;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.observation-card:hover {
|
|
505
|
+
border-color: var(--accent-dim);
|
|
506
|
+
background: var(--bg-hover);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.obs-header {
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
justify-content: space-between;
|
|
513
|
+
margin-bottom: 8px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.obs-badges {
|
|
517
|
+
display: flex;
|
|
518
|
+
gap: 6px;
|
|
519
|
+
align-items: center;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.badge {
|
|
523
|
+
font-size: 11px;
|
|
524
|
+
padding: 2px 8px;
|
|
525
|
+
border-radius: 12px;
|
|
526
|
+
font-weight: 500;
|
|
527
|
+
text-transform: uppercase;
|
|
528
|
+
letter-spacing: 0.3px;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.badge-decision { background: #1f3a5f; color: var(--accent); }
|
|
532
|
+
.badge-pattern { background: #2a1f3f; color: var(--purple); }
|
|
533
|
+
.badge-correction { background: #3f1f1f; color: var(--red); }
|
|
534
|
+
.badge-insight { background: #2a3f1f; color: var(--green); }
|
|
535
|
+
.badge-preference { background: #3f2a1f; color: var(--orange); }
|
|
536
|
+
|
|
537
|
+
.badge-project {
|
|
538
|
+
background: var(--bg);
|
|
539
|
+
color: var(--text-dim);
|
|
540
|
+
border: 1px solid var(--border);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.obs-date {
|
|
544
|
+
font-size: 12px;
|
|
545
|
+
color: var(--text-muted);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.obs-content {
|
|
549
|
+
color: var(--text);
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
line-height: 1.6;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.obs-detail {
|
|
555
|
+
display: none;
|
|
556
|
+
margin-top: 12px;
|
|
557
|
+
padding-top: 12px;
|
|
558
|
+
border-top: 1px solid var(--border);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.obs-detail.open { display: block; }
|
|
562
|
+
|
|
563
|
+
.detail-row {
|
|
564
|
+
display: flex;
|
|
565
|
+
gap: 8px;
|
|
566
|
+
margin-bottom: 6px;
|
|
567
|
+
font-size: 13px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.detail-label {
|
|
571
|
+
color: var(--text-dim);
|
|
572
|
+
min-width: 80px;
|
|
573
|
+
font-weight: 500;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.detail-value {
|
|
577
|
+
color: var(--text);
|
|
578
|
+
word-break: break-word;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.obs-score {
|
|
582
|
+
font-size: 12px;
|
|
583
|
+
color: var(--text-muted);
|
|
584
|
+
font-family: var(--mono);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* Day group for timeline */
|
|
588
|
+
.day-group {
|
|
589
|
+
margin-bottom: 24px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.day-header {
|
|
593
|
+
font-size: 13px;
|
|
594
|
+
font-weight: 600;
|
|
595
|
+
color: var(--text-dim);
|
|
596
|
+
padding: 8px 0;
|
|
597
|
+
border-bottom: 1px solid var(--border);
|
|
598
|
+
margin-bottom: 12px;
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
gap: 8px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.day-count {
|
|
605
|
+
background: var(--bg);
|
|
606
|
+
border: 1px solid var(--border);
|
|
607
|
+
border-radius: 12px;
|
|
608
|
+
padding: 1px 8px;
|
|
609
|
+
font-size: 11px;
|
|
610
|
+
color: var(--text-muted);
|
|
611
|
+
font-weight: 400;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Code map */
|
|
615
|
+
.code-file {
|
|
616
|
+
background: var(--bg-card);
|
|
617
|
+
border: 1px solid var(--border);
|
|
618
|
+
border-radius: var(--radius);
|
|
619
|
+
padding: 12px 16px;
|
|
620
|
+
margin-bottom: 6px;
|
|
621
|
+
font-family: var(--mono);
|
|
622
|
+
font-size: 13px;
|
|
623
|
+
display: flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: space-between;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.code-file-path { color: var(--accent); }
|
|
629
|
+
.code-file-symbols { color: var(--text-dim); font-size: 12px; }
|
|
630
|
+
|
|
631
|
+
/* Empty state */
|
|
632
|
+
.empty-state {
|
|
633
|
+
text-align: center;
|
|
634
|
+
padding: 60px 20px;
|
|
635
|
+
color: var(--text-dim);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.empty-state-icon {
|
|
639
|
+
font-size: 40px;
|
|
640
|
+
margin-bottom: 16px;
|
|
641
|
+
opacity: 0.5;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.empty-state-title {
|
|
645
|
+
font-size: 16px;
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
margin-bottom: 8px;
|
|
648
|
+
color: var(--text);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* Loading */
|
|
652
|
+
.loading {
|
|
653
|
+
text-align: center;
|
|
654
|
+
padding: 40px;
|
|
655
|
+
color: var(--text-muted);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.spinner {
|
|
659
|
+
display: inline-block;
|
|
660
|
+
width: 20px;
|
|
661
|
+
height: 20px;
|
|
662
|
+
border: 2px solid var(--border);
|
|
663
|
+
border-top-color: var(--accent);
|
|
664
|
+
border-radius: 50%;
|
|
665
|
+
animation: spin 0.6s linear infinite;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
669
|
+
|
|
670
|
+
/* Panel hidden */
|
|
671
|
+
.panel { display: none; }
|
|
672
|
+
.panel.active { display: block; }
|
|
673
|
+
|
|
674
|
+
/* Responsive */
|
|
675
|
+
@media (max-width: 768px) {
|
|
676
|
+
.main { padding: 16px; }
|
|
677
|
+
.stats-bar { grid-template-columns: repeat(2, 1fr); }
|
|
678
|
+
.search-row { flex-direction: column; }
|
|
679
|
+
select.filter-select { min-width: 100%; }
|
|
680
|
+
}
|
|
681
|
+
</style>
|
|
682
|
+
</head>
|
|
683
|
+
<body>
|
|
684
|
+
|
|
685
|
+
<div class="header">
|
|
686
|
+
<div class="header-left">
|
|
687
|
+
<span class="logo">Claude Brain</span>
|
|
688
|
+
<span class="version" id="version-badge">loading...</span>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="header-right">
|
|
691
|
+
<span class="status-dot" id="status-dot"></span>
|
|
692
|
+
<span id="status-text" style="font-size:12px;color:var(--text-dim)">Connecting...</span>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div class="tabs">
|
|
697
|
+
<div class="tab active" data-tab="dashboard" onclick="switchTab('dashboard')">Dashboard</div>
|
|
698
|
+
<div class="tab" data-tab="search" onclick="switchTab('search')">Search</div>
|
|
699
|
+
<div class="tab" data-tab="timeline" onclick="switchTab('timeline')">Timeline</div>
|
|
700
|
+
<div class="tab" data-tab="codemap" onclick="switchTab('codemap')">Code Map</div>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<div class="main">
|
|
704
|
+
<!-- Dashboard panel -->
|
|
705
|
+
<div class="panel active" id="panel-dashboard">
|
|
706
|
+
<div class="stats-bar" id="stats-bar">
|
|
707
|
+
<div class="stat-card">
|
|
708
|
+
<div class="stat-label">Total Observations</div>
|
|
709
|
+
<div class="stat-value" id="stat-total">--</div>
|
|
710
|
+
</div>
|
|
711
|
+
<div class="stat-card">
|
|
712
|
+
<div class="stat-label">Last 24 Hours</div>
|
|
713
|
+
<div class="stat-value" id="stat-24h">--</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="stat-card">
|
|
716
|
+
<div class="stat-label">Projects</div>
|
|
717
|
+
<div class="stat-value" id="stat-projects">--</div>
|
|
718
|
+
</div>
|
|
719
|
+
<div class="stat-card">
|
|
720
|
+
<div class="stat-label">Code Index</div>
|
|
721
|
+
<div class="stat-value" id="stat-code">--</div>
|
|
722
|
+
<div class="stat-sub" id="stat-code-sub"></div>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
<h3 style="margin-bottom:12px;color:var(--text-dim);font-size:13px;text-transform:uppercase;letter-spacing:0.5px">Category Breakdown</h3>
|
|
727
|
+
<div id="category-bars" style="margin-bottom:24px"></div>
|
|
728
|
+
|
|
729
|
+
<h3 style="margin-bottom:12px;color:var(--text-dim);font-size:13px;text-transform:uppercase;letter-spacing:0.5px">Recent Activity</h3>
|
|
730
|
+
<div id="recent-activity"></div>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<!-- Search panel -->
|
|
734
|
+
<div class="panel" id="panel-search">
|
|
735
|
+
<div class="search-section">
|
|
736
|
+
<div class="search-row">
|
|
737
|
+
<input class="search-input" id="search-input" type="text" placeholder="Search observations..." autocomplete="off">
|
|
738
|
+
<select class="filter-select" id="project-filter" onchange="doSearch()">
|
|
739
|
+
<option value="">All Projects</option>
|
|
740
|
+
</select>
|
|
741
|
+
<select class="filter-select" id="category-filter" onchange="doSearch()">
|
|
742
|
+
<option value="">All Categories</option>
|
|
743
|
+
<option value="decision">Decision</option>
|
|
744
|
+
<option value="pattern">Pattern</option>
|
|
745
|
+
<option value="correction">Correction</option>
|
|
746
|
+
<option value="insight">Insight</option>
|
|
747
|
+
<option value="preference">Preference</option>
|
|
748
|
+
</select>
|
|
749
|
+
</div>
|
|
750
|
+
<div class="search-meta" id="search-meta"></div>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="results-container" id="search-results"></div>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<!-- Timeline panel -->
|
|
756
|
+
<div class="panel" id="panel-timeline">
|
|
757
|
+
<div class="search-section">
|
|
758
|
+
<div class="search-row">
|
|
759
|
+
<select class="filter-select" id="timeline-project" onchange="loadTimeline()">
|
|
760
|
+
<option value="">All Projects</option>
|
|
761
|
+
</select>
|
|
762
|
+
<select class="filter-select" id="timeline-days" onchange="loadTimeline()">
|
|
763
|
+
<option value="3">Last 3 days</option>
|
|
764
|
+
<option value="7" selected>Last 7 days</option>
|
|
765
|
+
<option value="14">Last 14 days</option>
|
|
766
|
+
<option value="30">Last 30 days</option>
|
|
767
|
+
</select>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
<div id="timeline-container"></div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<!-- Code Map panel -->
|
|
774
|
+
<div class="panel" id="panel-codemap">
|
|
775
|
+
<div class="search-section">
|
|
776
|
+
<div class="search-row">
|
|
777
|
+
<select class="filter-select" id="codemap-project" onchange="loadCodeMap()">
|
|
778
|
+
<option value="">Select project...</option>
|
|
779
|
+
</select>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div id="codemap-container"></div>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<script>
|
|
787
|
+
// ─── State ──────────────────────────────────────────
|
|
788
|
+
let searchDebounce = null;
|
|
789
|
+
|
|
790
|
+
// ─── Tab switching ──────────────────────────────────
|
|
791
|
+
function switchTab(name) {
|
|
792
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
|
793
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
|
|
794
|
+
|
|
795
|
+
if (name === 'timeline') loadTimeline();
|
|
796
|
+
if (name === 'codemap') loadCodeMap();
|
|
797
|
+
if (name === 'dashboard') loadDashboard();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ─── API helpers ────────────────────────────────────
|
|
801
|
+
async function api(url) {
|
|
802
|
+
try {
|
|
803
|
+
const res = await fetch(url);
|
|
804
|
+
const data = await res.json();
|
|
805
|
+
return data;
|
|
806
|
+
} catch (err) {
|
|
807
|
+
console.error('API error:', url, err);
|
|
808
|
+
return { success: false, error: err.message };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ─── Health check ───────────────────────────────────
|
|
813
|
+
async function checkHealth() {
|
|
814
|
+
const data = await api('/api/health');
|
|
815
|
+
const dot = document.getElementById('status-dot');
|
|
816
|
+
const text = document.getElementById('status-text');
|
|
817
|
+
if (data.success && data.initialized) {
|
|
818
|
+
dot.className = 'status-dot';
|
|
819
|
+
text.textContent = 'Connected';
|
|
820
|
+
} else if (data.success) {
|
|
821
|
+
dot.className = 'status-dot';
|
|
822
|
+
dot.style.background = 'var(--orange)';
|
|
823
|
+
text.textContent = 'Initializing...';
|
|
824
|
+
} else {
|
|
825
|
+
dot.className = 'status-dot offline';
|
|
826
|
+
text.textContent = 'Disconnected';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ─── Load projects into filter dropdowns ────────────
|
|
831
|
+
async function loadProjects() {
|
|
832
|
+
const data = await api('/api/viewer/projects');
|
|
833
|
+
if (!data.success) return;
|
|
834
|
+
|
|
835
|
+
const selects = ['project-filter', 'timeline-project', 'codemap-project'];
|
|
836
|
+
for (const id of selects) {
|
|
837
|
+
const el = document.getElementById(id);
|
|
838
|
+
if (!el) continue;
|
|
839
|
+
const current = el.value;
|
|
840
|
+
// Keep first option
|
|
841
|
+
while (el.options.length > 1) el.remove(1);
|
|
842
|
+
for (const p of data.data) {
|
|
843
|
+
const opt = document.createElement('option');
|
|
844
|
+
opt.value = p;
|
|
845
|
+
opt.textContent = p;
|
|
846
|
+
el.appendChild(opt);
|
|
847
|
+
}
|
|
848
|
+
if (current) el.value = current;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ─── Dashboard ──────────────────────────────────────
|
|
853
|
+
async function loadDashboard() {
|
|
854
|
+
const data = await api('/api/stats');
|
|
855
|
+
if (!data.success) return;
|
|
856
|
+
|
|
857
|
+
const d = data.data;
|
|
858
|
+
document.getElementById('stat-total').textContent = d.totalObservations || 0;
|
|
859
|
+
document.getElementById('stat-24h').textContent = d.activityLast24h || 0;
|
|
860
|
+
document.getElementById('stat-projects').textContent = d.projects?.length || 0;
|
|
861
|
+
|
|
862
|
+
if (d.codeIndex) {
|
|
863
|
+
document.getElementById('stat-code').textContent = d.codeIndex.totalFiles || 0;
|
|
864
|
+
document.getElementById('stat-code-sub').textContent =
|
|
865
|
+
(d.codeIndex.totalSymbols || 0) + ' symbols';
|
|
866
|
+
} else {
|
|
867
|
+
document.getElementById('stat-code').textContent = 'N/A';
|
|
868
|
+
document.getElementById('stat-code-sub').textContent = 'Not indexed';
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Category bars
|
|
872
|
+
const cats = d.categories || {};
|
|
873
|
+
const total = Object.values(cats).reduce((a, b) => a + b, 0) || 1;
|
|
874
|
+
const barsEl = document.getElementById('category-bars');
|
|
875
|
+
const catColors = {
|
|
876
|
+
decision: 'var(--accent)',
|
|
877
|
+
pattern: 'var(--purple)',
|
|
878
|
+
correction: 'var(--red)',
|
|
879
|
+
insight: 'var(--green)',
|
|
880
|
+
preference: 'var(--orange)'
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
let barsHTML = '';
|
|
884
|
+
for (const [cat, count] of Object.entries(cats).sort((a, b) => b[1] - a[1])) {
|
|
885
|
+
const pct = Math.round((count / total) * 100);
|
|
886
|
+
const color = catColors[cat] || 'var(--text-dim)';
|
|
887
|
+
barsHTML += '<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">' +
|
|
888
|
+
'<span style="min-width:90px;font-size:13px;color:var(--text-dim);text-transform:capitalize">' + esc(cat) + '</span>' +
|
|
889
|
+
'<div style="flex:1;height:20px;background:var(--bg);border-radius:4px;overflow:hidden;border:1px solid var(--border)">' +
|
|
890
|
+
'<div style="width:' + pct + '%;height:100%;background:' + color + ';opacity:0.7;border-radius:4px;transition:width 0.3s"></div>' +
|
|
891
|
+
'</div>' +
|
|
892
|
+
'<span style="min-width:60px;text-align:right;font-size:13px;color:var(--text);font-family:var(--mono)">' + count + '</span>' +
|
|
893
|
+
'</div>';
|
|
894
|
+
}
|
|
895
|
+
barsEl.innerHTML = barsHTML || '<div class="empty-state"><div class="empty-state-title">No observations yet</div></div>';
|
|
896
|
+
|
|
897
|
+
// Recent activity
|
|
898
|
+
const tData = await api('/api/memory/timeline?days=2');
|
|
899
|
+
const recentEl = document.getElementById('recent-activity');
|
|
900
|
+
if (tData.success) {
|
|
901
|
+
const all = [];
|
|
902
|
+
for (const [day, items] of Object.entries(tData.data)) {
|
|
903
|
+
for (const item of items) all.push(item);
|
|
904
|
+
}
|
|
905
|
+
all.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
906
|
+
recentEl.innerHTML = renderObservations(all.slice(0, 10));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ─── Search ─────────────────────────────────────────
|
|
911
|
+
const searchInput = document.getElementById('search-input');
|
|
912
|
+
searchInput.addEventListener('input', () => {
|
|
913
|
+
clearTimeout(searchDebounce);
|
|
914
|
+
searchDebounce = setTimeout(doSearch, 300);
|
|
915
|
+
});
|
|
916
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
917
|
+
if (e.key === 'Enter') { clearTimeout(searchDebounce); doSearch(); }
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
async function doSearch() {
|
|
921
|
+
const query = searchInput.value.trim();
|
|
922
|
+
const project = document.getElementById('project-filter').value;
|
|
923
|
+
const category = document.getElementById('category-filter').value;
|
|
924
|
+
const resultsEl = document.getElementById('search-results');
|
|
925
|
+
const metaEl = document.getElementById('search-meta');
|
|
926
|
+
|
|
927
|
+
if (!query) {
|
|
928
|
+
resultsEl.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-title">Type to search</div><div>Search across all your observations, decisions, patterns, and corrections.</div></div>';
|
|
929
|
+
metaEl.textContent = '';
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
resultsEl.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
934
|
+
|
|
935
|
+
const start = performance.now();
|
|
936
|
+
let url = '/api/memory/search?q=' + encodeURIComponent(query) + '&limit=30';
|
|
937
|
+
if (project) url += '&project=' + encodeURIComponent(project);
|
|
938
|
+
if (category) url += '&category=' + encodeURIComponent(category);
|
|
939
|
+
|
|
940
|
+
const data = await api(url);
|
|
941
|
+
const elapsed = Math.round(performance.now() - start);
|
|
942
|
+
|
|
943
|
+
if (!data.success || !data.data.length) {
|
|
944
|
+
resultsEl.innerHTML = '<div class="empty-state"><div class="empty-state-title">No results</div><div>Try different keywords or broaden your filters.</div></div>';
|
|
945
|
+
metaEl.textContent = 'Searched in ' + elapsed + 'ms';
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
metaEl.textContent = data.data.length + ' result' + (data.data.length !== 1 ? 's' : '') + ' in ' + elapsed + 'ms';
|
|
950
|
+
resultsEl.innerHTML = renderObservations(data.data);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ─── Timeline ───────────────────────────────────────
|
|
954
|
+
async function loadTimeline() {
|
|
955
|
+
const project = document.getElementById('timeline-project').value;
|
|
956
|
+
const days = document.getElementById('timeline-days').value;
|
|
957
|
+
const container = document.getElementById('timeline-container');
|
|
958
|
+
|
|
959
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
960
|
+
|
|
961
|
+
let url = '/api/memory/timeline?days=' + days;
|
|
962
|
+
if (project) url += '&project=' + encodeURIComponent(project);
|
|
963
|
+
|
|
964
|
+
const data = await api(url);
|
|
965
|
+
|
|
966
|
+
if (!data.success || !Object.keys(data.data).length) {
|
|
967
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-title">No activity</div><div>No observations found for this period.</div></div>';
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Sort days descending
|
|
972
|
+
const days_sorted = Object.keys(data.data).sort().reverse();
|
|
973
|
+
let html = '';
|
|
974
|
+
|
|
975
|
+
for (const day of days_sorted) {
|
|
976
|
+
const items = data.data[day];
|
|
977
|
+
const dateLabel = formatDayLabel(day);
|
|
978
|
+
html += '<div class="day-group">';
|
|
979
|
+
html += '<div class="day-header">' + esc(dateLabel) + ' <span class="day-count">' + items.length + '</span></div>';
|
|
980
|
+
html += renderObservations(items);
|
|
981
|
+
html += '</div>';
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
container.innerHTML = html;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ─── Code Map ───────────────────────────────────────
|
|
988
|
+
async function loadCodeMap() {
|
|
989
|
+
const project = document.getElementById('codemap-project').value;
|
|
990
|
+
const container = document.getElementById('codemap-container');
|
|
991
|
+
|
|
992
|
+
if (!project) {
|
|
993
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-title">Select a project</div><div>Choose a project to view its code map.</div></div>';
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
998
|
+
|
|
999
|
+
const data = await api('/api/code/file-map?project=' + encodeURIComponent(project));
|
|
1000
|
+
|
|
1001
|
+
if (!data.success || !data.data?.map) {
|
|
1002
|
+
container.innerHTML = '<div class="empty-state"><div class="empty-state-title">No code index</div><div>This project has not been indexed yet. Run the indexer first.</div></div>';
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const lines = data.data.map.split('\\n').filter(Boolean);
|
|
1007
|
+
let html = '';
|
|
1008
|
+
for (const line of lines) {
|
|
1009
|
+
html += '<div class="code-file"><span class="code-file-path">' + esc(line) + '</span></div>';
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
container.innerHTML = html || '<div class="empty-state"><div class="empty-state-title">Empty index</div></div>';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ─── Render helpers ─────────────────────────────────
|
|
1016
|
+
function renderObservations(items) {
|
|
1017
|
+
if (!items || items.length === 0) {
|
|
1018
|
+
return '<div class="empty-state"><div class="empty-state-title">Nothing here</div></div>';
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let html = '';
|
|
1022
|
+
for (const item of items) {
|
|
1023
|
+
const cat = item.category || 'insight';
|
|
1024
|
+
const proj = item.project || '';
|
|
1025
|
+
const content = item.content || '';
|
|
1026
|
+
const reasoning = item.reasoning || '';
|
|
1027
|
+
const context = item.context || '';
|
|
1028
|
+
const score = item.score != null ? item.score.toFixed(2) : '';
|
|
1029
|
+
const date = item.created_at ? formatDate(item.created_at) : '';
|
|
1030
|
+
const tags = item.tags || [];
|
|
1031
|
+
const id = item.id || '';
|
|
1032
|
+
|
|
1033
|
+
html += '<div class="observation-card" onclick="toggleDetail(this)">';
|
|
1034
|
+
html += '<div class="obs-header">';
|
|
1035
|
+
html += '<div class="obs-badges">';
|
|
1036
|
+
html += '<span class="badge badge-' + esc(cat) + '">' + esc(cat) + '</span>';
|
|
1037
|
+
if (proj) html += '<span class="badge badge-project">' + esc(proj) + '</span>';
|
|
1038
|
+
if (score) html += '<span class="obs-score">' + score + '</span>';
|
|
1039
|
+
html += '</div>';
|
|
1040
|
+
html += '<span class="obs-date">' + esc(date) + '</span>';
|
|
1041
|
+
html += '</div>';
|
|
1042
|
+
html += '<div class="obs-content">' + esc(truncate(content, 200)) + '</div>';
|
|
1043
|
+
|
|
1044
|
+
// Expandable detail
|
|
1045
|
+
html += '<div class="obs-detail">';
|
|
1046
|
+
if (content.length > 200) {
|
|
1047
|
+
html += '<div class="detail-row"><span class="detail-label">Full text</span><span class="detail-value">' + esc(content) + '</span></div>';
|
|
1048
|
+
}
|
|
1049
|
+
if (reasoning) {
|
|
1050
|
+
html += '<div class="detail-row"><span class="detail-label">Reasoning</span><span class="detail-value">' + esc(reasoning) + '</span></div>';
|
|
1051
|
+
}
|
|
1052
|
+
if (context) {
|
|
1053
|
+
html += '<div class="detail-row"><span class="detail-label">Context</span><span class="detail-value">' + esc(context) + '</span></div>';
|
|
1054
|
+
}
|
|
1055
|
+
if (tags.length > 0) {
|
|
1056
|
+
html += '<div class="detail-row"><span class="detail-label">Tags</span><span class="detail-value">' + tags.map(t => esc(t)).join(', ') + '</span></div>';
|
|
1057
|
+
}
|
|
1058
|
+
if (id) {
|
|
1059
|
+
html += '<div class="detail-row"><span class="detail-label">ID</span><span class="detail-value" style="font-family:var(--mono);font-size:12px;color:var(--text-muted)">' + esc(id) + '</span></div>';
|
|
1060
|
+
}
|
|
1061
|
+
html += '</div>';
|
|
1062
|
+
|
|
1063
|
+
html += '</div>';
|
|
1064
|
+
}
|
|
1065
|
+
return html;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function toggleDetail(card) {
|
|
1069
|
+
const detail = card.querySelector('.obs-detail');
|
|
1070
|
+
if (detail) detail.classList.toggle('open');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function esc(str) {
|
|
1074
|
+
if (!str) return '';
|
|
1075
|
+
return String(str)
|
|
1076
|
+
.replace(/&/g, '&')
|
|
1077
|
+
.replace(/</g, '<')
|
|
1078
|
+
.replace(/>/g, '>')
|
|
1079
|
+
.replace(/"/g, '"');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function truncate(str, len) {
|
|
1083
|
+
if (!str) return '';
|
|
1084
|
+
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function formatDate(iso) {
|
|
1088
|
+
try {
|
|
1089
|
+
const d = new Date(iso);
|
|
1090
|
+
const now = new Date();
|
|
1091
|
+
const diffMs = now - d;
|
|
1092
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
1093
|
+
if (diffMin < 1) return 'just now';
|
|
1094
|
+
if (diffMin < 60) return diffMin + 'm ago';
|
|
1095
|
+
const diffH = Math.floor(diffMin / 60);
|
|
1096
|
+
if (diffH < 24) return diffH + 'h ago';
|
|
1097
|
+
const diffD = Math.floor(diffH / 24);
|
|
1098
|
+
if (diffD === 1) return 'yesterday';
|
|
1099
|
+
if (diffD < 30) return diffD + 'd ago';
|
|
1100
|
+
return d.toLocaleDateString();
|
|
1101
|
+
} catch { return iso; }
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function formatDayLabel(dayStr) {
|
|
1105
|
+
try {
|
|
1106
|
+
const d = new Date(dayStr + 'T00:00:00');
|
|
1107
|
+
const today = new Date();
|
|
1108
|
+
today.setHours(0,0,0,0);
|
|
1109
|
+
const yesterday = new Date(today);
|
|
1110
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
1111
|
+
|
|
1112
|
+
if (d.getTime() === today.getTime()) return 'Today';
|
|
1113
|
+
if (d.getTime() === yesterday.getTime()) return 'Yesterday';
|
|
1114
|
+
|
|
1115
|
+
return d.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
|
|
1116
|
+
} catch { return dayStr; }
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ─── Init ───────────────────────────────────────────
|
|
1120
|
+
async function init() {
|
|
1121
|
+
await checkHealth();
|
|
1122
|
+
await loadProjects();
|
|
1123
|
+
await loadDashboard();
|
|
1124
|
+
|
|
1125
|
+
// Version badge
|
|
1126
|
+
try {
|
|
1127
|
+
const health = await api('/api/health');
|
|
1128
|
+
document.getElementById('version-badge').textContent = health.version || 'v0';
|
|
1129
|
+
} catch {
|
|
1130
|
+
document.getElementById('version-badge').textContent = '';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Set initial search state
|
|
1134
|
+
document.getElementById('search-results').innerHTML =
|
|
1135
|
+
'<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-title">Type to search</div><div>Search across all your observations, decisions, patterns, and corrections.</div></div>';
|
|
1136
|
+
|
|
1137
|
+
// Refresh health every 30s
|
|
1138
|
+
setInterval(checkHealth, 30000);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
init();
|
|
1142
|
+
</script>
|
|
1143
|
+
</body>
|
|
1144
|
+
</html>`
|
|
1145
|
+
|