code-context-engine 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_context_engine-0.4.0.dist-info/METADATA +389 -0
- code_context_engine-0.4.0.dist-info/RECORD +63 -0
- code_context_engine-0.4.0.dist-info/WHEEL +5 -0
- code_context_engine-0.4.0.dist-info/entry_points.txt +4 -0
- code_context_engine-0.4.0.dist-info/licenses/LICENSE +21 -0
- code_context_engine-0.4.0.dist-info/top_level.txt +1 -0
- context_engine/__init__.py +3 -0
- context_engine/cli.py +2848 -0
- context_engine/cli_style.py +66 -0
- context_engine/compression/__init__.py +0 -0
- context_engine/compression/compressor.py +144 -0
- context_engine/compression/ollama_client.py +33 -0
- context_engine/compression/output_rules.py +77 -0
- context_engine/compression/prompts.py +9 -0
- context_engine/compression/quality.py +37 -0
- context_engine/config.py +198 -0
- context_engine/dashboard/__init__.py +0 -0
- context_engine/dashboard/_page.py +1548 -0
- context_engine/dashboard/server.py +429 -0
- context_engine/editors.py +265 -0
- context_engine/event_bus.py +24 -0
- context_engine/indexer/__init__.py +0 -0
- context_engine/indexer/chunker.py +147 -0
- context_engine/indexer/embedder.py +154 -0
- context_engine/indexer/embedding_cache.py +168 -0
- context_engine/indexer/git_hooks.py +73 -0
- context_engine/indexer/git_indexer.py +136 -0
- context_engine/indexer/ignorefile.py +96 -0
- context_engine/indexer/manifest.py +78 -0
- context_engine/indexer/pipeline.py +624 -0
- context_engine/indexer/secrets.py +332 -0
- context_engine/indexer/watcher.py +109 -0
- context_engine/integration/__init__.py +0 -0
- context_engine/integration/bootstrap.py +76 -0
- context_engine/integration/git_context.py +132 -0
- context_engine/integration/mcp_server.py +1825 -0
- context_engine/integration/session_capture.py +306 -0
- context_engine/memory/__init__.py +6 -0
- context_engine/memory/compressor.py +344 -0
- context_engine/memory/db.py +922 -0
- context_engine/memory/extractive.py +106 -0
- context_engine/memory/grammar.py +419 -0
- context_engine/memory/hook_installer.py +258 -0
- context_engine/memory/hook_server.py +83 -0
- context_engine/memory/hooks.py +327 -0
- context_engine/memory/migrate.py +268 -0
- context_engine/models.py +96 -0
- context_engine/pricing.py +104 -0
- context_engine/project_commands.py +296 -0
- context_engine/retrieval/__init__.py +0 -0
- context_engine/retrieval/confidence.py +47 -0
- context_engine/retrieval/query_parser.py +105 -0
- context_engine/retrieval/retriever.py +199 -0
- context_engine/serve_http.py +208 -0
- context_engine/services.py +252 -0
- context_engine/storage/__init__.py +0 -0
- context_engine/storage/backend.py +39 -0
- context_engine/storage/fts_store.py +112 -0
- context_engine/storage/graph_store.py +219 -0
- context_engine/storage/local_backend.py +109 -0
- context_engine/storage/remote_backend.py +117 -0
- context_engine/storage/vector_store.py +357 -0
- context_engine/utils.py +72 -0
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
"""Embedded HTML page for the CCE dashboard.
|
|
2
|
+
|
|
3
|
+
Single-file SPA. Fetches data from /api/* on tab switch.
|
|
4
|
+
Polls /api/status every 5 seconds for live updates.
|
|
5
|
+
No external dependencies — all CSS and JS inline.
|
|
6
|
+
Grafana-inspired dark theme with SVG/CSS charts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
PAGE_HTML = """<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
|
+
<title>CCE Dashboard</title>
|
|
15
|
+
<style>
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #111217;
|
|
18
|
+
--bg2: #0d0f13;
|
|
19
|
+
--panel: #181b1f;
|
|
20
|
+
--panel2: #1e2128;
|
|
21
|
+
--panel3: #23272e;
|
|
22
|
+
--border: #2d3035;
|
|
23
|
+
--border2: #3a3f47;
|
|
24
|
+
--text: #d8dce2;
|
|
25
|
+
--text2: #8e97a5;
|
|
26
|
+
--text3: #555d6b;
|
|
27
|
+
--blue: #5794f2;
|
|
28
|
+
--blue-bg: rgba(87,148,242,.1);
|
|
29
|
+
--green: #3ecf8e;
|
|
30
|
+
--green-bg: rgba(62,207,142,.1);
|
|
31
|
+
--yellow: #f2cc0c;
|
|
32
|
+
--yellow-bg: rgba(242,204,12,.1);
|
|
33
|
+
--red: #f15f5f;
|
|
34
|
+
--red-bg: rgba(241,95,95,.1);
|
|
35
|
+
--orange: #ff9830;
|
|
36
|
+
--orange-bg: rgba(255,152,48,.1);
|
|
37
|
+
--purple: #b877d9;
|
|
38
|
+
--purple-bg: rgba(184,119,217,.1);
|
|
39
|
+
--mono: "DM Mono","JetBrains Mono","Fira Code","Cascadia Code","SF Mono",monospace;
|
|
40
|
+
--sans: "DM Sans",Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
|
|
41
|
+
--r: 6px;
|
|
42
|
+
--r2: 10px;
|
|
43
|
+
--shadow: 0 2px 8px rgba(0,0,0,.3), 0 1px 3px rgba(0,0,0,.2);
|
|
44
|
+
--shadow-hover: 0 8px 24px rgba(0,0,0,.4), 0 2px 8px rgba(0,0,0,.3);
|
|
45
|
+
--shadow-glow-blue: 0 0 20px rgba(87,148,242,.15);
|
|
46
|
+
--shadow-glow-green: 0 0 20px rgba(115,191,105,.15);
|
|
47
|
+
--shadow-glow-purple: 0 0 20px rgba(184,119,217,.15);
|
|
48
|
+
--ease: cubic-bezier(0.22,1,0.36,1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600;700&display=swap');
|
|
52
|
+
|
|
53
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
54
|
+
html, body { height: 100%; overflow: hidden; }
|
|
55
|
+
body { background: var(--bg2); color: var(--text); font-family: var(--sans); font-size: 13px; display: flex; flex-direction: column; }
|
|
56
|
+
|
|
57
|
+
/* ── Top bar ─────────────────────────────────────── */
|
|
58
|
+
|
|
59
|
+
.topbar {
|
|
60
|
+
height: 41px; min-height: 41px;
|
|
61
|
+
background: var(--panel);
|
|
62
|
+
border-bottom: 1px solid var(--border);
|
|
63
|
+
display: flex; align-items: center;
|
|
64
|
+
padding: 0 16px; gap: 0; z-index: 10;
|
|
65
|
+
}
|
|
66
|
+
.topbar-logo {
|
|
67
|
+
display: flex; align-items: center; gap: 8px;
|
|
68
|
+
padding-right: 16px;
|
|
69
|
+
border-right: 1px solid var(--border);
|
|
70
|
+
margin-right: 16px; flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
.logo-icon {
|
|
73
|
+
width: 24px; height: 24px;
|
|
74
|
+
background: linear-gradient(135deg, var(--blue), var(--purple));
|
|
75
|
+
border-radius: var(--r);
|
|
76
|
+
display: flex; align-items: center; justify-content: center;
|
|
77
|
+
font-size: 9px; font-weight: 900; color: #fff;
|
|
78
|
+
letter-spacing: -.5px; font-family: var(--mono);
|
|
79
|
+
box-shadow: 0 2px 8px rgba(87,148,242,.3), 0 0 12px rgba(184,119,217,.2);
|
|
80
|
+
}
|
|
81
|
+
.topbar-title { font-size: 13.5px; font-weight: 600; color: var(--text); letter-spacing: .1px; }
|
|
82
|
+
.breadcrumb { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); }
|
|
83
|
+
.breadcrumb-sep { color: var(--text3); }
|
|
84
|
+
.breadcrumb-project {
|
|
85
|
+
font-family: var(--mono); font-size: 11.5px;
|
|
86
|
+
color: var(--blue); background: var(--blue-bg);
|
|
87
|
+
padding: 2px 8px; border-radius: var(--r);
|
|
88
|
+
border: 1px solid rgba(87,148,242,.2);
|
|
89
|
+
}
|
|
90
|
+
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
|
91
|
+
.live-badge {
|
|
92
|
+
display: flex; align-items: center; gap: 5px;
|
|
93
|
+
font-size: 11px; color: var(--text3); font-family: var(--mono);
|
|
94
|
+
}
|
|
95
|
+
.live-dot {
|
|
96
|
+
width: 6px; height: 6px; border-radius: 50%;
|
|
97
|
+
background: var(--green);
|
|
98
|
+
box-shadow: 0 0 0 0 rgba(115,191,105,.5);
|
|
99
|
+
animation: livepulse 2s ease-in-out infinite;
|
|
100
|
+
}
|
|
101
|
+
@keyframes livepulse {
|
|
102
|
+
0% { box-shadow: 0 0 0 0 rgba(62,207,142,.5); }
|
|
103
|
+
70% { box-shadow: 0 0 0 6px rgba(62,207,142,0); }
|
|
104
|
+
100% { box-shadow: 0 0 0 0 rgba(62,207,142,0); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ── Layout ──────────────────────────────────────── */
|
|
108
|
+
.layout { display: flex; flex: 1; overflow: hidden; }
|
|
109
|
+
|
|
110
|
+
/* ── Sidebar ─────────────────────────────────────── */
|
|
111
|
+
.sidebar {
|
|
112
|
+
width: 200px; min-width: 200px;
|
|
113
|
+
background: var(--panel); border-right: 1px solid var(--border);
|
|
114
|
+
display: flex; flex-direction: column;
|
|
115
|
+
overflow-y: auto; overflow-x: hidden;
|
|
116
|
+
}
|
|
117
|
+
.nav-section-label {
|
|
118
|
+
padding: 14px 14px 5px;
|
|
119
|
+
font-size: 10px; font-weight: 700;
|
|
120
|
+
letter-spacing: 1px; text-transform: uppercase;
|
|
121
|
+
color: var(--text3);
|
|
122
|
+
}
|
|
123
|
+
.nav-item {
|
|
124
|
+
display: flex; align-items: center; gap: 9px;
|
|
125
|
+
padding: 8px 14px;
|
|
126
|
+
color: var(--text2); font-size: 13px;
|
|
127
|
+
cursor: pointer; border: none; background: none;
|
|
128
|
+
width: 100%; text-align: left;
|
|
129
|
+
transition: background .1s, color .1s;
|
|
130
|
+
border-left: 3px solid transparent;
|
|
131
|
+
}
|
|
132
|
+
.nav-item svg { flex-shrink: 0; opacity: .6; }
|
|
133
|
+
.nav-item:hover { background: var(--panel2); color: var(--text); }
|
|
134
|
+
.nav-item:hover svg { opacity: .85; }
|
|
135
|
+
.nav-item.active { background: var(--panel2); color: var(--text); border-left-color: var(--blue); font-weight: 500; }
|
|
136
|
+
.nav-item.active svg { opacity: 1; color: var(--blue); }
|
|
137
|
+
.nav-count {
|
|
138
|
+
margin-left: auto; font-size: 10.5px;
|
|
139
|
+
font-family: var(--mono); color: var(--text3);
|
|
140
|
+
background: var(--panel3); padding: 1px 6px;
|
|
141
|
+
border-radius: 10px; min-width: 22px; text-align: center;
|
|
142
|
+
}
|
|
143
|
+
.nav-item.active .nav-count { color: var(--blue); background: var(--blue-bg); }
|
|
144
|
+
.sidebar-spacer { flex: 1; }
|
|
145
|
+
|
|
146
|
+
/* ── Main ────────────────────────────────────────── */
|
|
147
|
+
.main { flex: 1; overflow-y: auto; background: var(--bg); }
|
|
148
|
+
.page { display: none; padding: 20px 24px; }
|
|
149
|
+
.page.active { display: block; }
|
|
150
|
+
|
|
151
|
+
/* ── Page header ─────────────────────────────────── */
|
|
152
|
+
.page-hdr {
|
|
153
|
+
display: flex; align-items: flex-start;
|
|
154
|
+
justify-content: space-between; margin-bottom: 18px;
|
|
155
|
+
}
|
|
156
|
+
.page-hdr-title { font-size: 17px; font-weight: 700; color: var(--text); letter-spacing: -.2px; }
|
|
157
|
+
.page-hdr-sub { font-size: 12px; color: var(--text2); margin-top: 2px; }
|
|
158
|
+
|
|
159
|
+
/* ── Stat row ────────────────────────────────────── */
|
|
160
|
+
.stat-row {
|
|
161
|
+
display: grid;
|
|
162
|
+
grid-template-columns: repeat(4, 1fr);
|
|
163
|
+
gap: 10px; margin-bottom: 14px;
|
|
164
|
+
}
|
|
165
|
+
.stat-card {
|
|
166
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
167
|
+
border-radius: var(--r2); padding: 14px 16px;
|
|
168
|
+
border-top: 2px solid transparent;
|
|
169
|
+
display: flex; align-items: flex-start; justify-content: space-between;
|
|
170
|
+
box-shadow: var(--shadow);
|
|
171
|
+
transition: transform .25s var(--ease), box-shadow .25s var(--ease);
|
|
172
|
+
}
|
|
173
|
+
.stat-card:hover {
|
|
174
|
+
transform: translateY(-2px) scale(1.02);
|
|
175
|
+
box-shadow: var(--shadow-hover);
|
|
176
|
+
}
|
|
177
|
+
.stat-card.blue:hover { box-shadow: var(--shadow-hover), var(--shadow-glow-blue); }
|
|
178
|
+
.stat-card.green:hover { box-shadow: var(--shadow-hover), var(--shadow-glow-green); }
|
|
179
|
+
.stat-card.purple:hover { box-shadow: var(--shadow-hover), var(--shadow-glow-purple); }
|
|
180
|
+
.stat-card.blue { border-top-color: var(--blue); }
|
|
181
|
+
.stat-card.green { border-top-color: var(--green); }
|
|
182
|
+
.stat-card.yellow { border-top-color: var(--yellow); }
|
|
183
|
+
.stat-card.purple { border-top-color: var(--purple); }
|
|
184
|
+
.stat-left { flex: 1; }
|
|
185
|
+
.stat-label {
|
|
186
|
+
font-size: 11px; font-weight: 600;
|
|
187
|
+
letter-spacing: .6px; text-transform: uppercase;
|
|
188
|
+
color: var(--text2); margin-bottom: 8px;
|
|
189
|
+
}
|
|
190
|
+
.stat-num {
|
|
191
|
+
font-size: 28px; font-weight: 800;
|
|
192
|
+
font-family: var(--mono);
|
|
193
|
+
letter-spacing: -1px; line-height: 1; color: var(--text);
|
|
194
|
+
}
|
|
195
|
+
.stat-num.blue { color: var(--blue); }
|
|
196
|
+
.stat-num.green { color: var(--green); }
|
|
197
|
+
.stat-num.yellow { color: var(--yellow); }
|
|
198
|
+
.stat-num.purple { color: var(--purple); }
|
|
199
|
+
/* mini sparkline in stat card */
|
|
200
|
+
.stat-spark {
|
|
201
|
+
display: flex; align-items: flex-end;
|
|
202
|
+
gap: 2px; height: 32px; margin-left: 10px; flex-shrink: 0;
|
|
203
|
+
}
|
|
204
|
+
.spark-bar {
|
|
205
|
+
width: 4px; border-radius: 2px 2px 0 0; min-height: 3px;
|
|
206
|
+
transition: height .4s;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ── Panel grid ─────────────────────────────────── */
|
|
210
|
+
.panel-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
|
|
211
|
+
.panel {
|
|
212
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden;
|
|
213
|
+
box-shadow: var(--shadow);
|
|
214
|
+
transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s;
|
|
215
|
+
}
|
|
216
|
+
.panel:hover {
|
|
217
|
+
transform: translateY(-1px);
|
|
218
|
+
box-shadow: var(--shadow-hover);
|
|
219
|
+
border-color: var(--border2);
|
|
220
|
+
}
|
|
221
|
+
.panel-head {
|
|
222
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
223
|
+
padding: 10px 14px;
|
|
224
|
+
border-bottom: 1px solid var(--border);
|
|
225
|
+
background: var(--panel2);
|
|
226
|
+
}
|
|
227
|
+
.panel-title {
|
|
228
|
+
font-size: 11px; font-weight: 700;
|
|
229
|
+
letter-spacing: .6px; text-transform: uppercase;
|
|
230
|
+
color: var(--text2); display: flex; align-items: center; gap: 6px;
|
|
231
|
+
}
|
|
232
|
+
.panel-title svg { opacity: .5; }
|
|
233
|
+
.panel-body { padding: 12px 14px; }
|
|
234
|
+
|
|
235
|
+
/* ── Charts ──────────────────────────────────────── */
|
|
236
|
+
|
|
237
|
+
/* Donut */
|
|
238
|
+
.donut-wrap {
|
|
239
|
+
display: flex; align-items: center; gap: 18px;
|
|
240
|
+
padding: 16px 16px 14px;
|
|
241
|
+
}
|
|
242
|
+
.donut-svg { flex-shrink: 0; }
|
|
243
|
+
.donut-center-big {
|
|
244
|
+
font-family: var(--mono); font-size: 20px; font-weight: 800;
|
|
245
|
+
dominant-baseline: auto;
|
|
246
|
+
}
|
|
247
|
+
.donut-center-sub {
|
|
248
|
+
font-family: var(--sans); font-size: 10px;
|
|
249
|
+
fill: var(--text3); dominant-baseline: auto;
|
|
250
|
+
}
|
|
251
|
+
.donut-legend { flex: 1; display: flex; flex-direction: column; gap: 8px; }
|
|
252
|
+
.legend-item { display: flex; align-items: center; gap: 8px; }
|
|
253
|
+
.legend-color { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
254
|
+
.legend-label { font-size: 12px; color: var(--text2); flex: 1; }
|
|
255
|
+
.legend-val { font-size: 12px; font-family: var(--mono); font-weight: 600; color: var(--text); }
|
|
256
|
+
.legend-pct { font-size: 11px; font-family: var(--mono); color: var(--text3); margin-left: 4px; }
|
|
257
|
+
|
|
258
|
+
/* Horizontal bar chart */
|
|
259
|
+
.hbar-chart { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
|
|
260
|
+
.hbar-row { display: flex; align-items: center; gap: 8px; }
|
|
261
|
+
.hbar-label {
|
|
262
|
+
font-size: 11px; font-family: var(--mono); color: var(--text2);
|
|
263
|
+
width: 120px; min-width: 120px;
|
|
264
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
.hbar-track { flex: 1; height: 8px; background: var(--panel3); border-radius: 4px; overflow: hidden; }
|
|
267
|
+
.hbar-fill { height: 100%; border-radius: 4px; transition: width .8s cubic-bezier(.4,0,.2,1); }
|
|
268
|
+
.hbar-num { font-size: 11px; font-family: var(--mono); color: var(--text3); width: 32px; text-align: right; flex-shrink: 0; }
|
|
269
|
+
|
|
270
|
+
/* Vertical bar chart */
|
|
271
|
+
.vbar-chart {
|
|
272
|
+
display: flex; align-items: flex-end; gap: 5px;
|
|
273
|
+
height: 90px; padding: 12px 14px 0;
|
|
274
|
+
}
|
|
275
|
+
.vbar-col {
|
|
276
|
+
flex: 1; display: flex; flex-direction: column;
|
|
277
|
+
align-items: center; gap: 4px; height: 100%; justify-content: flex-end;
|
|
278
|
+
}
|
|
279
|
+
.vbar-fill { width: 100%; border-radius: 3px 3px 0 0; min-height: 3px; }
|
|
280
|
+
.vbar-labels {
|
|
281
|
+
display: flex; gap: 5px; padding: 4px 14px 12px;
|
|
282
|
+
}
|
|
283
|
+
.vbar-lbl {
|
|
284
|
+
flex: 1; font-size: 8px; font-family: var(--mono);
|
|
285
|
+
color: var(--text3); text-align: center;
|
|
286
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Stacked bar */
|
|
290
|
+
.stacked-bar {
|
|
291
|
+
height: 18px; border-radius: 4px; overflow: hidden;
|
|
292
|
+
display: flex; background: var(--panel3); margin: 14px 14px 8px;
|
|
293
|
+
}
|
|
294
|
+
.stacked-seg { height: 100%; transition: width .8s cubic-bezier(.4,0,.2,1); }
|
|
295
|
+
.stacked-labels { display: flex; justify-content: space-between; padding: 0 14px 12px; }
|
|
296
|
+
.stacked-lbl { font-size: 11px; font-family: var(--mono); }
|
|
297
|
+
|
|
298
|
+
/* ── Savings stat row ────────────────────────────── */
|
|
299
|
+
.savings-stat-row {
|
|
300
|
+
display: grid; grid-template-columns: repeat(3, 1fr);
|
|
301
|
+
gap: 10px; margin-bottom: 14px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ── Health rows ─────────────────────────────────── */
|
|
305
|
+
.health-row {
|
|
306
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
307
|
+
padding: 8px 0; border-bottom: 1px solid var(--border);
|
|
308
|
+
}
|
|
309
|
+
.health-row:last-child { border-bottom: none; }
|
|
310
|
+
.health-left { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--text); }
|
|
311
|
+
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
312
|
+
.status-dot.ok { background: var(--green); box-shadow: 0 0 8px rgba(62,207,142,.5); }
|
|
313
|
+
.status-dot.stale { background: var(--yellow); box-shadow: 0 0 6px rgba(242,204,12,.4); }
|
|
314
|
+
.status-dot.missing { background: var(--red); box-shadow: 0 0 6px rgba(241,95,95,.4); }
|
|
315
|
+
|
|
316
|
+
/* ── Badges ──────────────────────────────────────── */
|
|
317
|
+
.badge {
|
|
318
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
319
|
+
padding: 2px 8px; border-radius: var(--r);
|
|
320
|
+
font-size: 11px; font-weight: 600; font-family: var(--mono);
|
|
321
|
+
border: 1px solid transparent;
|
|
322
|
+
}
|
|
323
|
+
.badge::before { content: ''; width: 5px; height: 5px; border-radius: 50%; }
|
|
324
|
+
.badge-ok { background: var(--green-bg); color: var(--green); border-color: rgba(115,191,105,.2); }
|
|
325
|
+
.badge-ok::before { background: var(--green); }
|
|
326
|
+
.badge-stale { background: var(--yellow-bg); color: var(--yellow); border-color: rgba(242,204,12,.2); }
|
|
327
|
+
.badge-stale::before { background: var(--yellow); }
|
|
328
|
+
.badge-missing { background: var(--red-bg); color: var(--red); border-color: rgba(241,95,95,.2); }
|
|
329
|
+
.badge-missing::before { background: var(--red); }
|
|
330
|
+
.badge-active { background: var(--blue-bg); color: var(--blue); border-color: rgba(87,148,242,.2); }
|
|
331
|
+
.badge-active::before { background: var(--blue); }
|
|
332
|
+
.badge-closed { background: var(--panel3); color: var(--text2); border-color: var(--border); }
|
|
333
|
+
.badge-closed::before { background: var(--text3); }
|
|
334
|
+
.badge-num { background: var(--panel3); color: var(--text2); border-color: var(--border); font-family: var(--mono); }
|
|
335
|
+
.badge-num::before { display: none; }
|
|
336
|
+
|
|
337
|
+
/* ── Buttons ─────────────────────────────────────── */
|
|
338
|
+
.btn {
|
|
339
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
340
|
+
padding: 6px 12px; border-radius: var(--r2);
|
|
341
|
+
font-size: 12.5px; font-weight: 500; border: 1px solid transparent;
|
|
342
|
+
cursor: pointer; transition: all .12s; font-family: var(--sans); white-space: nowrap;
|
|
343
|
+
}
|
|
344
|
+
.btn-primary { background: var(--blue); color: #fff; border-color: var(--blue); box-shadow: 0 2px 8px rgba(87,148,242,.3); }
|
|
345
|
+
.btn-primary:hover { background: #6aa3f5; box-shadow: 0 4px 16px rgba(87,148,242,.5); transform: translateY(-1px); }
|
|
346
|
+
.btn-ghost { background: transparent; color: var(--text2); border-color: var(--border2); }
|
|
347
|
+
.btn-ghost:hover { background: var(--panel2); color: var(--text); }
|
|
348
|
+
.btn-danger { background: transparent; color: var(--red); border-color: rgba(241,95,95,.3); }
|
|
349
|
+
.btn-danger:hover { background: var(--red-bg); border-color: rgba(241,95,95,.5); }
|
|
350
|
+
.btn-row {
|
|
351
|
+
display: flex; gap: 8px; padding: 12px 14px;
|
|
352
|
+
border-top: 1px solid var(--border); background: var(--panel2);
|
|
353
|
+
}
|
|
354
|
+
.btn-icon {
|
|
355
|
+
width: 26px; height: 26px; display: inline-flex;
|
|
356
|
+
align-items: center; justify-content: center;
|
|
357
|
+
background: var(--panel2); border: 1px solid var(--border);
|
|
358
|
+
border-radius: var(--r); color: var(--text2);
|
|
359
|
+
cursor: pointer; transition: all .12s; padding: 0;
|
|
360
|
+
}
|
|
361
|
+
.btn-icon:hover { background: var(--panel3); color: var(--text); border-color: var(--border2); }
|
|
362
|
+
.btn-icon.del:hover { background: var(--red-bg); color: var(--red); border-color: rgba(241,95,95,.3); }
|
|
363
|
+
|
|
364
|
+
/* ── Toolbar ─────────────────────────────────────── */
|
|
365
|
+
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
366
|
+
.search-wrap { position: relative; flex: 1; max-width: 260px; }
|
|
367
|
+
.search-wrap .ico {
|
|
368
|
+
position: absolute; left: 9px; top: 50%;
|
|
369
|
+
transform: translateY(-50%); color: var(--text3);
|
|
370
|
+
pointer-events: none; display: flex;
|
|
371
|
+
}
|
|
372
|
+
.search-input {
|
|
373
|
+
width: 100%; background: var(--panel); border: 1px solid var(--border);
|
|
374
|
+
color: var(--text); padding: 7px 10px 7px 30px;
|
|
375
|
+
border-radius: var(--r2); font-size: 12.5px; font-family: var(--sans);
|
|
376
|
+
outline: none; transition: border-color .15s;
|
|
377
|
+
}
|
|
378
|
+
.search-input:focus { border-color: var(--blue); }
|
|
379
|
+
.search-input::placeholder { color: var(--text3); }
|
|
380
|
+
|
|
381
|
+
/* ── Data table ──────────────────────────────────── */
|
|
382
|
+
.data-table { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden; box-shadow: var(--shadow); }
|
|
383
|
+
.table-head {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: minmax(0,1fr) 80px 110px 72px;
|
|
386
|
+
padding: 8px 14px; background: var(--panel2);
|
|
387
|
+
border-bottom: 1px solid var(--border);
|
|
388
|
+
font-size: 10.5px; font-weight: 700;
|
|
389
|
+
letter-spacing: .7px; text-transform: uppercase; color: var(--text3); gap: 12px;
|
|
390
|
+
}
|
|
391
|
+
.table-row {
|
|
392
|
+
display: grid;
|
|
393
|
+
grid-template-columns: minmax(0,1fr) 80px 110px 72px;
|
|
394
|
+
padding: 9px 14px; border-top: 1px solid var(--border);
|
|
395
|
+
align-items: center; gap: 12px; transition: background .1s;
|
|
396
|
+
}
|
|
397
|
+
.table-row:nth-child(even) { background: rgba(255,255,255,.012); }
|
|
398
|
+
.table-row:hover { background: var(--panel2); }
|
|
399
|
+
.file-path { font-family: var(--mono); font-size: 11.5px; color: var(--blue); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
400
|
+
.chunk-num { font-family: var(--mono); font-size: 12px; color: var(--text2); }
|
|
401
|
+
.row-acts { display: flex; gap: 4px; align-items: center; }
|
|
402
|
+
|
|
403
|
+
/* ── Empty state ─────────────────────────────────── */
|
|
404
|
+
.empty {
|
|
405
|
+
display: flex; flex-direction: column; align-items: center;
|
|
406
|
+
justify-content: center; padding: 40px 20px;
|
|
407
|
+
gap: 8px; color: var(--text3);
|
|
408
|
+
}
|
|
409
|
+
.empty-icon { opacity: .25; }
|
|
410
|
+
.empty-title { font-size: 13px; font-weight: 600; color: var(--text2); }
|
|
411
|
+
.empty-hint { font-size: 11.5px; color: var(--text3); }
|
|
412
|
+
|
|
413
|
+
/* ── Sessions ────────────────────────────────────── */
|
|
414
|
+
.session-list { display: flex; flex-direction: column; gap: 8px; }
|
|
415
|
+
.session-card {
|
|
416
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden;
|
|
417
|
+
box-shadow: var(--shadow);
|
|
418
|
+
transition: border-color .25s var(--ease), box-shadow .25s var(--ease), transform .25s var(--ease);
|
|
419
|
+
}
|
|
420
|
+
.session-card:hover { border-color: var(--border2); box-shadow: var(--shadow-hover); transform: translateY(-1px); }
|
|
421
|
+
.session-header { display: flex; align-items: center; padding: 11px 14px; cursor: pointer; gap: 10px; }
|
|
422
|
+
.session-header:hover { background: var(--panel2); }
|
|
423
|
+
.session-info { flex: 1; min-width: 0; }
|
|
424
|
+
.session-name { font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
425
|
+
.session-meta { font-size: 11.5px; color: var(--text2); margin-top: 2px; font-family: var(--mono); display: flex; gap: 8px; flex-wrap: wrap; }
|
|
426
|
+
.session-meta span { color: var(--text3); }
|
|
427
|
+
.session-meta b { color: var(--text2); font-weight: 400; }
|
|
428
|
+
.chevron { color: var(--text3); transition: transform .2s; flex-shrink: 0; }
|
|
429
|
+
.chevron.open { transform: rotate(90deg); }
|
|
430
|
+
.session-body { display: none; border-top: 1px solid var(--border); padding: 12px 14px; background: var(--bg2); }
|
|
431
|
+
.session-body.open { display: block; }
|
|
432
|
+
.decisions-label { font-size: 10px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase; color: var(--text3); margin-bottom: 8px; }
|
|
433
|
+
.decision-item {
|
|
434
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
435
|
+
border-left: 3px solid var(--blue); border-radius: var(--r);
|
|
436
|
+
padding: 7px 11px; font-size: 12.5px; color: var(--text);
|
|
437
|
+
margin-bottom: 5px; line-height: 1.55;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ── Savings ─────────────────────────────────────── */
|
|
441
|
+
.savings-summary {
|
|
442
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
443
|
+
margin: 14px 14px 0; padding: 12px 14px;
|
|
444
|
+
background: var(--green-bg); border: 1px solid rgba(62,207,142,.25);
|
|
445
|
+
border-radius: var(--r2);
|
|
446
|
+
box-shadow: 0 0 20px rgba(62,207,142,.08);
|
|
447
|
+
}
|
|
448
|
+
.savings-summary-lbl { font-size: 12px; color: var(--text2); }
|
|
449
|
+
.savings-summary-val { font-size: 20px; font-weight: 800; font-family: var(--mono); color: var(--green); letter-spacing: -1px; }
|
|
450
|
+
.savings-summary-pct { font-size: 12px; color: var(--green); opacity: .7; margin-left: 4px; }
|
|
451
|
+
|
|
452
|
+
/* Compression */
|
|
453
|
+
.comp-label { font-size: 12px; color: var(--text2); margin-bottom: 10px; line-height: 1.6; }
|
|
454
|
+
.comp-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; }
|
|
455
|
+
.comp-btn {
|
|
456
|
+
padding: 9px 4px; border-radius: var(--r);
|
|
457
|
+
font-size: 11.5px; font-weight: 600; font-family: var(--mono);
|
|
458
|
+
background: var(--panel2); border: 1px solid var(--border);
|
|
459
|
+
color: var(--text2); cursor: pointer; text-align: center;
|
|
460
|
+
transition: all .12s; letter-spacing: .3px;
|
|
461
|
+
}
|
|
462
|
+
.comp-btn:hover { border-color: var(--border2); color: var(--text); background: var(--panel3); }
|
|
463
|
+
.comp-btn.active { background: var(--blue-bg); border-color: rgba(87,148,242,.4); color: var(--blue); }
|
|
464
|
+
|
|
465
|
+
/* ── Banner ──────────────────────────────────────── */
|
|
466
|
+
.banner {
|
|
467
|
+
display: flex; align-items: center; gap: 10px;
|
|
468
|
+
background: var(--blue-bg); border: 1px solid rgba(87,148,242,.25);
|
|
469
|
+
border-radius: var(--r2); padding: 10px 14px;
|
|
470
|
+
font-size: 12.5px; color: #7eb8ff; margin-bottom: 16px;
|
|
471
|
+
}
|
|
472
|
+
.banner code { font-family: var(--mono); background: rgba(87,148,242,.15); padding: 1px 6px; border-radius: var(--r); font-size: 11.5px; }
|
|
473
|
+
|
|
474
|
+
/* ── Toast ───────────────────────────────────────── */
|
|
475
|
+
.toast {
|
|
476
|
+
position: fixed; bottom: 20px; right: 20px;
|
|
477
|
+
background: var(--panel2); border: 1px solid var(--border2);
|
|
478
|
+
border-left: 3px solid var(--blue); border-radius: var(--r2);
|
|
479
|
+
padding: 10px 14px; font-size: 12.5px; color: var(--text);
|
|
480
|
+
box-shadow: 0 4px 20px rgba(0,0,0,.5);
|
|
481
|
+
opacity: 0; transform: translateX(12px);
|
|
482
|
+
transition: opacity .18s, transform .18s;
|
|
483
|
+
pointer-events: none; z-index: 200; max-width: 300px;
|
|
484
|
+
}
|
|
485
|
+
.toast.show { opacity: 1; transform: translateX(0); }
|
|
486
|
+
|
|
487
|
+
/* ── Spinner ─────────────────────────────────────── */
|
|
488
|
+
.spinner {
|
|
489
|
+
display: inline-block; width: 12px; height: 12px;
|
|
490
|
+
border: 1.5px solid var(--border2); border-top-color: var(--blue);
|
|
491
|
+
border-radius: 50%; animation: spin .6s linear infinite; vertical-align: middle;
|
|
492
|
+
}
|
|
493
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
494
|
+
|
|
495
|
+
/* ── Fade-in animation ───────────────────────────── */
|
|
496
|
+
@keyframes fadeUp {
|
|
497
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
498
|
+
to { opacity: 1; transform: translateY(0); }
|
|
499
|
+
}
|
|
500
|
+
.fade-in {
|
|
501
|
+
animation: fadeUp .5s var(--ease) both;
|
|
502
|
+
}
|
|
503
|
+
.stat-row .stat-card:nth-child(1) { animation-delay: 0ms; }
|
|
504
|
+
.stat-row .stat-card:nth-child(2) { animation-delay: 60ms; }
|
|
505
|
+
.stat-row .stat-card:nth-child(3) { animation-delay: 120ms; }
|
|
506
|
+
.stat-row .stat-card:nth-child(4) { animation-delay: 180ms; }
|
|
507
|
+
.panel-row { animation: fadeUp .6s var(--ease) both; }
|
|
508
|
+
.panel-row:nth-child(3) { animation-delay: 200ms; }
|
|
509
|
+
.panel-row:nth-child(4) { animation-delay: 320ms; }
|
|
510
|
+
.page.active .stat-card,
|
|
511
|
+
.page.active .panel-row,
|
|
512
|
+
.page.active .data-table,
|
|
513
|
+
.page.active .session-list,
|
|
514
|
+
.page.active .savings-stat-row { animation: fadeUp .5s var(--ease) both; }
|
|
515
|
+
|
|
516
|
+
/* ── Scrollbar ───────────────────────────────────── */
|
|
517
|
+
::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
518
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
519
|
+
::-webkit-scrollbar-thumb { background: var(--panel3); border-radius: 3px; }
|
|
520
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border2); }
|
|
521
|
+
</style>
|
|
522
|
+
</head>
|
|
523
|
+
<body>
|
|
524
|
+
|
|
525
|
+
<!-- Top bar -->
|
|
526
|
+
<div class="topbar">
|
|
527
|
+
<div class="topbar-logo">
|
|
528
|
+
<div class="logo-icon">CCE</div>
|
|
529
|
+
<span class="topbar-title">Context Engine</span>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="breadcrumb">
|
|
532
|
+
<span>Dashboards</span>
|
|
533
|
+
<span class="breadcrumb-sep">/</span>
|
|
534
|
+
<span class="breadcrumb-project" id="nav-project">loading\u2026</span>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="topbar-right">
|
|
537
|
+
<div class="live-badge">
|
|
538
|
+
<div class="live-dot"></div>
|
|
539
|
+
LIVE 5s
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<!-- Layout -->
|
|
545
|
+
<div class="layout">
|
|
546
|
+
|
|
547
|
+
<!-- Sidebar -->
|
|
548
|
+
<aside class="sidebar">
|
|
549
|
+
<div class="nav-section-label">General</div>
|
|
550
|
+
|
|
551
|
+
<button class="nav-item active" onclick="showPage('overview')">
|
|
552
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
|
553
|
+
Overview
|
|
554
|
+
</button>
|
|
555
|
+
|
|
556
|
+
<button class="nav-item" onclick="showPage('files')">
|
|
557
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
558
|
+
Files
|
|
559
|
+
<span class="nav-count" id="nav-files-count">\u2014</span>
|
|
560
|
+
</button>
|
|
561
|
+
|
|
562
|
+
<button class="nav-item" onclick="showPage('sessions')">
|
|
563
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
564
|
+
Sessions
|
|
565
|
+
<span class="nav-count" id="nav-sessions-count">\u2014</span>
|
|
566
|
+
</button>
|
|
567
|
+
|
|
568
|
+
<button class="nav-item" onclick="showPage('memory')">
|
|
569
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9 1.65 1.65 0 0 0 4.27 7.18l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
570
|
+
Memory
|
|
571
|
+
</button>
|
|
572
|
+
|
|
573
|
+
<div class="nav-section-label" style="margin-top:4px">Analytics</div>
|
|
574
|
+
|
|
575
|
+
<button class="nav-item" onclick="showPage('savings')">
|
|
576
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
577
|
+
Savings
|
|
578
|
+
</button>
|
|
579
|
+
|
|
580
|
+
<div class="sidebar-spacer"></div>
|
|
581
|
+
</aside>
|
|
582
|
+
|
|
583
|
+
<!-- Main content -->
|
|
584
|
+
<main class="main">
|
|
585
|
+
|
|
586
|
+
<!-- ═══════════════════ OVERVIEW ═══════════════════ -->
|
|
587
|
+
<div class="page active" id="page-overview">
|
|
588
|
+
<div class="page-hdr">
|
|
589
|
+
<div class="page-hdr-left">
|
|
590
|
+
<div class="page-hdr-title">Overview</div>
|
|
591
|
+
<div class="page-hdr-sub">Index health, token metrics and session activity</div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<div id="uninit-banner" class="banner" style="display:none">
|
|
596
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
597
|
+
Index not initialised \u2014 run <code>cce init</code> then <code>cce index</code> in your project directory.
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<!-- Stat cards with inline sparklines -->
|
|
601
|
+
<div class="stat-row">
|
|
602
|
+
<div class="stat-card blue">
|
|
603
|
+
<div class="stat-left">
|
|
604
|
+
<div class="stat-label">Chunks indexed</div>
|
|
605
|
+
<div class="stat-num blue" id="stat-chunks">\u2014</div>
|
|
606
|
+
</div>
|
|
607
|
+
<div class="stat-spark" id="spark-chunks"></div>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="stat-card green">
|
|
610
|
+
<div class="stat-left">
|
|
611
|
+
<div class="stat-label">Files indexed</div>
|
|
612
|
+
<div class="stat-num green" id="stat-files">\u2014</div>
|
|
613
|
+
</div>
|
|
614
|
+
<svg width="32" height="32" viewBox="0 0 32 32" id="spark-files-ring" style="flex-shrink:0;margin-left:8px"></svg>
|
|
615
|
+
</div>
|
|
616
|
+
<div class="stat-card yellow">
|
|
617
|
+
<div class="stat-left">
|
|
618
|
+
<div class="stat-label">Queries run</div>
|
|
619
|
+
<div class="stat-num yellow" id="stat-queries">\u2014</div>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="stat-spark" id="spark-queries"></div>
|
|
622
|
+
</div>
|
|
623
|
+
<div class="stat-card purple">
|
|
624
|
+
<div class="stat-left">
|
|
625
|
+
<div class="stat-label">Tokens saved</div>
|
|
626
|
+
<div class="stat-num purple" id="stat-saved">\u2014</div>
|
|
627
|
+
</div>
|
|
628
|
+
<svg width="32" height="32" viewBox="0 0 32 32" id="spark-saved-ring" style="flex-shrink:0;margin-left:8px"></svg>
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<!-- Row 1: Token Savings Donut + File Health Donut -->
|
|
633
|
+
<div class="panel-row">
|
|
634
|
+
<div class="panel">
|
|
635
|
+
<div class="panel-head">
|
|
636
|
+
<div class="panel-title">
|
|
637
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
638
|
+
Token Savings
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
<div id="chart-token-savings">
|
|
642
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<div class="panel">
|
|
647
|
+
<div class="panel-head">
|
|
648
|
+
<div class="panel-title">
|
|
649
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
650
|
+
File Health
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
<div id="chart-file-health">
|
|
654
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<!-- Row 2: Top Files Bar Chart + Session Activity -->
|
|
660
|
+
<div class="panel-row">
|
|
661
|
+
<div class="panel">
|
|
662
|
+
<div class="panel-head">
|
|
663
|
+
<div class="panel-title">
|
|
664
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="4" height="18"/><rect x="10" y="8" width="4" height="13"/><rect x="18" y="13" width="4" height="8"/></svg>
|
|
665
|
+
Top Files by Chunks
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
<div id="chart-top-files">
|
|
669
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<div class="panel">
|
|
674
|
+
<div class="panel-head">
|
|
675
|
+
<div class="panel-title">
|
|
676
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
677
|
+
Session Decisions
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
<div id="chart-sessions-bars">
|
|
681
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
682
|
+
</div>
|
|
683
|
+
<div class="btn-row">
|
|
684
|
+
<button class="btn btn-primary" onclick="doReindex(false)" id="btn-reindex-changed">
|
|
685
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
686
|
+
Reindex changed
|
|
687
|
+
</button>
|
|
688
|
+
<button class="btn btn-ghost" onclick="doReindex(true)" id="btn-reindex-full">Full reindex</button>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
694
|
+
<!-- ═══════════════════ FILES ═══════════════════ -->
|
|
695
|
+
<div class="page" id="page-files">
|
|
696
|
+
<div class="page-hdr">
|
|
697
|
+
<div class="page-hdr-left">
|
|
698
|
+
<div class="page-hdr-title">Files</div>
|
|
699
|
+
<div class="page-hdr-sub">Indexed files with staleness status and chunk counts</div>
|
|
700
|
+
</div>
|
|
701
|
+
<div style="display:flex;gap:8px">
|
|
702
|
+
<button class="btn btn-ghost" onclick="doExport()">
|
|
703
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
704
|
+
Export
|
|
705
|
+
</button>
|
|
706
|
+
<button class="btn btn-danger" onclick="doClear()">
|
|
707
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>
|
|
708
|
+
Clear index
|
|
709
|
+
</button>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<!-- File status distribution bar -->
|
|
714
|
+
<div id="files-dist-bar" style="margin-bottom:12px"></div>
|
|
715
|
+
|
|
716
|
+
<div class="toolbar">
|
|
717
|
+
<div class="search-wrap">
|
|
718
|
+
<span class="ico">
|
|
719
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
720
|
+
</span>
|
|
721
|
+
<input class="search-input" placeholder="Filter by path\u2026" oninput="filterFiles(this.value)">
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="data-table">
|
|
725
|
+
<div class="table-head"><div>Path</div><div>Chunks</div><div>Status</div><div>Actions</div></div>
|
|
726
|
+
<div id="file-rows"><div class="empty"><div class="spinner"></div></div></div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<!-- ═══════════════════ SESSIONS ═══════════════════ -->
|
|
731
|
+
<div class="page" id="page-sessions">
|
|
732
|
+
<div class="page-hdr">
|
|
733
|
+
<div class="page-hdr-left">
|
|
734
|
+
<div class="page-hdr-title">Sessions</div>
|
|
735
|
+
<div class="page-hdr-sub">Captured Claude coding sessions and architectural decisions</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
<div id="session-list"><div class="empty"><div class="spinner"></div></div></div>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<!-- ═══════════════════ SAVINGS ═══════════════════ -->
|
|
742
|
+
<div class="page" id="page-savings">
|
|
743
|
+
<div class="page-hdr">
|
|
744
|
+
<div class="page-hdr-left">
|
|
745
|
+
<div class="page-hdr-title">Savings</div>
|
|
746
|
+
<div class="page-hdr-sub">Token reduction metrics and output compression settings</div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
<!-- Savings stat cards -->
|
|
751
|
+
<div class="savings-stat-row">
|
|
752
|
+
<div class="stat-card yellow">
|
|
753
|
+
<div class="stat-left">
|
|
754
|
+
<div class="stat-label">Queries processed</div>
|
|
755
|
+
<div class="stat-num yellow" id="sv-queries">\u2014</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
<div class="stat-card green">
|
|
759
|
+
<div class="stat-left">
|
|
760
|
+
<div class="stat-label">Tokens saved</div>
|
|
761
|
+
<div class="stat-num green" id="sv-saved">\u2014</div>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="stat-card purple">
|
|
765
|
+
<div class="stat-left">
|
|
766
|
+
<div class="stat-label">Savings rate</div>
|
|
767
|
+
<div class="stat-num purple" id="sv-pct">\u2014</div>
|
|
768
|
+
</div>
|
|
769
|
+
<svg width="32" height="32" viewBox="0 0 32 32" id="sv-ring" style="flex-shrink:0;margin-left:8px"></svg>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<!-- Chart row: big donut + stacked breakdown -->
|
|
774
|
+
<div class="panel-row" style="margin-bottom:10px">
|
|
775
|
+
<div class="panel">
|
|
776
|
+
<div class="panel-head">
|
|
777
|
+
<div class="panel-title">
|
|
778
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
|
779
|
+
Token Usage Breakdown
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div id="sv-donut-chart">
|
|
783
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
<div class="panel">
|
|
788
|
+
<div class="panel-head">
|
|
789
|
+
<div class="panel-title">
|
|
790
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="4" rx="1"/><rect x="2" y="10" width="20" height="4" rx="1"/><rect x="2" y="17" width="20" height="4" rx="1"/></svg>
|
|
791
|
+
Token Budget
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
<div id="sv-budget-panel">
|
|
795
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<!-- Compression panel -->
|
|
801
|
+
<div class="panel">
|
|
802
|
+
<div class="panel-head">
|
|
803
|
+
<div class="panel-title">
|
|
804
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>
|
|
805
|
+
Output Compression
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
<div class="panel-body">
|
|
809
|
+
<p class="comp-label">Controls how Claude compresses its responses. Higher levels reduce output token usage at the cost of verbosity.</p>
|
|
810
|
+
<div class="comp-grid" id="comp-buttons">
|
|
811
|
+
<button class="comp-btn" onclick="setCompression('off')">off</button>
|
|
812
|
+
<button class="comp-btn" onclick="setCompression('lite')">lite</button>
|
|
813
|
+
<button class="comp-btn" onclick="setCompression('standard')">standard</button>
|
|
814
|
+
<button class="comp-btn" onclick="setCompression('max')">max</button>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
<!-- ═══════════════════ MEMORY ═══════════════════ -->
|
|
821
|
+
<div class="page" id="page-memory">
|
|
822
|
+
<div class="page-hdr">
|
|
823
|
+
<div class="page-hdr-left">
|
|
824
|
+
<div class="page-hdr-title">Memory</div>
|
|
825
|
+
<div class="page-hdr-sub">Cross-session memory store (memory.db) — sessions, timelines, decisions</div>
|
|
826
|
+
</div>
|
|
827
|
+
<div class="page-hdr-right">
|
|
828
|
+
<div class="comp-grid" style="display:flex; gap:6px">
|
|
829
|
+
<button class="comp-btn comp-btn-active" id="mem-tab-sessions" onclick="memShowTab('sessions')">Sessions</button>
|
|
830
|
+
<button class="comp-btn" id="mem-tab-decisions" onclick="memShowTab('decisions')">Decisions</button>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<!-- Sessions list + drill-down view (kept side by side; the drill panel
|
|
836
|
+
appears when a session is selected). -->
|
|
837
|
+
<div class="memory-pane" id="mem-pane-sessions">
|
|
838
|
+
<div class="data-table">
|
|
839
|
+
<div class="table-head"><div>Session</div><div>Started</div><div>Status</div><div>Prompts</div><div>Rollup</div></div>
|
|
840
|
+
<div id="mem-sessions-rows"></div>
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div class="panel" id="mem-timeline-panel" style="margin-top:14px; display:none">
|
|
844
|
+
<div class="panel-head">
|
|
845
|
+
<div class="panel-title" id="mem-timeline-title">Session timeline</div>
|
|
846
|
+
</div>
|
|
847
|
+
<div class="panel-body" id="mem-timeline-body"></div>
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<!-- Decisions search -->
|
|
852
|
+
<div class="memory-pane" id="mem-pane-decisions" style="display:none">
|
|
853
|
+
<div class="panel">
|
|
854
|
+
<div class="panel-head">
|
|
855
|
+
<div class="panel-title">Decisions search</div>
|
|
856
|
+
</div>
|
|
857
|
+
<div class="panel-body">
|
|
858
|
+
<div style="display:flex; gap:8px; margin-bottom:10px">
|
|
859
|
+
<input id="mem-decision-q" type="text" placeholder="search decisions (FTS5)…" style="flex:1; padding:6px 10px; background:var(--panel2); border:1px solid var(--border); color:var(--text); border-radius:4px">
|
|
860
|
+
<select id="mem-decision-source" style="padding:6px 10px; background:var(--panel2); border:1px solid var(--border); color:var(--text); border-radius:4px">
|
|
861
|
+
<option value="">all sources</option>
|
|
862
|
+
<option value="manual">manual</option>
|
|
863
|
+
<option value="auto">auto</option>
|
|
864
|
+
<option value="migrated">migrated</option>
|
|
865
|
+
</select>
|
|
866
|
+
<button class="comp-btn" onclick="memDecisionSearch()">search</button>
|
|
867
|
+
</div>
|
|
868
|
+
<div class="data-table">
|
|
869
|
+
<div class="table-head"><div>Decision</div><div>Reason</div><div>Source</div><div>When</div></div>
|
|
870
|
+
<div id="mem-decision-rows"></div>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
</main>
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
<div class="toast" id="toast"></div>
|
|
881
|
+
|
|
882
|
+
<script>
|
|
883
|
+
var API = '';
|
|
884
|
+
var allFiles = [];
|
|
885
|
+
var currentLevel = 'standard';
|
|
886
|
+
var PAGES = ['overview','files','sessions','memory','savings'];
|
|
887
|
+
|
|
888
|
+
// Pick up an optional bearer token from the URL (?token=...). The server
|
|
889
|
+
// only enforces it on mutating endpoints when CCE_DASHBOARD_TOKEN is set;
|
|
890
|
+
// when it's not set the token query param is harmless. Monkey-patches
|
|
891
|
+
// fetch() to attach the header on every request — sending it on GETs as
|
|
892
|
+
// well is harmless and keeps the rest of the code untouched.
|
|
893
|
+
(function() {
|
|
894
|
+
var token = new URLSearchParams(window.location.search).get('token');
|
|
895
|
+
if (!token) return;
|
|
896
|
+
var origFetch = window.fetch.bind(window);
|
|
897
|
+
window.fetch = function(input, init) {
|
|
898
|
+
init = init || {};
|
|
899
|
+
var headers = new Headers(init.headers || (input && input.headers) || {});
|
|
900
|
+
if (!headers.has('Authorization')) {
|
|
901
|
+
headers.set('Authorization', 'Bearer ' + token);
|
|
902
|
+
}
|
|
903
|
+
init.headers = headers;
|
|
904
|
+
return origFetch(input, init);
|
|
905
|
+
};
|
|
906
|
+
})();
|
|
907
|
+
|
|
908
|
+
// ── Chart helpers ────────────────────────────────
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Draw SVG donut segments into an <svg> element.
|
|
912
|
+
* segments: [{value, color}]
|
|
913
|
+
* The SVG must be 132x132 with cx=66, cy=66, r=52.
|
|
914
|
+
*/
|
|
915
|
+
function drawDonutSVG(svgEl, segments) {
|
|
916
|
+
var r = 52, cx = 66, cy = 66;
|
|
917
|
+
var circ = 2 * Math.PI * r;
|
|
918
|
+
var total = segments.reduce(function(a,s){ return a+(s.value||0); }, 0);
|
|
919
|
+
// background track
|
|
920
|
+
var html = '<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="var(--panel3)" stroke-width="13"/>';
|
|
921
|
+
var acc = 0;
|
|
922
|
+
segments.forEach(function(seg) {
|
|
923
|
+
if (!seg.value || !total) return;
|
|
924
|
+
var dash = seg.value / total * circ;
|
|
925
|
+
var gap = circ - dash;
|
|
926
|
+
// offset: start at top (12 o'clock) = circ/4, then advance by accumulated
|
|
927
|
+
var offset = circ / 4 - acc;
|
|
928
|
+
html += '<circle cx="'+cx+'" cy="'+cy+'" r="'+r
|
|
929
|
+
+'" fill="none" stroke="'+seg.color+'" stroke-width="13"'
|
|
930
|
+
+' stroke-dasharray="'+dash+' '+gap+'"'
|
|
931
|
+
+' stroke-dashoffset="'+offset+'"/>';
|
|
932
|
+
acc += dash;
|
|
933
|
+
});
|
|
934
|
+
svgEl.innerHTML = html;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** Render a full donut panel: SVG + legend. */
|
|
938
|
+
function renderDonutPanel(containerId, segments, centerBig, centerSub, centerColor) {
|
|
939
|
+
var el = document.getElementById(containerId);
|
|
940
|
+
if (!el) return;
|
|
941
|
+
var total = segments.reduce(function(a,s){ return a+(s.value||0); }, 0);
|
|
942
|
+
var legendHtml = segments.map(function(s) {
|
|
943
|
+
var pct = total > 0 ? Math.round(s.value/total*100) : 0;
|
|
944
|
+
return '<div class="legend-item">'
|
|
945
|
+
+'<div class="legend-color" style="background:'+s.color+'"></div>'
|
|
946
|
+
+'<span class="legend-label">'+s.label+'</span>'
|
|
947
|
+
+'<span class="legend-val">'+s.display+'</span>'
|
|
948
|
+
+'<span class="legend-pct">'+pct+'%</span>'
|
|
949
|
+
+'</div>';
|
|
950
|
+
}).join('');
|
|
951
|
+
el.innerHTML =
|
|
952
|
+
'<div class="donut-wrap">'
|
|
953
|
+
+'<svg class="donut-svg" id="'+containerId+'-svg" width="132" height="132" viewBox="0 0 132 132">'
|
|
954
|
+
+'<text x="66" y="62" text-anchor="middle" font-family="monospace" font-size="19" font-weight="800" fill="'+(centerColor||'var(--text)')+'">'+centerBig+'</text>'
|
|
955
|
+
+'<text x="66" y="78" text-anchor="middle" font-family="sans-serif" font-size="10" fill="var(--text3)">'+centerSub+'</text>'
|
|
956
|
+
+'</svg>'
|
|
957
|
+
+'<div class="donut-legend">'+legendHtml+'</div>'
|
|
958
|
+
+'</div>';
|
|
959
|
+
var svg = document.getElementById(containerId+'-svg');
|
|
960
|
+
if (svg) drawDonutSVG(svg, segments);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Render mini ring in a 32x32 SVG (for stat cards). */
|
|
964
|
+
function drawMiniRing(svgId, pct, color) {
|
|
965
|
+
var svg = document.getElementById(svgId);
|
|
966
|
+
if (!svg) return;
|
|
967
|
+
var r = 12, cx = 16, cy = 16;
|
|
968
|
+
var circ = 2 * Math.PI * r;
|
|
969
|
+
var dash = Math.min(pct/100, 1) * circ;
|
|
970
|
+
svg.innerHTML =
|
|
971
|
+
'<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="var(--panel3)" stroke-width="4"/>'
|
|
972
|
+
+'<circle cx="'+cx+'" cy="'+cy+'" r="'+r+'" fill="none" stroke="'+color+'" stroke-width="4"'
|
|
973
|
+
+' stroke-dasharray="'+dash+' '+(circ-dash)+'" stroke-dashoffset="'+(circ/4)+'" stroke-linecap="round"/>';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Render sparkline bars in a stat-spark container. */
|
|
977
|
+
function drawSparkline(containerId, values, color) {
|
|
978
|
+
var el = document.getElementById(containerId);
|
|
979
|
+
if (!el) return;
|
|
980
|
+
var max = Math.max.apply(null, values);
|
|
981
|
+
if (!max) { el.innerHTML = ''; return; }
|
|
982
|
+
el.innerHTML = values.map(function(v, i) {
|
|
983
|
+
var h = Math.max(10, Math.round(v/max*32));
|
|
984
|
+
var opacity = 0.3 + (i / values.length) * 0.7;
|
|
985
|
+
return '<div class="spark-bar" style="height:'+h+'px;background:'+color+';opacity:'+opacity+'"></div>';
|
|
986
|
+
}).join('');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Render horizontal bar chart. items: [{label,value,color}] */
|
|
990
|
+
function renderHBarChart(containerId, items) {
|
|
991
|
+
var el = document.getElementById(containerId);
|
|
992
|
+
if (!el) return;
|
|
993
|
+
if (!items.length) {
|
|
994
|
+
el.innerHTML = '<div class="empty"><span class="empty-title">No data yet</span></div>';
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
var max = Math.max.apply(null, items.map(function(i){ return i.value||0; }));
|
|
998
|
+
el.innerHTML = '<div class="hbar-chart">'
|
|
999
|
+
+ items.map(function(item) {
|
|
1000
|
+
var pct = max > 0 ? item.value/max*100 : 0;
|
|
1001
|
+
var name = item.label.split('/').pop() || item.label;
|
|
1002
|
+
return '<div class="hbar-row">'
|
|
1003
|
+
+'<div class="hbar-label" title="'+item.label+'">'+name+'</div>'
|
|
1004
|
+
+'<div class="hbar-track"><div class="hbar-fill" style="width:'+pct+'%;background:'+item.color+'"></div></div>'
|
|
1005
|
+
+'<div class="hbar-num">'+item.value+'</div>'
|
|
1006
|
+
+'</div>';
|
|
1007
|
+
}).join('')
|
|
1008
|
+
+'</div>';
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Render vertical bar chart. items: [{label,value}] */
|
|
1012
|
+
function renderVBarChart(containerId, items, color) {
|
|
1013
|
+
var el = document.getElementById(containerId);
|
|
1014
|
+
if (!el) return;
|
|
1015
|
+
if (!items.length) {
|
|
1016
|
+
el.innerHTML = '<div class="empty"><span class="empty-title">No sessions yet</span></div>';
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
var max = Math.max.apply(null, items.map(function(i){ return i.value||0; })) || 1;
|
|
1020
|
+
var bars = items.map(function(item, i) {
|
|
1021
|
+
var h = Math.max(4, Math.round(item.value/max*80));
|
|
1022
|
+
var opacity = 0.35 + (i / items.length) * 0.65;
|
|
1023
|
+
return '<div class="vbar-col">'
|
|
1024
|
+
+'<div class="vbar-fill" style="height:'+h+'px;background:'+color+';opacity:'+opacity+'"></div>'
|
|
1025
|
+
+'</div>';
|
|
1026
|
+
}).join('');
|
|
1027
|
+
var labels = items.map(function(item) {
|
|
1028
|
+
return '<div class="vbar-lbl" title="'+item.label+'">'+item.label+'</div>';
|
|
1029
|
+
}).join('');
|
|
1030
|
+
el.innerHTML =
|
|
1031
|
+
'<div class="vbar-chart">'+bars+'</div>'
|
|
1032
|
+
+'<div class="vbar-labels">'+labels+'</div>';
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── Page routing ──────────────────────────────────
|
|
1036
|
+
|
|
1037
|
+
function showPage(name) {
|
|
1038
|
+
PAGES.forEach(function(p) {
|
|
1039
|
+
document.getElementById('page-'+p).classList.toggle('active', p===name);
|
|
1040
|
+
});
|
|
1041
|
+
document.querySelectorAll('.nav-item').forEach(function(el, i) {
|
|
1042
|
+
el.classList.toggle('active', PAGES[i]===name);
|
|
1043
|
+
});
|
|
1044
|
+
if (name==='files') loadFiles();
|
|
1045
|
+
if (name==='sessions') loadSessions();
|
|
1046
|
+
if (name==='memory') loadMemorySessions();
|
|
1047
|
+
if (name==='savings') loadSavings();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ── Memory page (PR 5) ───────────────────────────
|
|
1051
|
+
|
|
1052
|
+
function memShowTab(tab) {
|
|
1053
|
+
document.getElementById('mem-pane-sessions').style.display = tab==='sessions' ? '' : 'none';
|
|
1054
|
+
document.getElementById('mem-pane-decisions').style.display = tab==='decisions' ? '' : 'none';
|
|
1055
|
+
document.getElementById('mem-tab-sessions').classList.toggle('comp-btn-active', tab==='sessions');
|
|
1056
|
+
document.getElementById('mem-tab-decisions').classList.toggle('comp-btn-active', tab==='decisions');
|
|
1057
|
+
if (tab==='decisions') memDecisionSearch();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function _esc(s) {
|
|
1061
|
+
return String(s||'').replace(/[&<>"']/g, function(c) {
|
|
1062
|
+
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function loadMemorySessions() {
|
|
1067
|
+
try {
|
|
1068
|
+
var r = await fetch(API+'/api/memory/sessions');
|
|
1069
|
+
var rows = await r.json();
|
|
1070
|
+
var box = document.getElementById('mem-sessions-rows');
|
|
1071
|
+
if (!rows.length) {
|
|
1072
|
+
box.innerHTML = '<div class="empty">No sessions captured yet. Start a Claude Code session in this project — hooks will populate the timeline.</div>';
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
box.innerHTML = rows.map(function(s) {
|
|
1076
|
+
var rollup = (s.rollup_summary || '').slice(0, 80);
|
|
1077
|
+
return ''
|
|
1078
|
+
+ '<div class="table-row" onclick="loadMemoryTimeline(\\\''+_esc(s.id)+'\\\')" style="cursor:pointer">'
|
|
1079
|
+
+ '<div><code>'+_esc(s.id)+'</code></div>'
|
|
1080
|
+
+ '<div>'+_esc(s.started_at||'')+'</div>'
|
|
1081
|
+
+ '<div>'+_esc(s.status||'')+'</div>'
|
|
1082
|
+
+ '<div>'+_esc(s.prompt_count||0)+'</div>'
|
|
1083
|
+
+ '<div>'+_esc(rollup)+(rollup && s.rollup_summary && s.rollup_summary.length>80?'…':'')+'</div>'
|
|
1084
|
+
+ '</div>';
|
|
1085
|
+
}).join('');
|
|
1086
|
+
} catch(e) {
|
|
1087
|
+
document.getElementById('mem-sessions-rows').innerHTML =
|
|
1088
|
+
'<div class="empty">Memory store unavailable.</div>';
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function loadMemoryTimeline(sessionId) {
|
|
1093
|
+
try {
|
|
1094
|
+
var r = await fetch(API+'/api/memory/sessions/'+encodeURIComponent(sessionId)+'/timeline');
|
|
1095
|
+
var data = await r.json();
|
|
1096
|
+
var panel = document.getElementById('mem-timeline-panel');
|
|
1097
|
+
var title = document.getElementById('mem-timeline-title');
|
|
1098
|
+
var body = document.getElementById('mem-timeline-body');
|
|
1099
|
+
panel.style.display = '';
|
|
1100
|
+
title.textContent = 'Session ' + sessionId;
|
|
1101
|
+
if (!data.session || !data.turns.length) {
|
|
1102
|
+
body.innerHTML = '<div class="empty">No turn summaries yet for this session.</div>';
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
var rollup = data.session.rollup_summary
|
|
1106
|
+
? '<div style="margin-bottom:10px; padding:8px; background:var(--panel2); border-radius:4px"><strong>rollup:</strong> '+_esc(data.session.rollup_summary)+'</div>'
|
|
1107
|
+
: '';
|
|
1108
|
+
var turns = data.turns.map(function(t) {
|
|
1109
|
+
return ''
|
|
1110
|
+
+ '<div style="padding:6px 0; border-bottom:1px solid var(--border)">'
|
|
1111
|
+
+ '<div style="font-size:11px; color:var(--muted)">turn '+t.prompt_number+' · ['+_esc(t.tier)+']</div>'
|
|
1112
|
+
+ '<div>'+_esc(t.summary)+'</div>'
|
|
1113
|
+
+ '</div>';
|
|
1114
|
+
}).join('');
|
|
1115
|
+
body.innerHTML = rollup + turns;
|
|
1116
|
+
} catch(e) {
|
|
1117
|
+
document.getElementById('mem-timeline-body').innerHTML =
|
|
1118
|
+
'<div class="empty">Failed to load timeline.</div>';
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function memDecisionSearch() {
|
|
1123
|
+
var q = (document.getElementById('mem-decision-q').value || '').trim();
|
|
1124
|
+
var src = document.getElementById('mem-decision-source').value || '';
|
|
1125
|
+
var url = API+'/api/memory/decisions';
|
|
1126
|
+
var params = [];
|
|
1127
|
+
if (q) params.push('q='+encodeURIComponent(q));
|
|
1128
|
+
if (src) params.push('source='+encodeURIComponent(src));
|
|
1129
|
+
if (params.length) url += '?' + params.join('&');
|
|
1130
|
+
try {
|
|
1131
|
+
var r = await fetch(url);
|
|
1132
|
+
var rows = await r.json();
|
|
1133
|
+
var box = document.getElementById('mem-decision-rows');
|
|
1134
|
+
if (!rows.length) {
|
|
1135
|
+
box.innerHTML = '<div class="empty">No decisions match.</div>';
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
box.innerHTML = rows.map(function(d) {
|
|
1139
|
+
return ''
|
|
1140
|
+
+ '<div class="table-row">'
|
|
1141
|
+
+ '<div>'+_esc(d.decision)+'</div>'
|
|
1142
|
+
+ '<div>'+_esc(d.reason)+'</div>'
|
|
1143
|
+
+ '<div><span class="tag tag-'+_esc(d.source)+'">'+_esc(d.source)+'</span></div>'
|
|
1144
|
+
+ '<div>'+_esc(d.created_at||'')+'</div>'
|
|
1145
|
+
+ '</div>';
|
|
1146
|
+
}).join('');
|
|
1147
|
+
} catch(e) {
|
|
1148
|
+
document.getElementById('mem-decision-rows').innerHTML =
|
|
1149
|
+
'<div class="empty">Decisions search failed.</div>';
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function toast(msg) {
|
|
1154
|
+
var el = document.getElementById('toast');
|
|
1155
|
+
el.textContent = msg;
|
|
1156
|
+
el.classList.add('show');
|
|
1157
|
+
clearTimeout(el._t);
|
|
1158
|
+
el._t = setTimeout(function(){ el.classList.remove('show'); }, 3000);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function reltime(ts) {
|
|
1162
|
+
var d = Math.floor(Date.now()/1000 - ts);
|
|
1163
|
+
if (d < 60) return 'just now';
|
|
1164
|
+
if (d < 3600) return Math.floor(d/60)+'m ago';
|
|
1165
|
+
if (d < 86400) return Math.floor(d/3600)+'h ago';
|
|
1166
|
+
return Math.floor(d/86400)+'d ago';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function fmt(n) { return Number(n).toLocaleString(); }
|
|
1170
|
+
function fmtK(n) { return n>=1000 ? (n/1000).toFixed(1)+'k' : String(n); }
|
|
1171
|
+
|
|
1172
|
+
var SVG = {
|
|
1173
|
+
refresh: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
|
|
1174
|
+
trash: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>',
|
|
1175
|
+
chevron: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>',
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// ── Data loaders ──────────────────────────────────
|
|
1179
|
+
|
|
1180
|
+
async function loadStatus() {
|
|
1181
|
+
try {
|
|
1182
|
+
var r = await fetch(API+'/api/status');
|
|
1183
|
+
var d = await r.json();
|
|
1184
|
+
document.getElementById('nav-project').textContent = d.project||'';
|
|
1185
|
+
document.getElementById('stat-chunks').textContent = fmt(d.chunks);
|
|
1186
|
+
document.getElementById('stat-files').textContent = fmt(d.files);
|
|
1187
|
+
document.getElementById('stat-queries').textContent = fmt(d.queries);
|
|
1188
|
+
document.getElementById('stat-saved').textContent = (d.tokens_saved_pct||0)+'%';
|
|
1189
|
+
document.getElementById('uninit-banner').style.display = d.initialized?'none':'flex';
|
|
1190
|
+
currentLevel = d.output_level;
|
|
1191
|
+
refreshCompButtons(d.output_level);
|
|
1192
|
+
|
|
1193
|
+
// Mini rings in stat cards
|
|
1194
|
+
drawMiniRing('spark-saved-ring', d.tokens_saved_pct||0, 'var(--purple)');
|
|
1195
|
+
|
|
1196
|
+
loadOverviewPanels();
|
|
1197
|
+
} catch(e) {}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function loadOverviewPanels() {
|
|
1201
|
+
// Load files + sessions in parallel for the chart panels
|
|
1202
|
+
try {
|
|
1203
|
+
var [rFiles, rSessions, rSavings] = await Promise.all([
|
|
1204
|
+
fetch(API+'/api/files'),
|
|
1205
|
+
fetch(API+'/api/sessions'),
|
|
1206
|
+
fetch(API+'/api/savings'),
|
|
1207
|
+
]);
|
|
1208
|
+
var files = await rFiles.json();
|
|
1209
|
+
var sessions = await rSessions.json();
|
|
1210
|
+
var savings = await rSavings.json();
|
|
1211
|
+
|
|
1212
|
+
// Update nav counts
|
|
1213
|
+
document.getElementById('nav-files-count').textContent = files.length;
|
|
1214
|
+
document.getElementById('nav-sessions-count').textContent = sessions.length;
|
|
1215
|
+
|
|
1216
|
+
// ── Token Savings Donut ──
|
|
1217
|
+
var served = savings.served_tokens || 0;
|
|
1218
|
+
var saved = savings.tokens_saved || 0;
|
|
1219
|
+
var baseline = savings.baseline_tokens || 0;
|
|
1220
|
+
var savePct = savings.savings_pct || 0;
|
|
1221
|
+
|
|
1222
|
+
if (baseline > 0) {
|
|
1223
|
+
renderDonutPanel('chart-token-savings',
|
|
1224
|
+
[
|
|
1225
|
+
{value: saved, color: 'var(--green)', label: 'Tokens saved', display: fmtK(saved)},
|
|
1226
|
+
{value: served, color: 'var(--blue)', label: 'Tokens used', display: fmtK(served)},
|
|
1227
|
+
],
|
|
1228
|
+
savePct+'%', 'saved', 'var(--green)'
|
|
1229
|
+
);
|
|
1230
|
+
} else {
|
|
1231
|
+
document.getElementById('chart-token-savings').innerHTML =
|
|
1232
|
+
'<div class="empty"><span class="empty-title">No queries yet</span><span class="empty-hint">Run context_search via MCP to start tracking</span></div>';
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ── File Health Donut ──
|
|
1236
|
+
var ok = files.filter(function(f){ return f.status==='ok'; }).length;
|
|
1237
|
+
var stale = files.filter(function(f){ return f.status==='stale'; }).length;
|
|
1238
|
+
var missing = files.filter(function(f){ return f.status==='missing'; }).length;
|
|
1239
|
+
var total = files.length;
|
|
1240
|
+
|
|
1241
|
+
// Mini ring: ok% in stat card
|
|
1242
|
+
drawMiniRing('spark-files-ring', total>0?Math.round(ok/total*100):0, 'var(--green)');
|
|
1243
|
+
|
|
1244
|
+
if (total > 0) {
|
|
1245
|
+
renderDonutPanel('chart-file-health',
|
|
1246
|
+
[
|
|
1247
|
+
{value: ok, color: 'var(--green)', label: 'Up to date', display: String(ok)},
|
|
1248
|
+
{value: stale, color: 'var(--yellow)', label: 'Stale', display: String(stale)},
|
|
1249
|
+
{value: missing, color: 'var(--red)', label: 'Missing', display: String(missing)},
|
|
1250
|
+
],
|
|
1251
|
+
total, 'files', 'var(--text)'
|
|
1252
|
+
);
|
|
1253
|
+
} else {
|
|
1254
|
+
document.getElementById('chart-file-health').innerHTML =
|
|
1255
|
+
'<div class="empty"><span class="empty-title">No files indexed</span></div>';
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// ── Top Files Bar Chart ──
|
|
1259
|
+
var sorted = files.slice().sort(function(a,b){ return b.chunks - a.chunks; }).slice(0,10);
|
|
1260
|
+
var barColors = ['var(--blue)','var(--blue)','var(--blue)','var(--blue)','var(--blue)',
|
|
1261
|
+
'var(--purple)','var(--purple)','var(--purple)','var(--purple)','var(--purple)'];
|
|
1262
|
+
renderHBarChart('chart-top-files',
|
|
1263
|
+
sorted.map(function(f, i){
|
|
1264
|
+
return {label: f.path, value: f.chunks, color: barColors[i]||'var(--blue)'};
|
|
1265
|
+
})
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
// ── Session Decisions Bar Chart ──
|
|
1269
|
+
var sessionItems = sessions.slice(0,12).map(function(s) {
|
|
1270
|
+
var label = s.project ? s.project.slice(0,8) : s.id.slice(0,6);
|
|
1271
|
+
return {label: label, value: (s.decisions||[]).length};
|
|
1272
|
+
});
|
|
1273
|
+
// Sparkline for queries (fake trend from session count)
|
|
1274
|
+
drawSparkline('spark-queries',
|
|
1275
|
+
sessions.slice(-5).map(function(s){ return (s.decisions||[]).length || 1; }),
|
|
1276
|
+
'var(--yellow)'
|
|
1277
|
+
);
|
|
1278
|
+
drawSparkline('spark-chunks', [2,4,3,5,6,8,7], 'var(--blue)');
|
|
1279
|
+
|
|
1280
|
+
if (sessionItems.length) {
|
|
1281
|
+
renderVBarChart('chart-sessions-bars', sessionItems, 'var(--purple)');
|
|
1282
|
+
} else {
|
|
1283
|
+
document.getElementById('chart-sessions-bars').innerHTML =
|
|
1284
|
+
'<div class="empty"><span class="empty-title">No sessions yet</span></div>';
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
} catch(e) {}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// ── Files page ────────────────────────────────────
|
|
1291
|
+
|
|
1292
|
+
async function loadFiles() {
|
|
1293
|
+
var el = document.getElementById('file-rows');
|
|
1294
|
+
el.innerHTML = '<div class="empty"><div class="spinner"></div></div>';
|
|
1295
|
+
try {
|
|
1296
|
+
var r = await fetch(API+'/api/files');
|
|
1297
|
+
allFiles = await r.json();
|
|
1298
|
+
renderFilesDistBar(allFiles);
|
|
1299
|
+
renderFiles(allFiles);
|
|
1300
|
+
} catch(e) {
|
|
1301
|
+
el.innerHTML = '<div class="empty"><span class="empty-title">Failed to load</span></div>';
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function renderFilesDistBar(files) {
|
|
1306
|
+
var el = document.getElementById('files-dist-bar');
|
|
1307
|
+
if (!el || !files.length) { if(el) el.innerHTML=''; return; }
|
|
1308
|
+
var ok = files.filter(function(f){ return f.status==='ok'; }).length;
|
|
1309
|
+
var stale = files.filter(function(f){ return f.status==='stale'; }).length;
|
|
1310
|
+
var missing = files.filter(function(f){ return f.status==='missing'; }).length;
|
|
1311
|
+
var total = files.length;
|
|
1312
|
+
var pOk = Math.round(ok/total*100);
|
|
1313
|
+
var pSt = Math.round(stale/total*100);
|
|
1314
|
+
var pMi = 100 - pOk - pSt;
|
|
1315
|
+
el.innerHTML =
|
|
1316
|
+
'<div style="background:var(--panel);border:1px solid var(--border);border-radius:var(--r2);overflow:hidden">'
|
|
1317
|
+
+'<div style="height:6px;display:flex">'
|
|
1318
|
+
+'<div style="width:'+pOk+'%;background:var(--green);transition:width .6s"></div>'
|
|
1319
|
+
+'<div style="width:'+pSt+'%;background:var(--yellow);transition:width .6s"></div>'
|
|
1320
|
+
+'<div style="flex:1;background:var(--red);transition:width .6s"></div>'
|
|
1321
|
+
+'</div>'
|
|
1322
|
+
+'<div style="display:flex;gap:16px;padding:8px 12px;font-size:11px;font-family:var(--mono)">'
|
|
1323
|
+
+'<span style="color:var(--green)">● ok '+ok+'</span>'
|
|
1324
|
+
+'<span style="color:var(--yellow)">● stale '+stale+'</span>'
|
|
1325
|
+
+'<span style="color:var(--red)">● missing '+missing+'</span>'
|
|
1326
|
+
+'<span style="margin-left:auto;color:var(--text3)">'+total+' files total</span>'
|
|
1327
|
+
+'</div>'
|
|
1328
|
+
+'</div>';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function renderFiles(files) {
|
|
1332
|
+
var el = document.getElementById('file-rows');
|
|
1333
|
+
if (!files.length) {
|
|
1334
|
+
el.innerHTML = '<div class="empty">'
|
|
1335
|
+
+'<svg class="empty-icon" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
|
|
1336
|
+
+'<span class="empty-title">No files indexed</span>'
|
|
1337
|
+
+'<span class="empty-hint">Run <code style="font-family:var(--mono)">cce index</code> to index your project</span>'
|
|
1338
|
+
+'</div>';
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
el.innerHTML = files.map(function(f) {
|
|
1342
|
+
return '<div class="table-row">'
|
|
1343
|
+
+'<div class="file-path" title="'+f.path+'">'+f.path+'</div>'
|
|
1344
|
+
+'<div class="chunk-num">'+f.chunks+'</div>'
|
|
1345
|
+
+'<div><span class="badge badge-'+f.status+'">'+f.status+'</span></div>'
|
|
1346
|
+
+'<div class="row-acts">'
|
|
1347
|
+
+'<button class="btn-icon" title="Reindex" onclick="reindexFile('+JSON.stringify(f.path)+')">'+SVG.refresh+'</button>'
|
|
1348
|
+
+'<button class="btn-icon del" title="Remove" onclick="deleteFile('+JSON.stringify(f.path)+')">'+SVG.trash+'</button>'
|
|
1349
|
+
+'</div>'
|
|
1350
|
+
+'</div>';
|
|
1351
|
+
}).join('');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function filterFiles(q) {
|
|
1355
|
+
q = q.toLowerCase();
|
|
1356
|
+
renderFiles(q ? allFiles.filter(function(f){ return f.path.toLowerCase().includes(q); }) : allFiles);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ── Sessions page ─────────────────────────────────
|
|
1360
|
+
|
|
1361
|
+
async function loadSessions() {
|
|
1362
|
+
var el = document.getElementById('session-list');
|
|
1363
|
+
el.innerHTML = '<div class="empty"><div class="spinner"></div></div>';
|
|
1364
|
+
try {
|
|
1365
|
+
var r = await fetch(API+'/api/sessions');
|
|
1366
|
+
var sessions = await r.json();
|
|
1367
|
+
if (!sessions.length) {
|
|
1368
|
+
el.innerHTML = '<div class="empty">'
|
|
1369
|
+
+'<svg class="empty-icon" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>'
|
|
1370
|
+
+'<span class="empty-title">No sessions recorded</span>'
|
|
1371
|
+
+'<span class="empty-hint">Sessions are captured during Claude coding sessions</span>'
|
|
1372
|
+
+'</div>';
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
el.innerHTML = '<div class="session-list">'+sessions.map(function(s,i) {
|
|
1376
|
+
var isActive = !s.ended_at;
|
|
1377
|
+
var decs = s.decisions || [];
|
|
1378
|
+
var areas = s.code_areas || [];
|
|
1379
|
+
return '<div class="session-card">'
|
|
1380
|
+
+'<div class="session-header" onclick="toggleSession('+i+')">'
|
|
1381
|
+
+'<div class="chevron" id="chev-'+i+'">'+SVG.chevron+'</div>'
|
|
1382
|
+
+'<div class="session-info">'
|
|
1383
|
+
+'<div class="session-name">'+(s.project||s.id)+'</div>'
|
|
1384
|
+
+'<div class="session-meta">'
|
|
1385
|
+
+'<b>'+decs.length+'</b><span>decisions</span>'
|
|
1386
|
+
+'<b>'+areas.length+'</b><span>code areas</span>'
|
|
1387
|
+
+(s.started_at?'<b>'+reltime(s.started_at)+'</b>':'')
|
|
1388
|
+
+'</div>'
|
|
1389
|
+
+'</div>'
|
|
1390
|
+
+'<span class="badge '+(isActive?'badge-active':'badge-closed')+'">'+(isActive?'active':'closed')+'</span>'
|
|
1391
|
+
+'</div>'
|
|
1392
|
+
+(decs.length
|
|
1393
|
+
?'<div class="session-body" id="sb-'+i+'">'
|
|
1394
|
+
+'<div class="decisions-label">Decisions</div>'
|
|
1395
|
+
+decs.map(function(d){ return '<div class="decision-item">'+d.decision+'</div>'; }).join('')
|
|
1396
|
+
+'</div>'
|
|
1397
|
+
:'')
|
|
1398
|
+
+'</div>';
|
|
1399
|
+
}).join('')+'</div>';
|
|
1400
|
+
} catch(e) {
|
|
1401
|
+
el.innerHTML = '<div class="empty"><span class="empty-title">Failed to load</span></div>';
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function toggleSession(i) {
|
|
1406
|
+
var body = document.getElementById('sb-'+i);
|
|
1407
|
+
var chev = document.getElementById('chev-'+i);
|
|
1408
|
+
if (body) body.classList.toggle('open');
|
|
1409
|
+
if (chev) chev.classList.toggle('open');
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// ── Savings page ──────────────────────────────────
|
|
1413
|
+
|
|
1414
|
+
async function loadSavings() {
|
|
1415
|
+
try {
|
|
1416
|
+
var r = await fetch(API+'/api/savings');
|
|
1417
|
+
var d = await r.json();
|
|
1418
|
+
|
|
1419
|
+
var queries = d.queries || 0;
|
|
1420
|
+
var saved = d.tokens_saved || 0;
|
|
1421
|
+
var served = d.served_tokens || 0;
|
|
1422
|
+
var baseline = d.baseline_tokens || 0;
|
|
1423
|
+
var pct = d.savings_pct || 0;
|
|
1424
|
+
var usedPct = baseline > 0 ? Math.round(served/baseline*100) : 0;
|
|
1425
|
+
|
|
1426
|
+
// Stat cards
|
|
1427
|
+
document.getElementById('sv-queries').textContent = fmt(queries);
|
|
1428
|
+
document.getElementById('sv-saved').textContent = fmtK(saved);
|
|
1429
|
+
document.getElementById('sv-pct').textContent = pct+'%';
|
|
1430
|
+
drawMiniRing('sv-ring', pct, 'var(--purple)');
|
|
1431
|
+
|
|
1432
|
+
// Big donut
|
|
1433
|
+
if (baseline > 0) {
|
|
1434
|
+
renderDonutPanel('sv-donut-chart',
|
|
1435
|
+
[
|
|
1436
|
+
{value: saved, color: 'var(--green)', label: 'Tokens saved', display: fmtK(saved)},
|
|
1437
|
+
{value: served, color: 'var(--blue)', label: 'Tokens used', display: fmtK(served)},
|
|
1438
|
+
],
|
|
1439
|
+
pct+'%', 'saved', 'var(--green)'
|
|
1440
|
+
);
|
|
1441
|
+
} else {
|
|
1442
|
+
document.getElementById('sv-donut-chart').innerHTML =
|
|
1443
|
+
'<div class="empty"><span class="empty-title">No usage recorded yet</span></div>';
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Budget panel: stacked bar + summary
|
|
1447
|
+
if (baseline > 0) {
|
|
1448
|
+
document.getElementById('sv-budget-panel').innerHTML =
|
|
1449
|
+
'<div style="padding:14px 14px 4px;font-size:11px;font-family:var(--mono);color:var(--text3)">Token distribution across '+fmt(queries)+' queries</div>'
|
|
1450
|
+
+'<div class="stacked-bar">'
|
|
1451
|
+
+'<div class="stacked-seg" style="width:'+usedPct+'%;background:var(--blue)"></div>'
|
|
1452
|
+
+'<div class="stacked-seg" style="flex:1;background:var(--green);opacity:.7"></div>'
|
|
1453
|
+
+'</div>'
|
|
1454
|
+
+'<div class="stacked-labels">'
|
|
1455
|
+
+'<span class="stacked-lbl" style="color:var(--blue)">'+fmtK(served)+' used ('+usedPct+'%)</span>'
|
|
1456
|
+
+'<span class="stacked-lbl" style="color:var(--green)">'+fmtK(saved)+' saved ('+pct+'%)</span>'
|
|
1457
|
+
+'</div>'
|
|
1458
|
+
+'<div class="savings-summary" style="margin:0 14px 14px">'
|
|
1459
|
+
+'<div>'
|
|
1460
|
+
+'<div class="savings-summary-lbl">Total tokens saved vs reading raw files</div>'
|
|
1461
|
+
+'<div style="font-size:11px;color:var(--text3);margin-top:2px;font-family:var(--mono)">'+fmt(baseline)+' tokens baseline</div>'
|
|
1462
|
+
+'</div>'
|
|
1463
|
+
+'<div>'
|
|
1464
|
+
+'<span class="savings-summary-val">'+fmtK(saved)+'</span>'
|
|
1465
|
+
+'<span class="savings-summary-pct">('+pct+'%)</span>'
|
|
1466
|
+
+'</div>'
|
|
1467
|
+
+'</div>';
|
|
1468
|
+
} else {
|
|
1469
|
+
document.getElementById('sv-budget-panel').innerHTML =
|
|
1470
|
+
'<div class="empty"><span class="empty-title">No usage recorded yet</span><span class="empty-hint">Run context_search via MCP to start tracking</span></div>';
|
|
1471
|
+
}
|
|
1472
|
+
} catch(e) {}
|
|
1473
|
+
refreshCompButtons(currentLevel);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function refreshCompButtons(level) {
|
|
1477
|
+
document.querySelectorAll('.comp-btn').forEach(function(btn) {
|
|
1478
|
+
btn.classList.toggle('active', btn.textContent.trim()===level);
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ── Actions ───────────────────────────────────────
|
|
1483
|
+
|
|
1484
|
+
async function doReindex(full) {
|
|
1485
|
+
var id = full ? 'btn-reindex-full' : 'btn-reindex-changed';
|
|
1486
|
+
var btn = document.getElementById(id);
|
|
1487
|
+
var orig = btn.innerHTML;
|
|
1488
|
+
btn.disabled = true;
|
|
1489
|
+
btn.innerHTML = '<div class="spinner"></div> Indexing\u2026';
|
|
1490
|
+
try {
|
|
1491
|
+
var r = await fetch(API+'/api/reindex', {
|
|
1492
|
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
1493
|
+
body: JSON.stringify({full: full})
|
|
1494
|
+
});
|
|
1495
|
+
var d = await r.json();
|
|
1496
|
+
if (d.errors && d.errors.length) toast('Error: '+d.errors[0]);
|
|
1497
|
+
else toast('Indexed '+d.indexed_files.length+' files \u2014 '+fmt(d.total_chunks)+' chunks');
|
|
1498
|
+
loadStatus();
|
|
1499
|
+
} catch(e) { toast('Reindex failed'); }
|
|
1500
|
+
finally { btn.disabled=false; btn.innerHTML=orig; }
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async function reindexFile(path) {
|
|
1504
|
+
try {
|
|
1505
|
+
await fetch(API+'/api/reindex/'+encodeURIComponent(path), {method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
|
|
1506
|
+
toast('Reindexed '+path);
|
|
1507
|
+
loadFiles(); loadStatus();
|
|
1508
|
+
} catch(e) { toast('Failed'); }
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
async function deleteFile(path) {
|
|
1512
|
+
if (!confirm('Remove "'+path+'" from index?')) return;
|
|
1513
|
+
try {
|
|
1514
|
+
await fetch(API+'/api/files/'+encodeURIComponent(path), {method:'DELETE'});
|
|
1515
|
+
toast('Removed '+path);
|
|
1516
|
+
loadFiles(); loadStatus();
|
|
1517
|
+
} catch(e) { toast('Failed'); }
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function doClear() {
|
|
1521
|
+
if (!confirm('Clear entire index? This cannot be undone.')) return;
|
|
1522
|
+
try {
|
|
1523
|
+
await fetch(API+'/api/clear', {method:'POST'});
|
|
1524
|
+
toast('Index cleared');
|
|
1525
|
+
loadStatus(); loadFiles();
|
|
1526
|
+
} catch(e) { toast('Failed'); }
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function doExport() { window.location.href = API+'/api/export'; }
|
|
1530
|
+
|
|
1531
|
+
async function setCompression(level) {
|
|
1532
|
+
try {
|
|
1533
|
+
await fetch(API+'/api/compression', {
|
|
1534
|
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
1535
|
+
body: JSON.stringify({level: level})
|
|
1536
|
+
});
|
|
1537
|
+
currentLevel = level;
|
|
1538
|
+
refreshCompButtons(level);
|
|
1539
|
+
toast('Compression: '+level);
|
|
1540
|
+
} catch(e) { toast('Failed'); }
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// ── Boot ──────────────────────────────────────────
|
|
1544
|
+
loadStatus();
|
|
1545
|
+
setInterval(loadStatus, 5000);
|
|
1546
|
+
</script>
|
|
1547
|
+
</body>
|
|
1548
|
+
</html>"""
|