@echomem/mcp 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forensics.js +717 -0
- package/dist/index.js +61 -4
- package/dist/migrate.js +417 -99
- package/dist/report.js +1 -0
- package/dist/setup-page.js +852 -306
- package/dist/setup.js +606 -75
- package/package.json +1 -1
package/dist/setup-page.js
CHANGED
|
@@ -8,11 +8,24 @@ export function renderSetupPage() {
|
|
|
8
8
|
<style>
|
|
9
9
|
:root {
|
|
10
10
|
color-scheme: light;
|
|
11
|
-
--
|
|
12
|
-
--ink: #
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
11
|
+
--echo-ink-primary: #1a3a8f;
|
|
12
|
+
--echo-ink-accent: #4ecdc4;
|
|
13
|
+
--echo-ink-seal: #c7372f;
|
|
14
|
+
--echo-ink-text: #2b2a26;
|
|
15
|
+
--echo-ink-mute: #6b6f69;
|
|
16
|
+
--echo-ink-faint: #9a9b94;
|
|
17
|
+
--echo-paper-canvas: #faf7f0;
|
|
18
|
+
--echo-paper-white: #ffffff;
|
|
19
|
+
--echo-paper-soft: #f0efe9;
|
|
20
|
+
--echo-line-soft: #e4e0d6;
|
|
21
|
+
--echo-font-brand: "Avenir Next", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
22
|
+
--echo-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
23
|
+
--echo-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
24
|
+
--bg: var(--echo-paper-canvas);
|
|
25
|
+
--ink: var(--echo-ink-text);
|
|
26
|
+
--muted: var(--echo-ink-mute);
|
|
27
|
+
--line: var(--echo-line-soft);
|
|
28
|
+
--panel: var(--echo-paper-white);
|
|
16
29
|
--good-bg: #eff7ec;
|
|
17
30
|
--good-line: #cbd9c8;
|
|
18
31
|
--warn-bg: #fff6dc;
|
|
@@ -24,13 +37,15 @@ export function renderSetupPage() {
|
|
|
24
37
|
body {
|
|
25
38
|
margin: 0;
|
|
26
39
|
min-height: 100vh;
|
|
27
|
-
background:
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
background:
|
|
41
|
+
linear-gradient(180deg, rgba(245, 241, 231, 0.92), rgba(255, 255, 255, 0.96)),
|
|
42
|
+
var(--echo-paper-canvas);
|
|
43
|
+
color: var(--echo-ink-text);
|
|
44
|
+
font-family: var(--echo-font-body);
|
|
30
45
|
letter-spacing: 0;
|
|
31
46
|
}
|
|
32
47
|
main {
|
|
33
|
-
width: min(
|
|
48
|
+
width: min(1120px, 100%);
|
|
34
49
|
margin: 0 auto;
|
|
35
50
|
padding: clamp(18px, 4vw, 34px);
|
|
36
51
|
}
|
|
@@ -46,25 +61,33 @@ export function renderSetupPage() {
|
|
|
46
61
|
}
|
|
47
62
|
.kicker {
|
|
48
63
|
margin: 0;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
font-
|
|
64
|
+
font-family: var(--echo-font-mono);
|
|
65
|
+
color: var(--echo-ink-mute);
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
letter-spacing: 0.08em;
|
|
52
69
|
text-transform: uppercase;
|
|
53
70
|
}
|
|
54
71
|
h1 {
|
|
55
72
|
margin: 4px 0 0;
|
|
56
|
-
font-
|
|
73
|
+
font-family: var(--echo-font-brand);
|
|
74
|
+
font-size: clamp(30px, 5vw, 56px);
|
|
75
|
+
font-weight: 900;
|
|
57
76
|
line-height: 1;
|
|
58
77
|
letter-spacing: 0;
|
|
78
|
+
color: var(--echo-ink-primary);
|
|
59
79
|
}
|
|
60
80
|
h2 {
|
|
61
81
|
margin: 0;
|
|
82
|
+
font-family: var(--echo-font-brand);
|
|
62
83
|
font-size: 22px;
|
|
84
|
+
font-weight: 900;
|
|
63
85
|
line-height: 1.2;
|
|
64
86
|
letter-spacing: 0;
|
|
87
|
+
color: var(--echo-ink-primary);
|
|
65
88
|
}
|
|
66
89
|
.pill {
|
|
67
|
-
min-height:
|
|
90
|
+
min-height: 36px;
|
|
68
91
|
display: inline-flex;
|
|
69
92
|
align-items: center;
|
|
70
93
|
gap: 8px;
|
|
@@ -72,7 +95,11 @@ export function renderSetupPage() {
|
|
|
72
95
|
border-radius: 999px;
|
|
73
96
|
padding: 0 14px;
|
|
74
97
|
background: var(--panel);
|
|
75
|
-
font-
|
|
98
|
+
font-family: var(--echo-font-mono);
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
letter-spacing: 0.04em;
|
|
102
|
+
text-transform: uppercase;
|
|
76
103
|
white-space: nowrap;
|
|
77
104
|
}
|
|
78
105
|
.state {
|
|
@@ -80,208 +107,363 @@ export function renderSetupPage() {
|
|
|
80
107
|
display: flex;
|
|
81
108
|
align-items: center;
|
|
82
109
|
justify-content: center;
|
|
83
|
-
color:
|
|
84
|
-
font-size:
|
|
110
|
+
color: var(--echo-ink-mute);
|
|
111
|
+
font-size: 16px;
|
|
85
112
|
text-align: center;
|
|
86
113
|
}
|
|
87
|
-
|
|
114
|
+
|
|
115
|
+
/* ---- pure loading skeleton (no copy) ---- */
|
|
116
|
+
.loading {
|
|
117
|
+
display: grid;
|
|
118
|
+
gap: 22px;
|
|
119
|
+
padding: clamp(24px, 6vw, 64px) 0;
|
|
120
|
+
}
|
|
121
|
+
.skeleton {
|
|
88
122
|
display: grid;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
margin-bottom: 18px;
|
|
123
|
+
gap: 14px;
|
|
124
|
+
max-width: 720px;
|
|
92
125
|
}
|
|
93
|
-
.
|
|
94
|
-
|
|
126
|
+
.sk {
|
|
127
|
+
height: 16px;
|
|
128
|
+
border-radius: 6px;
|
|
129
|
+
background: linear-gradient(90deg, #ece9e0, #d7dbe6, #ece9e0);
|
|
130
|
+
background-size: 220% 100%;
|
|
131
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
95
132
|
}
|
|
96
|
-
.
|
|
97
|
-
|
|
133
|
+
.sk.title { height: 46px; width: 78%; }
|
|
134
|
+
.sk.w70 { width: 70%; }
|
|
135
|
+
.sk.w52 { width: 52%; }
|
|
136
|
+
.sk.w38 { width: 38%; }
|
|
137
|
+
.sk-grid {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
140
|
+
gap: 10px;
|
|
141
|
+
max-width: 720px;
|
|
142
|
+
}
|
|
143
|
+
.sk-card {
|
|
144
|
+
height: 92px;
|
|
98
145
|
border-radius: 8px;
|
|
99
|
-
background:
|
|
100
|
-
|
|
146
|
+
background: linear-gradient(90deg, #ece9e0, #d7dbe6, #ece9e0);
|
|
147
|
+
background-size: 220% 100%;
|
|
148
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
101
149
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
flex-direction: column;
|
|
106
|
-
justify-content: space-between;
|
|
150
|
+
@keyframes pulse {
|
|
151
|
+
0% { background-position: 0% 50%; }
|
|
152
|
+
100% { background-position: 220% 50%; }
|
|
107
153
|
}
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
154
|
+
.loadProgress { display: grid; gap: 8px; max-width: 720px; margin-top: 4px; }
|
|
155
|
+
.loadBar { height: 6px; border-radius: 999px; background: var(--echo-paper-soft); overflow: hidden; }
|
|
156
|
+
.loadFill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 300ms ease; }
|
|
157
|
+
.loadCaption { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
|
|
158
|
+
|
|
159
|
+
/* ---- forensic report (ContextDoctorTwo) ---- */
|
|
160
|
+
.hero { padding: 4px 0 24px; }
|
|
161
|
+
.privacy {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: auto 1fr;
|
|
164
|
+
gap: 10px;
|
|
165
|
+
align-items: baseline;
|
|
166
|
+
margin-bottom: 24px;
|
|
167
|
+
padding: 10px 0;
|
|
168
|
+
border-bottom: 1px solid var(--echo-line-soft);
|
|
169
|
+
}
|
|
170
|
+
.privacy span,
|
|
171
|
+
.kicker-mono,
|
|
172
|
+
.sectionHead span {
|
|
173
|
+
font-family: var(--echo-font-mono);
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
font-weight: 700;
|
|
176
|
+
letter-spacing: 0.08em;
|
|
113
177
|
text-transform: uppercase;
|
|
178
|
+
color: var(--echo-ink-primary);
|
|
114
179
|
}
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
180
|
+
.privacy p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
181
|
+
.heroGrid {
|
|
182
|
+
display: grid;
|
|
183
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
184
|
+
gap: clamp(20px, 3vw, 36px);
|
|
185
|
+
align-items: stretch;
|
|
120
186
|
}
|
|
121
|
-
.
|
|
187
|
+
.heroCopy { display: flex; flex-direction: column; justify-content: center; }
|
|
188
|
+
.heroCopy .kicker-mono { margin: 0 0 14px; color: var(--echo-ink-primary); }
|
|
189
|
+
.heroCopy h2 {
|
|
190
|
+
max-width: 720px;
|
|
122
191
|
margin: 0;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
line-height: 1.45;
|
|
192
|
+
font-size: clamp(34px, 5vw, 58px);
|
|
193
|
+
line-height: 1.02;
|
|
126
194
|
}
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
margin-bottom: 12px;
|
|
195
|
+
.heroCopy p:not(.kicker-mono) {
|
|
196
|
+
margin: 18px 0 0;
|
|
197
|
+
font-size: clamp(15px, 1.25vw, 18px);
|
|
198
|
+
line-height: 1.62;
|
|
199
|
+
color: var(--echo-ink-text);
|
|
133
200
|
}
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
201
|
+
.scoreCard, .promiseCard, .panel {
|
|
202
|
+
border: 1px solid var(--echo-line-soft);
|
|
203
|
+
border-radius: 8px;
|
|
204
|
+
background: var(--echo-paper-white);
|
|
205
|
+
}
|
|
206
|
+
.scoreCard { display: grid; align-content: center; min-height: 282px; padding: 22px; }
|
|
207
|
+
.scoreCard span, .promiseCard span {
|
|
208
|
+
font-family: var(--echo-font-mono);
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
font-weight: 700;
|
|
211
|
+
letter-spacing: 0.08em;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
color: var(--echo-ink-mute);
|
|
140
214
|
}
|
|
141
|
-
.
|
|
142
|
-
max-width: 640px;
|
|
215
|
+
.scoreCard strong {
|
|
143
216
|
margin-top: 12px;
|
|
217
|
+
font-family: var(--echo-font-brand);
|
|
218
|
+
font-size: clamp(72px, 8vw, 96px);
|
|
219
|
+
font-weight: 900;
|
|
220
|
+
line-height: 0.9;
|
|
221
|
+
color: var(--echo-ink-seal);
|
|
222
|
+
font-variant-numeric: tabular-nums;
|
|
223
|
+
}
|
|
224
|
+
.scoreCard em {
|
|
225
|
+
margin-top: 4px;
|
|
226
|
+
font-style: normal;
|
|
227
|
+
font-weight: 900;
|
|
228
|
+
font-size: 22px;
|
|
229
|
+
color: var(--echo-ink-primary);
|
|
144
230
|
}
|
|
145
|
-
.
|
|
231
|
+
.scoreCard p, .promiseCard p { margin: 14px 0 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
232
|
+
.statStrip {
|
|
146
233
|
display: grid;
|
|
147
|
-
|
|
234
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
235
|
+
gap: 10px;
|
|
236
|
+
margin-top: 22px;
|
|
148
237
|
}
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
border-
|
|
154
|
-
|
|
238
|
+
.stat {
|
|
239
|
+
min-height: 92px;
|
|
240
|
+
padding: 14px;
|
|
241
|
+
border: 1px solid var(--echo-line-soft);
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
background: var(--echo-paper-white);
|
|
155
244
|
}
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
245
|
+
.stat strong {
|
|
246
|
+
display: block;
|
|
247
|
+
font-family: var(--echo-font-brand);
|
|
248
|
+
font-size: clamp(24px, 2.8vw, 34px);
|
|
249
|
+
font-weight: 900;
|
|
250
|
+
line-height: 1;
|
|
251
|
+
color: var(--echo-ink-primary);
|
|
252
|
+
font-variant-numeric: tabular-nums;
|
|
161
253
|
}
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
254
|
+
.stat.alert strong { color: var(--echo-ink-seal); }
|
|
255
|
+
.stat span {
|
|
256
|
+
display: block;
|
|
257
|
+
margin-top: 8px;
|
|
258
|
+
font-family: var(--echo-font-mono);
|
|
259
|
+
font-size: 10px;
|
|
260
|
+
font-weight: 700;
|
|
261
|
+
letter-spacing: 0.06em;
|
|
262
|
+
text-transform: uppercase;
|
|
263
|
+
color: var(--echo-ink-mute);
|
|
166
264
|
}
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
font-
|
|
265
|
+
.sourceNote {
|
|
266
|
+
max-width: 980px;
|
|
267
|
+
margin: 16px 0 0;
|
|
268
|
+
font-family: var(--echo-font-mono);
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
line-height: 1.55;
|
|
271
|
+
color: var(--echo-ink-faint);
|
|
171
272
|
}
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
273
|
+
.sourceNote code {
|
|
274
|
+
padding: 1px 5px;
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
background: var(--echo-paper-soft);
|
|
277
|
+
color: var(--echo-ink-primary);
|
|
176
278
|
}
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
279
|
+
.section { padding: 30px 0 0; margin-top: 30px; border-top: 1px solid var(--echo-line-soft); }
|
|
280
|
+
.sectionHead { display: flex; gap: 12px; align-items: baseline; margin-bottom: 18px; }
|
|
281
|
+
.sectionHead h2 { margin: 0; font-size: clamp(24px, 3.4vw, 38px); line-height: 1.05; }
|
|
282
|
+
.twoCol {
|
|
283
|
+
display: grid;
|
|
284
|
+
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.8fr);
|
|
285
|
+
gap: clamp(20px, 3.5vw, 36px);
|
|
286
|
+
align-items: start;
|
|
181
287
|
}
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
288
|
+
.storyText p { margin: 0 0 18px; color: var(--echo-ink-text); font-size: clamp(15px, 1.25vw, 18px); line-height: 1.62; }
|
|
289
|
+
.storyText p:last-child { margin-bottom: 0; }
|
|
290
|
+
.storyText b { color: var(--echo-ink-primary); }
|
|
291
|
+
.panel { display: grid; gap: 12px; padding: 16px; }
|
|
292
|
+
.promiseCard { padding: 22px; }
|
|
293
|
+
.promiseCard strong {
|
|
294
|
+
display: block;
|
|
295
|
+
margin-top: 12px;
|
|
296
|
+
font-family: var(--echo-font-brand);
|
|
297
|
+
font-size: clamp(34px, 4vw, 48px);
|
|
298
|
+
font-weight: 900;
|
|
299
|
+
line-height: 1;
|
|
300
|
+
color: var(--echo-ink-primary);
|
|
186
301
|
}
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
302
|
+
.promiseActions { display: grid; gap: 10px; margin-top: 18px; }
|
|
303
|
+
.promiseCard button, .promiseCard .button {
|
|
304
|
+
width: 100%;
|
|
305
|
+
min-height: 48px;
|
|
306
|
+
border-radius: 6px;
|
|
307
|
+
font-family: var(--echo-font-mono);
|
|
308
|
+
font-size: 13px;
|
|
309
|
+
font-weight: 800;
|
|
310
|
+
letter-spacing: 0.08em;
|
|
311
|
+
text-transform: uppercase;
|
|
191
312
|
}
|
|
192
|
-
.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
313
|
+
.promiseCard .primary { border-color: var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; }
|
|
314
|
+
.promiseCard .secondary { background: #fff; color: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
315
|
+
|
|
316
|
+
/* histogram */
|
|
317
|
+
.histo { display: flex; align-items: flex-end; gap: 3px; height: 120px; padding-top: 8px; }
|
|
318
|
+
.histoBar { flex: 1; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--echo-ink-primary); }
|
|
319
|
+
.histoBar.night { background: var(--echo-ink-seal); }
|
|
320
|
+
.histoAxis {
|
|
321
|
+
display: flex; justify-content: space-between; margin-top: 8px;
|
|
322
|
+
font-family: var(--echo-font-mono); font-size: 10px; letter-spacing: 0.06em; color: var(--echo-ink-faint);
|
|
197
323
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
324
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
325
|
+
.chips span {
|
|
326
|
+
padding: 6px 8px; border: 1px solid var(--echo-line-soft); border-radius: 6px;
|
|
327
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-primary);
|
|
202
328
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
gap: 12px;
|
|
207
|
-
border-bottom: 1px solid
|
|
208
|
-
padding-bottom: 8px;
|
|
209
|
-
}
|
|
210
|
-
dt { color: #222; }
|
|
211
|
-
dd { margin: 0; text-align: right; color: #222; }
|
|
212
|
-
ol {
|
|
213
|
-
list-style: none;
|
|
214
|
-
margin: 14px 0 0;
|
|
215
|
-
padding: 0;
|
|
216
|
-
display: grid;
|
|
217
|
-
gap: 10px;
|
|
329
|
+
|
|
330
|
+
/* reread / repo rows */
|
|
331
|
+
.rrow2 {
|
|
332
|
+
display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
|
|
333
|
+
padding: 11px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
218
334
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
335
|
+
.row2:last-child { border-bottom: 0; }
|
|
336
|
+
.row2 b { font-family: var(--echo-font-mono); font-size: 13px; color: var(--echo-ink-primary); }
|
|
337
|
+
.row2 small { display: block; margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
|
|
338
|
+
.row2 strong {
|
|
339
|
+
font-family: var(--echo-font-brand); font-size: 24px; font-weight: 900;
|
|
340
|
+
color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
223
341
|
}
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
342
|
+
.repoRow strong { color: var(--echo-ink-primary); }
|
|
343
|
+
.tag {
|
|
344
|
+
display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px;
|
|
345
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 10px;
|
|
346
|
+
font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal);
|
|
229
347
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
348
|
+
|
|
349
|
+
/* cost split + model rows */
|
|
350
|
+
.segBar { display: flex; width: 100%; height: 22px; overflow: hidden; border-radius: 5px; background: var(--echo-paper-soft); }
|
|
351
|
+
.segBar span { display: block; min-width: 2px; }
|
|
352
|
+
.seg-output { background: var(--echo-ink-primary); }
|
|
353
|
+
.seg-cold { background: var(--echo-ink-mute); }
|
|
354
|
+
.seg-cw { background: var(--echo-ink-accent); }
|
|
355
|
+
.seg-cr { background: var(--echo-ink-seal); }
|
|
356
|
+
.costLegend { display: grid; gap: 6px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
|
|
357
|
+
.modelRow {
|
|
358
|
+
display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: baseline;
|
|
359
|
+
padding: 9px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
360
|
+
font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute);
|
|
243
361
|
}
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
362
|
+
.modelRow:last-child { border-bottom: 0; }
|
|
363
|
+
.modelRow b { font-size: 13px; color: var(--echo-ink-primary); }
|
|
364
|
+
.modelRow strong { font-family: var(--echo-font-brand); font-size: 18px; font-weight: 900; color: var(--echo-ink-seal); }
|
|
365
|
+
|
|
366
|
+
.nightCard {
|
|
367
|
+
margin-top: 18px; padding: 20px 22px; border: 1px solid var(--echo-ink-seal);
|
|
368
|
+
border-radius: 8px; background: var(--echo-paper-white);
|
|
247
369
|
}
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
color:
|
|
370
|
+
.nightCard span {
|
|
371
|
+
font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
|
|
372
|
+
text-transform: uppercase; color: var(--echo-ink-seal);
|
|
251
373
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
374
|
+
.nightCard p { margin: 10px 0 0; font-size: 17px; line-height: 1.5; color: var(--echo-ink-text); }
|
|
375
|
+
.nightCard b { color: var(--echo-ink-primary); }
|
|
376
|
+
|
|
377
|
+
/* ---- dashboard (post-auth extraction) ---- */
|
|
378
|
+
.loading-panel, .doctor-copy, .doctor-score, .metric, .extract-panel {
|
|
379
|
+
border: 1px solid var(--line);
|
|
380
|
+
border-radius: 8px;
|
|
381
|
+
background: var(--panel);
|
|
255
382
|
}
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
background: #e8e8e1;
|
|
261
|
-
margin: 16px 0 10px;
|
|
383
|
+
.status-strip {
|
|
384
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
385
|
+
border: 1px solid #c7d6e5; border-radius: 8px; background: #eef5fb; color: #1e3449;
|
|
386
|
+
padding: 13px 15px; margin-bottom: 18px;
|
|
262
387
|
}
|
|
263
|
-
.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
388
|
+
.status-strip.good { border-color: var(--good-line); background: var(--good-bg); color: #203820; }
|
|
389
|
+
.status-strip.warn { border-color: var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
390
|
+
.status-strip .dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 999px; margin-top: 4px; background: currentColor; }
|
|
391
|
+
.status-strip strong { display: block; line-height: 1.25; }
|
|
392
|
+
.status-strip p { margin-top: 3px; color: currentColor; font-size: 14px; }
|
|
393
|
+
.doctor-hero {
|
|
394
|
+
display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
|
|
395
|
+
gap: 14px; margin-bottom: 14px; align-items: stretch;
|
|
268
396
|
}
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
397
|
+
.doctor-copy, .doctor-score { padding: clamp(18px, 3vw, 26px); }
|
|
398
|
+
.doctor-copy { display: flex; flex-direction: column; justify-content: center; min-height: 240px; }
|
|
399
|
+
.doctor-copy h2 { max-width: 760px; margin-top: 10px; font-size: clamp(30px, 4.4vw, 52px); line-height: 1.02; }
|
|
400
|
+
.doctor-copy p:not(.label):not(.helper) { max-width: 720px; margin-top: 14px; font-size: clamp(15px, 1.5vw, 18px); color: var(--echo-ink-text); }
|
|
401
|
+
.doctor-score { display: grid; align-content: center; gap: 12px; }
|
|
402
|
+
.doctor-score > span { font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
403
|
+
.doctor-score > strong { display: block; font-family: var(--echo-font-brand); font-size: clamp(34px, 5.5vw, 60px); font-weight: 900; line-height: 0.95; color: var(--echo-ink-primary); }
|
|
404
|
+
.doctor-mini { display: grid; gap: 8px; margin-top: 12px; }
|
|
405
|
+
.mini-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 8px; }
|
|
406
|
+
.mini-row span { color: var(--echo-ink-text); font-size: 14px; }
|
|
407
|
+
.mini-row strong { font-family: var(--echo-font-brand); font-size: 16px; font-weight: 900; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
408
|
+
.doctor-stat-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 14px; }
|
|
409
|
+
.doctor-actions { margin: 0 0 18px; }
|
|
410
|
+
.metric { min-height: 122px; padding: 18px; display: flex; flex-direction: column; justify-content: space-between; }
|
|
411
|
+
.label { margin: 0; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
412
|
+
.value { display: block; font-family: var(--echo-font-brand); font-size: 34px; font-weight: 900; line-height: 1; color: var(--echo-ink-primary); }
|
|
413
|
+
.detail { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.45; }
|
|
414
|
+
.extract-panel { padding: 18px; }
|
|
415
|
+
.bucket-row { align-items: center; display: grid; grid-template-columns: minmax(90px, 1fr) minmax(100px, auto); gap: 12px; }
|
|
416
|
+
.bucket-value { text-align: right; font-weight: 800; white-space: nowrap; }
|
|
417
|
+
.helper { margin-top: 10px; color: var(--echo-ink-mute); font-size: 14px; }
|
|
418
|
+
.notice, .done, .warning, .error { border-radius: 8px; padding: 14px 16px; margin-bottom: 18px; }
|
|
419
|
+
.notice, .done { border: 1px solid var(--good-line); background: var(--good-bg); color: #203820; }
|
|
420
|
+
.warning { border: 1px solid var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
421
|
+
.error { border: 1px solid var(--error-line); background: var(--error-bg); color: #7d2117; }
|
|
422
|
+
.details { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); gap: 12px; margin-bottom: 18px; }
|
|
423
|
+
dl { margin: 14px 0 0; display: grid; gap: 10px; }
|
|
424
|
+
.row { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid #ecece7; padding-bottom: 8px; }
|
|
425
|
+
dt { color: var(--echo-ink-text); }
|
|
426
|
+
dd { margin: 0; text-align: right; color: var(--echo-ink-text); }
|
|
427
|
+
ol { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px; }
|
|
428
|
+
li { display: flex; justify-content: space-between; gap: 10px; }
|
|
429
|
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
430
|
+
button, .button {
|
|
431
|
+
min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
432
|
+
border-radius: 8px; border: 1px solid var(--echo-ink-primary); padding: 0 16px;
|
|
433
|
+
font: inherit; font-weight: 800; text-decoration: none; cursor: pointer;
|
|
276
434
|
}
|
|
435
|
+
.primary { background: var(--echo-ink-primary); color: #fff; }
|
|
436
|
+
.secondary { background: #fff; color: var(--echo-ink-primary); }
|
|
437
|
+
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
438
|
+
.track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
|
|
439
|
+
.fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
|
|
440
|
+
/* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
|
|
441
|
+
.track.indef .fill { width: 40%; transition: none; animation: prepSlide 1.2s ease-in-out infinite; }
|
|
442
|
+
@keyframes prepSlide { 0% { transform: translateX(-110%); } 100% { transform: translateX(350%); } }
|
|
443
|
+
.prep-steps { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
|
|
444
|
+
.prep-steps li { display: grid; grid-template-columns: 16px 1fr; gap: 11px; align-items: center; font-size: 15px; color: var(--echo-ink-mute); }
|
|
445
|
+
.prep-steps .dot { width: 13px; height: 13px; border-radius: 999px; border: 2px solid #cfcfc6; box-sizing: border-box; }
|
|
446
|
+
.prep-steps li.done { color: var(--echo-ink-text); }
|
|
447
|
+
.prep-steps li.done .dot { background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
448
|
+
.prep-steps li.active { color: var(--echo-ink-primary); font-weight: 700; }
|
|
449
|
+
.prep-steps li.active .dot { border-color: var(--echo-ink-primary); animation: prepDot 1.1s ease-in-out infinite; }
|
|
450
|
+
@keyframes prepDot { 0%, 100% { box-shadow: 0 0 0 0 rgba(26,58,143,0.4); } 60% { box-shadow: 0 0 0 6px rgba(26,58,143,0); } }
|
|
451
|
+
.counts { display: flex; gap: 14px; flex-wrap: wrap; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 13px; letter-spacing: 0.02em; margin-bottom: 6px; }
|
|
452
|
+
.finish { display: grid; gap: 6px; place-items: center; text-align: center; padding: clamp(28px, 6vw, 64px) 0; }
|
|
453
|
+
.finishMark { width: 54px; height: 54px; border-radius: 999px; display: grid; place-items: center; font-size: 26px; font-weight: 900; margin-bottom: 8px; }
|
|
454
|
+
.finishMark.ok { background: var(--good-bg); color: #2f7f4f; border: 1px solid var(--good-line); }
|
|
455
|
+
.finishMark.warn { background: var(--warn-bg); color: #5e4300; border: 1px solid var(--warn-line); }
|
|
456
|
+
.finishBig { font-family: var(--echo-font-brand); font-size: clamp(48px, 8vw, 84px); font-weight: 900; line-height: 0.9; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; }
|
|
457
|
+
.finishLabel { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
458
|
+
.finish p { max-width: 560px; margin: 12px 0 0; color: var(--echo-ink-text); font-size: 16px; line-height: 1.6; }
|
|
459
|
+
.finish p.helper { color: var(--echo-ink-mute); font-size: 14px; margin-top: 6px; }
|
|
460
|
+
code { font-family: var(--echo-font-mono); font-size: 0.9em; padding: 1px 5px; border-radius: 4px; background: var(--echo-paper-soft); color: var(--echo-ink-primary); }
|
|
277
461
|
.hidden { display: none; }
|
|
278
|
-
@media (max-width:
|
|
279
|
-
.
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
.
|
|
283
|
-
font-size: clamp(38px, 13vw, 58px);
|
|
284
|
-
}
|
|
462
|
+
@media (max-width: 820px) {
|
|
463
|
+
.heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .sk-grid, .doctor-stat-strip { grid-template-columns: 1fr; }
|
|
464
|
+
.doctor-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
465
|
+
.heroCopy h2 { font-size: 34px; }
|
|
466
|
+
.scoreCard { min-height: 220px; }
|
|
285
467
|
}
|
|
286
468
|
</style>
|
|
287
469
|
</head>
|
|
@@ -290,11 +472,11 @@ export function renderSetupPage() {
|
|
|
290
472
|
<header>
|
|
291
473
|
<div>
|
|
292
474
|
<p class="kicker">EchoMem device setup</p>
|
|
293
|
-
<h1 id="title">
|
|
475
|
+
<h1 id="title">Context Doctor</h1>
|
|
294
476
|
</div>
|
|
295
|
-
<div class="pill" id="status">
|
|
477
|
+
<div class="pill" id="status">Local</div>
|
|
296
478
|
</header>
|
|
297
|
-
<section id="app" class="state">Starting local
|
|
479
|
+
<section id="app" class="state">Starting local scan...</section>
|
|
298
480
|
</main>
|
|
299
481
|
<script>
|
|
300
482
|
(function () {
|
|
@@ -303,10 +485,17 @@ export function renderSetupPage() {
|
|
|
303
485
|
var app = document.getElementById("app");
|
|
304
486
|
var title = document.getElementById("title");
|
|
305
487
|
var status = document.getElementById("status");
|
|
488
|
+
var report = null;
|
|
306
489
|
var stats = null;
|
|
307
490
|
var statsSlow = false;
|
|
308
491
|
var decisionMade = false;
|
|
492
|
+
var connected = false;
|
|
309
493
|
var authUrl = "";
|
|
494
|
+
var authWindow = null;
|
|
495
|
+
var connectionPollStarted = false;
|
|
496
|
+
var statsPollStarted = false;
|
|
497
|
+
var reportPollStarted = false;
|
|
498
|
+
var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
310
499
|
|
|
311
500
|
function esc(value) {
|
|
312
501
|
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
|
|
@@ -320,23 +509,37 @@ export function renderSetupPage() {
|
|
|
320
509
|
function number(value) {
|
|
321
510
|
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
322
511
|
}
|
|
512
|
+
function countOrChecking(value) {
|
|
513
|
+
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "checking";
|
|
514
|
+
}
|
|
323
515
|
function compact(value) {
|
|
324
516
|
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
325
517
|
var abs = Math.abs(value);
|
|
326
|
-
if (abs >=
|
|
327
|
-
if (abs >=
|
|
328
|
-
if (abs >=
|
|
518
|
+
if (abs >= 1e9) return (value / 1e9).toFixed(1) + "B";
|
|
519
|
+
if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
520
|
+
if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
|
|
329
521
|
return String(Math.round(value));
|
|
330
522
|
}
|
|
331
|
-
function
|
|
332
|
-
if (typeof
|
|
333
|
-
var
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return
|
|
523
|
+
function big(value) {
|
|
524
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
525
|
+
var n = Math.abs(value);
|
|
526
|
+
if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
|
|
527
|
+
if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
528
|
+
if (n >= 1e3) return Math.round(value / 1e3) + "K";
|
|
529
|
+
return String(Math.round(value));
|
|
530
|
+
}
|
|
531
|
+
function money(value) {
|
|
532
|
+
return "$" + number(value);
|
|
337
533
|
}
|
|
338
|
-
function sleep(ms) {
|
|
339
|
-
|
|
534
|
+
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
535
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
536
|
+
var controller = new AbortController();
|
|
537
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
538
|
+
try {
|
|
539
|
+
return await fetch(url, Object.assign({}, options || {}, { signal: controller.signal }));
|
|
540
|
+
} finally {
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
}
|
|
340
543
|
}
|
|
341
544
|
async function getJson(path) {
|
|
342
545
|
var sep = path.indexOf("?") >= 0 ? "&" : "?";
|
|
@@ -344,18 +547,16 @@ export function renderSetupPage() {
|
|
|
344
547
|
if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
345
548
|
return await res.json();
|
|
346
549
|
}
|
|
347
|
-
async function postJson(path, body) {
|
|
348
|
-
var
|
|
550
|
+
async function postJson(path, body, timeoutMs) {
|
|
551
|
+
var options = {
|
|
349
552
|
method: "POST",
|
|
350
553
|
credentials: "omit",
|
|
351
554
|
headers: { "Content-Type": "application/json" },
|
|
352
555
|
body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
|
|
353
|
-
}
|
|
556
|
+
};
|
|
557
|
+
var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
|
|
354
558
|
var data = await res.json().catch(function () { return {}; });
|
|
355
|
-
if (!res.ok)
|
|
356
|
-
var msg = data.message || data.error || ("HTTP " + res.status);
|
|
357
|
-
throw new Error(msg);
|
|
358
|
-
}
|
|
559
|
+
if (!res.ok) throw new Error(data.message || data.error || ("HTTP " + res.status));
|
|
359
560
|
return data;
|
|
360
561
|
}
|
|
361
562
|
function metric(label, value, detail) {
|
|
@@ -366,17 +567,13 @@ export function renderSetupPage() {
|
|
|
366
567
|
}
|
|
367
568
|
function migrationBuckets(migratable) {
|
|
368
569
|
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
369
|
-
|
|
370
|
-
? migratable.buckets
|
|
371
|
-
: (Array.isArray(eta.buckets) ? eta.buckets : []);
|
|
372
|
-
return buckets;
|
|
570
|
+
return Array.isArray(migratable && migratable.buckets) ? migratable.buckets : (Array.isArray(eta.buckets) ? eta.buckets : []);
|
|
373
571
|
}
|
|
374
572
|
function bucketCount(buckets, key) {
|
|
375
573
|
var found = buckets.filter(function (b) { return b && b.key === key; })[0];
|
|
376
574
|
return found && typeof found.count === "number" ? found.count : 0;
|
|
377
575
|
}
|
|
378
576
|
function bucketList(buckets) {
|
|
379
|
-
if (!buckets.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
|
|
380
577
|
var visible = buckets.filter(function (b) { return b && (b.count || b.chars); });
|
|
381
578
|
if (!visible.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
|
|
382
579
|
return '<ol>' + visible.map(function (b) {
|
|
@@ -391,118 +588,340 @@ export function renderSetupPage() {
|
|
|
391
588
|
if (code === "NOT_LOGGED_IN") return "The local bridge is not logged in yet. Sign in again and retry.";
|
|
392
589
|
return code || "Something went wrong. Retry in a moment.";
|
|
393
590
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
591
|
+
function closeAuthWindow() {
|
|
592
|
+
try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
|
|
593
|
+
authWindow = null;
|
|
594
|
+
}
|
|
595
|
+
function notifyOpenerConnected() {
|
|
596
|
+
try {
|
|
597
|
+
if (window.opener && window.opener !== window && !window.opener.closed) {
|
|
598
|
+
window.opener.postMessage({ type: "echomem:connected", nonce: nonce }, location.origin);
|
|
599
|
+
setTimeout(function () { window.close(); }, 250);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
} catch (_) {}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function openAuthWindow(url) {
|
|
606
|
+
var targetUrl = url || authUrl;
|
|
607
|
+
if (!targetUrl) return false;
|
|
608
|
+
try {
|
|
609
|
+
authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
|
|
610
|
+
if (!authWindow) return false;
|
|
611
|
+
authWindow.focus();
|
|
612
|
+
return true;
|
|
613
|
+
} catch (_) { return false; }
|
|
614
|
+
}
|
|
615
|
+
function showPopupFallback(message, url) {
|
|
616
|
+
var hint = document.getElementById("signin-help");
|
|
617
|
+
if (hint) hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
|
|
618
|
+
var fallback = document.getElementById("signin-fallback");
|
|
619
|
+
if (fallback) { fallback.setAttribute("href", url || authUrl); fallback.classList.remove("hidden"); }
|
|
620
|
+
}
|
|
621
|
+
function onConnectClick() {
|
|
622
|
+
if (connected) { void waitForStats(); return; }
|
|
623
|
+
if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
|
|
624
|
+
startConnectionPoll();
|
|
625
|
+
}
|
|
626
|
+
function bindConnect() {
|
|
627
|
+
var signin = document.getElementById("signin");
|
|
628
|
+
if (signin) signin.onclick = onConnectClick;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* ---------- pure loading (pic 5) ---------- */
|
|
632
|
+
function renderLoading(prog) {
|
|
633
|
+
var pct = prog && prog.total ? Math.round((prog.scanned / prog.total) * 100) : 0;
|
|
634
|
+
var caption = prog && prog.total
|
|
635
|
+
? "Reading your local history — " + number(prog.scanned) + " / " + number(prog.total) + " sessions"
|
|
636
|
+
: "Reading your local history…";
|
|
637
|
+
// Update the bar in place on later polls so the skeleton animation doesn't restart/flicker.
|
|
638
|
+
var fill = document.getElementById("loadFill");
|
|
639
|
+
if (app.className === "loading" && fill) {
|
|
640
|
+
fill.style.width = pct + "%";
|
|
641
|
+
var cap = document.getElementById("loadCaption");
|
|
642
|
+
if (cap) cap.textContent = caption;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
setHead("Context Doctor", "Reading");
|
|
646
|
+
app.className = "loading";
|
|
397
647
|
app.innerHTML =
|
|
398
|
-
'<
|
|
399
|
-
|
|
400
|
-
'
|
|
648
|
+
'<div class="skeleton">' +
|
|
649
|
+
'<div class="sk title"></div><div class="sk w70"></div><div class="sk w52"></div>' +
|
|
650
|
+
'</div>' +
|
|
651
|
+
'<div class="sk-grid"><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div></div>' +
|
|
652
|
+
'<div class="skeleton"><div class="sk w38"></div><div class="sk w70"></div></div>' +
|
|
653
|
+
'<div class="loadProgress"><div class="loadBar"><div id="loadFill" class="loadFill" style="width:' + pct + '%"></div></div><p id="loadCaption" class="loadCaption">' + esc(caption) + '</p></div>';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* ---------- forensic report (pic 1/2) ---------- */
|
|
657
|
+
function histogram(hours) {
|
|
658
|
+
var list = Array.isArray(hours) ? hours : [];
|
|
659
|
+
var max = Math.max.apply(null, [1].concat(list));
|
|
660
|
+
var bars = list.map(function (v, h) {
|
|
661
|
+
var height = Math.max(2, Math.round((v / max) * 100));
|
|
662
|
+
return '<span class="histoBar' + (NIGHT_HOURS[h] ? ' night' : '') + '" style="height:' + height + '%" title="' + h + ':00 — ' + number(v) + ' prompts"></span>';
|
|
663
|
+
}).join("");
|
|
664
|
+
return '<div class="histo">' + bars + '</div><div class="histoAxis"><span>12a</span><span>6a</span><span>12p</span><span>6p</span><span>11p</span></div>';
|
|
665
|
+
}
|
|
666
|
+
function rereadRows(files) {
|
|
667
|
+
return (files || []).slice(0, 6).map(function (f) {
|
|
668
|
+
var meta = esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + (f.nightReads ? ' · ' + number(f.nightReads) + ' at night' : '');
|
|
669
|
+
var tag = f.everChanged ? '' : '<span class="tag">never changed</span>';
|
|
670
|
+
return '<div class="row2"><div><b>' + esc(f.file) + '</b>' + tag + '<small>' + meta + '</small></div><strong>' + number(f.totalReads) + '×</strong></div>';
|
|
671
|
+
}).join("");
|
|
401
672
|
}
|
|
673
|
+
function repoRows(repos) {
|
|
674
|
+
return (repos || []).slice(0, 5).map(function (r) {
|
|
675
|
+
var meta = number(r.sessions) + ' sessions · ' + number(r.activeAttentionDays) + ' active days · ' + (r.dailyFocusHours || 0) + 'h/day · ' + big(r.tokens) + ' tokens';
|
|
676
|
+
return '<div class="row2 repoRow"><div><b style="font-family:var(--echo-font-brand);font-size:18px">' + esc(r.name) + '</b><small>' + esc(meta) + '</small></div><strong style="color:var(--echo-ink-primary)">' + (r.attentionHours || 0) + 'h</strong></div>';
|
|
677
|
+
}).join("");
|
|
678
|
+
}
|
|
679
|
+
function costSplit(cost) {
|
|
680
|
+
var total = cost.total || 1;
|
|
681
|
+
function seg(cls, v) { return '<span class="' + cls + '" style="width:' + Math.max(0, (v / total) * 100) + '%"></span>'; }
|
|
682
|
+
var byModel = cost.byModel || {};
|
|
683
|
+
var rows = Object.keys(byModel).map(function (k) { return [k, byModel[k]]; })
|
|
684
|
+
.sort(function (a, b) { return b[1].total - a[1].total; })
|
|
685
|
+
.map(function (e) { return '<div class="modelRow"><b>' + esc(e[0]) + '</b><span>' + big(e[1].total) + ' tok</span><strong>' + money(e[1].cost) + '</strong></div>'; })
|
|
686
|
+
.join("");
|
|
687
|
+
return '<div class="segBar">' + seg("seg-output", cost.outputCost) + seg("seg-cold", cost.coldInputCost) + seg("seg-cw", cost.cacheWriteCost) + seg("seg-cr", cost.cacheReadCost) + '</div>' +
|
|
688
|
+
'<div class="costLegend"><span>output ' + money(cost.outputCost) + '</span><span>cold input ' + money(cost.coldInputCost) + '</span><span>cache write ' + money(cost.cacheWriteCost) + '</span><span>cache read ' + money(cost.cacheReadCost) + '</span></div>' +
|
|
689
|
+
'<div style="margin-top:14px">' + rows + '</div>';
|
|
690
|
+
}
|
|
691
|
+
function renderForensicReport(message) {
|
|
692
|
+
var r = report;
|
|
693
|
+
if (!r) { showReport(); return; }
|
|
694
|
+
var s = r.scale || {}, c = r.cost || {}, mu = r.modelUsage || {}, uwh = r.userWorkingHours || {}, rh = r.rhythm || {}, cl = r.cleanliness || {}, rf = r.rereadForensics || {}, aw = r.avoidableWait || {}, cf = r.counterfactual || {};
|
|
695
|
+
var night = rf.nightExample;
|
|
696
|
+
setHead("Context Doctor", connected ? "Signed in" : "Local");
|
|
697
|
+
app.className = "";
|
|
698
|
+
app.innerHTML =
|
|
699
|
+
(message ? '<div class="notice">' + esc(message) + '</div>' : '') +
|
|
700
|
+
'<section class="hero">' +
|
|
701
|
+
'<div class="privacy"><span>Local scan</span><p>Hi, I am Echo. I read your Codex and Claude Code history from ' + esc(r.scanStartDate) + ' to ' + esc(r.scanEndDate) + ', straight off your machine. Nothing was uploaded, moved to the cloud, or shared.</p></div>' +
|
|
702
|
+
'<div class="heroGrid">' +
|
|
703
|
+
'<div class="heroCopy">' +
|
|
704
|
+
'<p class="kicker-mono">Workspace Physical-Exam</p>' +
|
|
705
|
+
'<h2>You shipped a lot. Your agent kept re-reading the same files.</h2>' +
|
|
706
|
+
'<p>Across ' + number(uwh.activeAttentionDays) + ' active days you put in ' + (uwh.userAttentionHours || 0) + ' hours of real focus, moved ' + big(s.totalTokens) + ' tokens through ' + number(s.repoCount) + ' repos, and ran ' + number(s.sessionCount) + ' sessions. That is a season of work.</p>' +
|
|
707
|
+
'</div>' +
|
|
708
|
+
'<aside class="scoreCard"><span>Context Cleanliness</span><strong>' + number(cl.score) + '</strong><em>/100</em><p>' + number(cl.staleReadSharePct) + '% of everything the agent read was re-reading files that had not changed. That is ' + number(cl.staleRereads) + ' stale rereads.</p></aside>' +
|
|
709
|
+
'</div>' +
|
|
710
|
+
'<div class="statStrip">' +
|
|
711
|
+
'<div class="stat"><strong>' + (uwh.userAttentionHours || 0) + 'h</strong><span>focused attention</span></div>' +
|
|
712
|
+
'<div class="stat"><strong>' + big(s.totalTokens) + '</strong><span>tokens moved</span></div>' +
|
|
713
|
+
'<div class="stat alert"><strong>' + money(c.total) + '</strong><span>API-equivalent worth</span></div>' +
|
|
714
|
+
'<div class="stat"><strong>' + number(rh.lateNightShare) + '%</strong><span>prompts after 10pm</span></div>' +
|
|
715
|
+
'</div>' +
|
|
716
|
+
'<p class="sourceNote">Source: <code>~/.codex/sessions</code> + <code>~/.claude/projects</code> · ' + number(s.sessionCount) + ' sessions · model ' + esc(mu.topModel) + ' ' + number(mu.topModelShare) + '%. Cost is an API-equivalent estimate at list prices, not your subscription bill. Codex reads are extracted best-effort from shell commands (~73% coverage); Claude Code reads are exact.</p>' +
|
|
717
|
+
'</section>' +
|
|
718
|
+
|
|
719
|
+
'<section class="section"><div class="sectionHead"><span>01</span><h2>This is your trail.</h2></div>' +
|
|
720
|
+
'<div class="twoCol">' +
|
|
721
|
+
'<div class="storyText">' +
|
|
722
|
+
'<p>On an active day you average ' + (uwh.averageDailyAttentionHours || 0) + ' hours of focus. Your longest single day was ' + (uwh.maxDailyAttentionHours || 0) + ' hours, on ' + esc(uwh.peakDay && uwh.peakDay.date) + '. You are a <b>' + esc(mu.identityLabel) + '</b> — ' + esc(mu.topModel) + ' carried ' + number(mu.topModelShare) + '% of the load.</p>' +
|
|
723
|
+
'<p>And I can see when you work. ' + number(rh.lateNightShare) + '% of your prompts land after 10pm — ' + number(rh.lateNightSessionCount) + ' sessions ran deep into the night. Your busiest day is ' + esc(rh.busiestWeekday) + '. The red bars are the late hours you kept going.</p>' +
|
|
724
|
+
'</div>' +
|
|
725
|
+
'<div class="panel">' + histogram(rh.hourHistogram) + '<div class="chips">' + (r.repos || []).slice(0, 6).map(function (x) { return '<span>' + esc(x.name) + ' · ' + (x.attentionHours || 0) + 'h</span>'; }).join("") + '</div></div>' +
|
|
726
|
+
'</div>' +
|
|
727
|
+
'<div class="panel" style="margin-top:18px">' + repoRows(r.repos) + '</div>' +
|
|
728
|
+
'</section>' +
|
|
729
|
+
|
|
730
|
+
'<section class="section"><div class="sectionHead"><span>02</span><h2>The money feeds input, not output.</h2></div>' +
|
|
731
|
+
'<div class="twoCol">' +
|
|
732
|
+
'<div class="storyText">' +
|
|
733
|
+
'<p>' + number(s.inputPct) + '% of your tokens are input. Output — the part you picture paying for — is ' + money(c.outputCost) + '. Cold input is only ' + money(c.coldInputCost) + '.</p>' +
|
|
734
|
+
'<p>Cache is cheap per token, so you stop worrying about it. But the volume is enormous: cache reads alone cost ' + money(c.cacheReadCost) + ', cache writes ' + money(c.cacheWriteCost) + '. You paid ' + money(c.total) + ' in API-equivalent worth, and almost all of it is the agent re-loading context, turn after turn.</p>' +
|
|
735
|
+
'</div>' +
|
|
736
|
+
'<div class="panel">' + costSplit(c) + '</div>' +
|
|
737
|
+
'</div>' +
|
|
738
|
+
'</section>' +
|
|
739
|
+
|
|
740
|
+
'<section class="section"><div class="sectionHead"><span>03</span><h2>Why the score is ' + number(cl.score) + ': it kept re-reading.</h2></div>' +
|
|
741
|
+
'<div class="twoCol">' +
|
|
742
|
+
'<div class="storyText">' +
|
|
743
|
+
(night ? '<p>Late on ' + esc(night.day) + ', around ' + esc(night.hour) + ':00, you were heads-down trying to land something. And right then your agent opened <b>' + esc(night.file) + '</b> again — the ' + number(night.priorReads) + 'th time it had pulled that file into this workspace, unchanged since the read before.</p>' : '') +
|
|
744
|
+
'<p>That is the pattern everywhere. ' + number(cl.staleReadSharePct) + '% of the content the agent pulled in was re-reading files that never changed since the last look. It re-learns your repo, your design rules, your structure — over and over — instead of remembering them.</p>' +
|
|
745
|
+
'<p>This is not your fault. The agent is stateless. Every new session is a blank page; every long session pushes earlier reads out of the window. So it reads the same files again, and you wait.</p>' +
|
|
746
|
+
'</div>' +
|
|
747
|
+
'<div class="panel">' + rereadRows(rf.topFiles) + '</div>' +
|
|
748
|
+
'</div>' +
|
|
749
|
+
(night ? '<div class="nightCard"><span>Deep-night reread</span><p><b>' + esc(night.file) + '</b> re-read at ' + esc(night.hour) + ':00 on ' + esc(night.day) + ' — the <b>' + number(night.priorReads) + 'th</b> read of that file in your workspace, unchanged since the previous one.</p></div>' : '') +
|
|
750
|
+
'</section>' +
|
|
751
|
+
|
|
752
|
+
'<section class="section"><div class="sectionHead"><span>04</span><h2>Echo remembers so it stops re-reading.</h2></div>' +
|
|
753
|
+
'<div class="twoCol">' +
|
|
754
|
+
'<div class="storyText">' +
|
|
755
|
+
'<p>I do not touch the real work — the building, the judgment, the direction. I remove the part that should never repeat: warm-up, unchanged-file rereads, re-learning your repo and your rules every session.</p>' +
|
|
756
|
+
'<p>Direct estimate from your own data: about ' + (aw.hours || 0) + ' hours of avoidable wait, roughly ' + (aw.days || 0) + ' focused days. Every file I hold becomes a compounding asset across every future session and every tool. You stop paying the re-reading tax twice.</p>' +
|
|
757
|
+
'<p id="signin-help">Connect the same EchoMem account you want memories saved to. After sign-in returns here, you choose what to extract.</p>' +
|
|
758
|
+
'</div>' +
|
|
759
|
+
'<aside class="promiseCard">' +
|
|
760
|
+
'<span>Next-week target</span>' +
|
|
761
|
+
'<strong>' + number(cl.score) + ' → ' + number(cf.targetScore) + '</strong>' +
|
|
762
|
+
'<p>Turn your repo map, hot files, design rules, and recent decisions into memory the agent never rebuilds.</p>' +
|
|
763
|
+
'<div class="promiseActions">' +
|
|
764
|
+
'<button id="signin" class="primary">' + (connected ? "Continue" : "Connect Echo") + '</button>' +
|
|
765
|
+
'<a id="signin-fallback" class="button secondary hidden" href="' + esc(authUrl) + '">Open sign-in in this tab</a>' +
|
|
766
|
+
'</div>' +
|
|
767
|
+
'</aside>' +
|
|
768
|
+
'</div>' +
|
|
769
|
+
'</section>';
|
|
770
|
+
bindConnect();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/* ---------- dashboard (post-auth extraction) ---------- */
|
|
774
|
+
function statusStrip(title, detail, kind) {
|
|
775
|
+
return '<div class="status-strip ' + esc(kind || "") + '"><span class="dot"></span><div><strong>' + esc(title) + '</strong><p>' + esc(detail) + '</p></div></div>';
|
|
776
|
+
}
|
|
777
|
+
function renderBridgeIssue(error) {
|
|
778
|
+
setHead("Local bridge issue", "Retrying");
|
|
779
|
+
app.className = "loading-panel";
|
|
780
|
+
app.innerHTML =
|
|
781
|
+
'<div style="padding:24px"><h2>Still waiting for the local bridge</h2>' +
|
|
782
|
+
'<p class="helper">Keep the terminal running. If it already exited or printed an error, close this tab and run echomem-mcp setup again.</p>' +
|
|
783
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') + '</div>';
|
|
784
|
+
}
|
|
785
|
+
function renderLogoutPending() {
|
|
786
|
+
setHead("Signing out locally", "Working");
|
|
787
|
+
app.className = "loading-panel";
|
|
788
|
+
app.innerHTML = '<div style="padding:24px"><h2>Clearing this device token</h2><p class="helper">If the bridge is scanning local files, this can take a few seconds to answer.</p></div>';
|
|
789
|
+
}
|
|
790
|
+
function renderLogoutIssue(error) {
|
|
791
|
+
setHead("Sign out did not finish", "Needs attention");
|
|
792
|
+
app.className = "loading-panel";
|
|
793
|
+
app.innerHTML =
|
|
794
|
+
'<div style="padding:24px"><h2>The local bridge did not answer</h2>' +
|
|
795
|
+
'<p class="helper">Keep the terminal open and retry. If the command exited, close this tab and run setup again.</p>' +
|
|
796
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') +
|
|
797
|
+
'<div class="actions"><button id="logout" class="secondary">Retry sign out</button></div></div>';
|
|
798
|
+
var lo = document.getElementById("logout"); if (lo) lo.onclick = localLogout;
|
|
799
|
+
}
|
|
800
|
+
async function waitForConnection() {
|
|
801
|
+
if (connectionPollStarted) return;
|
|
802
|
+
connectionPollStarted = true;
|
|
803
|
+
try {
|
|
804
|
+
for (;;) {
|
|
805
|
+
if (decisionMade) return;
|
|
806
|
+
try {
|
|
807
|
+
var cfg = await getJson("/config");
|
|
808
|
+
authUrl = cfg.authUrl || authUrl;
|
|
809
|
+
if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
|
|
810
|
+
} catch (_) {}
|
|
811
|
+
await sleep(900);
|
|
812
|
+
}
|
|
813
|
+
} finally { connectionPollStarted = false; }
|
|
814
|
+
}
|
|
815
|
+
function startConnectionPoll() { void waitForConnection(); }
|
|
402
816
|
async function waitForStats() {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
await
|
|
417
|
-
|
|
817
|
+
if (statsPollStarted) return;
|
|
818
|
+
statsPollStarted = true;
|
|
819
|
+
setHead("Context Doctor", "Working");
|
|
820
|
+
app.className = "loading";
|
|
821
|
+
app.innerHTML = '<div class="skeleton"><div class="sk title"></div><div class="sk w70"></div></div><div class="sk-grid"><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div></div>';
|
|
822
|
+
try {
|
|
823
|
+
var started = Date.now();
|
|
824
|
+
var lastError = "";
|
|
825
|
+
for (;;) {
|
|
826
|
+
if (decisionMade) return;
|
|
827
|
+
try {
|
|
828
|
+
var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
|
|
829
|
+
if (res.status === 200) {
|
|
830
|
+
stats = await res.json();
|
|
831
|
+
if (decisionMade) return;
|
|
832
|
+
renderDashboard();
|
|
833
|
+
if (stats && stats.partial) { await sleep(1200); continue; }
|
|
834
|
+
return;
|
|
418
835
|
}
|
|
419
|
-
|
|
836
|
+
if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
837
|
+
} catch (e) {
|
|
838
|
+
lastError = e && e.message ? e.message : String(e);
|
|
839
|
+
if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
|
|
840
|
+
if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
|
|
420
841
|
}
|
|
421
|
-
|
|
422
|
-
} catch (e) {
|
|
423
|
-
if (Date.now() - started > 20000) {
|
|
424
|
-
statsSlow = true;
|
|
425
|
-
renderDashboard();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
if (Date.now() - started > 20000 && !statsSlow) {
|
|
429
|
-
statsSlow = true;
|
|
430
|
-
renderDashboard();
|
|
842
|
+
await sleep(statsSlow ? 1500 : 500);
|
|
431
843
|
}
|
|
432
|
-
|
|
433
|
-
}
|
|
844
|
+
} finally { statsPollStarted = false; }
|
|
434
845
|
}
|
|
435
846
|
function renderDashboard(error) {
|
|
436
847
|
var migratable = stats && stats.migratable ? stats.migratable : {};
|
|
437
848
|
var pending = stats && stats.migratable ? stats.migratable.pending : null;
|
|
438
849
|
var hasPendingCount = typeof pending === "number";
|
|
439
|
-
var
|
|
440
|
-
|
|
850
|
+
var discovery = stats && stats.discovery ? stats.discovery : {};
|
|
851
|
+
var isPartial = !!(stats && stats.partial);
|
|
852
|
+
var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
|
|
441
853
|
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
442
854
|
var buckets = migrationBuckets(migratable);
|
|
443
|
-
var over1m = bucketCount(buckets, "huge") + bucketCount(buckets, "massive");
|
|
444
|
-
var processed = stats && stats.migratable ? stats.migratable.alreadyMigrated : null;
|
|
445
855
|
var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
|
|
446
|
-
var
|
|
856
|
+
var knownDone = hasPendingCount && pending === 0;
|
|
857
|
+
var canExtract = hasPendingCount && pending > 0;
|
|
447
858
|
var sessions = stats && stats.sessions ? stats.sessions : {};
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
859
|
+
setHead(knownDone ? "History is ready" : "Context Doctor", localScanReady ? "Ready" : "Scanning");
|
|
860
|
+
var heroValue = knownDone ? "Complete" : (eta.estimatedLabel || migratable.estimatedLabel || "checking");
|
|
861
|
+
// Size profile: fold the char buckets into a simple small / medium / large summary.
|
|
862
|
+
var tierOf = function (keys) {
|
|
863
|
+
var count = 0, chars = 0;
|
|
864
|
+
keys.forEach(function (k) {
|
|
865
|
+
var b = buckets.filter(function (x) { return x && x.key === k; })[0];
|
|
866
|
+
if (b) { count += b.count || 0; chars += b.chars || 0; }
|
|
867
|
+
});
|
|
868
|
+
return { count: count, chars: chars };
|
|
869
|
+
};
|
|
870
|
+
var smallT = tierOf(["small"]);
|
|
871
|
+
var mediumT = tierOf(["routine", "large"]);
|
|
872
|
+
var largeT = tierOf(["veryLarge", "huge", "massive"]);
|
|
873
|
+
var convWord = pending === 1 ? "conversation" : "conversations";
|
|
874
|
+
var heroTitle = knownDone
|
|
875
|
+
? "Your local AI history is already covered."
|
|
876
|
+
: (pending === 1 ? "Echo found one conversation ready for memory." : "Echo found local AI history it can turn into memory.");
|
|
877
|
+
var subtext;
|
|
878
|
+
if (knownDone) {
|
|
879
|
+
subtext = skippedActive
|
|
880
|
+
? skippedActive.toLocaleString() + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing and will be picked up later."
|
|
881
|
+
: "Your historical conversations are already in this account.";
|
|
882
|
+
} else if (hasPendingCount) {
|
|
883
|
+
subtext = number(pending) + " " + convWord + " ready to extract — already-processed ones are skipped, so re-running stays incremental.";
|
|
884
|
+
} else {
|
|
885
|
+
subtext = "Checking your local history…";
|
|
886
|
+
}
|
|
887
|
+
var candidateDetail = knownDone ? "Nothing to extract right now." : number(pending) + " " + convWord + (localScanReady ? " · ready" : " · still sizing");
|
|
456
888
|
app.className = "";
|
|
457
889
|
app.innerHTML =
|
|
458
|
-
(statsSlow ? '<div class="warning">Local
|
|
459
|
-
(stats && stats.partial ? '<div class="warning">Fast local counts are ready. Detailed local stats are still loading.</div>' : '') +
|
|
890
|
+
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
|
|
460
891
|
(error ? '<div class="error">' + esc(error) + '</div>' : '') +
|
|
461
|
-
'<section class="
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
'<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
'
|
|
470
|
-
|
|
471
|
-
'<div class="mini-row"><span>Active skipped</span><strong>' + esc(number(skippedActive)) + '</strong></div>' +
|
|
472
|
-
'<div class="mini-row"><span>1M+ sessions</span><strong>' + esc(number(over1m)) + '</strong></div>' +
|
|
473
|
-
'</div>' +
|
|
892
|
+
'<section class="privacy"><span>Local scan</span><p>Echo checked your Codex and Claude Code history on this device. No transcripts leave this machine unless you start extraction.</p></section>' +
|
|
893
|
+
'<section class="doctor-hero">' +
|
|
894
|
+
'<div class="doctor-copy"><h2>' + esc(heroTitle) + '</h2><p>' + esc(subtext) + '</p></div>' +
|
|
895
|
+
'<aside class="doctor-score"><span>' + esc(knownDone ? "Extraction complete" : "Estimated extraction time") + '</span><strong>' + esc(heroValue) + '</strong><p>' + esc(candidateDetail) + '</p>' +
|
|
896
|
+
'<div class="doctor-mini">' +
|
|
897
|
+
'<div class="mini-row"><span>Small ≤30k</span><strong>' + esc(number(smallT.count)) + ' · ' + esc(compact(smallT.chars)) + '</strong></div>' +
|
|
898
|
+
'<div class="mini-row"><span>Medium 30k–350k</span><strong>' + esc(number(mediumT.count)) + ' · ' + esc(compact(mediumT.chars)) + '</strong></div>' +
|
|
899
|
+
'<div class="mini-row"><span>Large >350k</span><strong>' + esc(number(largeT.count)) + ' · ' + esc(compact(largeT.chars)) + '</strong></div>' +
|
|
900
|
+
'</div>' +
|
|
901
|
+
'</aside>' +
|
|
474
902
|
'</section>' +
|
|
475
|
-
'<div class="
|
|
903
|
+
'<div class="doctor-stat-strip">' +
|
|
476
904
|
metric("Sessions found", number(sessions.total), number(sessions.codex) + " Codex + " + number(sessions.claudeCode) + " Claude Code") +
|
|
477
|
-
metric("
|
|
478
|
-
metric("Approx input tokens", compact(migratable.approxInputTokens || (eta && eta.approxInputTokens)), "local estimate") +
|
|
479
|
-
metric("Memories captured", number(stats && stats.memoriesCaptured), "already in EchoMem") +
|
|
905
|
+
metric("Memories captured", countOrChecking(stats && stats.memoriesCaptured), "already in EchoMem") +
|
|
480
906
|
'</div>' +
|
|
481
|
-
'<div class="
|
|
482
|
-
'<
|
|
483
|
-
'<section class="panel"><h2>Extraction Plan</h2><dl>' +
|
|
484
|
-
row("Mode", "local bridge queue") +
|
|
485
|
-
row("Uploads", "text turns only") +
|
|
486
|
-
row("Already processed", "skipped") +
|
|
487
|
-
row("Active sessions", "picked up later") +
|
|
488
|
-
'</dl></section>' +
|
|
489
|
-
'</div>' +
|
|
490
|
-
'<div class="actions">' +
|
|
491
|
-
'<button id="migrate" class="primary" ' + (knownDone ? "disabled" : "") + '>-> Extract unprocessed conversations</button>' +
|
|
907
|
+
'<div class="actions doctor-actions">' +
|
|
908
|
+
'<button id="migrate" class="primary" ' + (canExtract ? "" : "disabled") + '>Extract conversations</button>' +
|
|
492
909
|
'<button id="skip" class="secondary">' + (knownDone ? "Finish setup" : "Not today") + '</button>' +
|
|
910
|
+
'<button id="logout" class="secondary">Sign out locally</button>' +
|
|
493
911
|
'</div>';
|
|
494
912
|
document.getElementById("migrate").onclick = startMigrate;
|
|
495
913
|
document.getElementById("skip").onclick = skip;
|
|
914
|
+
document.getElementById("logout").onclick = localLogout;
|
|
496
915
|
}
|
|
497
916
|
async function startMigrate() {
|
|
498
917
|
if (decisionMade) return;
|
|
499
918
|
decisionMade = true;
|
|
500
|
-
setHead("
|
|
501
|
-
app.className = "
|
|
502
|
-
app.innerHTML = '<
|
|
919
|
+
setHead("Extracting", "Starting");
|
|
920
|
+
app.className = "";
|
|
921
|
+
app.innerHTML = '<section class="privacy"><span>Extracting</span><p>Starting your extraction. Echo sizes the remaining jobs, then imports automatically — keep this terminal open.</p></section><div class="loading" style="padding:18px 0"><div class="skeleton"><div class="sk title"></div><div class="sk w70"></div></div></div>';
|
|
503
922
|
try {
|
|
504
923
|
var started = await postJson("/migrate", {});
|
|
505
|
-
renderProgress(Object.assign({ status: "
|
|
924
|
+
renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
|
|
506
925
|
await pollProgress();
|
|
507
926
|
} catch (e) {
|
|
508
927
|
decisionMade = false;
|
|
@@ -518,21 +937,84 @@ export function renderSetupPage() {
|
|
|
518
937
|
}
|
|
519
938
|
}
|
|
520
939
|
function renderProgress(progress) {
|
|
940
|
+
var status = progress.status;
|
|
941
|
+
var done = status === "completed";
|
|
942
|
+
var stopped = status === "failed";
|
|
521
943
|
var total = progress.total || progress.jobCount || 0;
|
|
522
944
|
var completed = progress.completed || 0;
|
|
523
945
|
var failed = progress.failed || 0;
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
946
|
+
var running = progress.running || 0;
|
|
947
|
+
var queued = progress.queued || 0;
|
|
948
|
+
var extracted = progress.extracted || 0;
|
|
949
|
+
var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
|
|
950
|
+
app.className = "";
|
|
951
|
+
if (done || stopped) {
|
|
952
|
+
setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
|
|
953
|
+
app.innerHTML =
|
|
954
|
+
'<section class="finish">' +
|
|
955
|
+
'<div class="finishMark ' + (done ? "ok" : "warn") + '">' + (done ? "✓" : "!") + '</div>' +
|
|
956
|
+
'<strong class="finishBig">' + esc(number(extracted)) + '</strong>' +
|
|
957
|
+
'<span class="finishLabel">' + (extracted === 1 ? "memory" : "memories") + ' captured</span>' +
|
|
958
|
+
'<p>' + (done
|
|
959
|
+
? "from " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session will recall instead of re-reading — and it keeps compounding."
|
|
960
|
+
: (String(progress.error) === "key-expired"
|
|
961
|
+
? "Your encryption key wasn't accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
|
|
962
|
+
: esc(progress.error || "Some conversations did not finish.") + " Re-run <code>echomem-mcp migrate</code> to finish the rest; already-done conversations are skipped.")) + '</p>' +
|
|
963
|
+
(done && failed ? '<p class="helper">' + esc(number(failed)) + ' could not be extracted.</p>' : "") +
|
|
964
|
+
'</section>';
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (status === "starting" || status === "preparing") {
|
|
968
|
+
// Prep phase — no conversation has been sent yet. Name the specific step we're on
|
|
969
|
+
// (with a moving bar + stepper) so this window reads as active work, not a frozen "0 of N".
|
|
970
|
+
setHead("Preparing", "Preparing");
|
|
971
|
+
var note = progress.latest || progress.message || "";
|
|
972
|
+
var activeStep = /creat/i.test(note) ? 1 : 0;
|
|
973
|
+
var prepLabels = [
|
|
974
|
+
"Reading your local sessions",
|
|
975
|
+
"Creating your EchoMem import session",
|
|
976
|
+
"Queuing conversations to extract",
|
|
977
|
+
];
|
|
978
|
+
var stepsHtml = "";
|
|
979
|
+
for (var i = 0; i < prepLabels.length; i++) {
|
|
980
|
+
var stepCls = i < activeStep ? "done" : (i === activeStep ? "active" : "");
|
|
981
|
+
stepsHtml += '<li class="' + stepCls + '"><span class="dot"></span><span>' + esc(prepLabels[i]) + '</span></li>';
|
|
982
|
+
}
|
|
983
|
+
app.innerHTML =
|
|
984
|
+
'<section class="privacy"><span>Preparing</span><p>Nothing has left your machine yet — Echo is getting your sessions ready locally. This takes a few seconds before extraction starts.</p></section>' +
|
|
985
|
+
'<section class="doctor-hero">' +
|
|
986
|
+
'<div class="doctor-copy">' +
|
|
987
|
+
'<p class="label">Now working on</p>' +
|
|
988
|
+
'<h2>' + esc(prepLabels[activeStep]) + '…</h2>' +
|
|
989
|
+
'<div class="track indef"><div class="fill"></div></div>' +
|
|
990
|
+
'<ul class="prep-steps">' + stepsHtml + '</ul>' +
|
|
991
|
+
'</div>' +
|
|
992
|
+
'<aside class="doctor-score">' +
|
|
993
|
+
'<span>Conversations</span>' +
|
|
994
|
+
'<strong>' + (total ? esc(number(total)) : "…") + '</strong>' +
|
|
995
|
+
'<p>found locally</p>' +
|
|
996
|
+
'</aside>' +
|
|
997
|
+
'</section>';
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
setHead("Extracting", "Extracting");
|
|
527
1001
|
app.innerHTML =
|
|
528
|
-
'<
|
|
529
|
-
'<
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1002
|
+
'<section class="privacy"><span>Extracting</span><p>Echo is turning your history into memory. Keep this terminal open — it advances while the bridge runs. You can start using EchoMem in your editor now.</p></section>' +
|
|
1003
|
+
'<section class="doctor-hero">' +
|
|
1004
|
+
'<div class="doctor-copy">' +
|
|
1005
|
+
'<p class="label">Extraction progress</p>' +
|
|
1006
|
+
'<h2>' + (total ? esc(number(completed)) + ' of ' + esc(number(total)) : "Starting…") + '</h2>' +
|
|
1007
|
+
'<div class="track"><div class="fill" style="width:' + pct + '%"></div></div>' +
|
|
1008
|
+
'<div class="counts"><span>' + esc(number(completed)) + ' done</span><span>' + esc(number(running)) + ' running</span><span>' + esc(number(queued)) + ' queued</span>' + (failed ? '<span>' + esc(number(failed)) + ' failed</span>' : "") + '</div>' +
|
|
1009
|
+
(progress.latest ? '<p class="helper" style="margin-top:10px">Now: ' + esc(progress.latest) + '</p>' : "") +
|
|
1010
|
+
(progress.capped ? '<p class="helper">Importing the newest ' + esc(number(progress.capped)) + '; re-run <code>echomem-mcp migrate</code> later for older sessions.</p>' : "") +
|
|
1011
|
+
'</div>' +
|
|
1012
|
+
'<aside class="doctor-score">' +
|
|
1013
|
+
'<span>Memories captured</span>' +
|
|
1014
|
+
'<strong>' + esc(number(extracted)) + '</strong>' +
|
|
1015
|
+
'<p>' + (extracted ? "and growing" : "starting…") + '</p>' +
|
|
1016
|
+
'</aside>' +
|
|
1017
|
+
'</section>';
|
|
536
1018
|
}
|
|
537
1019
|
async function skip() {
|
|
538
1020
|
if (decisionMade) return;
|
|
@@ -540,27 +1022,91 @@ export function renderSetupPage() {
|
|
|
540
1022
|
try {
|
|
541
1023
|
await postJson("/skip", {});
|
|
542
1024
|
setHead("Setup complete", "Done");
|
|
543
|
-
app.className = "
|
|
544
|
-
app.innerHTML = '<h2>All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
|
|
1025
|
+
app.className = "notice";
|
|
1026
|
+
app.innerHTML = '<h2 style="color:#203820">All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
|
|
545
1027
|
} catch (e) {
|
|
546
1028
|
decisionMade = false;
|
|
547
1029
|
renderDashboard(e && e.message);
|
|
548
1030
|
}
|
|
549
1031
|
}
|
|
1032
|
+
async function localLogout() {
|
|
1033
|
+
if (decisionMade) return;
|
|
1034
|
+
decisionMade = true;
|
|
1035
|
+
renderLogoutPending();
|
|
1036
|
+
try {
|
|
1037
|
+
await postJson("/logout", {}, 12000);
|
|
1038
|
+
stats = null;
|
|
1039
|
+
statsSlow = false;
|
|
1040
|
+
connected = false;
|
|
1041
|
+
decisionMade = false;
|
|
1042
|
+
renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
decisionMade = false;
|
|
1045
|
+
renderLogoutIssue(e && e.message ? e.message : String(e));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/* ---------- scan-first poll for the forensic report ---------- */
|
|
1050
|
+
async function showReport() {
|
|
1051
|
+
if (reportPollStarted) return;
|
|
1052
|
+
reportPollStarted = true;
|
|
1053
|
+
if (!report) renderLoading();
|
|
1054
|
+
try {
|
|
1055
|
+
var fails = 0;
|
|
1056
|
+
for (;;) {
|
|
1057
|
+
if (decisionMade) return;
|
|
1058
|
+
try {
|
|
1059
|
+
var res = await fetchWithTimeout("/report?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 12000);
|
|
1060
|
+
if (res.status === 200) { report = await res.json(); renderForensicReport(); return; }
|
|
1061
|
+
if (res.status === 202) {
|
|
1062
|
+
fails = 0;
|
|
1063
|
+
var prog = await res.json().catch(function () { return null; });
|
|
1064
|
+
renderLoading(prog);
|
|
1065
|
+
} else {
|
|
1066
|
+
throw new Error(await res.text() || ("HTTP " + res.status));
|
|
1067
|
+
}
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
// Only a RUN of consecutive failures is a real bridge problem. A long scan that keeps
|
|
1070
|
+
// answering 202 must never trip this (that was the old false "Local bridge issue").
|
|
1071
|
+
fails++;
|
|
1072
|
+
if (!report && fails >= 8) renderBridgeIssue(e && e.message ? e.message : String(e));
|
|
1073
|
+
}
|
|
1074
|
+
await sleep(700);
|
|
1075
|
+
}
|
|
1076
|
+
} finally { reportPollStarted = false; }
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
window.addEventListener("message", function (event) {
|
|
1080
|
+
if (event.origin !== location.origin) return;
|
|
1081
|
+
var data = event.data || {};
|
|
1082
|
+
if (data.type === "echomem:connected" && data.nonce === nonce) {
|
|
1083
|
+
connected = true;
|
|
1084
|
+
closeAuthWindow();
|
|
1085
|
+
void waitForStats();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
550
1088
|
async function init() {
|
|
551
1089
|
if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp setup.");
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1090
|
+
if (params.get("connected") === "1") {
|
|
1091
|
+
connected = true;
|
|
1092
|
+
if (notifyOpenerConnected()) {
|
|
1093
|
+
// Popup flow: hand the main tab back control, then this popup closes.
|
|
1094
|
+
setHead("EchoMem connected", "Done");
|
|
1095
|
+
app.className = "notice";
|
|
1096
|
+
app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
|
|
1097
|
+
} else {
|
|
1098
|
+
// Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
|
|
1099
|
+
void waitForStats();
|
|
560
1100
|
}
|
|
561
1101
|
return;
|
|
562
1102
|
}
|
|
563
|
-
|
|
1103
|
+
// Learn the auth URL + connection state, but don't block the local scan on it.
|
|
1104
|
+
getJson("/config").then(function (cfg) {
|
|
1105
|
+
authUrl = cfg.authUrl || authUrl;
|
|
1106
|
+
connected = !!cfg.connected;
|
|
1107
|
+
}).catch(function () {});
|
|
1108
|
+
// Scan-first: show the local forensic report before asking the user to connect.
|
|
1109
|
+
await showReport();
|
|
564
1110
|
}
|
|
565
1111
|
init().catch(function (e) {
|
|
566
1112
|
setHead("Connection issue", "Needs attention");
|