@agentmemory/agentmemory 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +28 -0
  4. package/AGENTS.md +113 -0
  5. package/LICENSE +190 -0
  6. package/README.md +828 -0
  7. package/assets/banner.png +0 -0
  8. package/assets/demo.gif +0 -0
  9. package/assets/demo.mp4 +0 -0
  10. package/benchmark/QUALITY.md +73 -0
  11. package/benchmark/REAL-EMBEDDINGS.md +67 -0
  12. package/benchmark/SCALE.md +110 -0
  13. package/benchmark/dataset.ts +293 -0
  14. package/benchmark/quality-eval.ts +643 -0
  15. package/benchmark/real-embeddings-eval.ts +405 -0
  16. package/benchmark/scale-eval.ts +398 -0
  17. package/dist/cli.d.mts +1 -0
  18. package/dist/cli.mjs +137 -0
  19. package/dist/cli.mjs.map +1 -0
  20. package/dist/docker-compose.yml +14 -0
  21. package/dist/hooks/notification.d.mts +1 -0
  22. package/dist/hooks/notification.mjs +45 -0
  23. package/dist/hooks/notification.mjs.map +1 -0
  24. package/dist/hooks/post-tool-failure.d.mts +1 -0
  25. package/dist/hooks/post-tool-failure.mjs +45 -0
  26. package/dist/hooks/post-tool-failure.mjs.map +1 -0
  27. package/dist/hooks/post-tool-use.d.mts +1 -0
  28. package/dist/hooks/post-tool-use.mjs +53 -0
  29. package/dist/hooks/post-tool-use.mjs.map +1 -0
  30. package/dist/hooks/pre-compact.d.mts +1 -0
  31. package/dist/hooks/pre-compact.mjs +50 -0
  32. package/dist/hooks/pre-compact.mjs.map +1 -0
  33. package/dist/hooks/pre-tool-use.d.mts +1 -0
  34. package/dist/hooks/pre-tool-use.mjs +69 -0
  35. package/dist/hooks/pre-tool-use.mjs.map +1 -0
  36. package/dist/hooks/prompt-submit.d.mts +1 -0
  37. package/dist/hooks/prompt-submit.mjs +40 -0
  38. package/dist/hooks/prompt-submit.mjs.map +1 -0
  39. package/dist/hooks/session-end.d.mts +1 -0
  40. package/dist/hooks/session-end.mjs +61 -0
  41. package/dist/hooks/session-end.mjs.map +1 -0
  42. package/dist/hooks/session-start.d.mts +1 -0
  43. package/dist/hooks/session-start.mjs +42 -0
  44. package/dist/hooks/session-start.mjs.map +1 -0
  45. package/dist/hooks/stop.d.mts +1 -0
  46. package/dist/hooks/stop.mjs +33 -0
  47. package/dist/hooks/stop.mjs.map +1 -0
  48. package/dist/hooks/subagent-start.d.mts +1 -0
  49. package/dist/hooks/subagent-start.mjs +43 -0
  50. package/dist/hooks/subagent-start.mjs.map +1 -0
  51. package/dist/hooks/subagent-stop.d.mts +1 -0
  52. package/dist/hooks/subagent-stop.mjs +45 -0
  53. package/dist/hooks/subagent-stop.mjs.map +1 -0
  54. package/dist/hooks/task-completed.d.mts +1 -0
  55. package/dist/hooks/task-completed.mjs +46 -0
  56. package/dist/hooks/task-completed.mjs.map +1 -0
  57. package/dist/iii-config.yaml +51 -0
  58. package/dist/index.d.mts +2 -0
  59. package/dist/index.mjs +13776 -0
  60. package/dist/index.mjs.map +1 -0
  61. package/dist/src-QxitMPfJ.mjs +13775 -0
  62. package/dist/src-QxitMPfJ.mjs.map +1 -0
  63. package/dist/standalone.d.mts +1 -0
  64. package/dist/standalone.mjs +1155 -0
  65. package/dist/standalone.mjs.map +1 -0
  66. package/dist/transformers-BX_tgxdO.mjs +38684 -0
  67. package/dist/transformers-BX_tgxdO.mjs.map +1 -0
  68. package/dist/transformers-KMm1i9no.mjs +38683 -0
  69. package/dist/transformers-KMm1i9no.mjs.map +1 -0
  70. package/docker-compose.yml +14 -0
  71. package/iii-config.yaml +51 -0
  72. package/package.json +59 -0
  73. package/plugin/.claude-plugin/plugin.json +10 -0
  74. package/plugin/hooks/hooks.json +77 -0
  75. package/plugin/scripts/diagnostics.mjs +551 -0
  76. package/plugin/scripts/notification.mjs +45 -0
  77. package/plugin/scripts/post-tool-failure.mjs +45 -0
  78. package/plugin/scripts/post-tool-use.mjs +53 -0
  79. package/plugin/scripts/pre-compact.mjs +50 -0
  80. package/plugin/scripts/pre-tool-use.mjs +69 -0
  81. package/plugin/scripts/prompt-submit.mjs +40 -0
  82. package/plugin/scripts/session-end.mjs +61 -0
  83. package/plugin/scripts/session-start.mjs +42 -0
  84. package/plugin/scripts/stop.mjs +33 -0
  85. package/plugin/scripts/subagent-start.mjs +43 -0
  86. package/plugin/scripts/subagent-stop.mjs +45 -0
  87. package/plugin/scripts/task-completed.mjs +46 -0
  88. package/plugin/skills/forget/SKILL.md +32 -0
  89. package/plugin/skills/recall/SKILL.md +18 -0
  90. package/plugin/skills/remember/SKILL.md +25 -0
  91. package/plugin/skills/session-history/SKILL.md +17 -0
  92. package/src/auth.ts +12 -0
  93. package/src/cli.ts +159 -0
  94. package/src/config.ts +221 -0
  95. package/src/eval/metrics-store.ts +65 -0
  96. package/src/eval/quality.ts +51 -0
  97. package/src/eval/schemas.ts +124 -0
  98. package/src/eval/self-correct.ts +28 -0
  99. package/src/eval/validator.ts +31 -0
  100. package/src/functions/actions.ts +288 -0
  101. package/src/functions/audit.ts +61 -0
  102. package/src/functions/auto-forget.ts +169 -0
  103. package/src/functions/branch-aware.ts +169 -0
  104. package/src/functions/cascade.ts +80 -0
  105. package/src/functions/checkpoints.ts +209 -0
  106. package/src/functions/claude-bridge.ts +161 -0
  107. package/src/functions/compress.ts +194 -0
  108. package/src/functions/consolidate.ts +212 -0
  109. package/src/functions/consolidation-pipeline.ts +258 -0
  110. package/src/functions/context.ts +169 -0
  111. package/src/functions/crystallize.ts +293 -0
  112. package/src/functions/dedup.ts +57 -0
  113. package/src/functions/diagnostics.ts +785 -0
  114. package/src/functions/enrich.ts +132 -0
  115. package/src/functions/evict.ts +163 -0
  116. package/src/functions/export-import.ts +508 -0
  117. package/src/functions/facets.ts +248 -0
  118. package/src/functions/file-index.ts +106 -0
  119. package/src/functions/flow-compress.ts +214 -0
  120. package/src/functions/frontier.ts +196 -0
  121. package/src/functions/governance.ts +131 -0
  122. package/src/functions/graph-retrieval.ts +277 -0
  123. package/src/functions/graph.ts +275 -0
  124. package/src/functions/leases.ts +216 -0
  125. package/src/functions/lessons.ts +253 -0
  126. package/src/functions/mesh.ts +434 -0
  127. package/src/functions/migrate.ts +165 -0
  128. package/src/functions/observe.ts +144 -0
  129. package/src/functions/obsidian-export.ts +310 -0
  130. package/src/functions/patterns.ts +138 -0
  131. package/src/functions/privacy.ts +39 -0
  132. package/src/functions/profile.ts +155 -0
  133. package/src/functions/query-expansion.ts +186 -0
  134. package/src/functions/relations.ts +237 -0
  135. package/src/functions/remember.ts +162 -0
  136. package/src/functions/retention.ts +235 -0
  137. package/src/functions/routines.ts +289 -0
  138. package/src/functions/search.ts +80 -0
  139. package/src/functions/sentinels.ts +417 -0
  140. package/src/functions/signals.ts +186 -0
  141. package/src/functions/sketches.ts +274 -0
  142. package/src/functions/sliding-window.ts +257 -0
  143. package/src/functions/smart-search.ts +115 -0
  144. package/src/functions/snapshot.ts +219 -0
  145. package/src/functions/summarize.ts +155 -0
  146. package/src/functions/team.ts +147 -0
  147. package/src/functions/temporal-graph.ts +476 -0
  148. package/src/functions/timeline.ts +138 -0
  149. package/src/functions/verify.ts +117 -0
  150. package/src/health/monitor.ts +110 -0
  151. package/src/health/thresholds.ts +73 -0
  152. package/src/hooks/notification.ts +52 -0
  153. package/src/hooks/post-tool-failure.ts +58 -0
  154. package/src/hooks/post-tool-use.ts +62 -0
  155. package/src/hooks/pre-compact.ts +60 -0
  156. package/src/hooks/pre-tool-use.ts +72 -0
  157. package/src/hooks/prompt-submit.ts +46 -0
  158. package/src/hooks/session-end.ts +71 -0
  159. package/src/hooks/session-start.ts +48 -0
  160. package/src/hooks/stop.ts +39 -0
  161. package/src/hooks/subagent-start.ts +49 -0
  162. package/src/hooks/subagent-stop.ts +54 -0
  163. package/src/hooks/task-completed.ts +54 -0
  164. package/src/index.ts +342 -0
  165. package/src/mcp/in-memory-kv.ts +61 -0
  166. package/src/mcp/server.ts +1455 -0
  167. package/src/mcp/standalone.ts +177 -0
  168. package/src/mcp/tools-registry.ts +769 -0
  169. package/src/mcp/transport.ts +91 -0
  170. package/src/prompts/compression.ts +67 -0
  171. package/src/prompts/consolidation.ts +48 -0
  172. package/src/prompts/graph-extraction.ts +35 -0
  173. package/src/prompts/summary.ts +38 -0
  174. package/src/prompts/xml.ts +26 -0
  175. package/src/providers/agent-sdk.ts +34 -0
  176. package/src/providers/anthropic.ts +35 -0
  177. package/src/providers/circuit-breaker.ts +82 -0
  178. package/src/providers/embedding/cohere.ts +46 -0
  179. package/src/providers/embedding/gemini.ts +54 -0
  180. package/src/providers/embedding/index.ts +39 -0
  181. package/src/providers/embedding/local.ts +52 -0
  182. package/src/providers/embedding/openai.ts +45 -0
  183. package/src/providers/embedding/openrouter.ts +51 -0
  184. package/src/providers/embedding/voyage.ts +46 -0
  185. package/src/providers/fallback-chain.ts +31 -0
  186. package/src/providers/index.ts +84 -0
  187. package/src/providers/openrouter.ts +71 -0
  188. package/src/providers/resilient.ts +37 -0
  189. package/src/state/hybrid-search.ts +295 -0
  190. package/src/state/index-persistence.ts +63 -0
  191. package/src/state/keyed-mutex.ts +18 -0
  192. package/src/state/kv.ts +33 -0
  193. package/src/state/schema.ts +71 -0
  194. package/src/state/search-index.ts +245 -0
  195. package/src/state/stemmer.ts +104 -0
  196. package/src/state/synonyms.ts +63 -0
  197. package/src/state/vector-index.ts +130 -0
  198. package/src/telemetry/setup.ts +116 -0
  199. package/src/triggers/api.ts +1904 -0
  200. package/src/triggers/events.ts +71 -0
  201. package/src/types.ts +769 -0
  202. package/src/version.ts +1 -0
  203. package/src/viewer/index.html +2497 -0
  204. package/src/viewer/server.ts +207 -0
  205. package/src/xenova.d.ts +3 -0
  206. package/test/actions.test.ts +490 -0
  207. package/test/audit.test.ts +108 -0
  208. package/test/auto-forget.test.ts +188 -0
  209. package/test/cascade.test.ts +277 -0
  210. package/test/checkpoints.test.ts +493 -0
  211. package/test/circuit-breaker.test.ts +107 -0
  212. package/test/claude-bridge.test.ts +178 -0
  213. package/test/confidence.test.ts +247 -0
  214. package/test/consistency.test.ts +61 -0
  215. package/test/consolidation-pipeline.test.ts +251 -0
  216. package/test/crystallize.test.ts +521 -0
  217. package/test/diagnostics.test.ts +638 -0
  218. package/test/embedding-provider.test.ts +49 -0
  219. package/test/enrich.test.ts +209 -0
  220. package/test/eval.test.ts +300 -0
  221. package/test/export-import.test.ts +251 -0
  222. package/test/facets.test.ts +448 -0
  223. package/test/fallback-chain.test.ts +93 -0
  224. package/test/frontier.test.ts +485 -0
  225. package/test/governance.test.ts +147 -0
  226. package/test/graph-retrieval.test.ts +186 -0
  227. package/test/graph.test.ts +160 -0
  228. package/test/helpers/mocks.ts +40 -0
  229. package/test/hybrid-search.test.ts +145 -0
  230. package/test/index-persistence.test.ts +124 -0
  231. package/test/integration.test.ts +265 -0
  232. package/test/leases.test.ts +399 -0
  233. package/test/mcp-prompts.test.ts +218 -0
  234. package/test/mcp-resources.test.ts +286 -0
  235. package/test/mcp-standalone.test.ts +113 -0
  236. package/test/mesh.test.ts +700 -0
  237. package/test/privacy.test.ts +87 -0
  238. package/test/profile.test.ts +161 -0
  239. package/test/query-expansion.test.ts +154 -0
  240. package/test/relations.test.ts +198 -0
  241. package/test/retention.test.ts +245 -0
  242. package/test/routines.test.ts +497 -0
  243. package/test/schema-fingerprint.test.ts +81 -0
  244. package/test/schema.test.ts +42 -0
  245. package/test/search-index.test.ts +128 -0
  246. package/test/sentinels.test.ts +626 -0
  247. package/test/signals.test.ts +410 -0
  248. package/test/sketches.test.ts +549 -0
  249. package/test/sliding-window.test.ts +199 -0
  250. package/test/smart-search.test.ts +169 -0
  251. package/test/snapshot.test.ts +165 -0
  252. package/test/team.test.ts +156 -0
  253. package/test/temporal-graph.test.ts +378 -0
  254. package/test/timeline.test.ts +148 -0
  255. package/test/vector-index.test.ts +79 -0
  256. package/test/verify.test.ts +209 -0
  257. package/test/xml.test.ts +65 -0
  258. package/tsconfig.json +22 -0
  259. package/tsdown.config.ts +62 -0
@@ -0,0 +1,2497 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>agentmemory viewer</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700;900&family=Lora:wght@400;500;600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #F9F9F7;
11
+ --bg-alt: #F0F0EC;
12
+ --bg-inset: #E8E8E3;
13
+ --border: #111111;
14
+ --border-light: #D4D4CF;
15
+ --border-heavy: #111111;
16
+ --ink: #111111;
17
+ --ink-secondary: #333333;
18
+ --ink-muted: #666666;
19
+ --ink-faint: #999999;
20
+ --accent: #CC0000;
21
+ --accent-light: #FF1A1A;
22
+ --cream: #F5F0E8;
23
+ --node-file: #2D6A4F;
24
+ --node-function: #1D4E89;
25
+ --node-concept: #B8860B;
26
+ --node-error: #CC0000;
27
+ --node-decision: #6B3FA0;
28
+ --node-pattern: #2563EB;
29
+ --node-library: #C2410C;
30
+ --node-person: #111111;
31
+ --green: #2D6A4F;
32
+ --blue: #1D4E89;
33
+ --yellow: #B8860B;
34
+ --red: #CC0000;
35
+ --purple: #6B3FA0;
36
+ --orange: #C2410C;
37
+ --cyan: #0E7490;
38
+ --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
39
+ --font-body: 'Lora', Georgia, serif;
40
+ --font-ui: 'Inter', -apple-system, sans-serif;
41
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
42
+ }
43
+ * { margin: 0; padding: 0; box-sizing: border-box; }
44
+ body {
45
+ font-family: var(--font-body);
46
+ background: var(--bg);
47
+ color: var(--ink-secondary);
48
+ line-height: 1.6;
49
+ overflow: hidden;
50
+ height: 100vh;
51
+ background-image: radial-gradient(circle, #D4D4CF 0.5px, transparent 0.5px);
52
+ background-size: 16px 16px;
53
+ }
54
+ ::-webkit-scrollbar { width: 6px; }
55
+ ::-webkit-scrollbar-track { background: var(--bg); }
56
+ ::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 0; }
57
+ ::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); }
58
+
59
+ .app-header {
60
+ padding: 10px 24px;
61
+ border-bottom: 4px solid var(--border-heavy);
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: space-between;
65
+ background: var(--bg);
66
+ }
67
+ .app-header .brand {
68
+ display: flex;
69
+ align-items: baseline;
70
+ gap: 10px;
71
+ }
72
+ .app-header .brand h1 {
73
+ font-size: 22px;
74
+ color: var(--ink);
75
+ font-weight: 900;
76
+ font-family: var(--font-display);
77
+ letter-spacing: -0.02em;
78
+ text-transform: lowercase;
79
+ }
80
+ .app-header .brand .version {
81
+ font-size: 10px;
82
+ color: var(--ink-faint);
83
+ font-family: var(--font-mono);
84
+ text-transform: uppercase;
85
+ letter-spacing: 0.1em;
86
+ }
87
+ .header-right {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 12px;
91
+ }
92
+ .ws-status {
93
+ font-size: 10px;
94
+ padding: 3px 10px;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 5px;
98
+ font-family: var(--font-ui);
99
+ text-transform: uppercase;
100
+ letter-spacing: 0.08em;
101
+ font-weight: 600;
102
+ border: 1px solid var(--border-light);
103
+ }
104
+ .ws-status::before {
105
+ content: '';
106
+ width: 6px;
107
+ height: 6px;
108
+ display: inline-block;
109
+ }
110
+ .ws-status.connected { border-color: var(--green); color: var(--green); }
111
+ .ws-status.connected::before { background: var(--green); }
112
+ .ws-status.disconnected { border-color: var(--ink-faint); color: var(--ink-faint); }
113
+ .ws-status.disconnected::before { background: var(--ink-faint); }
114
+
115
+ .tab-bar {
116
+ display: flex;
117
+ border-bottom: 1px solid var(--border-light);
118
+ background: var(--bg);
119
+ overflow-x: auto;
120
+ }
121
+ .tab-bar button {
122
+ background: none;
123
+ border: none;
124
+ color: var(--ink-muted);
125
+ padding: 10px 20px;
126
+ font-size: 11px;
127
+ cursor: pointer;
128
+ border-bottom: 2px solid transparent;
129
+ white-space: nowrap;
130
+ font-family: var(--font-ui);
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.12em;
133
+ font-weight: 600;
134
+ transition: color 0.15s, border-color 0.15s;
135
+ }
136
+ .tab-bar button:hover { color: var(--ink); }
137
+ .tab-bar button.active {
138
+ color: var(--ink);
139
+ border-bottom-color: var(--accent);
140
+ }
141
+
142
+ .view { display: none; height: calc(100vh - 90px); overflow-y: auto; padding: 24px; }
143
+ .view.active { display: block; }
144
+
145
+ .stats-grid {
146
+ display: grid;
147
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
148
+ gap: 0;
149
+ margin-bottom: 24px;
150
+ border: 1px solid var(--border);
151
+ }
152
+ .stat-card {
153
+ background: var(--bg);
154
+ padding: 16px 20px;
155
+ border-right: 1px solid var(--border-light);
156
+ border-bottom: 1px solid var(--border-light);
157
+ }
158
+ .stat-card:last-child { border-right: none; }
159
+ .stat-card .label {
160
+ font-size: 9px;
161
+ color: var(--ink-muted);
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.15em;
164
+ margin-bottom: 4px;
165
+ font-family: var(--font-ui);
166
+ font-weight: 600;
167
+ }
168
+ .stat-card .value {
169
+ font-size: 32px;
170
+ font-weight: 900;
171
+ color: var(--ink);
172
+ font-family: var(--font-display);
173
+ line-height: 1.1;
174
+ }
175
+ .stat-card .sub {
176
+ font-size: 11px;
177
+ color: var(--ink-faint);
178
+ margin-top: 2px;
179
+ font-family: var(--font-ui);
180
+ }
181
+
182
+ .card {
183
+ background: var(--bg);
184
+ border: 1px solid var(--border);
185
+ padding: 20px;
186
+ margin-bottom: 16px;
187
+ transition: box-shadow 0.15s;
188
+ }
189
+ .card:hover {
190
+ box-shadow: 4px 4px 0px 0px var(--border);
191
+ }
192
+ .card-title {
193
+ font-size: 13px;
194
+ font-weight: 700;
195
+ color: var(--ink);
196
+ margin-bottom: 12px;
197
+ font-family: var(--font-display);
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.06em;
200
+ padding-bottom: 8px;
201
+ border-bottom: 1px solid var(--border-light);
202
+ }
203
+
204
+ .health-bar {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 8px;
208
+ margin-bottom: 8px;
209
+ }
210
+ .health-dot {
211
+ width: 10px;
212
+ height: 10px;
213
+ }
214
+ .health-dot.healthy { background: var(--green); }
215
+ .health-dot.degraded { background: var(--yellow); }
216
+ .health-dot.critical { background: var(--accent); }
217
+
218
+ .badge {
219
+ display: inline-block;
220
+ font-size: 9px;
221
+ padding: 2px 8px;
222
+ font-weight: 600;
223
+ font-family: var(--font-ui);
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.08em;
226
+ border: 1px solid;
227
+ }
228
+ .badge-blue { border-color: var(--blue); color: var(--blue); background: transparent; }
229
+ .badge-green { border-color: var(--green); color: var(--green); background: transparent; }
230
+ .badge-yellow { border-color: var(--yellow); color: var(--yellow); background: transparent; }
231
+ .badge-red { border-color: var(--accent); color: var(--accent); background: transparent; }
232
+ .badge-purple { border-color: var(--purple); color: var(--purple); background: transparent; }
233
+ .badge-orange { border-color: var(--orange); color: var(--orange); background: transparent; }
234
+ .badge-cyan { border-color: var(--cyan); color: var(--cyan); background: transparent; }
235
+ .badge-muted { border-color: var(--border-light); color: var(--ink-muted); background: transparent; }
236
+
237
+ table {
238
+ width: 100%;
239
+ border-collapse: collapse;
240
+ font-size: 13px;
241
+ font-family: var(--font-body);
242
+ }
243
+ th {
244
+ text-align: left;
245
+ padding: 8px 12px;
246
+ border-bottom: 2px solid var(--border);
247
+ color: var(--ink);
248
+ font-size: 9px;
249
+ text-transform: uppercase;
250
+ letter-spacing: 0.12em;
251
+ font-weight: 600;
252
+ font-family: var(--font-ui);
253
+ }
254
+ td {
255
+ padding: 8px 12px;
256
+ border-bottom: 1px solid var(--border-light);
257
+ vertical-align: top;
258
+ }
259
+ tr:hover td { background: var(--bg-alt); }
260
+
261
+ .strength-bar {
262
+ width: 60px;
263
+ height: 4px;
264
+ background: var(--bg-inset);
265
+ overflow: hidden;
266
+ display: inline-block;
267
+ vertical-align: middle;
268
+ }
269
+ .strength-bar .fill {
270
+ height: 100%;
271
+ transition: width 0.3s;
272
+ }
273
+
274
+ .toolbar {
275
+ display: flex;
276
+ gap: 10px;
277
+ margin-bottom: 20px;
278
+ align-items: center;
279
+ flex-wrap: wrap;
280
+ }
281
+ .toolbar input, .toolbar select {
282
+ background: var(--bg);
283
+ border: 1px solid var(--border);
284
+ color: var(--ink);
285
+ padding: 7px 12px;
286
+ font-size: 13px;
287
+ outline: none;
288
+ font-family: var(--font-ui);
289
+ }
290
+ .toolbar input:focus, .toolbar select:focus {
291
+ border-color: var(--ink);
292
+ box-shadow: 2px 2px 0px 0px var(--border);
293
+ }
294
+ .toolbar input { flex: 1; min-width: 200px; }
295
+
296
+ .btn {
297
+ background: var(--bg);
298
+ border: 1px solid var(--border);
299
+ color: var(--ink);
300
+ padding: 7px 16px;
301
+ font-size: 11px;
302
+ cursor: pointer;
303
+ transition: box-shadow 0.1s, transform 0.1s;
304
+ font-family: var(--font-ui);
305
+ font-weight: 600;
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.06em;
308
+ }
309
+ .btn:hover { box-shadow: 3px 3px 0px 0px var(--border); transform: translate(-1px, -1px); }
310
+ .btn:active { box-shadow: none; transform: translate(0, 0); }
311
+ .btn-danger { border-color: var(--accent); color: var(--accent); }
312
+ .btn-danger:hover { background: var(--accent); color: white; box-shadow: 3px 3px 0px 0px var(--border); }
313
+ .btn-primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
314
+ .btn-primary:hover { background: var(--ink-secondary); box-shadow: 3px 3px 0px 0px var(--ink-muted); }
315
+
316
+ .graph-container {
317
+ display: flex;
318
+ height: calc(100vh - 130px);
319
+ margin: -24px;
320
+ border-top: 1px solid var(--border-light);
321
+ }
322
+ .graph-canvas-wrap {
323
+ flex: 1;
324
+ position: relative;
325
+ overflow: hidden;
326
+ background: var(--bg);
327
+ }
328
+ .graph-canvas-wrap canvas {
329
+ display: block;
330
+ width: 100%;
331
+ height: 100%;
332
+ }
333
+ .graph-sidebar {
334
+ width: 260px;
335
+ border-left: 2px solid var(--border);
336
+ padding: 20px;
337
+ overflow-y: auto;
338
+ background: var(--bg);
339
+ }
340
+ .graph-sidebar h3 {
341
+ font-size: 9px;
342
+ color: var(--ink);
343
+ text-transform: uppercase;
344
+ letter-spacing: 0.15em;
345
+ margin-bottom: 12px;
346
+ font-family: var(--font-ui);
347
+ font-weight: 600;
348
+ padding-bottom: 6px;
349
+ border-bottom: 1px solid var(--border-light);
350
+ }
351
+ .filter-item {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 6px;
355
+ padding: 4px 0;
356
+ font-size: 12px;
357
+ cursor: pointer;
358
+ font-family: var(--font-ui);
359
+ }
360
+ .filter-item input[type="checkbox"] {
361
+ accent-color: var(--ink);
362
+ }
363
+ .filter-dot {
364
+ width: 8px;
365
+ height: 8px;
366
+ display: inline-block;
367
+ }
368
+ .graph-info {
369
+ margin-top: 16px;
370
+ padding-top: 16px;
371
+ border-top: 1px solid var(--border-light);
372
+ }
373
+ .graph-info .info-row {
374
+ display: flex;
375
+ justify-content: space-between;
376
+ font-size: 12px;
377
+ padding: 3px 0;
378
+ font-family: var(--font-ui);
379
+ }
380
+ .graph-info .info-row .info-label { color: var(--ink-muted); }
381
+ .graph-info .info-row .info-value { color: var(--ink); font-weight: 600; font-family: var(--font-mono); }
382
+
383
+ .obs-card {
384
+ background: var(--bg);
385
+ border: 1px solid var(--border-light);
386
+ padding: 16px 20px;
387
+ margin-bottom: 12px;
388
+ border-left: 3px solid var(--border-light);
389
+ transition: box-shadow 0.15s;
390
+ }
391
+ .obs-card:hover { box-shadow: 3px 3px 0px 0px var(--border-light); }
392
+ .obs-card.imp-high { border-left-color: var(--accent); }
393
+ .obs-card.imp-med { border-left-color: var(--yellow); }
394
+ .obs-card.imp-low { border-left-color: var(--green); }
395
+ .obs-card .obs-head {
396
+ display: flex;
397
+ justify-content: space-between;
398
+ align-items: center;
399
+ margin-bottom: 6px;
400
+ }
401
+ .obs-card .obs-title {
402
+ font-size: 14px;
403
+ font-weight: 700;
404
+ color: var(--ink);
405
+ font-family: var(--font-display);
406
+ }
407
+ .obs-card .obs-time {
408
+ font-size: 10px;
409
+ color: var(--ink-faint);
410
+ font-family: var(--font-mono);
411
+ letter-spacing: 0.04em;
412
+ }
413
+ .obs-card .obs-narrative {
414
+ font-size: 13px;
415
+ color: var(--ink-muted);
416
+ margin-bottom: 6px;
417
+ }
418
+ .obs-card .obs-facts {
419
+ margin: 6px 0 6px 16px;
420
+ font-size: 12px;
421
+ color: var(--ink-muted);
422
+ }
423
+ .obs-card .obs-facts li { margin-bottom: 2px; }
424
+ .tag-list { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 6px; }
425
+ .tag {
426
+ font-size: 10px;
427
+ padding: 1px 6px;
428
+ border: 1px solid var(--blue);
429
+ color: var(--blue);
430
+ font-family: var(--font-mono);
431
+ font-weight: 500;
432
+ }
433
+ .tag.file-tag { border-color: var(--green); color: var(--green); }
434
+
435
+ .session-list { display: flex; flex-direction: column; gap: 0; }
436
+ .session-item {
437
+ background: var(--bg);
438
+ border: 1px solid var(--border-light);
439
+ border-bottom: none;
440
+ padding: 14px 20px;
441
+ cursor: pointer;
442
+ transition: background 0.1s;
443
+ }
444
+ .session-item:last-child { border-bottom: 1px solid var(--border-light); }
445
+ .session-item:hover { background: var(--bg-alt); }
446
+ .session-item.selected { background: var(--bg-alt); border-left: 3px solid var(--accent); }
447
+ .session-item .session-top {
448
+ display: flex;
449
+ justify-content: space-between;
450
+ align-items: center;
451
+ margin-bottom: 4px;
452
+ }
453
+ .session-item .session-project {
454
+ font-weight: 700;
455
+ color: var(--ink);
456
+ font-size: 14px;
457
+ font-family: var(--font-display);
458
+ }
459
+ .session-item .session-meta {
460
+ font-size: 11px;
461
+ color: var(--ink-muted);
462
+ font-family: var(--font-mono);
463
+ }
464
+
465
+ .detail-panel {
466
+ background: var(--bg);
467
+ border: 1px solid var(--border);
468
+ padding: 24px;
469
+ margin-top: 20px;
470
+ }
471
+ .detail-panel h3 {
472
+ font-size: 15px;
473
+ font-weight: 700;
474
+ color: var(--ink);
475
+ margin-bottom: 16px;
476
+ font-family: var(--font-display);
477
+ text-transform: uppercase;
478
+ letter-spacing: 0.04em;
479
+ padding-bottom: 8px;
480
+ border-bottom: 2px solid var(--border);
481
+ }
482
+ .detail-row {
483
+ display: flex;
484
+ padding: 6px 0;
485
+ font-size: 13px;
486
+ border-bottom: 1px solid var(--bg-inset);
487
+ }
488
+ .detail-row .dl { color: var(--ink-muted); width: 140px; flex-shrink: 0; font-family: var(--font-ui); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; padding-top: 2px; }
489
+ .detail-row .dv { color: var(--ink); font-family: var(--font-body); }
490
+
491
+ .audit-entry {
492
+ padding: 12px 0;
493
+ border-bottom: 1px solid var(--border-light);
494
+ font-size: 13px;
495
+ }
496
+ .audit-entry:last-child { border-bottom: none; }
497
+ .audit-head {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 8px;
501
+ margin-bottom: 4px;
502
+ }
503
+ .audit-detail {
504
+ font-size: 12px;
505
+ color: var(--ink-faint);
506
+ margin-top: 4px;
507
+ max-height: 0;
508
+ overflow: hidden;
509
+ transition: max-height 0.2s;
510
+ }
511
+ .audit-detail.open { max-height: 200px; }
512
+ .audit-detail pre {
513
+ font-family: var(--font-mono);
514
+ font-size: 11px;
515
+ background: var(--bg-alt);
516
+ padding: 10px;
517
+ border: 1px solid var(--border-light);
518
+ overflow-x: auto;
519
+ }
520
+
521
+ .bar-chart { margin-top: 8px; }
522
+ .bar-row {
523
+ display: flex;
524
+ align-items: center;
525
+ gap: 8px;
526
+ margin-bottom: 6px;
527
+ font-size: 12px;
528
+ }
529
+ .bar-label { width: 120px; color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: 11px; }
530
+ .bar-track {
531
+ flex: 1;
532
+ height: 6px;
533
+ background: var(--bg-inset);
534
+ overflow: hidden;
535
+ }
536
+ .bar-fill {
537
+ height: 100%;
538
+ transition: width 0.3s;
539
+ }
540
+ .bar-value { width: 30px; text-align: right; color: var(--ink-muted); font-size: 11px; font-family: var(--font-mono); font-weight: 500; }
541
+
542
+ .empty-state {
543
+ text-align: center;
544
+ padding: 60px 20px;
545
+ color: var(--ink-faint);
546
+ }
547
+ .empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; opacity: 0.4; }
548
+ .empty-state p { font-size: 14px; font-family: var(--font-body); font-style: italic; }
549
+
550
+ .loading { color: var(--ink-faint); padding: 20px; text-align: center; font-style: italic; font-family: var(--font-body); }
551
+
552
+ .metric-table { width: 100%; border-collapse: collapse; font-size: 12px; }
553
+ .metric-table th { padding: 6px 8px; font-size: 9px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-muted); border-bottom: 2px solid var(--border); text-align: left; font-family: var(--font-ui); font-weight: 600; }
554
+ .metric-table td { padding: 5px 8px; border-bottom: 1px solid var(--border-light); }
555
+ .metric-table tr:hover td { background: var(--bg-alt); }
556
+ .metric-fn { font-family: var(--font-mono); font-size: 11px; color: var(--blue); }
557
+ .metric-num { font-family: var(--font-mono); color: var(--ink); text-align: right; }
558
+
559
+ .cb-indicator { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; font-size: 10px; font-weight: 600; font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; border: 1px solid; }
560
+ .cb-closed { border-color: var(--green); color: var(--green); }
561
+ .cb-open { border-color: var(--accent); color: var(--accent); }
562
+ .cb-half-open { border-color: var(--yellow); color: var(--yellow); }
563
+
564
+ .worker-row { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; font-family: var(--font-ui); }
565
+ .worker-dot { width: 8px; height: 8px; }
566
+ .worker-dot.running { background: var(--green); }
567
+ .worker-dot.stopped { background: var(--accent); }
568
+ .worker-dot.starting { background: var(--yellow); }
569
+
570
+ .gauge { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
571
+ .gauge-bar { flex: 1; height: 6px; background: var(--bg-inset); overflow: hidden; }
572
+ .gauge-fill { height: 100%; transition: width 0.5s; }
573
+ .gauge-label { width: 90px; font-size: 10px; color: var(--ink-muted); font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
574
+ .gauge-value { width: 70px; font-size: 11px; color: var(--ink); text-align: right; font-family: var(--font-mono); }
575
+
576
+ .obs-type-icon { font-size: 16px; margin-right: 4px; }
577
+ .obs-subtitle { font-size: 12px; color: var(--ink-faint); margin-top: 2px; font-style: italic; }
578
+ .obs-importance { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; font-size: 11px; font-weight: 700; font-family: var(--font-mono); border: 1px solid; }
579
+ .imp-1, .imp-2, .imp-3 { border-color: var(--green); color: var(--green); }
580
+ .imp-4, .imp-5, .imp-6 { border-color: var(--yellow); color: var(--yellow); }
581
+ .imp-7, .imp-8, .imp-9, .imp-10 { border-color: var(--accent); color: var(--accent); }
582
+
583
+ .three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
584
+ @media (max-width: 1100px) { .three-col { grid-template-columns: 1fr 1fr; } }
585
+ @media (max-width: 768px) { .three-col { grid-template-columns: 1fr; } }
586
+
587
+ .pagination {
588
+ display: flex;
589
+ justify-content: center;
590
+ gap: 8px;
591
+ margin-top: 20px;
592
+ }
593
+
594
+ .modal-overlay {
595
+ display: none;
596
+ position: fixed;
597
+ inset: 0;
598
+ background: rgba(0,0,0,0.3);
599
+ z-index: 100;
600
+ align-items: center;
601
+ justify-content: center;
602
+ }
603
+ .modal-overlay.open { display: flex; }
604
+ .modal {
605
+ background: var(--bg);
606
+ border: 2px solid var(--border);
607
+ padding: 28px;
608
+ max-width: 460px;
609
+ width: 90%;
610
+ box-shadow: 6px 6px 0px 0px var(--border);
611
+ }
612
+ .modal h3 {
613
+ font-size: 18px;
614
+ font-weight: 700;
615
+ color: var(--ink);
616
+ margin-bottom: 12px;
617
+ font-family: var(--font-display);
618
+ }
619
+ .modal p { font-size: 13px; color: var(--ink-muted); margin-bottom: 16px; }
620
+ .modal-actions {
621
+ display: flex;
622
+ justify-content: flex-end;
623
+ gap: 8px;
624
+ }
625
+ .selected-node-info {
626
+ margin-top: 16px;
627
+ padding-top: 16px;
628
+ border-top: 1px solid var(--border-light);
629
+ }
630
+ .selected-node-info h4 {
631
+ font-size: 13px;
632
+ font-weight: 700;
633
+ color: var(--ink);
634
+ margin-bottom: 6px;
635
+ font-family: var(--font-display);
636
+ }
637
+ .selected-node-info .prop {
638
+ font-size: 12px;
639
+ color: var(--ink-muted);
640
+ padding: 2px 0;
641
+ font-family: var(--font-ui);
642
+ }
643
+ .two-col {
644
+ display: grid;
645
+ grid-template-columns: 1fr 1fr;
646
+ gap: 16px;
647
+ }
648
+ @media (max-width: 768px) {
649
+ .two-col { grid-template-columns: 1fr; }
650
+ .graph-sidebar { width: 200px; }
651
+ .stats-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
652
+ }
653
+
654
+ .section-rule {
655
+ border: none;
656
+ border-top: 1px solid var(--border-light);
657
+ margin: 20px 0;
658
+ }
659
+ .dateline {
660
+ font-family: var(--font-mono);
661
+ font-size: 10px;
662
+ color: var(--ink-faint);
663
+ text-transform: uppercase;
664
+ letter-spacing: 0.1em;
665
+ }
666
+
667
+ .timeline-container { position: relative; padding: 20px 0; }
668
+ .timeline-container::before { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; background: var(--border-light); transform: translateX(-50%); }
669
+ .timeline-item { position: relative; width: 45%; margin-bottom: 20px; }
670
+ .timeline-item.tl-left { margin-left: 0; margin-right: auto; text-align: right; padding-right: 30px; }
671
+ .timeline-item.tl-right { margin-left: auto; margin-right: 0; padding-left: 30px; }
672
+ .timeline-dot { position: absolute; width: 12px; height: 12px; border-radius: 50%; top: 16px; z-index: 1; border: 2px solid var(--bg); }
673
+ .timeline-item.tl-left .timeline-dot { right: -6px; transform: translateX(50%); }
674
+ .timeline-item.tl-right .timeline-dot { left: -6px; transform: translateX(-50%); }
675
+ .timeline-connector { position: absolute; top: 21px; height: 1px; background: var(--border-light); width: 24px; }
676
+ .timeline-item.tl-left .timeline-connector { right: 0; }
677
+ .timeline-item.tl-right .timeline-connector { left: 0; }
678
+ .timeline-date-marker { text-align: center; position: relative; margin: 24px 0 16px; z-index: 2; }
679
+ .timeline-date-marker span { background: var(--bg); padding: 4px 16px; font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-muted); border: 1px solid var(--border-light); }
680
+
681
+ .heatmap-wrap { overflow-x: auto; padding: 8px 0; }
682
+ .heatmap-grid { display: grid; grid-template-rows: repeat(7, 1fr); grid-auto-flow: column; grid-auto-columns: 12px; gap: 2px; }
683
+ .heatmap-cell { width: 10px; height: 10px; background: var(--bg-inset); cursor: default; }
684
+ .heatmap-cell[title] { cursor: pointer; }
685
+ .heatmap-cell.level-1 { background: rgba(45,106,79,0.2); }
686
+ .heatmap-cell.level-2 { background: rgba(45,106,79,0.4); }
687
+ .heatmap-cell.level-3 { background: rgba(45,106,79,0.65); }
688
+ .heatmap-cell.level-4 { background: var(--green); }
689
+ .heatmap-labels { display: flex; gap: 2px; font-size: 9px; color: var(--ink-faint); font-family: var(--font-mono); margin-bottom: 4px; }
690
+
691
+ .graph-search { width: 100%; background: var(--bg); border: 1px solid var(--border); padding: 7px 12px; font-size: 12px; font-family: var(--font-ui); margin-bottom: 12px; outline: none; }
692
+ .graph-search:focus { border-color: var(--ink); box-shadow: 2px 2px 0px 0px var(--border); }
693
+ .graph-legend { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-light); }
694
+ .graph-legend-item { display: flex; align-items: center; gap: 6px; padding: 3px 0; font-size: 11px; font-family: var(--font-ui); color: var(--ink-muted); }
695
+ .graph-legend-shape { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; }
696
+ .graph-tooltip { position: absolute; background: var(--bg); border: 1px solid var(--border); padding: 8px 12px; font-size: 11px; font-family: var(--font-ui); pointer-events: none; z-index: 10; box-shadow: 3px 3px 0px 0px var(--border); max-width: 240px; display: none; }
697
+ .graph-tooltip.visible { display: block; }
698
+ .graph-tooltip .tt-name { font-weight: 700; color: var(--ink); margin-bottom: 2px; font-family: var(--font-display); }
699
+ .graph-tooltip .tt-type { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px; }
700
+ .graph-tooltip .tt-prop { font-size: 10px; color: var(--ink-muted); }
701
+ .graph-tooltip .tt-conns { font-size: 10px; color: var(--ink-faint); margin-top: 4px; border-top: 1px solid var(--border-light); padding-top: 4px; }
702
+
703
+ .type-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
704
+ .type-chip { font-size: 10px; padding: 3px 10px; border: 1px solid var(--border-light); cursor: pointer; font-family: var(--font-ui); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; transition: all 0.15s; background: var(--bg); }
705
+ .type-chip:hover { border-color: var(--ink); }
706
+ .type-chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
707
+
708
+ .memory-fact { padding: 8px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
709
+ .memory-fact:last-child { border-bottom: none; }
710
+ .procedure-item { padding: 10px 0; border-bottom: 1px solid var(--border-light); }
711
+ .procedure-item:last-child { border-bottom: none; }
712
+ .procedure-steps { margin: 6px 0 0 16px; font-size: 12px; color: var(--ink-muted); }
713
+ .procedure-steps li { margin-bottom: 2px; }
714
+ .consolidation-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 12px; font-family: var(--font-ui); }
715
+ .consolidation-row .cl { color: var(--ink-muted); }
716
+ .consolidation-row .cv { color: var(--ink); font-weight: 600; font-family: var(--font-mono); }
717
+
718
+ .activity-feed-item { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--border-light); font-size: 13px; }
719
+ .activity-feed-item:last-child { border-bottom: none; }
720
+ .activity-feed-icon { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; border: 1px solid var(--border-light); }
721
+ .activity-feed-body { flex: 1; min-width: 0; }
722
+ .activity-feed-title { font-weight: 600; color: var(--ink); font-family: var(--font-display); font-size: 13px; }
723
+ .activity-feed-meta { font-size: 10px; color: var(--ink-faint); font-family: var(--font-mono); margin-top: 2px; }
724
+ </style>
725
+ </head>
726
+ <body>
727
+ <div class="app-header">
728
+ <div class="brand">
729
+ <h1>agentmemory</h1>
730
+ <span class="version">v0.5.0</span>
731
+ </div>
732
+ <div class="header-right">
733
+ <span class="dateline" id="dateline"></span>
734
+ <span id="ws-status" class="ws-status disconnected">live updates off</span>
735
+ </div>
736
+ </div>
737
+
738
+ <div class="tab-bar" id="tab-bar">
739
+ <button class="active" data-tab="dashboard">Dashboard</button>
740
+ <button data-tab="graph">Graph</button>
741
+ <button data-tab="memories">Memories</button>
742
+ <button data-tab="timeline">Timeline</button>
743
+ <button data-tab="sessions">Sessions</button>
744
+ <button data-tab="audit">Audit</button>
745
+ <button data-tab="activity">Activity</button>
746
+ <button data-tab="profile">Profile</button>
747
+ </div>
748
+
749
+ <div id="view-dashboard" class="view active"></div>
750
+ <div id="view-graph" class="view"></div>
751
+ <div id="view-memories" class="view"></div>
752
+ <div id="view-timeline" class="view"></div>
753
+ <div id="view-sessions" class="view"></div>
754
+ <div id="view-audit" class="view"></div>
755
+ <div id="view-activity" class="view"></div>
756
+ <div id="view-profile" class="view"></div>
757
+
758
+ <div id="modal-overlay" class="modal-overlay">
759
+ <div class="modal" id="modal"></div>
760
+ </div>
761
+
762
+ <script>
763
+ var params = new URLSearchParams(window.location.search);
764
+ var viewerPort = params.get('port') || window.location.port || '3113';
765
+ var iiiPort = parseInt(viewerPort);
766
+ if (iiiPort === 3111) viewerPort = '3113';
767
+ var REST = window.location.protocol + '//' + window.location.hostname + ':' + viewerPort;
768
+ var wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
769
+ var wsPort = params.get('wsPort') || String(parseInt(viewerPort) - 1);
770
+ var WS_URL = wsProto + '//' + window.location.hostname + ':' + wsPort;
771
+
772
+ var dateEl = document.getElementById('dateline');
773
+ if (dateEl) dateEl.textContent = new Date().toLocaleDateString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
774
+
775
+ var NODE_COLORS = {
776
+ file: '#2D6A4F', function: '#1D4E89', concept: '#B8860B', error: '#CC0000',
777
+ decision: '#6B3FA0', pattern: '#2563EB', library: '#C2410C', person: '#111111'
778
+ };
779
+ var OP_BADGES = {
780
+ observe: 'badge-blue', compress: 'badge-cyan', remember: 'badge-green',
781
+ forget: 'badge-red', evolve: 'badge-purple', consolidate: 'badge-yellow',
782
+ share: 'badge-orange', delete: 'badge-red', import: 'badge-blue', export: 'badge-blue'
783
+ };
784
+ var TYPE_BADGES = {
785
+ pattern: 'badge-purple', preference: 'badge-blue', architecture: 'badge-cyan',
786
+ bug: 'badge-red', workflow: 'badge-green', fact: 'badge-yellow'
787
+ };
788
+ var OBS_TYPE_COLORS = {
789
+ file_read: '#1D4E89', file_write: '#2D6A4F', file_edit: '#B8860B',
790
+ command_run: '#C2410C', search: '#2563EB', web_fetch: '#6B3FA0',
791
+ conversation: '#111111', error: '#CC0000', decision: '#B8860B',
792
+ discovery: '#2D6A4F', subagent: '#6B3FA0', notification: '#0E7490',
793
+ task: '#1D4E89', other: '#666666'
794
+ };
795
+ var OBS_TYPE_ICONS = {
796
+ file_read: '&#128196;', file_write: '&#9999;', file_edit: '&#128221;',
797
+ command_run: '&#9889;', search: '&#128270;', web_fetch: '&#127760;',
798
+ conversation: '&#128172;', error: '&#9888;', decision: '&#129300;',
799
+ discovery: '&#128161;', subagent: '&#129302;', notification: '&#128276;',
800
+ task: '&#9745;', other: '&#128196;'
801
+ };
802
+ var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
803
+
804
+ var state = {
805
+ activeTab: 'dashboard',
806
+ dashboard: { loaded: false, health: null, sessions: [], memories: [], graphStats: null, recentAudit: [] },
807
+ graph: { loaded: false, nodes: [], edges: [], stats: null, filters: {}, selectedNode: null },
808
+ memories: { loaded: false, items: [], search: '', typeFilter: '' },
809
+ timeline: { loaded: false, observations: [], sessionId: '', minImportance: 0, page: 0, pageSize: 50 },
810
+ sessions: { loaded: false, items: [], selectedId: null },
811
+ audit: { loaded: false, entries: [], opFilter: '' },
812
+ activity: { loaded: false, observations: [], sessions: [], typeFilter: '' },
813
+ profile: { loaded: false, projects: [], selectedProject: '', data: null },
814
+ ws: null
815
+ };
816
+
817
+ function esc(s) {
818
+ if (!s) return '';
819
+ var d = document.createElement('div');
820
+ d.textContent = String(s);
821
+ return d.innerHTML;
822
+ }
823
+ function formatTime(ts) {
824
+ if (!ts) return '';
825
+ try { return new Date(ts).toLocaleString(); } catch { return ts; }
826
+ }
827
+ function shortTime(ts) {
828
+ if (!ts) return '';
829
+ try { return new Date(ts).toLocaleTimeString(); } catch { return ts; }
830
+ }
831
+ function truncate(s, n) {
832
+ if (!s) return '';
833
+ return s.length > n ? s.slice(0, n) + '...' : s;
834
+ }
835
+ function debounce(fn, ms) {
836
+ var t;
837
+ return function() {
838
+ var args = arguments, ctx = this;
839
+ clearTimeout(t);
840
+ t = setTimeout(function() { fn.apply(ctx, args); }, ms);
841
+ };
842
+ }
843
+
844
+ async function api(path, opts) {
845
+ try {
846
+ var url = REST + '/agentmemory/' + path;
847
+ var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
848
+ var fetchOpts = Object.assign({}, opts || {}, { headers: headers });
849
+ var res = await fetch(url, fetchOpts);
850
+ if (!res.ok) {
851
+ console.warn('[viewer] API ' + (fetchOpts.method || 'GET') + ' ' + path + ' returned ' + res.status);
852
+ return null;
853
+ }
854
+ return await res.json();
855
+ } catch (err) {
856
+ console.warn('[viewer] API error on ' + path + ':', err);
857
+ return null;
858
+ }
859
+ }
860
+ async function apiGet(path) { return api(path); }
861
+ async function apiPost(path, body) {
862
+ return api(path, {
863
+ method: 'POST',
864
+ headers: { 'Content-Type': 'application/json' },
865
+ body: JSON.stringify(body || {})
866
+ });
867
+ }
868
+ async function apiDelete(path, body) {
869
+ return api(path, {
870
+ method: 'DELETE',
871
+ headers: { 'Content-Type': 'application/json' },
872
+ body: JSON.stringify(body || {})
873
+ });
874
+ }
875
+
876
+ function switchTab(tab) {
877
+ state.activeTab = tab;
878
+ document.querySelectorAll('.tab-bar button').forEach(function(b) {
879
+ b.classList.toggle('active', b.dataset.tab === tab);
880
+ });
881
+ document.querySelectorAll('.view').forEach(function(v) {
882
+ v.classList.toggle('active', v.id === 'view-' + tab);
883
+ });
884
+ loadTab(tab);
885
+ }
886
+
887
+ async function loadTab(tab) {
888
+ switch(tab) {
889
+ case 'dashboard': if (!state.dashboard.loaded) await loadDashboard(); break;
890
+ case 'graph': if (!state.graph.loaded) await loadGraph(); break;
891
+ case 'memories': if (!state.memories.loaded) await loadMemories(); break;
892
+ case 'timeline': if (!state.timeline.loaded) await loadTimeline(); break;
893
+ case 'sessions': if (!state.sessions.loaded) await loadSessions(); break;
894
+ case 'audit': if (!state.audit.loaded) await loadAudit(); break;
895
+ case 'activity': if (!state.activity.loaded) await loadActivity(); break;
896
+ case 'profile': if (!state.profile.loaded) await loadProfile(); break;
897
+ }
898
+ }
899
+
900
+ async function loadDashboard() {
901
+ var el = document.getElementById('view-dashboard');
902
+ el.innerHTML = '<div class="loading">Loading dashboard...</div>';
903
+ var results = await Promise.all([
904
+ apiGet('health'),
905
+ apiGet('sessions'),
906
+ apiGet('memories?latest=true'),
907
+ apiGet('graph/stats'),
908
+ apiGet('audit?limit=5'),
909
+ apiGet('semantic'),
910
+ apiGet('procedural'),
911
+ apiGet('relations')
912
+ ]);
913
+ state.dashboard.health = results[0];
914
+ state.dashboard.sessions = (results[1] && results[1].sessions) || [];
915
+ state.dashboard.memories = (results[2] && results[2].memories) || [];
916
+ state.dashboard.graphStats = results[3];
917
+ state.dashboard.recentAudit = (results[4] && results[4].entries) || [];
918
+ state.dashboard.semantic = (results[5] && results[5].facts) || (results[5] && results[5].semantic) || [];
919
+ state.dashboard.procedural = (results[6] && results[6].procedures) || (results[6] && results[6].procedural) || [];
920
+ state.dashboard.relations = (results[7] && results[7].relations) || [];
921
+ state.dashboard.loaded = true;
922
+ renderDashboard();
923
+ }
924
+
925
+ function renderDashboard() {
926
+ var el = document.getElementById('view-dashboard');
927
+ var d = state.dashboard;
928
+ var h = d.health || {};
929
+ var snap = h.health || {};
930
+ var healthStatus = h.status || 'unknown';
931
+ var dotClass = healthStatus === 'healthy' ? 'healthy' : healthStatus === 'degraded' ? 'degraded' : healthStatus === 'critical' ? 'critical' : '';
932
+ var activeSessions = d.sessions.filter(function(s) { return s.status === 'active'; }).length;
933
+ var gs = d.graphStats || {};
934
+ var nodeCount = (gs.nodes !== undefined) ? gs.nodes : (gs.nodeCount || 0);
935
+ var edgeCount = (gs.edges !== undefined) ? gs.edges : (gs.edgeCount || 0);
936
+ var fMetrics = h.functionMetrics || [];
937
+ var cb = h.circuitBreaker || null;
938
+ var workers = snap.workers || [];
939
+
940
+ var html = '<div class="stats-grid">';
941
+ html += '<div class="stat-card"><div class="label">Sessions</div><div class="value">' + d.sessions.length + '</div><div class="sub">' + activeSessions + ' active</div></div>';
942
+ html += '<div class="stat-card"><div class="label">Memories</div><div class="value">' + d.memories.length + '</div><div class="sub">latest versions</div></div>';
943
+ html += '<div class="stat-card"><div class="label">Graph Nodes</div><div class="value">' + nodeCount + '</div><div class="sub">' + edgeCount + ' edges</div></div>';
944
+ html += '<div class="stat-card"><div class="label">Health</div><div class="value"><div class="health-bar"><span class="health-dot ' + dotClass + '"></span> ' + esc(healthStatus) + '</div></div>';
945
+ html += '<div class="sub">' + esc(snap.connectionState || 'unknown') + '</div></div>';
946
+ var totalCalls = fMetrics.reduce(function(a, m) { return a + (m.totalCalls || 0); }, 0);
947
+ html += '<div class="stat-card"><div class="label">Function Calls</div><div class="value">' + totalCalls + '</div><div class="sub">' + fMetrics.length + ' functions tracked</div></div>';
948
+ if (cb) {
949
+ var cbClass = cb.state === 'closed' ? 'cb-closed' : cb.state === 'open' ? 'cb-open' : 'cb-half-open';
950
+ html += '<div class="stat-card"><div class="label">Circuit Breaker</div><div class="value"><span class="cb-indicator ' + cbClass + '">' + esc(cb.state) + '</span></div>';
951
+ html += '<div class="sub">' + (cb.failures || 0) + ' failures</div></div>';
952
+ }
953
+ html += '</div>';
954
+
955
+ if (snap.memory || snap.cpu) {
956
+ html += '<div class="card" style="margin-bottom:16px"><div class="card-title">System Resources</div>';
957
+ if (snap.memory) {
958
+ var heapUsed = Math.round((snap.memory.heapUsed || 0) / 1024 / 1024);
959
+ var heapTotal = Math.round((snap.memory.heapTotal || 0) / 1024 / 1024);
960
+ var rss = Math.round((snap.memory.rss || 0) / 1024 / 1024);
961
+ var heapPct = heapTotal > 0 ? Math.round((heapUsed / heapTotal) * 100) : 0;
962
+ var heapColor = heapPct > 80 ? 'var(--red)' : heapPct > 60 ? 'var(--yellow)' : 'var(--green)';
963
+ html += '<div class="gauge"><span class="gauge-label">Heap</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + heapPct + '%;background:' + heapColor + '"></div></div><span class="gauge-value">' + heapUsed + ' / ' + heapTotal + ' MB</span></div>';
964
+ html += '<div class="gauge"><span class="gauge-label">RSS</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + Math.min(100, Math.round(rss / 512 * 100)) + '%;background:var(--blue)"></div></div><span class="gauge-value">' + rss + ' MB</span></div>';
965
+ if (snap.memory.external) {
966
+ var ext = Math.round(snap.memory.external / 1024 / 1024);
967
+ html += '<div class="gauge"><span class="gauge-label">External</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + Math.min(100, Math.round(ext / 128 * 100)) + '%;background:var(--purple)"></div></div><span class="gauge-value">' + ext + ' MB</span></div>';
968
+ }
969
+ }
970
+ if (snap.cpu) {
971
+ var cpuPct = snap.cpu.percent || 0;
972
+ var cpuColor = cpuPct > 80 ? 'var(--red)' : cpuPct > 50 ? 'var(--yellow)' : 'var(--green)';
973
+ html += '<div class="gauge"><span class="gauge-label">CPU</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + Math.min(100, cpuPct) + '%;background:' + cpuColor + '"></div></div><span class="gauge-value">' + cpuPct.toFixed(1) + '%</span></div>';
974
+ }
975
+ if (snap.eventLoopLagMs !== undefined) {
976
+ var lag = snap.eventLoopLagMs;
977
+ var lagColor = lag > 100 ? 'var(--red)' : lag > 20 ? 'var(--yellow)' : 'var(--green)';
978
+ html += '<div class="gauge"><span class="gauge-label">Event Loop</span><div class="gauge-bar"><div class="gauge-fill" style="width:' + Math.min(100, lag) + '%;background:' + lagColor + '"></div></div><span class="gauge-value">' + lag.toFixed(1) + ' ms</span></div>';
979
+ }
980
+ if (snap.uptimeSeconds) {
981
+ var mins = Math.floor(snap.uptimeSeconds / 60);
982
+ var hrs = Math.floor(mins / 60);
983
+ var upStr = hrs > 0 ? hrs + 'h ' + (mins % 60) + 'm' : mins + 'm';
984
+ html += '<div style="font-size:10px;color:var(--ink-faint);margin-top:6px;font-family:var(--font-mono);letter-spacing:0.04em;">UPTIME: ' + upStr + '</div>';
985
+ }
986
+ html += '</div>';
987
+ }
988
+
989
+ if (snap.alerts && snap.alerts.length > 0) {
990
+ html += '<div class="card" style="margin-bottom:16px;border-color:var(--accent);border-width:2px;"><div class="card-title" style="color:var(--accent);border-bottom-color:var(--accent);">Alerts (' + snap.alerts.length + ')</div>';
991
+ snap.alerts.forEach(function(al) {
992
+ html += '<div style="font-size:12px;color:var(--accent);padding:4px 0;border-bottom:1px solid var(--border-light);font-family:var(--font-ui);">' + esc(al) + '</div>';
993
+ });
994
+ html += '</div>';
995
+ }
996
+
997
+ html += '<div class="two-col">';
998
+
999
+ html += '<div class="card"><div class="card-title">Recent Sessions</div>';
1000
+ if (d.sessions.length === 0) {
1001
+ html += '<div class="empty-state"><p>No sessions yet. Start a coding session with agentmemory hooks enabled.</p></div>';
1002
+ } else {
1003
+ var recent = d.sessions.slice().sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); }).slice(0, 5);
1004
+ html += '<table><tr><th>Project</th><th>Status</th><th>Obs</th><th>Started</th></tr>';
1005
+ recent.forEach(function(s) {
1006
+ var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
1007
+ html += '<tr><td style="color:var(--ink);font-weight:500;">' + esc(s.project ? s.project.split('/').pop() : s.id.slice(0,8)) + '</td>';
1008
+ html += '<td><span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></td>';
1009
+ html += '<td style="color:var(--ink-muted);font-family:var(--font-mono);font-size:12px;">' + (s.observationCount || 0) + '</td>';
1010
+ html += '<td style="font-family:var(--font-mono);font-size:11px;color:var(--ink-faint);">' + esc(shortTime(s.startedAt)) + '</td></tr>';
1011
+ });
1012
+ html += '</table>';
1013
+ }
1014
+ html += '</div>';
1015
+
1016
+ html += '<div class="card"><div class="card-title">Recent Activity</div>';
1017
+ if (d.recentAudit.length === 0) {
1018
+ html += '<div class="empty-state"><p>No activity recorded yet</p></div>';
1019
+ } else {
1020
+ d.recentAudit.forEach(function(a) {
1021
+ var badgeClass = OP_BADGES[a.operation] || 'badge-muted';
1022
+ html += '<div style="padding:6px 0;border-bottom:1px solid var(--border-light);font-size:13px;">';
1023
+ html += '<span class="badge ' + badgeClass + '">' + esc(a.operation) + '</span> ';
1024
+ if (a.functionId) html += '<span style="font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);">' + esc(a.functionId) + '</span> ';
1025
+ html += '<span style="color:var(--ink-faint);font-size:10px;font-family:var(--font-mono);">' + esc(shortTime(a.timestamp)) + '</span>';
1026
+ if (a.targetIds && a.targetIds.length) html += '<span style="font-size:10px;color:var(--ink-faint);margin-left:4px;">(' + a.targetIds.length + ' targets)</span>';
1027
+ html += '</div>';
1028
+ });
1029
+ }
1030
+ html += '</div>';
1031
+
1032
+ html += '</div>';
1033
+
1034
+ if (fMetrics.length > 0) {
1035
+ var sorted = fMetrics.slice().sort(function(a, b) { return (b.totalCalls || 0) - (a.totalCalls || 0); });
1036
+ html += '<div class="card" style="margin-top:16px"><div class="card-title">Function Metrics (OTel)</div>';
1037
+ html += '<table class="metric-table"><tr><th>Function</th><th style="text-align:right">Calls</th><th style="text-align:right">Success</th><th style="text-align:right">Fail</th><th style="text-align:right">Avg Latency</th><th style="text-align:right">Quality</th></tr>';
1038
+ sorted.forEach(function(m) {
1039
+ var successRate = m.totalCalls > 0 ? Math.round((m.successCount / m.totalCalls) * 100) : 0;
1040
+ var rateColor = successRate >= 95 ? 'var(--green)' : successRate >= 80 ? 'var(--yellow)' : 'var(--red)';
1041
+ var latencyColor = m.avgLatencyMs > 1000 ? 'var(--red)' : m.avgLatencyMs > 200 ? 'var(--yellow)' : 'var(--green)';
1042
+ html += '<tr>';
1043
+ html += '<td class="metric-fn">' + esc(m.functionId) + '</td>';
1044
+ html += '<td class="metric-num">' + m.totalCalls + '</td>';
1045
+ html += '<td class="metric-num" style="color:' + rateColor + '">' + m.successCount + ' (' + successRate + '%)</td>';
1046
+ html += '<td class="metric-num" style="color:' + (m.failureCount > 0 ? 'var(--red)' : 'var(--ink-faint)') + '">' + m.failureCount + '</td>';
1047
+ html += '<td class="metric-num" style="color:' + latencyColor + '">' + Math.round(m.avgLatencyMs) + ' ms</td>';
1048
+ html += '<td class="metric-num">' + (m.avgQualityScore > 0 ? m.avgQualityScore.toFixed(2) : '-') + '</td>';
1049
+ html += '</tr>';
1050
+ });
1051
+ html += '</table></div>';
1052
+ }
1053
+
1054
+ if (workers.length > 0) {
1055
+ html += '<div class="card" style="margin-top:16px"><div class="card-title">Workers</div>';
1056
+ workers.forEach(function(w) {
1057
+ var statusClass = w.status === 'running' ? 'running' : w.status === 'starting' ? 'starting' : 'stopped';
1058
+ html += '<div class="worker-row"><span class="worker-dot ' + statusClass + '"></span>';
1059
+ html += '<span style="color:var(--ink);font-weight:600;font-family:var(--font-ui);font-size:12px;">' + esc(w.name) + '</span>';
1060
+ html += '<span class="badge ' + (w.status === 'running' ? 'badge-green' : 'badge-muted') + '">' + esc(w.status) + '</span>';
1061
+ html += '<span style="font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);">' + esc(w.id) + '</span></div>';
1062
+ });
1063
+ html += '</div>';
1064
+ }
1065
+
1066
+ if (cb && cb.state !== 'closed') {
1067
+ html += '<div class="card" style="margin-top:16px;border-color:var(--accent);border-width:2px;"><div class="card-title" style="color:var(--accent);">Circuit Breaker Details</div>';
1068
+ html += '<div class="detail-row"><div class="dl">State</div><div class="dv"><span class="cb-indicator ' + (cb.state === 'open' ? 'cb-open' : 'cb-half-open') + '">' + esc(cb.state) + '</span></div></div>';
1069
+ html += '<div class="detail-row"><div class="dl">Failures</div><div class="dv" style="color:var(--accent);font-family:var(--font-mono);">' + (cb.failures || 0) + '</div></div>';
1070
+ if (cb.lastFailureAt) html += '<div class="detail-row"><div class="dl">Last Failure</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(cb.lastFailureAt)) + '</div></div>';
1071
+ if (cb.openedAt) html += '<div class="detail-row"><div class="dl">Opened At</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(cb.openedAt)) + '</div></div>';
1072
+ html += '</div>';
1073
+ }
1074
+
1075
+ var semFacts = d.semantic || [];
1076
+ var procItems = d.procedural || [];
1077
+ var relItems = d.relations || [];
1078
+
1079
+ html += '<hr class="section-rule">';
1080
+ html += '<div class="two-col">';
1081
+
1082
+ html += '<div class="card"><div class="card-title">Semantic Memory</div>';
1083
+ if (semFacts.length === 0) {
1084
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No semantic facts yet. Observations will be consolidated into semantic memories over time.</div>';
1085
+ } else {
1086
+ semFacts.slice(0, 5).forEach(function(f) {
1087
+ var conf = typeof f.confidence === 'number' ? Math.round(f.confidence * 100) : null;
1088
+ var str = typeof f.strength === 'number' ? Math.round(f.strength * 100) : null;
1089
+ var barColor = (str || 0) > 70 ? 'var(--green)' : (str || 0) > 40 ? 'var(--yellow)' : 'var(--red)';
1090
+ html += '<div class="memory-fact">';
1091
+ html += '<span style="color:var(--ink);">' + esc(f.fact || f.content || f.title || 'Fact') + '</span>';
1092
+ html += '<span style="display:flex;align-items:center;gap:6px;">';
1093
+ if (str !== null) html += '<span class="strength-bar" style="width:40px;"><span class="fill" style="width:' + str + '%;background:' + barColor + '"></span></span>';
1094
+ if (conf !== null) html += '<span style="font-size:10px;font-family:var(--font-mono);color:var(--ink-faint);">' + conf + '%</span>';
1095
+ html += '</span></div>';
1096
+ });
1097
+ }
1098
+ html += '</div>';
1099
+
1100
+ html += '<div class="card"><div class="card-title">Procedural Memory</div>';
1101
+ if (procItems.length === 0) {
1102
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No procedures yet. Repeated patterns will be extracted as procedures.</div>';
1103
+ } else {
1104
+ procItems.slice(0, 5).forEach(function(p) {
1105
+ html += '<div class="procedure-item">';
1106
+ html += '<div style="font-weight:600;color:var(--ink);font-family:var(--font-display);font-size:13px;">' + esc(p.name || p.title || 'Procedure') + '</div>';
1107
+ if (p.trigger || p.triggerCondition) html += '<div style="font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);margin-top:2px;">Trigger: ' + esc(p.trigger || p.triggerCondition) + '</div>';
1108
+ if (p.frequency) html += '<div style="font-size:11px;color:var(--ink-faint);margin-top:2px;">Freq: ' + p.frequency + '</div>';
1109
+ if (p.steps && p.steps.length > 0) {
1110
+ html += '<ol class="procedure-steps">';
1111
+ p.steps.slice(0, 4).forEach(function(s) { html += '<li>' + esc(typeof s === 'string' ? s : s.description || s.action || JSON.stringify(s)) + '</li>'; });
1112
+ if (p.steps.length > 4) html += '<li style="color:var(--ink-faint);font-style:italic;">+ ' + (p.steps.length - 4) + ' more...</li>';
1113
+ html += '</ol>';
1114
+ }
1115
+ html += '</div>';
1116
+ });
1117
+ }
1118
+ html += '</div>';
1119
+
1120
+ html += '</div>';
1121
+
1122
+ html += '<div class="card" style="margin-top:16px;"><div class="card-title">Consolidation Status</div>';
1123
+ html += '<div class="consolidation-row"><span class="cl">Semantic facts</span><span class="cv">' + semFacts.length + '</span></div>';
1124
+ html += '<div class="consolidation-row"><span class="cl">Procedures</span><span class="cv">' + procItems.length + '</span></div>';
1125
+ html += '<div class="consolidation-row"><span class="cl">Relations</span><span class="cv">' + relItems.length + '</span></div>';
1126
+ html += '</div>';
1127
+
1128
+ if (relItems.length > 0) {
1129
+ html += '<div class="card" style="margin-top:16px;"><div class="card-title">Memory Relations</div>';
1130
+ relItems.slice(0, 8).forEach(function(r) {
1131
+ var relType = r.type || r.relationType || 'related';
1132
+ var badgeClass = relType === 'supersedes' ? 'badge-red' : relType === 'extends' ? 'badge-green' : relType === 'contradicts' ? 'badge-yellow' : 'badge-muted';
1133
+ html += '<div style="padding:4px 0;border-bottom:1px solid var(--border-light);font-size:12px;display:flex;align-items:center;gap:6px;">';
1134
+ html += '<span style="font-family:var(--font-mono);color:var(--blue);font-size:11px;">' + esc(truncate(r.sourceId || r.fromId || '', 8)) + '</span>';
1135
+ html += '<span class="badge ' + badgeClass + '">' + esc(relType) + '</span>';
1136
+ html += '<span style="font-family:var(--font-mono);color:var(--blue);font-size:11px;">' + esc(truncate(r.targetId || r.toId || '', 8)) + '</span>';
1137
+ html += '</div>';
1138
+ });
1139
+ html += '</div>';
1140
+ }
1141
+
1142
+ html += '<div style="text-align:center;margin-top:20px;"><button class="btn btn-primary" onclick="refreshDashboard()">Refresh</button>';
1143
+ html += '<span style="font-size:10px;color:var(--ink-faint);margin-left:10px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.08em;">Auto-refresh 30s</span></div>';
1144
+
1145
+ el.innerHTML = html;
1146
+ }
1147
+
1148
+ var dashboardTimer = null;
1149
+ function refreshDashboard() {
1150
+ state.dashboard.loaded = false;
1151
+ loadDashboard();
1152
+ }
1153
+ function startDashboardAutoRefresh() {
1154
+ if (dashboardTimer) clearInterval(dashboardTimer);
1155
+ dashboardTimer = setInterval(function() {
1156
+ if (state.activeTab === 'dashboard') refreshDashboard();
1157
+ }, 30000);
1158
+ }
1159
+
1160
+ var graphSim = { nodes: [], edges: [], running: false, canvas: null, ctx: null, raf: null, panX: 0, panY: 0, zoom: 1, dragNode: null, mouseX: 0, mouseY: 0 };
1161
+
1162
+ async function loadGraph() {
1163
+ var el = document.getElementById('view-graph');
1164
+ el.innerHTML = '<div class="graph-container"><div class="graph-canvas-wrap"><canvas id="graph-canvas"></canvas><div class="graph-tooltip" id="graph-tooltip"></div></div><div class="graph-sidebar" id="graph-sidebar"></div></div>';
1165
+
1166
+ var results = await Promise.all([
1167
+ apiPost('graph/query', {}),
1168
+ apiGet('graph/stats')
1169
+ ]);
1170
+ var queryResult = results[0] || { nodes: [], edges: [] };
1171
+ state.graph.nodes = queryResult.nodes || [];
1172
+ state.graph.edges = queryResult.edges || [];
1173
+ state.graph.stats = results[1] || {};
1174
+
1175
+ if (state.graph.nodes.length === 0) {
1176
+ var sb = document.getElementById('graph-sidebar');
1177
+ if (sb) sb.innerHTML = '<h3>Graph</h3><p style="font-size:12px;color:var(--ink-faint);margin:8px 0;font-style:italic;">No graph data yet. Building from observations and memories...</p>';
1178
+ var buildResult = await apiPost('graph/build', {});
1179
+ if (buildResult && buildResult.success && buildResult.nodes > 0) {
1180
+ var freshResults = await Promise.all([
1181
+ apiPost('graph/query', {}),
1182
+ apiGet('graph/stats')
1183
+ ]);
1184
+ var freshQuery = freshResults[0] || { nodes: [], edges: [] };
1185
+ state.graph.nodes = freshQuery.nodes || [];
1186
+ state.graph.edges = freshQuery.edges || [];
1187
+ state.graph.stats = freshResults[1] || {};
1188
+ }
1189
+ }
1190
+
1191
+ state.graph.loaded = true;
1192
+ var types = {};
1193
+ state.graph.nodes.forEach(function(n) { types[n.type] = true; });
1194
+ state.graph.filters = types;
1195
+
1196
+ renderGraphSidebar();
1197
+ initGraph();
1198
+ }
1199
+
1200
+ var NODE_SHAPES = {
1201
+ file: 'rect', function: 'circle', concept: 'circle', error: 'diamond',
1202
+ decision: 'diamond', pattern: 'circle', library: 'hexagon', person: 'circle'
1203
+ };
1204
+ var graphSearchTerm = '';
1205
+
1206
+ function renderGraphSidebar() {
1207
+ var sb = document.getElementById('graph-sidebar');
1208
+ if (!sb) return;
1209
+ var gs = state.graph.stats || {};
1210
+ var nodeCount = gs.nodes !== undefined ? gs.nodes : (gs.nodeCount || state.graph.nodes.length);
1211
+ var edgeCount = gs.edges !== undefined ? gs.edges : (gs.edgeCount || state.graph.edges.length);
1212
+
1213
+ var html = '<input type="text" class="graph-search" id="graph-search" placeholder="Search nodes...">';
1214
+
1215
+ html += '<h3>Graph Stats</h3>';
1216
+ html += '<div style="display:flex;gap:16px;margin-bottom:12px;">';
1217
+ html += '<div><span style="font-size:24px;font-weight:900;font-family:var(--font-display);color:var(--ink);">' + nodeCount + '</span><div style="font-size:9px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:0.1em;font-family:var(--font-ui);font-weight:600;">Nodes</div></div>';
1218
+ html += '<div><span style="font-size:24px;font-weight:900;font-family:var(--font-display);color:var(--ink);">' + edgeCount + '</span><div style="font-size:9px;color:var(--ink-muted);text-transform:uppercase;letter-spacing:0.1em;font-family:var(--font-ui);font-weight:600;">Edges</div></div>';
1219
+ html += '</div>';
1220
+
1221
+ html += '<h3 style="margin-top:16px;">Filter by Type</h3>';
1222
+ Object.keys(state.graph.filters).forEach(function(type) {
1223
+ var color = NODE_COLORS[type] || '#666666';
1224
+ html += '<label class="filter-item"><input type="checkbox" checked data-type="' + esc(type) + '"><span class="filter-dot" style="background:' + color + '"></span>' + esc(type) + '</label>';
1225
+ });
1226
+
1227
+ html += '<div class="graph-legend"><h3>Legend</h3>';
1228
+ var shapeLabels = { rect: '&#9645;', circle: '&#9679;', diamond: '&#9670;', hexagon: '&#11042;' };
1229
+ var shownShapes = {};
1230
+ Object.keys(NODE_COLORS).forEach(function(type) {
1231
+ var shape = NODE_SHAPES[type] || 'circle';
1232
+ var color = NODE_COLORS[type];
1233
+ var key = type;
1234
+ if (shownShapes[key]) return;
1235
+ shownShapes[key] = true;
1236
+ html += '<div class="graph-legend-item"><span class="graph-legend-shape" style="color:' + color + ';font-size:14px;">' + (shapeLabels[shape] || '&#9679;') + '</span><span>' + esc(type) + '</span></div>';
1237
+ });
1238
+ html += '</div>';
1239
+
1240
+ html += '<button class="btn" style="margin-top:14px;width:100%;" onclick="rebuildGraph()">Rebuild Graph</button>';
1241
+ html += '<div id="selected-node-panel"></div>';
1242
+ sb.innerHTML = html;
1243
+
1244
+ sb.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
1245
+ cb.addEventListener('change', function() {
1246
+ state.graph.filters[this.dataset.type] = this.checked;
1247
+ renderGraph();
1248
+ });
1249
+ });
1250
+
1251
+ var searchInput = document.getElementById('graph-search');
1252
+ if (searchInput) {
1253
+ searchInput.addEventListener('input', debounce(function() {
1254
+ graphSearchTerm = this.value.toLowerCase();
1255
+ renderGraph();
1256
+ }, 150));
1257
+ }
1258
+ }
1259
+
1260
+ function initGraph() {
1261
+ var canvas = document.getElementById('graph-canvas');
1262
+ if (!canvas) return;
1263
+ graphSim.canvas = canvas;
1264
+ graphSim.ctx = canvas.getContext('2d');
1265
+
1266
+ function resize() {
1267
+ var r = canvas.parentElement.getBoundingClientRect();
1268
+ canvas.width = r.width * window.devicePixelRatio;
1269
+ canvas.height = r.height * window.devicePixelRatio;
1270
+ canvas.style.width = r.width + 'px';
1271
+ canvas.style.height = r.height + 'px';
1272
+ graphSim.ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
1273
+ }
1274
+ resize();
1275
+ window.addEventListener('resize', resize);
1276
+
1277
+ var cw = canvas.width / window.devicePixelRatio;
1278
+ var ch = canvas.height / window.devicePixelRatio;
1279
+ graphSim.panX = cw / 2;
1280
+ graphSim.panY = ch / 2;
1281
+
1282
+ var edgeMap = {};
1283
+ state.graph.edges.forEach(function(e) {
1284
+ edgeMap[e.sourceNodeId] = (edgeMap[e.sourceNodeId] || 0) + 1;
1285
+ edgeMap[e.targetNodeId] = (edgeMap[e.targetNodeId] || 0) + 1;
1286
+ });
1287
+
1288
+ graphSim.nodes = state.graph.nodes.map(function(n, i) {
1289
+ var angle = (2 * Math.PI * i) / Math.max(state.graph.nodes.length, 1);
1290
+ var radius = Math.min(cw, ch) * 0.3;
1291
+ var deg = edgeMap[n.id] || 0;
1292
+ return {
1293
+ id: n.id, type: n.type, name: n.name, properties: n.properties,
1294
+ x: Math.cos(angle) * radius + (Math.random() - 0.5) * 50,
1295
+ y: Math.sin(angle) * radius + (Math.random() - 0.5) * 50,
1296
+ vx: 0, vy: 0,
1297
+ r: Math.max(6, Math.min(20, 6 + deg * 2))
1298
+ };
1299
+ });
1300
+ graphSim.edges = state.graph.edges.slice();
1301
+ graphSim.running = true;
1302
+ graphSim.dragNode = null;
1303
+
1304
+ setupGraphInteraction(canvas);
1305
+ runSimulation();
1306
+ }
1307
+
1308
+ function setupGraphInteraction(canvas) {
1309
+ var isPanning = false;
1310
+ var lastMX = 0, lastMY = 0;
1311
+
1312
+ function canvasCoords(e) {
1313
+ var rect = canvas.getBoundingClientRect();
1314
+ return {
1315
+ x: (e.clientX - rect.left - graphSim.panX) / graphSim.zoom,
1316
+ y: (e.clientY - rect.top - graphSim.panY) / graphSim.zoom
1317
+ };
1318
+ }
1319
+ function findNode(cx, cy) {
1320
+ for (var i = graphSim.nodes.length - 1; i >= 0; i--) {
1321
+ var n = graphSim.nodes[i];
1322
+ if (!state.graph.filters[n.type]) continue;
1323
+ var dx = n.x - cx, dy = n.y - cy;
1324
+ if (dx * dx + dy * dy < n.r * n.r + 25) return n;
1325
+ }
1326
+ return null;
1327
+ }
1328
+
1329
+ canvas.addEventListener('mousedown', function(e) {
1330
+ var c = canvasCoords(e);
1331
+ var node = findNode(c.x, c.y);
1332
+ if (node) {
1333
+ graphSim.dragNode = node;
1334
+ } else {
1335
+ isPanning = true;
1336
+ }
1337
+ lastMX = e.clientX;
1338
+ lastMY = e.clientY;
1339
+ });
1340
+ canvas.addEventListener('mousemove', function(e) {
1341
+ var dx = e.clientX - lastMX;
1342
+ var dy = e.clientY - lastMY;
1343
+ if (graphSim.dragNode) {
1344
+ graphSim.dragNode.x += dx / graphSim.zoom;
1345
+ graphSim.dragNode.y += dy / graphSim.zoom;
1346
+ graphSim.dragNode.vx = 0;
1347
+ graphSim.dragNode.vy = 0;
1348
+ } else if (isPanning) {
1349
+ graphSim.panX += dx;
1350
+ graphSim.panY += dy;
1351
+ }
1352
+ lastMX = e.clientX;
1353
+ lastMY = e.clientY;
1354
+ graphSim.mouseX = e.clientX;
1355
+ graphSim.mouseY = e.clientY;
1356
+
1357
+ var c = canvasCoords(e);
1358
+ var hoverNode = findNode(c.x, c.y);
1359
+ var tooltip = document.getElementById('graph-tooltip');
1360
+ if (tooltip) {
1361
+ if (hoverNode && !graphSim.dragNode && !isPanning) {
1362
+ var conns = graphSim.edges.filter(function(ed) { return ed.sourceNodeId === hoverNode.id || ed.targetNodeId === hoverNode.id; }).length;
1363
+ var ttHtml = '<div class="tt-name">' + esc(hoverNode.name) + '</div>';
1364
+ ttHtml += '<div class="tt-type" style="color:' + (NODE_COLORS[hoverNode.type] || '#666') + '">' + esc(hoverNode.type) + '</div>';
1365
+ if (hoverNode.properties) {
1366
+ var propKeys = Object.keys(hoverNode.properties).slice(0, 3);
1367
+ propKeys.forEach(function(k) {
1368
+ ttHtml += '<div class="tt-prop">' + esc(k) + ': ' + esc(truncate(String(hoverNode.properties[k]), 30)) + '</div>';
1369
+ });
1370
+ }
1371
+ ttHtml += '<div class="tt-conns">' + conns + ' connection' + (conns !== 1 ? 's' : '') + '</div>';
1372
+ tooltip.innerHTML = ttHtml;
1373
+ var rect = canvas.getBoundingClientRect();
1374
+ tooltip.style.left = (e.clientX - rect.left + 12) + 'px';
1375
+ tooltip.style.top = (e.clientY - rect.top + 12) + 'px';
1376
+ tooltip.classList.add('visible');
1377
+ canvas.style.cursor = 'pointer';
1378
+ } else {
1379
+ tooltip.classList.remove('visible');
1380
+ canvas.style.cursor = graphSim.dragNode || isPanning ? 'grabbing' : 'grab';
1381
+ }
1382
+ }
1383
+ });
1384
+ canvas.addEventListener('mouseup', function(e) {
1385
+ if (graphSim.dragNode && !isPanning) {
1386
+ selectGraphNode(graphSim.dragNode);
1387
+ }
1388
+ graphSim.dragNode = null;
1389
+ isPanning = false;
1390
+ });
1391
+ canvas.addEventListener('wheel', function(e) {
1392
+ e.preventDefault();
1393
+ var factor = e.deltaY > 0 ? 0.9 : 1.1;
1394
+ graphSim.zoom = Math.max(0.1, Math.min(5, graphSim.zoom * factor));
1395
+ }, { passive: false });
1396
+ }
1397
+
1398
+ function selectGraphNode(simNode) {
1399
+ state.graph.selectedNode = simNode;
1400
+ var panel = document.getElementById('selected-node-panel');
1401
+ if (!panel) return;
1402
+ var color = NODE_COLORS[simNode.type] || '#666666';
1403
+ var html = '<div class="selected-node-info">';
1404
+ html += '<h4 style="color:' + color + '">' + esc(simNode.name) + '</h4>';
1405
+ html += '<div class="prop">Type: ' + esc(simNode.type) + '</div>';
1406
+ if (simNode.properties) {
1407
+ Object.keys(simNode.properties).forEach(function(k) {
1408
+ html += '<div class="prop">' + esc(k) + ': ' + esc(truncate(simNode.properties[k], 50)) + '</div>';
1409
+ });
1410
+ }
1411
+ var conns = graphSim.edges.filter(function(e) { return e.sourceNodeId === simNode.id || e.targetNodeId === simNode.id; }).length;
1412
+ html += '<div class="prop">Connections: ' + conns + '</div>';
1413
+ html += '<button class="btn btn-primary" style="margin-top:8px;width:100%;" onclick="expandNode(\'' + esc(simNode.id) + '\')">Expand neighbors</button>';
1414
+ html += '</div>';
1415
+ panel.innerHTML = html;
1416
+ }
1417
+
1418
+ async function expandNode(nodeId) {
1419
+ var result = await apiPost('graph/query', { startNodeId: nodeId, maxDepth: 1 });
1420
+ if (!result) return;
1421
+ var existingIds = {};
1422
+ graphSim.nodes.forEach(function(n) { existingIds[n.id] = true; });
1423
+ var parentNode = graphSim.nodes.find(function(n) { return n.id === nodeId; });
1424
+ var px = parentNode ? parentNode.x : 0;
1425
+ var py = parentNode ? parentNode.y : 0;
1426
+
1427
+ (result.nodes || []).forEach(function(n) {
1428
+ if (!existingIds[n.id]) {
1429
+ state.graph.nodes.push(n);
1430
+ if (!state.graph.filters.hasOwnProperty(n.type)) state.graph.filters[n.type] = true;
1431
+ var angle = Math.random() * Math.PI * 2;
1432
+ graphSim.nodes.push({
1433
+ id: n.id, type: n.type, name: n.name, properties: n.properties,
1434
+ x: px + Math.cos(angle) * 80,
1435
+ y: py + Math.sin(angle) * 80,
1436
+ vx: 0, vy: 0, r: 8
1437
+ });
1438
+ }
1439
+ });
1440
+
1441
+ var existingEdges = {};
1442
+ graphSim.edges.forEach(function(e) { existingEdges[e.id] = true; });
1443
+ (result.edges || []).forEach(function(e) {
1444
+ if (!existingEdges[e.id]) {
1445
+ state.graph.edges.push(e);
1446
+ graphSim.edges.push(e);
1447
+ }
1448
+ });
1449
+ renderGraphSidebar();
1450
+ }
1451
+
1452
+ function runSimulation() {
1453
+ if (!graphSim.running) return;
1454
+ var nodes = graphSim.nodes;
1455
+ var edges = graphSim.edges;
1456
+ var nodeCount = nodes.length;
1457
+ var damping = 0.9;
1458
+ var repulsion = nodeCount > 100 ? 2000 : nodeCount > 50 ? 1200 : 800;
1459
+ var attraction = nodeCount > 100 ? 0.002 : 0.005;
1460
+ var centerGravity = nodeCount > 100 ? 0.005 : 0.01;
1461
+
1462
+ var nodeMap = {};
1463
+ nodes.forEach(function(n) { nodeMap[n.id] = n; });
1464
+
1465
+ for (var i = 0; i < nodes.length; i++) {
1466
+ if (graphSim.dragNode === nodes[i]) continue;
1467
+ var n = nodes[i];
1468
+ var fx = 0, fy = 0;
1469
+ for (var j = 0; j < nodes.length; j++) {
1470
+ if (i === j) continue;
1471
+ var dx = n.x - nodes[j].x;
1472
+ var dy = n.y - nodes[j].y;
1473
+ var dist = Math.sqrt(dx * dx + dy * dy) || 1;
1474
+ var force = repulsion / (dist * dist);
1475
+ fx += (dx / dist) * force;
1476
+ fy += (dy / dist) * force;
1477
+ }
1478
+ fx -= n.x * centerGravity;
1479
+ fy -= n.y * centerGravity;
1480
+ n.vx = (n.vx + fx) * damping;
1481
+ n.vy = (n.vy + fy) * damping;
1482
+ }
1483
+
1484
+ edges.forEach(function(e) {
1485
+ var s = nodeMap[e.sourceNodeId];
1486
+ var t = nodeMap[e.targetNodeId];
1487
+ if (!s || !t) return;
1488
+ var dx = t.x - s.x;
1489
+ var dy = t.y - s.y;
1490
+ var dist = Math.sqrt(dx * dx + dy * dy) || 1;
1491
+ var f = (dist - 100) * attraction;
1492
+ var fx = (dx / dist) * f;
1493
+ var fy = (dy / dist) * f;
1494
+ if (graphSim.dragNode !== s) { s.vx += fx; s.vy += fy; }
1495
+ if (graphSim.dragNode !== t) { t.vx -= fx; t.vy -= fy; }
1496
+ });
1497
+
1498
+ nodes.forEach(function(n) {
1499
+ if (graphSim.dragNode === n) return;
1500
+ n.x += n.vx;
1501
+ n.y += n.vy;
1502
+ });
1503
+
1504
+ renderGraph();
1505
+ graphSim.raf = requestAnimationFrame(runSimulation);
1506
+ }
1507
+
1508
+ async function rebuildGraph() {
1509
+ var sb = document.getElementById('graph-sidebar');
1510
+ if (sb) sb.innerHTML = '<h3>Graph</h3><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Rebuilding graph from observations...</p>';
1511
+ await apiPost('graph/build', {});
1512
+ state.graph.loaded = false;
1513
+ loadGraph();
1514
+ }
1515
+
1516
+ function drawNodeShape(ctx, x, y, r, type) {
1517
+ var shape = NODE_SHAPES[type] || 'circle';
1518
+ switch(shape) {
1519
+ case 'rect':
1520
+ ctx.beginPath();
1521
+ ctx.rect(x - r, y - r * 0.75, r * 2, r * 1.5);
1522
+ break;
1523
+ case 'diamond':
1524
+ ctx.beginPath();
1525
+ ctx.moveTo(x, y - r);
1526
+ ctx.lineTo(x + r, y);
1527
+ ctx.lineTo(x, y + r);
1528
+ ctx.lineTo(x - r, y);
1529
+ ctx.closePath();
1530
+ break;
1531
+ case 'hexagon':
1532
+ ctx.beginPath();
1533
+ for (var i = 0; i < 6; i++) {
1534
+ var angle = (Math.PI / 3) * i - Math.PI / 2;
1535
+ var hx = x + r * Math.cos(angle);
1536
+ var hy = y + r * Math.sin(angle);
1537
+ if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
1538
+ }
1539
+ ctx.closePath();
1540
+ break;
1541
+ default:
1542
+ ctx.beginPath();
1543
+ ctx.arc(x, y, r, 0, Math.PI * 2);
1544
+ break;
1545
+ }
1546
+ }
1547
+
1548
+ function renderGraph() {
1549
+ var ctx = graphSim.ctx;
1550
+ var canvas = graphSim.canvas;
1551
+ if (!ctx || !canvas) return;
1552
+ var w = canvas.width / window.devicePixelRatio;
1553
+ var h = canvas.height / window.devicePixelRatio;
1554
+
1555
+ ctx.clearRect(0, 0, w, h);
1556
+ ctx.save();
1557
+ ctx.translate(graphSim.panX, graphSim.panY);
1558
+ ctx.scale(graphSim.zoom, graphSim.zoom);
1559
+
1560
+ var nodeMap = {};
1561
+ graphSim.nodes.forEach(function(n) { nodeMap[n.id] = n; });
1562
+
1563
+ var searchActive = graphSearchTerm.length > 0;
1564
+ var totalVisible = graphSim.nodes.filter(function(n) { return state.graph.filters[n.type]; }).length;
1565
+ var isDense = totalVisible > 40;
1566
+ var labelZoomThreshold = isDense ? 1.5 : 0.5;
1567
+ var edgeLabelZoomThreshold = isDense ? 2.5 : 1.2;
1568
+ var selectedId = state.graph.selectedNode ? state.graph.selectedNode.id : null;
1569
+
1570
+ graphSim.edges.forEach(function(e) {
1571
+ var s = nodeMap[e.sourceNodeId];
1572
+ var t = nodeMap[e.targetNodeId];
1573
+ if (!s || !t) return;
1574
+ if (!state.graph.filters[s.type] || !state.graph.filters[t.type]) return;
1575
+
1576
+ var edgeDimmed = searchActive && !(s.name.toLowerCase().includes(graphSearchTerm) || t.name.toLowerCase().includes(graphSearchTerm));
1577
+ var isSelectedEdge = selectedId && (e.sourceNodeId === selectedId || e.targetNodeId === selectedId);
1578
+ var weight = typeof e.weight === 'number' ? e.weight : 0.5;
1579
+ var lineWidth = isSelectedEdge ? 2 + weight * 2 : 1 + weight * 1.5;
1580
+
1581
+ var dx = t.x - s.x;
1582
+ var dy = t.y - s.y;
1583
+ var len = Math.sqrt(dx * dx + dy * dy) || 1;
1584
+ var curveOffset = isDense ? 12 : 18;
1585
+ var offsetX = -dy / len * curveOffset;
1586
+ var offsetY = dx / len * curveOffset;
1587
+ var cpx = (s.x + t.x) / 2 + offsetX;
1588
+ var cpy = (s.y + t.y) / 2 + offsetY;
1589
+
1590
+ ctx.beginPath();
1591
+ ctx.moveTo(s.x, s.y);
1592
+ ctx.quadraticCurveTo(cpx, cpy, t.x, t.y);
1593
+ var edgeAlpha = edgeDimmed ? 0.08 : isSelectedEdge ? 0.5 : (isDense ? 0.1 : 0.2);
1594
+ ctx.strokeStyle = 'rgba(17,17,17,' + edgeAlpha + ')';
1595
+ ctx.lineWidth = lineWidth;
1596
+ ctx.stroke();
1597
+
1598
+ if (!isDense || isSelectedEdge) {
1599
+ var arrowAngle = Math.atan2(t.y - cpy, t.x - cpx);
1600
+ var arrowLen = 5 + lineWidth;
1601
+ ctx.beginPath();
1602
+ ctx.moveTo(t.x - t.r * Math.cos(arrowAngle), t.y - t.r * Math.sin(arrowAngle));
1603
+ ctx.lineTo(t.x - (t.r + arrowLen) * Math.cos(arrowAngle - 0.3), t.y - (t.r + arrowLen) * Math.sin(arrowAngle - 0.3));
1604
+ ctx.lineTo(t.x - (t.r + arrowLen) * Math.cos(arrowAngle + 0.3), t.y - (t.r + arrowLen) * Math.sin(arrowAngle + 0.3));
1605
+ ctx.closePath();
1606
+ ctx.fillStyle = 'rgba(17,17,17,' + (edgeDimmed ? 0.08 : isSelectedEdge ? 0.5 : 0.25) + ')';
1607
+ ctx.fill();
1608
+ }
1609
+
1610
+ var showEdgeLabel = e.type && !edgeDimmed && (isSelectedEdge ? graphSim.zoom > 0.6 : graphSim.zoom > edgeLabelZoomThreshold);
1611
+ if (showEdgeLabel) {
1612
+ ctx.save();
1613
+ ctx.fillStyle = 'rgba(80,80,80,0.8)';
1614
+ ctx.font = Math.round(7 / Math.max(graphSim.zoom, 0.8)) + 'px Inter, sans-serif';
1615
+ ctx.textAlign = 'center';
1616
+ ctx.fillText(e.type, cpx, cpy - 4);
1617
+ ctx.restore();
1618
+ }
1619
+ });
1620
+
1621
+ graphSim.nodes.forEach(function(n) {
1622
+ if (!state.graph.filters[n.type]) return;
1623
+ var color = NODE_COLORS[n.type] || '#666666';
1624
+ var isSelected = selectedId === n.id;
1625
+ var matchesSearch = !searchActive || n.name.toLowerCase().includes(graphSearchTerm);
1626
+
1627
+ ctx.save();
1628
+ if (matchesSearch && (isSelected || !searchActive)) {
1629
+ ctx.shadowColor = color;
1630
+ ctx.shadowBlur = isSelected ? 14 : (isDense ? 3 : 6);
1631
+ }
1632
+
1633
+ drawNodeShape(ctx, n.x, n.y, n.r, n.type);
1634
+ ctx.fillStyle = color;
1635
+ ctx.globalAlpha = matchesSearch ? 0.9 : 0.15;
1636
+ ctx.fill();
1637
+ ctx.globalAlpha = 1;
1638
+ ctx.restore();
1639
+
1640
+ if (isSelected) {
1641
+ drawNodeShape(ctx, n.x, n.y, n.r, n.type);
1642
+ ctx.strokeStyle = '#111111';
1643
+ ctx.lineWidth = 2.5;
1644
+ ctx.stroke();
1645
+ } else if (searchActive && matchesSearch) {
1646
+ drawNodeShape(ctx, n.x, n.y, n.r, n.type);
1647
+ ctx.strokeStyle = '#CC0000';
1648
+ ctx.lineWidth = 2;
1649
+ ctx.stroke();
1650
+ }
1651
+
1652
+ var showLabel = matchesSearch && (
1653
+ isSelected ||
1654
+ (searchActive && matchesSearch) ||
1655
+ (!isDense && graphSim.zoom > labelZoomThreshold) ||
1656
+ (isDense && graphSim.zoom > labelZoomThreshold && n.r > 10)
1657
+ );
1658
+ if (showLabel) {
1659
+ ctx.fillStyle = isSelected ? '#111111' : '#444444';
1660
+ ctx.font = (isSelected ? 'bold ' : '') + Math.round(9 / Math.max(graphSim.zoom, 0.6)) + 'px Inter, sans-serif';
1661
+ ctx.textAlign = 'center';
1662
+ ctx.fillText(truncate(n.name, 16), n.x, n.y + n.r + 12);
1663
+ }
1664
+ });
1665
+
1666
+ ctx.restore();
1667
+
1668
+ if (graphSim.nodes.length === 0) {
1669
+ ctx.fillStyle = '#999999';
1670
+ ctx.font = '14px Lora, Georgia, serif';
1671
+ ctx.textAlign = 'center';
1672
+ ctx.fillText('No graph data yet.', w / 2, h / 2 - 16);
1673
+ ctx.font = '12px Inter, sans-serif';
1674
+ ctx.fillText('Set AGENTMEMORY_GRAPH_ENABLED=true to enable knowledge graph extraction.', w / 2, h / 2 + 8);
1675
+ }
1676
+ }
1677
+
1678
+ async function loadMemories() {
1679
+ var el = document.getElementById('view-memories');
1680
+ el.innerHTML = '<div class="loading">Loading memories...</div>';
1681
+ var result = await apiGet('memories?latest=true');
1682
+ state.memories.items = (result && result.memories) || [];
1683
+ state.memories.loaded = true;
1684
+ renderMemories();
1685
+ }
1686
+
1687
+ function renderMemories() {
1688
+ var el = document.getElementById('view-memories');
1689
+ var items = state.memories.items;
1690
+ var search = state.memories.search.toLowerCase();
1691
+ var typeFilter = state.memories.typeFilter;
1692
+
1693
+ var filtered = items.filter(function(m) {
1694
+ if (typeFilter && m.type !== typeFilter) return false;
1695
+ if (search && !(m.title || '').toLowerCase().includes(search) && !(m.content || '').toLowerCase().includes(search)) return false;
1696
+ return true;
1697
+ });
1698
+
1699
+ var types = {};
1700
+ items.forEach(function(m) { types[m.type] = true; });
1701
+ var typeOptions = Object.keys(types).sort();
1702
+
1703
+ var html = '<div class="toolbar">';
1704
+ html += '<input type="text" id="mem-search" placeholder="Search memories..." value="' + esc(state.memories.search) + '">';
1705
+ html += '<select id="mem-type-filter"><option value="">All types</option>';
1706
+ typeOptions.forEach(function(t) {
1707
+ html += '<option value="' + esc(t) + '"' + (typeFilter === t ? ' selected' : '') + '>' + esc(t) + '</option>';
1708
+ });
1709
+ html += '</select></div>';
1710
+
1711
+ if (filtered.length === 0) {
1712
+ html += '<div class="empty-state"><div class="empty-icon">&#128218;</div><p>No memories found</p></div>';
1713
+ } else {
1714
+ html += '<table><tr><th>Title</th><th>Type</th><th>Strength</th><th>Version</th><th>Updated</th><th>Actions</th></tr>';
1715
+ filtered.forEach(function(m) {
1716
+ var badgeClass = TYPE_BADGES[m.type] || 'badge-muted';
1717
+ var strength = Math.round((m.strength || 0) * 100);
1718
+ var barColor = strength > 70 ? 'var(--green)' : strength > 40 ? 'var(--yellow)' : 'var(--red)';
1719
+ html += '<tr>';
1720
+ html += '<td><span style="color:var(--ink);font-weight:600;">' + esc(truncate(m.title, 50)) + '</span>';
1721
+ html += '<br><span style="font-size:11px;color:var(--ink-faint);font-style:italic;">' + esc(truncate(m.content, 80)) + '</span></td>';
1722
+ html += '<td><span class="badge ' + badgeClass + '">' + esc(m.type) + '</span></td>';
1723
+ html += '<td><div class="strength-bar"><div class="fill" style="width:' + strength + '%;background:' + barColor + '"></div></div> <span style="font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);">' + strength + '%</span></td>';
1724
+ html += '<td style="color:var(--ink-muted);font-family:var(--font-mono);font-size:12px;">v' + (m.version || 1) + '</td>';
1725
+ html += '<td style="font-size:11px;color:var(--ink-faint);font-family:var(--font-mono);">' + esc(formatTime(m.updatedAt)) + '</td>';
1726
+ html += '<td><button class="btn btn-danger" style="font-size:9px;padding:2px 8px;" onclick="deleteMemory(\'' + esc(m.id) + '\',\'' + esc((m.title || '').replace(/'/g, '')) + '\')">Delete</button></td>';
1727
+ html += '</tr>';
1728
+ });
1729
+ html += '</table>';
1730
+ }
1731
+
1732
+ el.innerHTML = html;
1733
+
1734
+ var searchInput = document.getElementById('mem-search');
1735
+ if (searchInput) {
1736
+ searchInput.addEventListener('input', debounce(function() {
1737
+ state.memories.search = this.value;
1738
+ renderMemories();
1739
+ }, 200));
1740
+ }
1741
+ var typeSelect = document.getElementById('mem-type-filter');
1742
+ if (typeSelect) {
1743
+ typeSelect.addEventListener('change', function() {
1744
+ state.memories.typeFilter = this.value;
1745
+ renderMemories();
1746
+ });
1747
+ }
1748
+ }
1749
+
1750
+ function deleteMemory(id, title) {
1751
+ var modal = document.getElementById('modal');
1752
+ var overlay = document.getElementById('modal-overlay');
1753
+ modal.innerHTML = '<h3>Delete Memory</h3><p>Are you sure you want to delete "' + esc(title) + '"? This action cannot be undone.</p><div class="modal-actions"><button class="btn" onclick="closeModal()">Cancel</button><button class="btn btn-danger" onclick="confirmDeleteMemory(\'' + esc(id) + '\')">Delete</button></div>';
1754
+ overlay.classList.add('open');
1755
+ }
1756
+
1757
+ async function confirmDeleteMemory(id) {
1758
+ closeModal();
1759
+ await apiDelete('governance/memories', { memoryIds: [id], reason: 'Deleted via viewer' });
1760
+ state.memories.loaded = false;
1761
+ loadMemories();
1762
+ }
1763
+
1764
+ function closeModal() {
1765
+ document.getElementById('modal-overlay').classList.remove('open');
1766
+ }
1767
+
1768
+ async function loadTimeline() {
1769
+ var el = document.getElementById('view-timeline');
1770
+ el.innerHTML = '<div class="loading">Loading timeline...</div>';
1771
+ var sessResult = await apiGet('sessions');
1772
+ var sessions = (sessResult && sessResult.sessions) || [];
1773
+ state.timeline.loaded = true;
1774
+
1775
+ if (sessions.length > 0 && !state.timeline.sessionId) {
1776
+ var sorted = sessions.slice().sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); });
1777
+ state.timeline.sessionId = sorted[0].id;
1778
+ }
1779
+
1780
+ renderTimelineToolbar(sessions);
1781
+ if (state.timeline.sessionId) await loadObservations();
1782
+ }
1783
+
1784
+ function renderTimelineToolbar(sessions) {
1785
+ var el = document.getElementById('view-timeline');
1786
+ var html = '<div class="toolbar">';
1787
+ html += '<select id="tl-session"><option value="">Select session</option>';
1788
+ sessions.sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); }).forEach(function(s) {
1789
+ var label = (s.project ? s.project.split('/').pop() : s.id.slice(0,8)) + ' (' + s.id.slice(0,8) + ')';
1790
+ html += '<option value="' + esc(s.id) + '"' + (state.timeline.sessionId === s.id ? ' selected' : '') + '>' + esc(label) + '</option>';
1791
+ });
1792
+ html += '</select>';
1793
+ html += '<select id="tl-importance"><option value="0">All importance</option>';
1794
+ for (var i = 1; i <= 9; i++) {
1795
+ html += '<option value="' + i + '"' + (state.timeline.minImportance === i ? ' selected' : '') + '>&ge; ' + i + '</option>';
1796
+ }
1797
+ html += '</select></div>';
1798
+ html += '<div id="tl-content"></div>';
1799
+ el.innerHTML = html;
1800
+
1801
+ document.getElementById('tl-session').addEventListener('change', function() {
1802
+ state.timeline.sessionId = this.value;
1803
+ state.timeline.page = 0;
1804
+ loadObservations();
1805
+ });
1806
+ document.getElementById('tl-importance').addEventListener('change', function() {
1807
+ state.timeline.minImportance = parseInt(this.value);
1808
+ renderObservations();
1809
+ });
1810
+ }
1811
+
1812
+ async function loadObservations() {
1813
+ var content = document.getElementById('tl-content');
1814
+ if (!content) return;
1815
+ if (!state.timeline.sessionId) {
1816
+ content.innerHTML = '<div class="empty-state"><div class="empty-icon">&#128337;</div><p>Select a session to view observations</p></div>';
1817
+ return;
1818
+ }
1819
+ content.innerHTML = '<div class="loading">Loading observations...</div>';
1820
+ var result = await apiGet('observations?sessionId=' + encodeURIComponent(state.timeline.sessionId));
1821
+ state.timeline.observations = (result && result.observations) || [];
1822
+ renderObservations();
1823
+ }
1824
+
1825
+ var tlTypeFilter = '';
1826
+
1827
+ function renderObservations() {
1828
+ var content = document.getElementById('tl-content');
1829
+ if (!content) return;
1830
+ var obs = state.timeline.observations;
1831
+ var minImp = state.timeline.minImportance;
1832
+ var filtered = minImp > 0 ? obs.filter(function(o) { return (o.importance || 0) >= minImp; }) : obs;
1833
+
1834
+ var TOOL_TYPE_MAP = { Read: 'file_read', Write: 'file_write', Edit: 'file_edit', Bash: 'command_run', Grep: 'search', Glob: 'search', WebFetch: 'web_fetch', WebSearch: 'web_fetch', AskUserQuestion: 'conversation', Task: 'subagent' };
1835
+
1836
+ var typeCounts = {};
1837
+ filtered.forEach(function(o) {
1838
+ var t = o.type || TOOL_TYPE_MAP[o.toolName] || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'other');
1839
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
1840
+ });
1841
+ var typeList = Object.keys(typeCounts).sort(function(a, b) { return typeCounts[b] - typeCounts[a]; });
1842
+
1843
+ if (tlTypeFilter) {
1844
+ filtered = filtered.filter(function(o) {
1845
+ var t = o.type || TOOL_TYPE_MAP[o.toolName] || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'other');
1846
+ return t === tlTypeFilter;
1847
+ });
1848
+ }
1849
+
1850
+ var pageSize = state.timeline.pageSize;
1851
+ var page = state.timeline.page;
1852
+ var start = page * pageSize;
1853
+ var paged = filtered.slice(start, start + pageSize);
1854
+ var totalPages = Math.ceil(filtered.length / pageSize);
1855
+
1856
+ var html = '<div class="type-chips">';
1857
+ html += '<span class="type-chip' + (!tlTypeFilter ? ' active' : '') + '" onclick="setTlTypeFilter(\'\')">All (' + obs.length + ')</span>';
1858
+ typeList.forEach(function(t) {
1859
+ var color = OBS_TYPE_COLORS[t] || '#666666';
1860
+ html += '<span class="type-chip' + (tlTypeFilter === t ? ' active' : '') + '" onclick="setTlTypeFilter(\'' + esc(t) + '\')" style="' + (tlTypeFilter === t ? 'background:' + color + ';border-color:' + color + ';' : 'border-color:' + color + ';color:' + color + ';') + '">' + esc(t.replace(/_/g, ' ')) + ' (' + typeCounts[t] + ')</span>';
1861
+ });
1862
+ html += '</div>';
1863
+
1864
+ if (paged.length === 0) {
1865
+ html += '<div class="empty-state"><div class="empty-icon">&#128337;</div><p>No observations' + (obs.length > 0 ? ' match the filter (' + obs.length + ' total)' : ' for this session') + '</p></div>';
1866
+ content.innerHTML = html;
1867
+ return;
1868
+ }
1869
+
1870
+ html += '<div style="font-size:11px;color:var(--ink-faint);margin-bottom:16px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:0.06em;">' + filtered.length + ' observations shown</div>';
1871
+
1872
+ html += '<div class="timeline-container">';
1873
+
1874
+ var lastDateGroup = '';
1875
+ paged.forEach(function(o, idx) {
1876
+ var isCompressed = !!o.narrative || !!o.type;
1877
+ var isRaw = !isCompressed;
1878
+ var type = o.type || TOOL_TYPE_MAP[o.toolName] || 'other';
1879
+ var impVal = typeof o.importance === 'number' ? o.importance : 5;
1880
+ var impClass = impVal >= 7 ? 'high' : impVal >= 4 ? 'med' : 'low';
1881
+ var title = o.title || o.toolName || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'Observation');
1882
+ var typeColor = OBS_TYPE_COLORS[type] || '#666666';
1883
+ var icon = OBS_TYPE_ICONS[type] || '&#128196;';
1884
+
1885
+ var dateGroup = '';
1886
+ try {
1887
+ var d = new Date(o.timestamp);
1888
+ dateGroup = d.toLocaleDateString() + ' ' + d.getHours() + ':00';
1889
+ } catch(e) { dateGroup = ''; }
1890
+
1891
+ if (dateGroup && dateGroup !== lastDateGroup) {
1892
+ html += '<div class="timeline-date-marker"><span>' + esc(dateGroup) + '</span></div>';
1893
+ lastDateGroup = dateGroup;
1894
+ }
1895
+
1896
+ var side = idx % 2 === 0 ? 'tl-left' : 'tl-right';
1897
+
1898
+ html += '<div class="timeline-item ' + side + '">';
1899
+ html += '<div class="timeline-dot" style="background:' + typeColor + ';"></div>';
1900
+ html += '<div class="timeline-connector"></div>';
1901
+
1902
+ html += '<div class="obs-card imp-' + impClass + '" style="border-left-color:' + typeColor + ';text-align:left;">';
1903
+ html += '<div class="obs-head">';
1904
+ html += '<div style="display:flex;align-items:center;gap:6px;">';
1905
+ html += '<span class="obs-type-icon">' + icon + '</span>';
1906
+ html += '<span class="obs-title">' + esc(title) + '</span>';
1907
+ if (isRaw) html += '<span class="badge badge-muted" style="font-size:8px;margin-left:4px;">raw</span>';
1908
+ html += '</div>';
1909
+ html += '<div style="display:flex;align-items:center;gap:8px;">';
1910
+ if (isCompressed) html += '<span class="obs-importance imp-' + impVal + '" title="Importance: ' + impVal + '/10">' + impVal + '</span>';
1911
+ html += '<span class="obs-time">' + esc(shortTime(o.timestamp)) + '</span>';
1912
+ html += '</div></div>';
1913
+
1914
+ if (o.subtitle) html += '<div class="obs-subtitle">' + esc(o.subtitle) + '</div>';
1915
+
1916
+ html += '<div style="margin-top:4px;">';
1917
+ html += '<span class="badge" style="border-color:' + typeColor + ';color:' + typeColor + ';margin-right:4px;">' + esc(type.replace(/_/g, ' ')) + '</span>';
1918
+ if (o.hookType) html += '<span class="badge badge-muted" style="margin-right:4px;">' + esc(o.hookType) + '</span>';
1919
+ html += '</div>';
1920
+
1921
+ if (isRaw && o.toolInput) {
1922
+ var inputStr = typeof o.toolInput === 'string' ? o.toolInput : JSON.stringify(o.toolInput);
1923
+ html += '<div style="margin-top:6px;"><span style="font-size:10px;color:var(--ink-muted);font-weight:600;font-family:var(--font-ui);text-transform:uppercase;letter-spacing:0.08em;">Input:</span>';
1924
+ html += '<pre style="font-size:11px;color:var(--ink-muted);background:var(--bg-alt);padding:8px 10px;border:1px solid var(--border-light);margin-top:3px;overflow-x:auto;max-height:80px;font-family:var(--font-mono);">' + esc(truncate(inputStr, 300)) + '</pre></div>';
1925
+ }
1926
+ if (isRaw && o.toolOutput) {
1927
+ var outputStr = typeof o.toolOutput === 'string' ? o.toolOutput : JSON.stringify(o.toolOutput);
1928
+ html += '<div style="margin-top:4px;"><span style="font-size:10px;color:var(--ink-muted);font-weight:600;font-family:var(--font-ui);text-transform:uppercase;letter-spacing:0.08em;">Output:</span>';
1929
+ html += '<div class="obs-narrative" style="margin-top:3px;">' + esc(truncate(outputStr, 300)) + '</div></div>';
1930
+ }
1931
+ if (o.narrative) html += '<div class="obs-narrative" style="margin-top:8px;">' + esc(o.narrative) + '</div>';
1932
+ if (o.facts && o.facts.length > 0) {
1933
+ html += '<ul class="obs-facts">';
1934
+ o.facts.forEach(function(f) { html += '<li>' + esc(f) + '</li>'; });
1935
+ html += '</ul>';
1936
+ }
1937
+
1938
+ var hasTags = (o.concepts && o.concepts.length) || (o.files && o.files.length);
1939
+ if (hasTags) {
1940
+ html += '<div class="tag-list">';
1941
+ (o.concepts || []).forEach(function(c) { html += '<span class="tag">' + esc(c) + '</span>'; });
1942
+ (o.files || []).forEach(function(f) {
1943
+ var short = f.split('/').pop();
1944
+ html += '<span class="tag file-tag" title="' + esc(f) + '">' + esc(short) + '</span>';
1945
+ });
1946
+ html += '</div>';
1947
+ }
1948
+ if (isRaw && o.toolInput) {
1949
+ var files = [];
1950
+ var ti = o.toolInput;
1951
+ if (typeof ti === 'object' && ti !== null) {
1952
+ if (ti.file_path) files.push(ti.file_path);
1953
+ if (ti.path) files.push(ti.path);
1954
+ }
1955
+ if (files.length > 0) {
1956
+ html += '<div class="tag-list">';
1957
+ files.forEach(function(f) {
1958
+ var short = String(f).split('/').pop();
1959
+ html += '<span class="tag file-tag" title="' + esc(f) + '">' + esc(short) + '</span>';
1960
+ });
1961
+ html += '</div>';
1962
+ }
1963
+ }
1964
+ html += '</div>';
1965
+ html += '</div>';
1966
+ });
1967
+
1968
+ html += '</div>';
1969
+
1970
+ if (totalPages > 1) {
1971
+ html += '<div class="pagination">';
1972
+ if (page > 0) html += '<button class="btn" onclick="tlPage(' + (page - 1) + ')">Prev</button>';
1973
+ html += '<span style="color:var(--ink-faint);font-size:12px;padding:6px;font-family:var(--font-mono);">Page ' + (page + 1) + ' of ' + totalPages + ' (' + filtered.length + ' total)</span>';
1974
+ if (page < totalPages - 1) html += '<button class="btn" onclick="tlPage(' + (page + 1) + ')">Next</button>';
1975
+ html += '</div>';
1976
+ }
1977
+
1978
+ content.innerHTML = html;
1979
+ }
1980
+
1981
+ function setTlTypeFilter(type) {
1982
+ tlTypeFilter = type;
1983
+ state.timeline.page = 0;
1984
+ renderObservations();
1985
+ }
1986
+
1987
+ function tlPage(p) {
1988
+ state.timeline.page = p;
1989
+ renderObservations();
1990
+ }
1991
+
1992
+ async function loadActivity() {
1993
+ var el = document.getElementById('view-activity');
1994
+ el.innerHTML = '<div class="loading">Loading activity...</div>';
1995
+ var results = await Promise.all([
1996
+ apiGet('sessions'),
1997
+ apiGet('audit?limit=200')
1998
+ ]);
1999
+ var sessions = (results[0] && results[0].sessions) || [];
2000
+ var auditEntries = (results[1] && results[1].entries) || [];
2001
+
2002
+ var allObs = [];
2003
+ var sorted = sessions.slice().sort(function(a, b) { return (b.startedAt || '').localeCompare(a.startedAt || ''); });
2004
+ var recentSessions = sorted.slice(0, 5);
2005
+
2006
+ var obsResults = await Promise.all(recentSessions.map(function(s) {
2007
+ return apiGet('observations?sessionId=' + encodeURIComponent(s.id));
2008
+ }));
2009
+ obsResults.forEach(function(r) {
2010
+ if (r && r.observations) allObs = allObs.concat(r.observations);
2011
+ });
2012
+
2013
+ state.activity.sessions = sessions;
2014
+ state.activity.observations = allObs;
2015
+ state.activity.audit = auditEntries;
2016
+ state.activity.loaded = true;
2017
+ renderActivity();
2018
+ }
2019
+
2020
+ function renderActivity() {
2021
+ var el = document.getElementById('view-activity');
2022
+ var obs = state.activity.observations;
2023
+ var sessions = state.activity.sessions;
2024
+
2025
+ var TOOL_TYPE_MAP = { Read: 'file_read', Write: 'file_write', Edit: 'file_edit', Bash: 'command_run', Grep: 'search', Glob: 'search', WebFetch: 'web_fetch', WebSearch: 'web_fetch', AskUserQuestion: 'conversation', Task: 'subagent' };
2026
+
2027
+ var html = '';
2028
+
2029
+ html += '<div class="card"><div class="card-title">Activity Heatmap (Past Year)</div>';
2030
+ var dayCounts = {};
2031
+ obs.forEach(function(o) {
2032
+ try {
2033
+ var d = new Date(o.timestamp);
2034
+ var key = d.toISOString().slice(0, 10);
2035
+ dayCounts[key] = (dayCounts[key] || 0) + 1;
2036
+ } catch(e) {}
2037
+ });
2038
+ sessions.forEach(function(s) {
2039
+ try {
2040
+ var d = new Date(s.startedAt);
2041
+ var key = d.toISOString().slice(0, 10);
2042
+ dayCounts[key] = (dayCounts[key] || 0) + 1;
2043
+ } catch(e) {}
2044
+ });
2045
+
2046
+ var maxCount = 0;
2047
+ Object.keys(dayCounts).forEach(function(k) { if (dayCounts[k] > maxCount) maxCount = dayCounts[k]; });
2048
+
2049
+ var today = new Date();
2050
+ var dayLabels = ['Mon', '', 'Wed', '', 'Fri', '', ''];
2051
+ html += '<div class="heatmap-labels">';
2052
+ dayLabels.forEach(function(l) { html += '<span style="width:10px;text-align:center;">' + l + '</span>'; });
2053
+ html += '</div>';
2054
+ html += '<div class="heatmap-wrap"><div class="heatmap-grid">';
2055
+ for (var w = 51; w >= 0; w--) {
2056
+ for (var d = 0; d < 7; d++) {
2057
+ var cellDate = new Date(today);
2058
+ cellDate.setDate(cellDate.getDate() - (w * 7 + (6 - d)));
2059
+ var key = cellDate.toISOString().slice(0, 10);
2060
+ var count = dayCounts[key] || 0;
2061
+ var level = count === 0 ? '' : count <= (maxCount * 0.25) ? 'level-1' : count <= (maxCount * 0.5) ? 'level-2' : count <= (maxCount * 0.75) ? 'level-3' : 'level-4';
2062
+ var title = key + ': ' + count + ' event' + (count !== 1 ? 's' : '');
2063
+ html += '<div class="heatmap-cell ' + level + '" title="' + esc(title) + '"></div>';
2064
+ }
2065
+ }
2066
+ html += '</div></div>';
2067
+ html += '<div style="display:flex;align-items:center;gap:4px;margin-top:8px;font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);justify-content:flex-end;">Less ';
2068
+ html += '<div class="heatmap-cell" style="display:inline-block;"></div>';
2069
+ html += '<div class="heatmap-cell level-1" style="display:inline-block;"></div>';
2070
+ html += '<div class="heatmap-cell level-2" style="display:inline-block;"></div>';
2071
+ html += '<div class="heatmap-cell level-3" style="display:inline-block;"></div>';
2072
+ html += '<div class="heatmap-cell level-4" style="display:inline-block;"></div>';
2073
+ html += ' More</div>';
2074
+ html += '</div>';
2075
+
2076
+ var typeCounts = {};
2077
+ obs.forEach(function(o) {
2078
+ var t = o.type || TOOL_TYPE_MAP[o.toolName] || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'other');
2079
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
2080
+ });
2081
+ var typeList = Object.keys(typeCounts).sort(function(a, b) { return typeCounts[b] - typeCounts[a]; });
2082
+ var totalObs = obs.length || 1;
2083
+
2084
+ html += '<div class="two-col" style="margin-top:16px;">';
2085
+
2086
+ html += '<div class="card"><div class="card-title">Type Breakdown</div>';
2087
+ if (typeList.length === 0) {
2088
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No observations yet</div>';
2089
+ } else {
2090
+ html += '<div class="bar-chart">';
2091
+ typeList.slice(0, 12).forEach(function(t) {
2092
+ var pct = Math.round((typeCounts[t] / totalObs) * 100);
2093
+ var color = OBS_TYPE_COLORS[t] || '#666666';
2094
+ html += '<div class="bar-row"><span class="bar-label">' + esc(t.replace(/_/g, ' ')) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:' + color + ';"></div></div><span class="bar-value">' + typeCounts[t] + '</span></div>';
2095
+ });
2096
+ html += '</div>';
2097
+ }
2098
+ html += '</div>';
2099
+
2100
+ html += '<div class="card"><div class="card-title">Activity Feed</div>';
2101
+ var sortedObs = obs.slice().sort(function(a, b) { return (b.timestamp || '').localeCompare(a.timestamp || ''); });
2102
+ if (sortedObs.length === 0) {
2103
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No recent activity</div>';
2104
+ } else {
2105
+ sortedObs.slice(0, 20).forEach(function(o) {
2106
+ var type = o.type || TOOL_TYPE_MAP[o.toolName] || 'other';
2107
+ var typeColor = OBS_TYPE_COLORS[type] || '#666666';
2108
+ var icon = OBS_TYPE_ICONS[type] || '&#128196;';
2109
+ var title = o.title || o.toolName || (o.hookType ? o.hookType.replace(/_/g, ' ') : 'Observation');
2110
+
2111
+ html += '<div class="activity-feed-item">';
2112
+ html += '<div class="activity-feed-icon" style="color:' + typeColor + ';border-color:' + typeColor + ';">' + icon + '</div>';
2113
+ html += '<div class="activity-feed-body">';
2114
+ html += '<div class="activity-feed-title">' + esc(truncate(title, 60)) + '</div>';
2115
+ if (o.narrative) html += '<div style="font-size:12px;color:var(--ink-muted);margin-top:2px;">' + esc(truncate(o.narrative, 100)) + '</div>';
2116
+ html += '<div class="activity-feed-meta">' + esc(type.replace(/_/g, ' '));
2117
+ if (o.files && o.files.length) html += ' &middot; <span class="tag file-tag" style="font-size:9px;padding:0 4px;">' + esc(o.files[0].split('/').pop()) + '</span>';
2118
+ html += ' &middot; ' + esc(shortTime(o.timestamp)) + '</div>';
2119
+ html += '</div></div>';
2120
+ });
2121
+ }
2122
+ html += '</div>';
2123
+
2124
+ html += '</div>';
2125
+
2126
+ el.innerHTML = html;
2127
+ }
2128
+
2129
+ async function loadSessions() {
2130
+ var el = document.getElementById('view-sessions');
2131
+ el.innerHTML = '<div class="loading">Loading sessions...</div>';
2132
+ var result = await apiGet('sessions');
2133
+ state.sessions.items = (result && result.sessions) || [];
2134
+ state.sessions.loaded = true;
2135
+ renderSessions();
2136
+ }
2137
+
2138
+ function renderSessions() {
2139
+ var el = document.getElementById('view-sessions');
2140
+ var items = state.sessions.items.slice().sort(function(a, b) {
2141
+ return (b.startedAt || '').localeCompare(a.startedAt || '');
2142
+ });
2143
+
2144
+ var html = '<div class="session-list">';
2145
+ if (items.length === 0) {
2146
+ html += '<div class="empty-state"><div class="empty-icon">&#128466;</div><p>No sessions</p></div>';
2147
+ } else {
2148
+ items.forEach(function(s) {
2149
+ var statusBadge = s.status === 'active' ? 'badge-green' : s.status === 'completed' ? 'badge-blue' : 'badge-muted';
2150
+ var selected = state.sessions.selectedId === s.id;
2151
+ html += '<div class="session-item' + (selected ? ' selected' : '') + '" onclick="selectSession(\'' + esc(s.id) + '\')">';
2152
+ html += '<div class="session-top"><span class="session-project">' + esc(s.project ? s.project.split('/').pop() : 'Unknown') + '</span>';
2153
+ html += '<span class="badge ' + statusBadge + '">' + esc(s.status) + '</span></div>';
2154
+ html += '<div class="session-meta">' + esc(s.id.slice(0, 12)) + ' &middot; ' + esc(formatTime(s.startedAt));
2155
+ html += ' &middot; ' + (s.observationCount || 0) + ' obs';
2156
+ if (s.model) html += ' &middot; ' + esc(s.model);
2157
+ html += '</div></div>';
2158
+ });
2159
+ }
2160
+ html += '</div>';
2161
+ html += '<div id="session-detail"></div>';
2162
+ el.innerHTML = html;
2163
+
2164
+ if (state.sessions.selectedId) renderSessionDetail();
2165
+ }
2166
+
2167
+ function selectSession(id) {
2168
+ state.sessions.selectedId = state.sessions.selectedId === id ? null : id;
2169
+ renderSessions();
2170
+ }
2171
+
2172
+ function renderSessionDetail() {
2173
+ var panel = document.getElementById('session-detail');
2174
+ if (!panel) return;
2175
+ var s = state.sessions.items.find(function(x) { return x.id === state.sessions.selectedId; });
2176
+ if (!s) { panel.innerHTML = ''; return; }
2177
+
2178
+ var html = '<div class="detail-panel"><h3>Session Details</h3>';
2179
+ html += '<div class="detail-row"><div class="dl">Session ID</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(s.id) + '</div></div>';
2180
+ html += '<div class="detail-row"><div class="dl">Project</div><div class="dv">' + esc(s.project) + '</div></div>';
2181
+ html += '<div class="detail-row"><div class="dl">Working Dir</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(s.cwd) + '</div></div>';
2182
+ html += '<div class="detail-row"><div class="dl">Status</div><div class="dv">' + esc(s.status) + '</div></div>';
2183
+ html += '<div class="detail-row"><div class="dl">Started</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(s.startedAt)) + '</div></div>';
2184
+ if (s.endedAt) html += '<div class="detail-row"><div class="dl">Ended</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(s.endedAt)) + '</div></div>';
2185
+ html += '<div class="detail-row"><div class="dl">Observations</div><div class="dv" style="font-family:var(--font-mono);">' + (s.observationCount || 0) + '</div></div>';
2186
+ if (s.model) html += '<div class="detail-row"><div class="dl">Model</div><div class="dv">' + esc(s.model) + '</div></div>';
2187
+ if (s.tags && s.tags.length) html += '<div class="detail-row"><div class="dl">Tags</div><div class="dv">' + s.tags.map(function(t) { return '<span class="badge badge-muted" style="margin-right:4px;">' + esc(t) + '</span>'; }).join('') + '</div></div>';
2188
+
2189
+ html += '<div style="margin-top:16px;display:flex;gap:8px;">';
2190
+ if (s.status === 'active') {
2191
+ html += '<button class="btn btn-danger" onclick="endSession(\'' + esc(s.id) + '\')">End Session</button>';
2192
+ }
2193
+ html += '<button class="btn btn-primary" onclick="summarizeSession(\'' + esc(s.id) + '\')">Summarize</button>';
2194
+ html += '</div></div>';
2195
+ panel.innerHTML = html;
2196
+ }
2197
+
2198
+ async function endSession(id) {
2199
+ await apiPost('session/end', { sessionId: id });
2200
+ state.sessions.loaded = false;
2201
+ loadSessions();
2202
+ }
2203
+
2204
+ async function summarizeSession(id) {
2205
+ var btn = event.target;
2206
+ btn.textContent = 'Summarizing...';
2207
+ btn.disabled = true;
2208
+ await apiPost('summarize', { sessionId: id });
2209
+ btn.textContent = 'Done';
2210
+ setTimeout(function() { btn.textContent = 'Summarize'; btn.disabled = false; }, 2000);
2211
+ }
2212
+
2213
+ async function loadAudit() {
2214
+ var el = document.getElementById('view-audit');
2215
+ el.innerHTML = '<div class="loading">Loading audit log...</div>';
2216
+ var result = await apiGet('audit?limit=100');
2217
+ state.audit.entries = (result && result.entries) || [];
2218
+ state.audit.loaded = true;
2219
+ renderAudit();
2220
+ }
2221
+
2222
+ function renderAudit() {
2223
+ var el = document.getElementById('view-audit');
2224
+ var entries = state.audit.entries;
2225
+ var opFilter = state.audit.opFilter;
2226
+
2227
+ var ops = {};
2228
+ entries.forEach(function(e) { ops[e.operation] = true; });
2229
+ var opList = Object.keys(ops).sort();
2230
+
2231
+ var filtered = opFilter ? entries.filter(function(e) { return e.operation === opFilter; }) : entries;
2232
+
2233
+ var html = '<div class="toolbar">';
2234
+ html += '<select id="audit-op-filter"><option value="">All operations</option>';
2235
+ opList.forEach(function(op) {
2236
+ html += '<option value="' + esc(op) + '"' + (opFilter === op ? ' selected' : '') + '>' + esc(op) + '</option>';
2237
+ });
2238
+ html += '</select></div>';
2239
+
2240
+ html += '<div class="card">';
2241
+ if (filtered.length === 0) {
2242
+ html += '<div class="empty-state"><div class="empty-icon">&#128220;</div><p>No audit entries yet</p><p style="font-size:12px;color:var(--ink-faint);font-style:italic;">Audit entries are created by governance operations (delete, evolve, consolidate).</p></div>';
2243
+ } else {
2244
+ filtered.forEach(function(a, idx) {
2245
+ var badgeClass = OP_BADGES[a.operation] || 'badge-muted';
2246
+ html += '<div class="audit-entry">';
2247
+ html += '<div class="audit-head">';
2248
+ html += '<span class="badge ' + badgeClass + '">' + esc(a.operation) + '</span>';
2249
+ html += '<span style="font-size:12px;color:var(--ink-muted);font-family:var(--font-mono);">' + esc(a.functionId || '') + '</span>';
2250
+ html += '<span style="font-size:10px;color:var(--ink-faint);margin-left:auto;font-family:var(--font-mono);">' + esc(formatTime(a.timestamp)) + '</span>';
2251
+ html += '<button class="btn" style="font-size:9px;padding:1px 6px;margin-left:8px;" onclick="toggleAuditDetail(' + idx + ')">&#9660;</button>';
2252
+ html += '</div>';
2253
+ if (a.targetIds && a.targetIds.length) {
2254
+ html += '<div style="font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);">' + a.targetIds.length + ' target(s): ' + esc(a.targetIds.slice(0, 3).join(', ')) + (a.targetIds.length > 3 ? '...' : '') + '</div>';
2255
+ }
2256
+ html += '<div class="audit-detail" id="audit-detail-' + idx + '"><pre>' + esc(JSON.stringify(a.details || {}, null, 2)) + '</pre></div>';
2257
+ html += '</div>';
2258
+ });
2259
+ }
2260
+ html += '</div>';
2261
+
2262
+ el.innerHTML = html;
2263
+
2264
+ document.getElementById('audit-op-filter').addEventListener('change', function() {
2265
+ state.audit.opFilter = this.value;
2266
+ renderAudit();
2267
+ });
2268
+ }
2269
+
2270
+ function toggleAuditDetail(idx) {
2271
+ var el = document.getElementById('audit-detail-' + idx);
2272
+ if (el) el.classList.toggle('open');
2273
+ }
2274
+
2275
+ async function loadProfile() {
2276
+ var el = document.getElementById('view-profile');
2277
+ el.innerHTML = '<div class="loading">Loading profile...</div>';
2278
+ var sessResult = await apiGet('sessions');
2279
+ var sessions = (sessResult && sessResult.sessions) || [];
2280
+
2281
+ var projects = {};
2282
+ sessions.forEach(function(s) { if (s.project) projects[s.project] = true; });
2283
+ state.profile.projects = Object.keys(projects).sort();
2284
+ state.profile.loaded = true;
2285
+
2286
+ if (state.profile.projects.length > 0 && !state.profile.selectedProject) {
2287
+ state.profile.selectedProject = state.profile.projects[0];
2288
+ }
2289
+
2290
+ renderProfileToolbar();
2291
+ if (state.profile.selectedProject) await loadProfileData();
2292
+ }
2293
+
2294
+ function renderProfileToolbar() {
2295
+ var el = document.getElementById('view-profile');
2296
+ var html = '<div class="toolbar">';
2297
+ html += '<select id="profile-project">';
2298
+ if (state.profile.projects.length === 0) {
2299
+ html += '<option value="">No projects</option>';
2300
+ } else {
2301
+ state.profile.projects.forEach(function(p) {
2302
+ html += '<option value="' + esc(p) + '"' + (state.profile.selectedProject === p ? ' selected' : '') + '>' + esc(p) + '</option>';
2303
+ });
2304
+ }
2305
+ html += '</select></div>';
2306
+ html += '<div id="profile-content"></div>';
2307
+ el.innerHTML = html;
2308
+
2309
+ document.getElementById('profile-project').addEventListener('change', function() {
2310
+ state.profile.selectedProject = this.value;
2311
+ loadProfileData();
2312
+ });
2313
+ }
2314
+
2315
+ async function loadProfileData() {
2316
+ var content = document.getElementById('profile-content');
2317
+ if (!content || !state.profile.selectedProject) return;
2318
+ content.innerHTML = '<div class="loading">Loading profile data...</div>';
2319
+ var result = await apiGet('profile?project=' + encodeURIComponent(state.profile.selectedProject));
2320
+ state.profile.data = (result && result.profile) ? result.profile : result;
2321
+ renderProfile();
2322
+ }
2323
+
2324
+ function renderProfile() {
2325
+ var content = document.getElementById('profile-content');
2326
+ if (!content) return;
2327
+ var p = state.profile.data;
2328
+
2329
+ if (!p) {
2330
+ content.innerHTML = '<div class="empty-state"><div class="empty-icon">&#128203;</div><p>No profile data for this project</p></div>';
2331
+ return;
2332
+ }
2333
+
2334
+ var html = '<div class="two-col">';
2335
+
2336
+ html += '<div class="card"><div class="card-title">Top Concepts</div>';
2337
+ var concepts = p.topConcepts || [];
2338
+ if (concepts.length === 0) {
2339
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No concepts yet</div>';
2340
+ } else {
2341
+ var maxC = Math.max.apply(null, concepts.map(function(c) { return c.frequency; })) || 1;
2342
+ html += '<div class="bar-chart">';
2343
+ concepts.slice(0, 10).forEach(function(c) {
2344
+ var pct = Math.round((c.frequency / maxC) * 100);
2345
+ html += '<div class="bar-row"><span class="bar-label">' + esc(c.concept) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--yellow);"></div></div><span class="bar-value">' + c.frequency + '</span></div>';
2346
+ });
2347
+ html += '</div>';
2348
+ }
2349
+ html += '</div>';
2350
+
2351
+ html += '<div class="card"><div class="card-title">Top Files</div>';
2352
+ var files = p.topFiles || [];
2353
+ if (files.length === 0) {
2354
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No files yet</div>';
2355
+ } else {
2356
+ var maxF = Math.max.apply(null, files.map(function(f) { return f.frequency; })) || 1;
2357
+ html += '<div class="bar-chart">';
2358
+ files.slice(0, 10).forEach(function(f) {
2359
+ var pct = Math.round((f.frequency / maxF) * 100);
2360
+ html += '<div class="bar-row"><span class="bar-label">' + esc(f.file.split('/').pop()) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + pct + '%;background:var(--green);"></div></div><span class="bar-value">' + f.frequency + '</span></div>';
2361
+ });
2362
+ html += '</div>';
2363
+ }
2364
+ html += '</div>';
2365
+
2366
+ html += '</div>';
2367
+
2368
+ html += '<div class="card" style="margin-top:16px;"><div class="card-title">Conventions</div>';
2369
+ var conventions = p.conventions || [];
2370
+ if (conventions.length === 0) {
2371
+ html += '<div style="font-size:13px;color:var(--ink-faint);font-style:italic;">No conventions detected yet</div>';
2372
+ } else {
2373
+ html += '<ul style="padding-left:16px;">';
2374
+ conventions.forEach(function(c) { html += '<li style="font-size:13px;color:var(--ink-muted);margin-bottom:4px;">' + esc(c) + '</li>'; });
2375
+ html += '</ul>';
2376
+ }
2377
+ html += '</div>';
2378
+
2379
+ if (p.summary) {
2380
+ html += '<div class="card" style="margin-top:16px;"><div class="card-title">Project Summary</div>';
2381
+ html += '<p style="font-size:13px;color:var(--ink-muted);line-height:1.7;">' + esc(p.summary) + '</p></div>';
2382
+ }
2383
+
2384
+ var stats = '<div class="card" style="margin-top:16px;"><div class="card-title">Project Stats</div>';
2385
+ stats += '<div class="detail-row"><div class="dl">Sessions</div><div class="dv" style="font-family:var(--font-mono);">' + (p.sessionCount || 0) + '</div></div>';
2386
+ stats += '<div class="detail-row"><div class="dl">Total Obs</div><div class="dv" style="font-family:var(--font-mono);">' + (p.totalObservations || 0) + '</div></div>';
2387
+ stats += '<div class="detail-row"><div class="dl">Updated</div><div class="dv" style="font-family:var(--font-mono);font-size:12px;">' + esc(formatTime(p.updatedAt)) + '</div></div>';
2388
+ stats += '</div>';
2389
+
2390
+ content.innerHTML = html + stats;
2391
+ }
2392
+
2393
+ var wsReconnectTimer = null;
2394
+ var wsRetries = 0;
2395
+ var WS_MAX_RETRIES = 10;
2396
+
2397
+ function connectWs() {
2398
+ if (wsRetries >= WS_MAX_RETRIES) return;
2399
+ try {
2400
+ state.ws = new WebSocket(WS_URL);
2401
+ state.ws.onopen = function() {
2402
+ wsRetries = 0;
2403
+ state.ws.send(JSON.stringify({
2404
+ type: 'join',
2405
+ data: {
2406
+ subscriptionId: 'viewer-' + Date.now(),
2407
+ streamName: 'mem-live',
2408
+ groupId: 'viewer'
2409
+ }
2410
+ }));
2411
+ document.getElementById('ws-status').textContent = 'live';
2412
+ document.getElementById('ws-status').className = 'ws-status connected';
2413
+ };
2414
+ state.ws.onmessage = function(e) {
2415
+ try {
2416
+ var msg = JSON.parse(e.data);
2417
+ if (msg.type === 'stream' && msg.event) {
2418
+ handleStreamEvent(msg);
2419
+ }
2420
+ } catch {}
2421
+ };
2422
+ state.ws.onclose = function() {
2423
+ document.getElementById('ws-status').textContent = 'reconnecting...';
2424
+ document.getElementById('ws-status').className = 'ws-status disconnected';
2425
+ wsRetries++;
2426
+ if (wsRetries < WS_MAX_RETRIES) {
2427
+ wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
2428
+ } else {
2429
+ document.getElementById('ws-status').textContent = 'disconnected';
2430
+ }
2431
+ };
2432
+ state.ws.onerror = function() { state.ws.close(); };
2433
+ } catch {
2434
+ wsRetries++;
2435
+ if (wsRetries < WS_MAX_RETRIES) {
2436
+ wsReconnectTimer = setTimeout(connectWs, 2000 + Math.min(wsRetries * 1000, 8000));
2437
+ }
2438
+ }
2439
+ }
2440
+
2441
+ function handleStreamEvent(msg) {
2442
+ var evt = msg.event;
2443
+ if ((evt.type === 'create' || evt.type === 'update') && evt.data) {
2444
+ var payload = evt.data;
2445
+ var observation = payload.observation || payload;
2446
+ if (observation) {
2447
+ routeWsMessage({ observation: observation });
2448
+ }
2449
+ } else if (evt.type === 'sync') {
2450
+ var items = Array.isArray(evt.data) ? evt.data : [];
2451
+ items.forEach(function(item) {
2452
+ var payload = item.data || item;
2453
+ var observation = payload.observation || payload;
2454
+ if (observation) {
2455
+ routeWsMessage({ observation: observation });
2456
+ }
2457
+ });
2458
+ }
2459
+ }
2460
+
2461
+ function routeWsMessage(msg) {
2462
+ if (state.activeTab === 'timeline' && msg.observation) {
2463
+ if (!state.timeline.sessionId || msg.observation.sessionId === state.timeline.sessionId) {
2464
+ var existing = state.timeline.observations.findIndex(function(o) { return o.id === msg.observation.id; });
2465
+ if (existing >= 0) {
2466
+ state.timeline.observations[existing] = msg.observation;
2467
+ } else {
2468
+ state.timeline.observations.unshift(msg.observation);
2469
+ }
2470
+ renderObservations();
2471
+ }
2472
+ }
2473
+ if (state.activeTab === 'dashboard') {
2474
+ state.dashboard.loaded = false;
2475
+ loadDashboard();
2476
+ }
2477
+ if (state.activeTab === 'activity' && msg.observation) {
2478
+ state.activity.observations.unshift(msg.observation);
2479
+ renderActivity();
2480
+ }
2481
+ }
2482
+
2483
+ document.getElementById('tab-bar').addEventListener('click', function(e) {
2484
+ if (e.target.tagName === 'BUTTON' && e.target.dataset.tab) {
2485
+ switchTab(e.target.dataset.tab);
2486
+ }
2487
+ });
2488
+ document.getElementById('modal-overlay').addEventListener('click', function(e) {
2489
+ if (e.target === this) closeModal();
2490
+ });
2491
+
2492
+ loadTab('dashboard');
2493
+ connectWs();
2494
+ startDashboardAutoRefresh();
2495
+ </script>
2496
+ </body>
2497
+ </html>