@echomem/mcp 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/city/_live.html +37 -0
- package/dist/city/_serve.mjs +45 -0
- package/dist/city/card-data.json +15 -0
- package/dist/city/city-data.json +248 -0
- package/dist/city/echo-ai-city-only.html +1254 -0
- package/dist/city/echo-ai-city-only.template.html +1254 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/generate-echo-city-only.mjs +112 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/forensics.js +753 -0
- package/dist/index.js +61 -4
- package/dist/migrate.js +459 -99
- package/dist/report.js +1 -0
- package/dist/setup-page.js +1356 -308
- package/dist/setup.js +666 -75
- package/package.json +2 -2
package/dist/setup-page.js
CHANGED
|
@@ -8,11 +8,27 @@ 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-ink-accent-deep: #2f9d93;
|
|
22
|
+
--echo-night: #8b95cf;
|
|
23
|
+
--rdo-ease: cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
24
|
+
--echo-font-brand: "Avenir Next", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
25
|
+
--echo-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
26
|
+
--echo-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
27
|
+
--bg: var(--echo-paper-canvas);
|
|
28
|
+
--ink: var(--echo-ink-text);
|
|
29
|
+
--muted: var(--echo-ink-mute);
|
|
30
|
+
--line: var(--echo-line-soft);
|
|
31
|
+
--panel: var(--echo-paper-white);
|
|
16
32
|
--good-bg: #eff7ec;
|
|
17
33
|
--good-line: #cbd9c8;
|
|
18
34
|
--warn-bg: #fff6dc;
|
|
@@ -24,13 +40,15 @@ export function renderSetupPage() {
|
|
|
24
40
|
body {
|
|
25
41
|
margin: 0;
|
|
26
42
|
min-height: 100vh;
|
|
27
|
-
background:
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
background:
|
|
44
|
+
linear-gradient(180deg, rgba(245, 241, 231, 0.92), rgba(255, 255, 255, 0.96)),
|
|
45
|
+
var(--echo-paper-canvas);
|
|
46
|
+
color: var(--echo-ink-text);
|
|
47
|
+
font-family: var(--echo-font-body);
|
|
30
48
|
letter-spacing: 0;
|
|
31
49
|
}
|
|
32
50
|
main {
|
|
33
|
-
width: min(
|
|
51
|
+
width: min(1120px, 100%);
|
|
34
52
|
margin: 0 auto;
|
|
35
53
|
padding: clamp(18px, 4vw, 34px);
|
|
36
54
|
}
|
|
@@ -46,25 +64,33 @@ export function renderSetupPage() {
|
|
|
46
64
|
}
|
|
47
65
|
.kicker {
|
|
48
66
|
margin: 0;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
font-
|
|
67
|
+
font-family: var(--echo-font-mono);
|
|
68
|
+
color: var(--echo-ink-mute);
|
|
69
|
+
font-size: 12px;
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
letter-spacing: 0.08em;
|
|
52
72
|
text-transform: uppercase;
|
|
53
73
|
}
|
|
54
74
|
h1 {
|
|
55
75
|
margin: 4px 0 0;
|
|
56
|
-
font-
|
|
76
|
+
font-family: var(--echo-font-brand);
|
|
77
|
+
font-size: clamp(30px, 5vw, 56px);
|
|
78
|
+
font-weight: 900;
|
|
57
79
|
line-height: 1;
|
|
58
80
|
letter-spacing: 0;
|
|
81
|
+
color: var(--echo-ink-primary);
|
|
59
82
|
}
|
|
60
83
|
h2 {
|
|
61
84
|
margin: 0;
|
|
85
|
+
font-family: var(--echo-font-brand);
|
|
62
86
|
font-size: 22px;
|
|
87
|
+
font-weight: 900;
|
|
63
88
|
line-height: 1.2;
|
|
64
89
|
letter-spacing: 0;
|
|
90
|
+
color: var(--echo-ink-primary);
|
|
65
91
|
}
|
|
66
92
|
.pill {
|
|
67
|
-
min-height:
|
|
93
|
+
min-height: 36px;
|
|
68
94
|
display: inline-flex;
|
|
69
95
|
align-items: center;
|
|
70
96
|
gap: 8px;
|
|
@@ -72,7 +98,11 @@ export function renderSetupPage() {
|
|
|
72
98
|
border-radius: 999px;
|
|
73
99
|
padding: 0 14px;
|
|
74
100
|
background: var(--panel);
|
|
75
|
-
font-
|
|
101
|
+
font-family: var(--echo-font-mono);
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
letter-spacing: 0.04em;
|
|
105
|
+
text-transform: uppercase;
|
|
76
106
|
white-space: nowrap;
|
|
77
107
|
}
|
|
78
108
|
.state {
|
|
@@ -80,209 +110,721 @@ export function renderSetupPage() {
|
|
|
80
110
|
display: flex;
|
|
81
111
|
align-items: center;
|
|
82
112
|
justify-content: center;
|
|
83
|
-
color:
|
|
84
|
-
font-size:
|
|
113
|
+
color: var(--echo-ink-mute);
|
|
114
|
+
font-size: 16px;
|
|
85
115
|
text-align: center;
|
|
86
116
|
}
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
|
90
|
-
gap: 12px;
|
|
91
|
-
margin-bottom: 18px;
|
|
117
|
+
body.cityMode {
|
|
118
|
+
background: #eef0f3;
|
|
92
119
|
}
|
|
93
|
-
.
|
|
94
|
-
|
|
120
|
+
body.cityMode main {
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: 0;
|
|
123
|
+
margin: 0;
|
|
95
124
|
}
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
border-radius: 8px;
|
|
99
|
-
background: var(--panel);
|
|
100
|
-
padding: 18px;
|
|
125
|
+
body.cityMode header {
|
|
126
|
+
display: none;
|
|
101
127
|
}
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
body.cityMode #app {
|
|
129
|
+
display: block;
|
|
130
|
+
min-height: 100vh;
|
|
131
|
+
color: var(--echo-ink-text);
|
|
132
|
+
text-align: left;
|
|
107
133
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
|
|
135
|
+
/* clean, consistent loader for the post-connect "working/starting" states */
|
|
136
|
+
.miniLoad { display: grid; gap: 12px; max-width: 520px; padding: clamp(24px, 6vw, 64px) 0; }
|
|
137
|
+
.miniLoad h2 { margin: 0; font-family: var(--echo-font-brand); font-size: clamp(22px, 3.6vw, 32px); color: var(--echo-ink-primary); }
|
|
138
|
+
.miniLoad p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.5; }
|
|
139
|
+
.miniBar { height: 6px; border-radius: 999px; background: var(--echo-paper-soft); overflow: hidden; max-width: 360px; margin-top: 4px; }
|
|
140
|
+
.miniBar i { display: block; height: 100%; width: 36%; border-radius: 999px; background: var(--echo-ink-primary); animation: miniIndet 1.3s ease-in-out infinite; }
|
|
141
|
+
@keyframes miniIndet { 0% { transform: translateX(-115%); } 100% { transform: translateX(330%); } }
|
|
142
|
+
|
|
143
|
+
/* ---- forensic report (ContextDoctorTwo) ---- */
|
|
144
|
+
.cityHero {
|
|
145
|
+
--city-display-w: min(calc(100vw - 28px), calc((100vh - 28px) * 0.7797));
|
|
146
|
+
--city-display-h: min(calc(100vh - 28px), calc((100vw - 28px) * 1.2826));
|
|
147
|
+
position: relative;
|
|
148
|
+
min-height: 100vh;
|
|
149
|
+
display: block;
|
|
150
|
+
padding: 0;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
background: #eef0f3;
|
|
114
153
|
}
|
|
115
|
-
.
|
|
154
|
+
.cityShell {
|
|
155
|
+
position: relative;
|
|
156
|
+
width: 100vw;
|
|
157
|
+
height: 100vh;
|
|
158
|
+
border-radius: 0;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
}
|
|
161
|
+
.cityShell iframe {
|
|
162
|
+
width: 100%;
|
|
163
|
+
height: 100%;
|
|
164
|
+
border: 0;
|
|
116
165
|
display: block;
|
|
117
|
-
|
|
166
|
+
border-radius: 0;
|
|
167
|
+
background: transparent;
|
|
168
|
+
}
|
|
169
|
+
.cityArrow {
|
|
170
|
+
position: absolute;
|
|
171
|
+
z-index: 5;
|
|
172
|
+
left: 50%;
|
|
173
|
+
transform: translateX(-50%);
|
|
174
|
+
bottom: clamp(22px, 3.5vh, 42px);
|
|
175
|
+
width: 44px;
|
|
176
|
+
height: 44px;
|
|
177
|
+
min-height: 44px;
|
|
178
|
+
padding: 0;
|
|
179
|
+
border-radius: 999px;
|
|
180
|
+
border-color: rgba(26, 58, 143, 0.2);
|
|
181
|
+
background: rgba(255, 255, 255, 0.92);
|
|
182
|
+
color: var(--echo-ink-primary);
|
|
183
|
+
font-size: 24px;
|
|
118
184
|
line-height: 1;
|
|
119
|
-
|
|
185
|
+
box-shadow: 0 10px 28px rgba(28, 38, 60, 0.12);
|
|
120
186
|
}
|
|
121
|
-
.
|
|
187
|
+
.detailsWrap {
|
|
188
|
+
width: 100%;
|
|
122
189
|
margin: 0;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
line-height: 1.45;
|
|
190
|
+
padding: clamp(42px, 6vw, 86px) clamp(28px, 7vw, 104px);
|
|
191
|
+
background: #fff;
|
|
126
192
|
}
|
|
127
|
-
|
|
193
|
+
#setup-details { scroll-margin-top: 0; }
|
|
194
|
+
.hero { padding: 4px 0 24px; }
|
|
195
|
+
.privacy {
|
|
128
196
|
display: grid;
|
|
129
|
-
grid-template-columns:
|
|
130
|
-
gap:
|
|
131
|
-
align-items:
|
|
132
|
-
margin-bottom:
|
|
197
|
+
grid-template-columns: auto 1fr;
|
|
198
|
+
gap: 10px;
|
|
199
|
+
align-items: baseline;
|
|
200
|
+
margin-bottom: 24px;
|
|
201
|
+
padding: 10px 0;
|
|
202
|
+
border-bottom: 1px solid var(--echo-line-soft);
|
|
133
203
|
}
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
font-
|
|
138
|
-
|
|
139
|
-
|
|
204
|
+
.privacy span,
|
|
205
|
+
.kicker-mono,
|
|
206
|
+
.sectionHead span {
|
|
207
|
+
font-family: var(--echo-font-mono);
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
font-weight: 700;
|
|
210
|
+
letter-spacing: 0.08em;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
color: var(--echo-ink-primary);
|
|
213
|
+
}
|
|
214
|
+
.privacy p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
215
|
+
.heroGrid {
|
|
216
|
+
display: grid;
|
|
217
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
218
|
+
gap: clamp(20px, 3vw, 36px);
|
|
219
|
+
align-items: stretch;
|
|
140
220
|
}
|
|
141
|
-
.
|
|
142
|
-
|
|
221
|
+
.heroCopy { display: flex; flex-direction: column; justify-content: center; }
|
|
222
|
+
.heroCopy .kicker-mono { margin: 0 0 14px; color: var(--echo-ink-primary); }
|
|
223
|
+
.heroCopy h2 {
|
|
224
|
+
max-width: 720px;
|
|
225
|
+
margin: 0;
|
|
226
|
+
font-size: clamp(34px, 5vw, 58px);
|
|
227
|
+
line-height: 1.02;
|
|
228
|
+
}
|
|
229
|
+
.heroCopy p:not(.kicker-mono) {
|
|
230
|
+
margin: 18px 0 0;
|
|
231
|
+
font-size: clamp(15px, 1.25vw, 18px);
|
|
232
|
+
line-height: 1.62;
|
|
233
|
+
color: var(--echo-ink-text);
|
|
234
|
+
}
|
|
235
|
+
.scoreCard, .promiseCard, .panel {
|
|
236
|
+
border: 1px solid var(--echo-line-soft);
|
|
237
|
+
border-radius: 8px;
|
|
238
|
+
background: var(--echo-paper-white);
|
|
239
|
+
}
|
|
240
|
+
.scoreCard { display: grid; align-content: center; min-height: 282px; padding: 22px; }
|
|
241
|
+
.scoreCard span, .promiseCard span {
|
|
242
|
+
font-family: var(--echo-font-mono);
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
font-weight: 700;
|
|
245
|
+
letter-spacing: 0.08em;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
color: var(--echo-ink-mute);
|
|
248
|
+
}
|
|
249
|
+
.scoreCard strong {
|
|
143
250
|
margin-top: 12px;
|
|
251
|
+
font-family: var(--echo-font-brand);
|
|
252
|
+
font-size: clamp(72px, 8vw, 96px);
|
|
253
|
+
font-weight: 900;
|
|
254
|
+
line-height: 0.9;
|
|
255
|
+
color: var(--echo-ink-seal);
|
|
256
|
+
font-variant-numeric: tabular-nums;
|
|
144
257
|
}
|
|
145
|
-
.
|
|
258
|
+
.scoreCard em {
|
|
259
|
+
margin-top: 4px;
|
|
260
|
+
font-style: normal;
|
|
261
|
+
font-weight: 900;
|
|
262
|
+
font-size: 22px;
|
|
263
|
+
color: var(--echo-ink-primary);
|
|
264
|
+
}
|
|
265
|
+
.scoreCard p, .promiseCard p { margin: 14px 0 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
266
|
+
.statStrip {
|
|
146
267
|
display: grid;
|
|
147
|
-
|
|
268
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
269
|
+
gap: 10px;
|
|
270
|
+
margin-top: 22px;
|
|
148
271
|
}
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
border-
|
|
154
|
-
|
|
272
|
+
.stat {
|
|
273
|
+
min-height: 92px;
|
|
274
|
+
padding: 14px;
|
|
275
|
+
border: 1px solid var(--echo-line-soft);
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
background: var(--echo-paper-white);
|
|
155
278
|
}
|
|
156
|
-
.
|
|
157
|
-
|
|
279
|
+
.stat strong {
|
|
280
|
+
display: block;
|
|
281
|
+
font-family: var(--echo-font-brand);
|
|
282
|
+
font-size: clamp(24px, 2.8vw, 34px);
|
|
283
|
+
font-weight: 900;
|
|
284
|
+
line-height: 1;
|
|
285
|
+
color: var(--echo-ink-primary);
|
|
286
|
+
font-variant-numeric: tabular-nums;
|
|
287
|
+
}
|
|
288
|
+
.stat.alert strong { color: var(--echo-ink-seal); }
|
|
289
|
+
.stat span {
|
|
290
|
+
display: block;
|
|
291
|
+
margin-top: 8px;
|
|
292
|
+
font-family: var(--echo-font-mono);
|
|
293
|
+
font-size: 10px;
|
|
294
|
+
font-weight: 700;
|
|
295
|
+
letter-spacing: 0.06em;
|
|
296
|
+
text-transform: uppercase;
|
|
297
|
+
color: var(--echo-ink-mute);
|
|
298
|
+
}
|
|
299
|
+
.sourceNote {
|
|
300
|
+
max-width: 980px;
|
|
301
|
+
margin: 16px 0 0;
|
|
302
|
+
font-family: var(--echo-font-mono);
|
|
303
|
+
font-size: 11px;
|
|
304
|
+
line-height: 1.55;
|
|
305
|
+
color: var(--echo-ink-faint);
|
|
306
|
+
}
|
|
307
|
+
.sourceNote code {
|
|
308
|
+
padding: 1px 5px;
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
background: var(--echo-paper-soft);
|
|
311
|
+
color: var(--echo-ink-primary);
|
|
312
|
+
}
|
|
313
|
+
.section { padding: 30px 0 0; margin-top: 30px; border-top: 1px solid var(--echo-line-soft); }
|
|
314
|
+
.sectionHead { display: flex; gap: 12px; align-items: baseline; margin-bottom: 18px; }
|
|
315
|
+
.sectionHead h2 { margin: 0; font-size: clamp(24px, 3.4vw, 38px); line-height: 1.05; }
|
|
316
|
+
.twoCol {
|
|
158
317
|
display: grid;
|
|
159
|
-
grid-template-columns: minmax(
|
|
160
|
-
gap:
|
|
318
|
+
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.8fr);
|
|
319
|
+
gap: clamp(20px, 3.5vw, 36px);
|
|
320
|
+
align-items: start;
|
|
161
321
|
}
|
|
162
|
-
.
|
|
163
|
-
|
|
322
|
+
.storyText p { margin: 0 0 18px; color: var(--echo-ink-text); font-size: clamp(15px, 1.25vw, 18px); line-height: 1.62; }
|
|
323
|
+
.storyText p:last-child { margin-bottom: 0; }
|
|
324
|
+
.storyText b { color: var(--echo-ink-primary); }
|
|
325
|
+
.panel { display: grid; gap: 12px; padding: 16px; }
|
|
326
|
+
.promiseCard { padding: 22px; }
|
|
327
|
+
.promiseCard strong {
|
|
328
|
+
display: block;
|
|
329
|
+
margin-top: 12px;
|
|
330
|
+
font-family: var(--echo-font-brand);
|
|
331
|
+
font-size: clamp(34px, 4vw, 48px);
|
|
332
|
+
font-weight: 900;
|
|
333
|
+
line-height: 1;
|
|
334
|
+
color: var(--echo-ink-primary);
|
|
335
|
+
}
|
|
336
|
+
.promiseActions { display: grid; gap: 10px; margin-top: 18px; }
|
|
337
|
+
.promiseCard button, .promiseCard .button {
|
|
338
|
+
width: 100%;
|
|
339
|
+
min-height: 48px;
|
|
340
|
+
border-radius: 6px;
|
|
341
|
+
font-family: var(--echo-font-mono);
|
|
342
|
+
font-size: 13px;
|
|
164
343
|
font-weight: 800;
|
|
165
|
-
|
|
344
|
+
letter-spacing: 0.08em;
|
|
345
|
+
text-transform: uppercase;
|
|
166
346
|
}
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
347
|
+
.promiseCard .primary { border-color: var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; }
|
|
348
|
+
.promiseCard .secondary { background: #fff; color: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
349
|
+
|
|
350
|
+
/* histogram */
|
|
351
|
+
.histo { display: flex; align-items: flex-end; gap: 3px; height: 120px; padding-top: 8px; }
|
|
352
|
+
.histoBar { flex: 1; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--echo-ink-primary); }
|
|
353
|
+
.histoBar.night { background: var(--echo-ink-seal); }
|
|
354
|
+
.histoAxis {
|
|
355
|
+
display: flex; justify-content: space-between; margin-top: 8px;
|
|
356
|
+
font-family: var(--echo-font-mono); font-size: 10px; letter-spacing: 0.06em; color: var(--echo-ink-faint);
|
|
171
357
|
}
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
padding:
|
|
175
|
-
|
|
358
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
359
|
+
.chips span {
|
|
360
|
+
padding: 6px 8px; border: 1px solid var(--echo-line-soft); border-radius: 6px;
|
|
361
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-primary);
|
|
176
362
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
363
|
+
|
|
364
|
+
/* reread / repo rows */
|
|
365
|
+
.row2 {
|
|
366
|
+
display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
|
|
367
|
+
padding: 11px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
181
368
|
}
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
369
|
+
.row2:last-child { border-bottom: 0; }
|
|
370
|
+
.row2 b { font-family: var(--echo-font-mono); font-size: 13px; color: var(--echo-ink-primary); }
|
|
371
|
+
.row2 small { display: block; margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
|
|
372
|
+
.row2 strong {
|
|
373
|
+
font-family: var(--echo-font-brand); font-size: 24px; font-weight: 900;
|
|
374
|
+
color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
186
375
|
}
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
376
|
+
.repoRow strong { color: var(--echo-ink-primary); }
|
|
377
|
+
.tag {
|
|
378
|
+
display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px;
|
|
379
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 10px;
|
|
380
|
+
font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal);
|
|
191
381
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
382
|
+
|
|
383
|
+
/* cost split + model rows */
|
|
384
|
+
.segBar { display: flex; width: 100%; height: 22px; overflow: hidden; border-radius: 5px; background: var(--echo-paper-soft); }
|
|
385
|
+
.segBar span { display: block; min-width: 2px; }
|
|
386
|
+
.seg-output { background: var(--echo-ink-primary); }
|
|
387
|
+
.seg-cold { background: var(--echo-ink-mute); }
|
|
388
|
+
.seg-cw { background: var(--echo-ink-accent); }
|
|
389
|
+
.seg-cr { background: var(--echo-ink-seal); }
|
|
390
|
+
.costLegend { display: grid; gap: 6px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
|
|
391
|
+
.modelRow {
|
|
392
|
+
display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: baseline;
|
|
393
|
+
padding: 9px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
394
|
+
font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute);
|
|
197
395
|
}
|
|
198
|
-
|
|
199
|
-
|
|
396
|
+
.modelRow:last-child { border-bottom: 0; }
|
|
397
|
+
.modelRow b { font-size: 13px; color: var(--echo-ink-primary); }
|
|
398
|
+
.modelRow strong { font-family: var(--echo-font-brand); font-size: 18px; font-weight: 900; color: var(--echo-ink-seal); }
|
|
399
|
+
|
|
400
|
+
.nightCard {
|
|
401
|
+
margin-top: 18px; padding: 20px 22px; border: 1px solid var(--echo-ink-seal);
|
|
402
|
+
border-radius: 8px; background: var(--echo-paper-white);
|
|
403
|
+
}
|
|
404
|
+
.nightCard span {
|
|
405
|
+
font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
|
|
406
|
+
text-transform: uppercase; color: var(--echo-ink-seal);
|
|
407
|
+
}
|
|
408
|
+
.nightCard p { margin: 10px 0 0; font-size: 17px; line-height: 1.5; color: var(--echo-ink-text); }
|
|
409
|
+
.nightCard b { color: var(--echo-ink-primary); }
|
|
410
|
+
|
|
411
|
+
/* optional city explanation page */
|
|
412
|
+
.explainPage {
|
|
413
|
+
max-width: 1260px;
|
|
414
|
+
margin: 0 auto;
|
|
415
|
+
}
|
|
416
|
+
.explainHero {
|
|
200
417
|
display: grid;
|
|
201
|
-
|
|
418
|
+
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
|
|
419
|
+
gap: clamp(28px, 5vw, 70px);
|
|
420
|
+
align-items: end;
|
|
421
|
+
padding-bottom: clamp(28px, 5vw, 58px);
|
|
422
|
+
border-bottom: 1px solid var(--echo-line-soft);
|
|
202
423
|
}
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
424
|
+
.explainCopy {
|
|
425
|
+
max-width: 760px;
|
|
426
|
+
}
|
|
427
|
+
.explainCopy h2 {
|
|
428
|
+
margin: 10px 0 0;
|
|
429
|
+
font-size: clamp(38px, 5.4vw, 74px);
|
|
430
|
+
line-height: 0.98;
|
|
431
|
+
color: var(--echo-ink-primary);
|
|
432
|
+
}
|
|
433
|
+
.explainCopy p:not(.kicker-mono) {
|
|
434
|
+
max-width: 680px;
|
|
435
|
+
margin: 20px 0 0;
|
|
436
|
+
font-size: clamp(16px, 1.35vw, 20px);
|
|
437
|
+
line-height: 1.55;
|
|
438
|
+
color: var(--echo-ink-text);
|
|
439
|
+
}
|
|
440
|
+
.cleanReadout {
|
|
441
|
+
border: 1px solid var(--echo-line-soft);
|
|
442
|
+
border-radius: 8px;
|
|
443
|
+
padding: 22px;
|
|
444
|
+
background: #fff;
|
|
445
|
+
}
|
|
446
|
+
.cleanReadout span {
|
|
447
|
+
display: block;
|
|
448
|
+
font-family: var(--echo-font-mono);
|
|
449
|
+
font-size: 11px;
|
|
450
|
+
font-weight: 800;
|
|
451
|
+
letter-spacing: 0.08em;
|
|
452
|
+
text-transform: uppercase;
|
|
453
|
+
color: var(--echo-ink-mute);
|
|
454
|
+
}
|
|
455
|
+
.cleanReadout strong {
|
|
456
|
+
display: block;
|
|
457
|
+
margin-top: 8px;
|
|
458
|
+
font-family: var(--echo-font-brand);
|
|
459
|
+
font-size: clamp(54px, 6vw, 88px);
|
|
460
|
+
font-weight: 900;
|
|
461
|
+
line-height: 0.9;
|
|
462
|
+
color: var(--echo-ink-seal);
|
|
463
|
+
font-variant-numeric: tabular-nums;
|
|
464
|
+
}
|
|
465
|
+
.cleanReadout p {
|
|
214
466
|
margin: 14px 0 0;
|
|
215
|
-
|
|
467
|
+
color: var(--echo-ink-mute);
|
|
468
|
+
font-size: 15px;
|
|
469
|
+
line-height: 1.55;
|
|
470
|
+
}
|
|
471
|
+
.insightPills {
|
|
216
472
|
display: grid;
|
|
473
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
217
474
|
gap: 10px;
|
|
475
|
+
margin-top: clamp(18px, 3vw, 32px);
|
|
218
476
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
477
|
+
.insightPill {
|
|
478
|
+
border: 1px solid var(--echo-line-soft);
|
|
479
|
+
border-radius: 8px;
|
|
480
|
+
padding: 14px 16px;
|
|
481
|
+
background: #fff;
|
|
223
482
|
}
|
|
224
|
-
.
|
|
225
|
-
display:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
483
|
+
.insightPill strong {
|
|
484
|
+
display: block;
|
|
485
|
+
font-family: var(--echo-font-brand);
|
|
486
|
+
font-size: clamp(24px, 2.2vw, 34px);
|
|
487
|
+
font-weight: 900;
|
|
488
|
+
line-height: 1;
|
|
489
|
+
color: var(--echo-ink-primary);
|
|
490
|
+
font-variant-numeric: tabular-nums;
|
|
229
491
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
display:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
border-radius: 8px;
|
|
237
|
-
border: 1px solid #171717;
|
|
238
|
-
padding: 0 16px;
|
|
239
|
-
font: inherit;
|
|
492
|
+
.insightPill.alert strong { color: var(--echo-ink-seal); }
|
|
493
|
+
.insightPill span {
|
|
494
|
+
display: block;
|
|
495
|
+
margin-top: 8px;
|
|
496
|
+
font-family: var(--echo-font-mono);
|
|
497
|
+
font-size: 10px;
|
|
240
498
|
font-weight: 800;
|
|
241
|
-
|
|
499
|
+
letter-spacing: 0.07em;
|
|
500
|
+
text-transform: uppercase;
|
|
501
|
+
color: var(--echo-ink-mute);
|
|
502
|
+
}
|
|
503
|
+
.calcNote {
|
|
504
|
+
margin-top: 18px;
|
|
505
|
+
border-top: 1px solid var(--echo-line-soft);
|
|
506
|
+
padding-top: 14px;
|
|
507
|
+
color: var(--echo-ink-faint);
|
|
508
|
+
font-family: var(--echo-font-mono);
|
|
509
|
+
font-size: 11px;
|
|
510
|
+
line-height: 1.6;
|
|
511
|
+
}
|
|
512
|
+
.calcNote summary {
|
|
242
513
|
cursor: pointer;
|
|
514
|
+
color: var(--echo-ink-primary);
|
|
515
|
+
font-weight: 800;
|
|
516
|
+
text-transform: uppercase;
|
|
517
|
+
letter-spacing: 0.06em;
|
|
243
518
|
}
|
|
244
|
-
.
|
|
245
|
-
|
|
246
|
-
|
|
519
|
+
.calcNote p { margin: 10px 0 0; }
|
|
520
|
+
.explainSections {
|
|
521
|
+
display: grid;
|
|
522
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
523
|
+
gap: 14px;
|
|
524
|
+
margin-top: clamp(28px, 4vw, 52px);
|
|
247
525
|
}
|
|
248
|
-
.
|
|
526
|
+
.explainCard {
|
|
527
|
+
border: 1px solid var(--echo-line-soft);
|
|
528
|
+
border-radius: 8px;
|
|
249
529
|
background: #fff;
|
|
250
|
-
|
|
530
|
+
padding: 18px;
|
|
531
|
+
min-height: 230px;
|
|
532
|
+
display: grid;
|
|
533
|
+
gap: 14px;
|
|
534
|
+
align-content: start;
|
|
251
535
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
536
|
+
.explainCard .kicker-mono { margin: 0; color: var(--echo-ink-mute); }
|
|
537
|
+
.explainCard h3 {
|
|
538
|
+
margin: 0;
|
|
539
|
+
font-family: var(--echo-font-brand);
|
|
540
|
+
font-size: clamp(21px, 2vw, 28px);
|
|
541
|
+
line-height: 1.08;
|
|
542
|
+
color: var(--echo-ink-primary);
|
|
255
543
|
}
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
margin: 16px 0 10px;
|
|
544
|
+
.explainCard p {
|
|
545
|
+
margin: 0;
|
|
546
|
+
color: var(--echo-ink-text);
|
|
547
|
+
font-size: 15px;
|
|
548
|
+
line-height: 1.55;
|
|
262
549
|
}
|
|
263
|
-
.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
background:
|
|
267
|
-
transition: width 180ms ease;
|
|
550
|
+
.explainCard .panel {
|
|
551
|
+
padding: 0;
|
|
552
|
+
border: 0;
|
|
553
|
+
background: transparent;
|
|
268
554
|
}
|
|
269
|
-
.
|
|
270
|
-
display:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
555
|
+
.explainCta {
|
|
556
|
+
display: grid;
|
|
557
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
558
|
+
gap: 24px;
|
|
559
|
+
align-items: center;
|
|
560
|
+
margin-top: 16px;
|
|
561
|
+
padding: 22px;
|
|
562
|
+
border: 1px solid var(--echo-line-soft);
|
|
563
|
+
border-radius: 8px;
|
|
564
|
+
background: var(--echo-paper-canvas);
|
|
565
|
+
}
|
|
566
|
+
.explainCta h3 {
|
|
567
|
+
margin: 0;
|
|
568
|
+
font-family: var(--echo-font-brand);
|
|
569
|
+
font-size: clamp(24px, 2.5vw, 34px);
|
|
570
|
+
line-height: 1.08;
|
|
571
|
+
color: var(--echo-ink-primary);
|
|
572
|
+
}
|
|
573
|
+
.explainCta p {
|
|
574
|
+
margin: 8px 0 0;
|
|
575
|
+
max-width: 700px;
|
|
576
|
+
color: var(--echo-ink-mute);
|
|
577
|
+
font-size: 15px;
|
|
578
|
+
line-height: 1.5;
|
|
276
579
|
}
|
|
580
|
+
.explainCta .primary, .explainCta .secondary {
|
|
581
|
+
min-width: 180px;
|
|
582
|
+
min-height: 50px;
|
|
583
|
+
border-radius: 8px;
|
|
584
|
+
font-family: var(--echo-font-mono);
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
font-weight: 800;
|
|
587
|
+
letter-spacing: 0.08em;
|
|
588
|
+
text-transform: uppercase;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ---- dashboard (post-auth extraction) ---- */
|
|
592
|
+
.loading-panel, .doctor-copy, .doctor-score, .metric, .extract-panel {
|
|
593
|
+
border: 1px solid var(--line);
|
|
594
|
+
border-radius: 8px;
|
|
595
|
+
background: var(--panel);
|
|
596
|
+
}
|
|
597
|
+
.status-strip {
|
|
598
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
599
|
+
border: 1px solid #c7d6e5; border-radius: 8px; background: #eef5fb; color: #1e3449;
|
|
600
|
+
padding: 13px 15px; margin-bottom: 18px;
|
|
601
|
+
}
|
|
602
|
+
.status-strip.good { border-color: var(--good-line); background: var(--good-bg); color: #203820; }
|
|
603
|
+
.status-strip.warn { border-color: var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
604
|
+
.status-strip .dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 999px; margin-top: 4px; background: currentColor; }
|
|
605
|
+
.status-strip strong { display: block; line-height: 1.25; }
|
|
606
|
+
.status-strip p { margin-top: 3px; color: currentColor; font-size: 14px; }
|
|
607
|
+
.doctor-hero {
|
|
608
|
+
display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
|
|
609
|
+
gap: 14px; margin-bottom: 14px; align-items: stretch;
|
|
610
|
+
}
|
|
611
|
+
.doctor-copy, .doctor-score { padding: clamp(18px, 3vw, 26px); }
|
|
612
|
+
.doctor-copy { display: flex; flex-direction: column; justify-content: center; min-height: 240px; }
|
|
613
|
+
.doctor-copy h2 { max-width: 760px; margin-top: 10px; font-size: clamp(30px, 4.4vw, 52px); line-height: 1.02; }
|
|
614
|
+
.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); }
|
|
615
|
+
.doctor-score { display: grid; align-content: center; gap: 12px; }
|
|
616
|
+
.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; }
|
|
617
|
+
.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); }
|
|
618
|
+
.doctor-mini { display: grid; gap: 8px; margin-top: 12px; }
|
|
619
|
+
.mini-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 8px; }
|
|
620
|
+
.mini-row span { color: var(--echo-ink-text); font-size: 14px; }
|
|
621
|
+
.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; }
|
|
622
|
+
.doctor-stat-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 14px; }
|
|
623
|
+
.doctor-actions { margin: 0 0 18px; }
|
|
624
|
+
.metric { min-height: 122px; padding: 18px; display: flex; flex-direction: column; justify-content: space-between; }
|
|
625
|
+
.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; }
|
|
626
|
+
.value { display: block; font-family: var(--echo-font-brand); font-size: 34px; font-weight: 900; line-height: 1; color: var(--echo-ink-primary); }
|
|
627
|
+
.detail { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.45; }
|
|
628
|
+
.extract-panel { padding: 18px; }
|
|
629
|
+
.bucket-row { align-items: center; display: grid; grid-template-columns: minmax(90px, 1fr) minmax(100px, auto); gap: 12px; }
|
|
630
|
+
.bucket-value { text-align: right; font-weight: 800; white-space: nowrap; }
|
|
631
|
+
.helper { margin-top: 10px; color: var(--echo-ink-mute); font-size: 14px; }
|
|
632
|
+
.notice, .done, .warning, .error { border-radius: 8px; padding: 14px 16px; margin-bottom: 18px; }
|
|
633
|
+
.notice, .done { border: 1px solid var(--good-line); background: var(--good-bg); color: #203820; }
|
|
634
|
+
.warning { border: 1px solid var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
635
|
+
.error { border: 1px solid var(--error-line); background: var(--error-bg); color: #7d2117; }
|
|
636
|
+
.details { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); gap: 12px; margin-bottom: 18px; }
|
|
637
|
+
dl { margin: 14px 0 0; display: grid; gap: 10px; }
|
|
638
|
+
.row { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid #ecece7; padding-bottom: 8px; }
|
|
639
|
+
dt { color: var(--echo-ink-text); }
|
|
640
|
+
dd { margin: 0; text-align: right; color: var(--echo-ink-text); }
|
|
641
|
+
ol { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px; }
|
|
642
|
+
li { display: flex; justify-content: space-between; gap: 10px; }
|
|
643
|
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
644
|
+
button, .button {
|
|
645
|
+
min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
646
|
+
border-radius: 8px; border: 1px solid var(--echo-ink-primary); padding: 0 16px;
|
|
647
|
+
font: inherit; font-weight: 800; text-decoration: none; cursor: pointer;
|
|
648
|
+
}
|
|
649
|
+
.primary { background: var(--echo-ink-primary); color: #fff; }
|
|
650
|
+
.secondary { background: #fff; color: var(--echo-ink-primary); }
|
|
651
|
+
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
652
|
+
.track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
|
|
653
|
+
.fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
|
|
654
|
+
/* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
|
|
655
|
+
.track.indef .fill { width: 40%; transition: none; animation: prepSlide 1.2s ease-in-out infinite; }
|
|
656
|
+
@keyframes prepSlide { 0% { transform: translateX(-110%); } 100% { transform: translateX(350%); } }
|
|
657
|
+
.prep-steps { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
|
|
658
|
+
.prep-steps li { display: grid; grid-template-columns: 16px 1fr; gap: 11px; align-items: center; font-size: 15px; color: var(--echo-ink-mute); }
|
|
659
|
+
.prep-steps .dot { width: 13px; height: 13px; border-radius: 999px; border: 2px solid #cfcfc6; box-sizing: border-box; }
|
|
660
|
+
.prep-steps li.done { color: var(--echo-ink-text); }
|
|
661
|
+
.prep-steps li.done .dot { background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
662
|
+
.prep-steps li.active { color: var(--echo-ink-primary); font-weight: 700; }
|
|
663
|
+
.prep-steps li.active .dot { border-color: var(--echo-ink-primary); animation: prepDot 1.1s ease-in-out infinite; }
|
|
664
|
+
@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); } }
|
|
665
|
+
.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; }
|
|
666
|
+
.finish { display: grid; gap: 6px; place-items: center; text-align: center; padding: clamp(28px, 6vw, 64px) 0; }
|
|
667
|
+
.finishMark { width: 54px; height: 54px; border-radius: 999px; display: grid; place-items: center; font-size: 26px; font-weight: 900; margin-bottom: 8px; }
|
|
668
|
+
.finishMark.ok { background: var(--good-bg); color: #2f7f4f; border: 1px solid var(--good-line); }
|
|
669
|
+
.finishMark.warn { background: var(--warn-bg); color: #5e4300; border: 1px solid var(--warn-line); }
|
|
670
|
+
.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; }
|
|
671
|
+
.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); }
|
|
672
|
+
.finish p { max-width: 560px; margin: 12px 0 0; color: var(--echo-ink-text); font-size: 16px; line-height: 1.6; }
|
|
673
|
+
.finish p.helper { color: var(--echo-ink-mute); font-size: 14px; margin-top: 6px; }
|
|
674
|
+
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
675
|
.hidden { display: none; }
|
|
278
|
-
@media (max-width:
|
|
279
|
-
.
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
.
|
|
283
|
-
|
|
676
|
+
@media (max-width: 820px) {
|
|
677
|
+
.heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .doctor-stat-strip,
|
|
678
|
+
.explainHero, .insightPills, .explainSections, .explainCta { grid-template-columns: 1fr; }
|
|
679
|
+
.doctor-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
680
|
+
.heroCopy h2 { font-size: 34px; }
|
|
681
|
+
.scoreCard { min-height: 220px; }
|
|
682
|
+
.explainCopy h2 { font-size: 38px; }
|
|
683
|
+
.explainCta .promiseActions { width: 100%; }
|
|
684
|
+
.explainCta .primary, .explainCta .secondary { width: 100%; }
|
|
685
|
+
.cityHero { min-height: auto; }
|
|
686
|
+
.cityShell {
|
|
687
|
+
width: 100vw;
|
|
688
|
+
height: 100vh;
|
|
284
689
|
}
|
|
285
690
|
}
|
|
691
|
+
/* extraction dashboard — plate layout */
|
|
692
|
+
.exHero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.95fr); gap: clamp(20px, 3vw, 40px); align-items: center; margin-top: 8px; }
|
|
693
|
+
.exCopy h2 { max-width: 560px; margin: 8px 0 0; font-size: clamp(30px, 4vw, 48px); line-height: 1.03; }
|
|
694
|
+
.exCopy .exSub { margin: 14px 0 0; max-width: 520px; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.55; color: var(--echo-ink-mute); }
|
|
695
|
+
.exLegend { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 16px; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
|
|
696
|
+
.exLegend .lgDot { width: 12px; height: 12px; border-radius: 3px; display: inline-block; margin-left: 10px; }
|
|
697
|
+
.exLegend .lgDot:first-child { margin-left: 0; }
|
|
698
|
+
.lgNew { background: #2f6df0; }
|
|
699
|
+
.lgDim { background: #cfc8b8; }
|
|
700
|
+
.exHero .actions { margin-top: 22px; }
|
|
701
|
+
.exHint { margin: 14px 0 0; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
|
|
702
|
+
.exPlate { width: 100%; }
|
|
703
|
+
/* WebGL clay city (echo-extraction-plate.html) — the only plate; no SVG layer */
|
|
704
|
+
.plateStack { position: relative; width: 100%; aspect-ratio: 640 / 470; }
|
|
705
|
+
.plateStack .plateGl { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: transparent; }
|
|
706
|
+
.recon { margin-top: 16px; display: inline-block; font-family: var(--echo-font-mono); font-size: 13px; font-weight: 700; color: var(--echo-ink-primary); background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); border-radius: 8px; padding: 8px 12px; }
|
|
707
|
+
@media (max-width: 820px) { .exHero { grid-template-columns: 1fr; } }
|
|
708
|
+
|
|
709
|
+
/* ===== workspace readout dashboard (Part 2 — replaces explainPage) ===== */
|
|
710
|
+
@keyframes rdoRise { from { opacity: 0; translate: 0 14px; } to { opacity: 1; translate: 0 0; } }
|
|
711
|
+
@keyframes rdoGrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
|
|
712
|
+
@keyframes rdoDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
|
|
713
|
+
@keyframes rdoWipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
|
|
714
|
+
body.cityMode .detailsWrap { background: linear-gradient(180deg, rgba(245,241,231,0.92), rgba(255,255,255,0.96)), var(--echo-paper-canvas); }
|
|
715
|
+
.echo-rdo { width: min(1280px, 100%); margin: 0 auto; color: var(--echo-ink-text); font-family: var(--echo-font-body); }
|
|
716
|
+
.echo-rdo .rise { opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) forwards; animation-delay: var(--d, 0ms); }
|
|
717
|
+
.echo-rdo .rdo-head { border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 20px; margin-bottom: 22px; }
|
|
718
|
+
.echo-rdo .rdo-kicker { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-primary); }
|
|
719
|
+
.echo-rdo .rdo-h1 { margin: 8px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(28px, 4vw, 46px); line-height: 1.02; letter-spacing: -0.01em; color: var(--echo-ink-primary); }
|
|
720
|
+
.echo-rdo .rdo-lead { margin: 12px 0 0; font-size: clamp(15px, 1.3vw, 17px); line-height: 1.55; color: var(--echo-ink-text); max-width: 56ch; }
|
|
721
|
+
.echo-rdo .rdo-lead b { color: var(--echo-ink-primary); }
|
|
722
|
+
.echo-rdo .rdo-meta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
723
|
+
.echo-rdo .rdo-chip { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--echo-line-soft); border-radius: 999px; background: var(--echo-paper-white); padding: 6px 13px; font-family: var(--echo-font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
724
|
+
.echo-rdo .rdo-chip b { color: var(--echo-ink-primary); }
|
|
725
|
+
.echo-rdo .rdo-chip .live { width: 7px; height: 7px; border-radius: 50%; background: var(--echo-ink-accent); }
|
|
726
|
+
.echo-rdo .rdo-card { position: relative; background: var(--echo-paper-white); border: 1px solid var(--echo-line-soft); border-radius: 10px; padding: 18px; transition: box-shadow 0.2s var(--rdo-ease), transform 0.2s var(--rdo-ease); }
|
|
727
|
+
.echo-rdo .rdo-card:hover { box-shadow: 0 14px 30px -16px rgba(26,58,143,0.28); transform: translateY(-2px); }
|
|
728
|
+
.echo-rdo .clab { margin: 0; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
729
|
+
.echo-rdo .csub { margin-top: 4px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-faint); }
|
|
730
|
+
.echo-rdo .csub b { color: var(--echo-ink-primary); }
|
|
731
|
+
.echo-rdo .rdo-help { position: absolute; top: 13px; right: 13px; width: 24px; height: 24px; min-height: 0; box-sizing: border-box; border-radius: 999px; border: 1px solid var(--echo-line-soft); background: var(--echo-paper-soft); color: var(--echo-ink-faint); font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; cursor: pointer; display: grid; place-items: center; padding: 0; line-height: 1; transition: all 0.15s ease; }
|
|
732
|
+
.echo-rdo .rdo-help:hover { color: #fff; background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
733
|
+
.echo-rdo .rdo-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 14px; }
|
|
734
|
+
.echo-rdo .rdo-kpi { min-height: 132px; display: flex; flex-direction: column; justify-content: flex-end; }
|
|
735
|
+
.echo-rdo .rdo-kpi .num { font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(34px, 4vw, 50px); line-height: 0.9; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; }
|
|
736
|
+
.echo-rdo .rdo-kpi.hero { border-color: rgba(199,55,47,0.30); }
|
|
737
|
+
.echo-rdo .rdo-kpi.hero::before { content: ""; position: absolute; left: 0; top: 14px; bottom: 14px; width: 3px; border-radius: 3px; background: var(--echo-ink-seal); }
|
|
738
|
+
.echo-rdo .rdo-kpi.hero .num { color: var(--echo-ink-seal); }
|
|
739
|
+
.echo-rdo .rdo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
740
|
+
.echo-rdo .figbox { margin-top: 16px; position: relative; }
|
|
741
|
+
.echo-rdo .rdo-histo { display: flex; align-items: flex-end; gap: 3px; height: 132px; border-bottom: 2px solid var(--echo-line-soft); }
|
|
742
|
+
.echo-rdo .rdo-histo .bar { flex: 1; align-self: flex-end; min-height: 3px; border-radius: 3px 3px 0 0; background: var(--echo-ink-primary); transform: scaleY(0); transform-origin: bottom; animation: rdoGrow 0.5s var(--rdo-ease) forwards; animation-delay: var(--bd, 0ms); }
|
|
743
|
+
.echo-rdo .rdo-histo .bar.night { background: var(--echo-night); }
|
|
744
|
+
.echo-rdo .rdo-histo .bar:hover { filter: brightness(1.12); }
|
|
745
|
+
.echo-rdo .rdo-axis { display: flex; justify-content: space-between; margin-top: 8px; font-family: var(--echo-font-mono); font-size: 10px; color: var(--echo-ink-faint); }
|
|
746
|
+
.echo-rdo .rdo-legend { display: flex; flex-wrap: wrap; gap: 7px 16px; margin-top: 13px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
|
|
747
|
+
.echo-rdo .rdo-legend i { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }
|
|
748
|
+
.echo-rdo .terr { display: grid; grid-template-columns: 150px 1fr; gap: 22px; align-items: center; margin-top: 16px; }
|
|
749
|
+
.echo-rdo .donut { width: 144px; height: 144px; position: relative; margin: 0 auto; }
|
|
750
|
+
.echo-rdo .donut .donutSvg { width: 100%; height: 100%; display: block; opacity: 0; animation: rdoRise 0.6s var(--rdo-ease) 0.2s forwards; }
|
|
751
|
+
.echo-rdo .donut .seg { transition: opacity 0.15s ease, stroke-width 0.15s ease; }
|
|
752
|
+
.echo-rdo .donut.hovering .seg { opacity: 0.3; }
|
|
753
|
+
.echo-rdo .donut.hovering .seg.on { opacity: 1; stroke-width: 7.5; }
|
|
754
|
+
.echo-rdo .donut .seg { cursor: pointer; }
|
|
755
|
+
.echo-rdo .donut-tip, .echo-rdo .histo-tip { position: absolute; left: 0; top: 0; pointer-events: none; transform: translate(-50%, -132%); background: var(--echo-ink-text); color: #fff; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; padding: 5px 9px; border-radius: 6px; white-space: nowrap; z-index: 6; opacity: 0; transition: opacity 0.12s ease; box-shadow: 0 6px 16px rgba(28,38,60,0.2); }
|
|
756
|
+
.echo-rdo .donut-tip.show, .echo-rdo .histo-tip.show { opacity: 1; }
|
|
757
|
+
.echo-rdo .donut-tip::after, .echo-rdo .histo-tip::after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--echo-ink-text); }
|
|
758
|
+
.echo-rdo .donut .center { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; pointer-events: none; }
|
|
759
|
+
.echo-rdo .donut .center b { display: block; font-family: var(--echo-font-brand); font-weight: 900; font-size: 28px; color: var(--echo-ink-primary); line-height: 0.9; }
|
|
760
|
+
.echo-rdo .donut .center span { font-family: var(--echo-font-mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
761
|
+
.echo-rdo .rrow { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: baseline; padding: 7px 0; }
|
|
762
|
+
.echo-rdo .rrow .nm { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
763
|
+
.echo-rdo .rrow .vl { font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-text); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
764
|
+
.echo-rdo .rbar { grid-column: 1 / -1; height: 5px; border-radius: 999px; background: var(--echo-paper-soft); margin-top: 3px; overflow: hidden; }
|
|
765
|
+
.echo-rdo .rbar i { display: block; height: 100%; border-radius: 999px; transform-origin: left; transform: scaleX(0); animation: rdoDraw 0.6s var(--rdo-ease) forwards; animation-delay: var(--rd, 0ms); }
|
|
766
|
+
.echo-rdo .lblrow { font-family: var(--echo-font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--echo-ink-mute); margin: 16px 0 7px; }
|
|
767
|
+
.echo-rdo .stack { display: flex; height: 28px; border-radius: 7px; overflow: hidden; background: var(--echo-paper-soft); }
|
|
768
|
+
.echo-rdo .stack .seg { min-width: 0; display: grid; place-items: center; font-family: var(--echo-font-mono); font-size: 10px; font-weight: 800; }
|
|
769
|
+
.echo-rdo .ledgerCols { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
|
|
770
|
+
.echo-rdo .ex-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline; padding: 11px 0; border-top: 1px solid var(--echo-line-soft); }
|
|
771
|
+
.echo-rdo .ex-row:first-of-type { border-top: 0; padding-top: 16px; }
|
|
772
|
+
.echo-rdo .collapse-inner .ex-row:first-of-type { border-top: 1px solid var(--echo-line-soft); padding-top: 11px; }
|
|
773
|
+
.echo-rdo .ex-file { font-family: var(--echo-font-mono); font-size: 12.5px; font-weight: 700; color: var(--echo-ink-primary); }
|
|
774
|
+
.echo-rdo .ex-meta { margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
|
|
775
|
+
.echo-rdo .ex-reads { font-family: var(--echo-font-brand); font-weight: 900; font-size: 20px; color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
776
|
+
.echo-rdo .rdo-tag { display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px; background: rgba(199,55,47,0.08); font-family: var(--echo-font-mono); font-size: 9px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal); }
|
|
777
|
+
.echo-rdo .collapse { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.34s var(--rdo-ease); }
|
|
778
|
+
.echo-rdo .collapse.open { grid-template-rows: 1fr; }
|
|
779
|
+
.echo-rdo .collapse-inner { overflow: hidden; min-height: 0; }
|
|
780
|
+
.echo-rdo .moreBtn { display: inline-block; min-height: 0; margin-top: 14px; background: none; border: 0; padding: 6px 0; cursor: pointer; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--echo-ink-primary); }
|
|
781
|
+
.echo-rdo .moreBtn:hover { opacity: 0.65; }
|
|
782
|
+
.echo-rdo .mrow2 { display: grid; grid-template-columns: 1fr auto auto; gap: 14px; align-items: baseline; padding: 10px 0; border-top: 1px solid var(--echo-line-soft); font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
|
|
783
|
+
.echo-rdo .mrow2:first-child { border-top: 0; }
|
|
784
|
+
.echo-rdo .mrow2 b { font-size: 13px; color: var(--echo-ink-primary); }
|
|
785
|
+
.echo-rdo .mrow2 strong { font-family: var(--echo-font-brand); font-size: 16px; color: var(--echo-ink-text); }
|
|
786
|
+
.echo-rdo .outcome { margin-top: 14px; border: 1px solid rgba(78,205,196,0.45); background: linear-gradient(180deg, rgba(78,205,196,0.10), rgba(78,205,196,0.04)); border-radius: 12px; padding: clamp(20px, 3vw, 30px); display: grid; grid-template-columns: 1.5fr 1fr; gap: clamp(22px, 3vw, 44px); align-items: center; }
|
|
787
|
+
.echo-rdo .outcome .rdo-kicker { color: var(--echo-ink-accent-deep); }
|
|
788
|
+
.echo-rdo .outcome h2 { margin: 9px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: clamp(22px, 2.7vw, 33px); line-height: 1.05; letter-spacing: -0.01em; color: var(--echo-ink-primary); }
|
|
789
|
+
.echo-rdo .outcome .rdo-lead { margin-top: 12px; max-width: 46ch; }
|
|
790
|
+
.echo-rdo .targetbar { position: relative; margin-top: 36px; }
|
|
791
|
+
.echo-rdo .tb-track { position: relative; height: 34px; border-radius: 999px; background: var(--echo-paper-soft); border: 1px solid var(--echo-line-soft); overflow: hidden; }
|
|
792
|
+
.echo-rdo .tb-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #d6473e, var(--echo-ink-seal)); display: flex; align-items: center; padding: 0 14px; color: #fff; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 800; white-space: nowrap; animation: rdoWipe 0.85s var(--rdo-ease) 0.2s both; }
|
|
793
|
+
.echo-rdo .tb-goal { position: absolute; top: 0; bottom: 0; width: 2px; background: rgba(255,255,255,0.9); border-radius: 2px; }
|
|
794
|
+
.echo-rdo .tb-flag { position: absolute; top: -29px; transform: translateX(-50%); background: var(--echo-ink-accent-deep); color: #fff; font-family: var(--echo-font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 9px; border-radius: 6px; white-space: nowrap; box-shadow: 0 3px 8px rgba(47,157,147,0.3); }
|
|
795
|
+
.echo-rdo .tb-flag::after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--echo-ink-accent-deep); }
|
|
796
|
+
.echo-rdo .oc-targets { background: var(--echo-paper-white); border: 1px solid var(--echo-line-soft); border-radius: 10px; padding: 18px; }
|
|
797
|
+
.echo-rdo .ot-title { margin: 0 0 6px; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
798
|
+
.echo-rdo .ot-note { margin: -2px 0 10px; font-size: 12px; line-height: 1.45; color: var(--echo-ink-mute); }
|
|
799
|
+
.echo-rdo .ot-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--echo-line-soft); }
|
|
800
|
+
.echo-rdo .ot-row:last-of-type { border-bottom: 0; }
|
|
801
|
+
.echo-rdo .ot-row .otk { display: flex; flex-direction: column; gap: 2px; }
|
|
802
|
+
.echo-rdo .ot-row .k { font-size: 13px; color: var(--echo-ink-text); }
|
|
803
|
+
.echo-rdo .ot-row .ks { font-family: var(--echo-font-mono); font-size: 10px; color: var(--echo-ink-faint); }
|
|
804
|
+
.echo-rdo .ot-row .v { font-family: var(--echo-font-brand); font-weight: 900; font-size: 22px; color: var(--echo-ink-accent-deep); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
805
|
+
.echo-rdo .rdo-cta { margin-top: 16px; width: 100%; min-height: 46px; border-radius: 8px; border: 1px solid var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; font-family: var(--echo-font-mono); font-size: 12px; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; cursor: pointer; transition: filter 0.15s ease; }
|
|
806
|
+
.echo-rdo .rdo-cta:hover { filter: brightness(1.12); }
|
|
807
|
+
.echo-rdo .rdo-foot { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--echo-line-soft); display: flex; flex-wrap: wrap; gap: 12px 24px; justify-content: space-between; font-family: var(--echo-font-mono); font-size: 11px; line-height: 1.6; color: var(--echo-ink-faint); }
|
|
808
|
+
.echo-rdo .rdo-foot code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
|
|
809
|
+
.echo-rdo .rdo-foot .priv { color: var(--echo-ink-accent-deep); font-weight: 700; }
|
|
810
|
+
@media (max-width: 860px) { .echo-rdo .rdo-kpis { grid-template-columns: repeat(2, 1fr); } .echo-rdo .rdo-grid { grid-template-columns: 1fr; } .echo-rdo .outcome { grid-template-columns: 1fr; } }
|
|
811
|
+
@media (max-width: 620px) { .echo-rdo .ledgerCols, .echo-rdo .terr { grid-template-columns: 1fr; } }
|
|
812
|
+
@media (prefers-reduced-motion: reduce) { .echo-rdo .rise, .echo-rdo .rdo-histo .bar, .echo-rdo .donut .donutSvg, .echo-rdo .rbar i, .echo-rdo .tb-fill { animation: none !important; opacity: 1 !important; transform: none !important; translate: none !important; clip-path: none !important; } }
|
|
813
|
+
/* annotation dialog */
|
|
814
|
+
.echo-note-dlg { border: 1px solid var(--echo-line-soft); border-radius: 14px; padding: 0; width: min(440px, calc(100vw - 32px)); background: var(--echo-paper-white); color: var(--echo-ink-text); box-shadow: 0 28px 60px -18px rgba(26,32,46,0.4); }
|
|
815
|
+
.echo-note-dlg[open] { animation: rdoRise 0.24s var(--rdo-ease); }
|
|
816
|
+
.echo-note-dlg::backdrop { background: rgba(26,32,46,0.4); }
|
|
817
|
+
.echo-note-dlg .nk, .echo-note-dlg .nt, .echo-note-dlg .nb { padding-left: 26px; padding-right: 26px; }
|
|
818
|
+
.echo-note-dlg .nk { margin: 24px 0 0; font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-accent-deep); }
|
|
819
|
+
.echo-note-dlg .nt { margin: 6px 0 0; font-family: var(--echo-font-brand); font-weight: 900; font-size: 24px; line-height: 1.1; color: var(--echo-ink-primary); }
|
|
820
|
+
.echo-note-dlg .nb { padding-top: 14px; padding-bottom: 26px; }
|
|
821
|
+
.echo-note-dlg .nb p { margin: 0 0 12px; font-size: 14px; line-height: 1.6; }
|
|
822
|
+
.echo-note-dlg .nb p:last-child { margin-bottom: 0; }
|
|
823
|
+
.echo-note-dlg .nb .echo { margin-top: 14px; padding: 12px 14px; border-radius: 8px; background: rgba(78,205,196,0.10); border: 1px solid rgba(78,205,196,0.3); font-size: 13px; line-height: 1.55; }
|
|
824
|
+
.echo-note-dlg .nb .echo b { color: var(--echo-ink-primary); }
|
|
825
|
+
.echo-note-dlg .nb code { background: var(--echo-paper-soft); color: var(--echo-ink-primary); padding: 1px 5px; border-radius: 4px; }
|
|
826
|
+
.echo-note-dlg .nclose { position: absolute; top: 13px; right: 14px; width: 30px; height: 30px; border-radius: 999px; border: 1px solid var(--echo-line-soft); background: var(--echo-paper-white); color: var(--echo-ink-mute); font-size: 18px; cursor: pointer; display: grid; place-items: center; }
|
|
827
|
+
.echo-note-dlg .nclose:hover { color: var(--echo-ink-seal); border-color: var(--echo-ink-seal); }
|
|
286
828
|
</style>
|
|
287
829
|
</head>
|
|
288
830
|
<body>
|
|
@@ -290,12 +832,18 @@ export function renderSetupPage() {
|
|
|
290
832
|
<header>
|
|
291
833
|
<div>
|
|
292
834
|
<p class="kicker">EchoMem device setup</p>
|
|
293
|
-
<h1 id="title">
|
|
835
|
+
<h1 id="title">Turn history into memory</h1>
|
|
294
836
|
</div>
|
|
295
|
-
<div class="pill" id="status">
|
|
837
|
+
<div class="pill" id="status">Local</div>
|
|
296
838
|
</header>
|
|
297
|
-
<section id="app" class="state">Starting local
|
|
839
|
+
<section id="app" class="state">Starting local scan...</section>
|
|
298
840
|
</main>
|
|
841
|
+
<dialog id="echo-note" class="echo-note-dlg">
|
|
842
|
+
<button class="nclose" aria-label="Close">×</button>
|
|
843
|
+
<p class="nk" id="echo-note-k"></p>
|
|
844
|
+
<h2 class="nt" id="echo-note-t"></h2>
|
|
845
|
+
<div class="nb" id="echo-note-b"></div>
|
|
846
|
+
</dialog>
|
|
299
847
|
<script>
|
|
300
848
|
(function () {
|
|
301
849
|
var params = new URLSearchParams(location.search);
|
|
@@ -303,10 +851,20 @@ export function renderSetupPage() {
|
|
|
303
851
|
var app = document.getElementById("app");
|
|
304
852
|
var title = document.getElementById("title");
|
|
305
853
|
var status = document.getElementById("status");
|
|
854
|
+
var report = null;
|
|
306
855
|
var stats = null;
|
|
307
856
|
var statsSlow = false;
|
|
308
857
|
var decisionMade = false;
|
|
858
|
+
var connected = false;
|
|
859
|
+
var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
|
|
860
|
+
var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
|
|
861
|
+
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
309
862
|
var authUrl = "";
|
|
863
|
+
var authWindow = null;
|
|
864
|
+
var connectionPollStarted = false;
|
|
865
|
+
var statsPollStarted = false;
|
|
866
|
+
var reportPollStarted = false;
|
|
867
|
+
var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
310
868
|
|
|
311
869
|
function esc(value) {
|
|
312
870
|
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
|
|
@@ -317,26 +875,43 @@ export function renderSetupPage() {
|
|
|
317
875
|
title.textContent = nextTitle;
|
|
318
876
|
status.textContent = nextStatus;
|
|
319
877
|
}
|
|
878
|
+
function setCityMode(enabled) {
|
|
879
|
+
document.body.classList.toggle("cityMode", !!enabled);
|
|
880
|
+
}
|
|
320
881
|
function number(value) {
|
|
321
882
|
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
322
883
|
}
|
|
884
|
+
function countOrChecking(value) {
|
|
885
|
+
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "checking";
|
|
886
|
+
}
|
|
323
887
|
function compact(value) {
|
|
324
888
|
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
325
889
|
var abs = Math.abs(value);
|
|
326
|
-
if (abs >=
|
|
327
|
-
if (abs >=
|
|
328
|
-
if (abs >=
|
|
890
|
+
if (abs >= 1e9) return (value / 1e9).toFixed(1) + "B";
|
|
891
|
+
if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
892
|
+
if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
|
|
329
893
|
return String(Math.round(value));
|
|
330
894
|
}
|
|
331
|
-
function
|
|
332
|
-
if (typeof
|
|
333
|
-
var
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return
|
|
895
|
+
function big(value) {
|
|
896
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
897
|
+
var n = Math.abs(value);
|
|
898
|
+
if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
|
|
899
|
+
if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
900
|
+
if (n >= 1e3) return Math.round(value / 1e3) + "K";
|
|
901
|
+
return String(Math.round(value));
|
|
337
902
|
}
|
|
338
|
-
function
|
|
339
|
-
return
|
|
903
|
+
function money(value) {
|
|
904
|
+
return "$" + number(value);
|
|
905
|
+
}
|
|
906
|
+
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
907
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
908
|
+
var controller = new AbortController();
|
|
909
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
910
|
+
try {
|
|
911
|
+
return await fetch(url, Object.assign({}, options || {}, { signal: controller.signal }));
|
|
912
|
+
} finally {
|
|
913
|
+
clearTimeout(timer);
|
|
914
|
+
}
|
|
340
915
|
}
|
|
341
916
|
async function getJson(path) {
|
|
342
917
|
var sep = path.indexOf("?") >= 0 ? "&" : "?";
|
|
@@ -344,18 +919,16 @@ export function renderSetupPage() {
|
|
|
344
919
|
if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
345
920
|
return await res.json();
|
|
346
921
|
}
|
|
347
|
-
async function postJson(path, body) {
|
|
348
|
-
var
|
|
922
|
+
async function postJson(path, body, timeoutMs) {
|
|
923
|
+
var options = {
|
|
349
924
|
method: "POST",
|
|
350
925
|
credentials: "omit",
|
|
351
926
|
headers: { "Content-Type": "application/json" },
|
|
352
927
|
body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
|
|
353
|
-
}
|
|
928
|
+
};
|
|
929
|
+
var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
|
|
354
930
|
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
|
-
}
|
|
931
|
+
if (!res.ok) throw new Error(data.message || data.error || ("HTTP " + res.status));
|
|
359
932
|
return data;
|
|
360
933
|
}
|
|
361
934
|
function metric(label, value, detail) {
|
|
@@ -366,17 +939,13 @@ export function renderSetupPage() {
|
|
|
366
939
|
}
|
|
367
940
|
function migrationBuckets(migratable) {
|
|
368
941
|
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
369
|
-
|
|
370
|
-
? migratable.buckets
|
|
371
|
-
: (Array.isArray(eta.buckets) ? eta.buckets : []);
|
|
372
|
-
return buckets;
|
|
942
|
+
return Array.isArray(migratable && migratable.buckets) ? migratable.buckets : (Array.isArray(eta.buckets) ? eta.buckets : []);
|
|
373
943
|
}
|
|
374
944
|
function bucketCount(buckets, key) {
|
|
375
945
|
var found = buckets.filter(function (b) { return b && b.key === key; })[0];
|
|
376
946
|
return found && typeof found.count === "number" ? found.count : 0;
|
|
377
947
|
}
|
|
378
948
|
function bucketList(buckets) {
|
|
379
|
-
if (!buckets.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
|
|
380
949
|
var visible = buckets.filter(function (b) { return b && (b.count || b.chars); });
|
|
381
950
|
if (!visible.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
|
|
382
951
|
return '<ol>' + visible.map(function (b) {
|
|
@@ -391,118 +960,499 @@ export function renderSetupPage() {
|
|
|
391
960
|
if (code === "NOT_LOGGED_IN") return "The local bridge is not logged in yet. Sign in again and retry.";
|
|
392
961
|
return code || "Something went wrong. Retry in a moment.";
|
|
393
962
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
963
|
+
function closeAuthWindow() {
|
|
964
|
+
try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
|
|
965
|
+
authWindow = null;
|
|
966
|
+
}
|
|
967
|
+
function notifyOpenerConnected() {
|
|
968
|
+
try {
|
|
969
|
+
if (window.opener && window.opener !== window && !window.opener.closed) {
|
|
970
|
+
window.opener.postMessage({ type: "echomem:connected", nonce: nonce }, location.origin);
|
|
971
|
+
setTimeout(function () { window.close(); }, 250);
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
} catch (_) {}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
function openAuthWindow(url) {
|
|
978
|
+
var targetUrl = url || authUrl;
|
|
979
|
+
if (!targetUrl) return false;
|
|
980
|
+
try {
|
|
981
|
+
authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
|
|
982
|
+
if (!authWindow) return false;
|
|
983
|
+
authWindow.focus();
|
|
984
|
+
return true;
|
|
985
|
+
} catch (_) { return false; }
|
|
986
|
+
}
|
|
987
|
+
function showPopupFallback(message, url) {
|
|
988
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-help]"), function (hint) {
|
|
989
|
+
hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
|
|
990
|
+
});
|
|
991
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-signin-fallback]"), function (fallback) {
|
|
992
|
+
fallback.setAttribute("href", url || authUrl);
|
|
993
|
+
fallback.classList.remove("hidden");
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
function onConnectClick() {
|
|
997
|
+
if (connected) { void waitForStats(); return; }
|
|
998
|
+
if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
|
|
999
|
+
startConnectionPoll();
|
|
1000
|
+
}
|
|
1001
|
+
function bindConnect() {
|
|
1002
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-connect-echo]"), function (signin) {
|
|
1003
|
+
signin.onclick = onConnectClick;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function bindDetailsArrow() {
|
|
1007
|
+
var arrow = document.getElementById("details-arrow");
|
|
1008
|
+
var target = document.getElementById("setup-details");
|
|
1009
|
+
if (arrow && target) {
|
|
1010
|
+
arrow.onclick = function () { target.scrollIntoView({ behavior: "smooth", block: "start" }); };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/* ---------- report shell (city iframe owns its own loading) ---------- */
|
|
1015
|
+
// The report page (full-width city hero + detail sections below) is mounted ONCE. The city iframe
|
|
1016
|
+
// shows its OWN in-canvas loading state while it scans, then fades into the rendered report — one
|
|
1017
|
+
// surface, no separate loading page, no reload. The parent only fills the detail sections on /report.
|
|
1018
|
+
function mountReportShell() {
|
|
1019
|
+
setCityMode(true);
|
|
1020
|
+
app.className = "";
|
|
397
1021
|
app.innerHTML =
|
|
398
|
-
'<
|
|
399
|
-
|
|
400
|
-
|
|
1022
|
+
'<section class="cityHero"><div class="cityShell">' +
|
|
1023
|
+
'<iframe title="AI Coding City" src="/city/echo-ai-city-only.html?setup=1&nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
|
|
1024
|
+
'<button id="details-arrow" class="cityArrow hidden" aria-label="Show detailed report">↓</button>' +
|
|
1025
|
+
'</div></section>' +
|
|
1026
|
+
'<div id="setup-details" class="detailsWrap"></div>';
|
|
1027
|
+
bindDetailsArrow();
|
|
1028
|
+
reportMounted = true;
|
|
1029
|
+
}
|
|
1030
|
+
function renderLoading() {
|
|
1031
|
+
if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
|
|
1032
|
+
setCityMode(true);
|
|
1033
|
+
setHead("Turn history into memory", "Reading");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/* ---------- forensic report (pic 1/2) ---------- */
|
|
1037
|
+
function histogram(hours) {
|
|
1038
|
+
var list = Array.isArray(hours) ? hours : [];
|
|
1039
|
+
var max = Math.max.apply(null, [1].concat(list));
|
|
1040
|
+
var bars = list.map(function (v, h) {
|
|
1041
|
+
var height = Math.max(2, Math.round((v / max) * 100));
|
|
1042
|
+
return '<span class="histoBar' + (NIGHT_HOURS[h] ? ' night' : '') + '" style="height:' + height + '%" title="' + h + ':00 — ' + number(v) + ' prompts"></span>';
|
|
1043
|
+
}).join("");
|
|
1044
|
+
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>';
|
|
1045
|
+
}
|
|
1046
|
+
function rereadRows(files) {
|
|
1047
|
+
return (files || []).slice(0, 6).map(function (f) {
|
|
1048
|
+
var meta = esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + (f.nightReads ? ' · ' + number(f.nightReads) + ' at night' : '');
|
|
1049
|
+
var tag = f.everChanged ? '' : '<span class="tag">never changed</span>';
|
|
1050
|
+
return '<div class="row2"><div><b>' + esc(f.file) + '</b>' + tag + '<small>' + meta + '</small></div><strong>' + number(f.totalReads) + '×</strong></div>';
|
|
1051
|
+
}).join("");
|
|
1052
|
+
}
|
|
1053
|
+
function repoRows(repos) {
|
|
1054
|
+
return (repos || []).slice(0, 5).map(function (r) {
|
|
1055
|
+
var meta = number(r.sessions) + ' sessions · ' + number(r.activeAttentionDays) + ' active days · ' + (r.dailyFocusHours || 0) + 'h/day · ' + big(r.tokens) + ' tokens';
|
|
1056
|
+
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>';
|
|
1057
|
+
}).join("");
|
|
1058
|
+
}
|
|
1059
|
+
function costSplit(cost) {
|
|
1060
|
+
var total = cost.total || 1;
|
|
1061
|
+
function seg(cls, v) { return '<span class="' + cls + '" style="width:' + Math.max(0, (v / total) * 100) + '%"></span>'; }
|
|
1062
|
+
var byModel = cost.byModel || {};
|
|
1063
|
+
var rows = Object.keys(byModel).map(function (k) { return [k, byModel[k]]; })
|
|
1064
|
+
.sort(function (a, b) { return b[1].total - a[1].total; })
|
|
1065
|
+
.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>'; })
|
|
1066
|
+
.join("");
|
|
1067
|
+
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>' +
|
|
1068
|
+
'<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>' +
|
|
1069
|
+
'<div style="margin-top:14px">' + rows + '</div>';
|
|
1070
|
+
}
|
|
1071
|
+
/* ---------- workspace readout dashboard (Part 2) ---------- */
|
|
1072
|
+
var RDO_NIGHT = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
1073
|
+
var RDO_PIE = ["#1a3a8f", "#4ecdc4", "#c7372f", "#7a86c2", "#d29a3f", "#7d9a6d", "#cf6f4e"]; // major-repo colors; "#b3aea0" (taupe) is reserved for "Others" only
|
|
1074
|
+
var rdoReduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
1075
|
+
function rdoDec(v) { return typeof v === "number" && isFinite(v) ? (Math.round(v * 10) / 10) : "-"; }
|
|
1076
|
+
function rdoFmt(v, k) { if (k === "pct") return number(v) + "%"; if (k === "hours") return number(v) + "h"; if (k === "money") return money(v); if (k === "big") return big(v); return number(v); }
|
|
1077
|
+
function rdoHourLabel(h) { var x = h % 24, ap = x < 12 ? "am" : "pm", hr = x % 12; if (hr === 0) hr = 12; return hr + ap; }
|
|
1078
|
+
function rdoHisto(hours) {
|
|
1079
|
+
var list = Array.isArray(hours) ? hours : [], n = list.length || 1, max = Math.max.apply(null, [1].concat(list));
|
|
1080
|
+
var bars = list.map(function (v, h) {
|
|
1081
|
+
var ht = Math.max(3, Math.round((v / max) * 100)), hour = Math.round(h * (24 / n));
|
|
1082
|
+
return '<div class="bar' + (RDO_NIGHT[hour] ? ' night' : '') + '" style="height:' + ht + '%;--bd:' + (300 + h * 14) + 'ms" data-tip="' + rdoHourLabel(hour) + ' · ' + number(v) + ' prompts"></div>';
|
|
1083
|
+
}).join("");
|
|
1084
|
+
return '<div class="figbox"><div class="rdo-histo">' + bars + '</div><div class="rdo-axis"><span>12am</span><span>6am</span><span>12pm</span><span>6pm</span><span>11pm</span></div>' +
|
|
1085
|
+
'<div class="rdo-legend"><span><i style="background:var(--echo-ink-primary)"></i>daytime</span><span><i style="background:var(--echo-night)"></i>late night (10pm–4am)</span></div><div class="histo-tip"></div></div>';
|
|
1086
|
+
}
|
|
1087
|
+
function rdoTerritory(repos) {
|
|
1088
|
+
var list = (repos || []).slice().sort(function (a, b) { return (b.attentionHours || 0) - (a.attentionHours || 0); });
|
|
1089
|
+
if (!list.length) return '<div class="terr"><div class="donut"></div><div></div></div>';
|
|
1090
|
+
var total = list.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0) || 1;
|
|
1091
|
+
var maxH = Math.max.apply(null, [1].concat(list.map(function (r) { return r.attentionHours || 0; })));
|
|
1092
|
+
var threshold = total * 0.05; // repos over 5% shown individually; the rest combine into "Others"
|
|
1093
|
+
var major = list.filter(function (r) { return (r.attentionHours || 0) > threshold; });
|
|
1094
|
+
var minor = list.filter(function (r) { return (r.attentionHours || 0) <= threshold; });
|
|
1095
|
+
var minorH = minor.reduce(function (t, r) { return t + (r.attentionHours || 0); }, 0);
|
|
1096
|
+
var slices = major.map(function (r, i) { return { i: i, name: r.name, hours: (r.attentionHours || 0), col: RDO_PIE[i % RDO_PIE.length] }; });
|
|
1097
|
+
if (minorH > 0) slices.push({ i: major.length, name: "Others (" + number(minor.length) + (minor.length === 1 ? " repo)" : " repos)"), hours: minorH, col: "#b3aea0" });
|
|
1098
|
+
var c0 = 0;
|
|
1099
|
+
var segs = slices.map(function (sl) {
|
|
1100
|
+
var pct = sl.hours / total * 100;
|
|
1101
|
+
var seg = '<circle class="seg" data-i="' + sl.i + '" data-name="' + esc(sl.name) + '" data-pct="' + Math.round(pct) + '%" cx="21" cy="21" r="15.915" fill="none" stroke="' + sl.col + '" stroke-width="6" stroke-dasharray="' + pct.toFixed(2) + ' ' + (100 - pct).toFixed(2) + '" stroke-dashoffset="' + (25 - c0).toFixed(2) + '"></circle>';
|
|
1102
|
+
c0 += pct; return seg;
|
|
1103
|
+
}).join("");
|
|
1104
|
+
var donut = '<div class="donut"><svg viewBox="0 0 42 42" class="donutSvg">' + segs + '</svg>' +
|
|
1105
|
+
'<div class="center"><div><b>' + number(list.length) + '</b><span>repos</span></div></div><div class="donut-tip"></div></div>';
|
|
1106
|
+
var rows = slices.map(function (sl) {
|
|
1107
|
+
var pct = Math.round(sl.hours / total * 100), w = Math.round(sl.hours / maxH * 100);
|
|
1108
|
+
return '<div class="rrow" data-i="' + sl.i + '" data-name="' + esc(sl.name) + '" data-pct="' + pct + '%"><span class="nm" style="color:' + sl.col + '">' + esc(sl.name) + '</span><span class="vl">' + rdoDec(sl.hours) + 'h · ' + pct + '%</span>' +
|
|
1109
|
+
'<span class="rbar"><i style="width:' + w + '%;background:' + sl.col + '"></i></span></div>';
|
|
1110
|
+
}).join("");
|
|
1111
|
+
return '<div class="terr">' + donut + '<div>' + rows + '</div></div>';
|
|
401
1112
|
}
|
|
1113
|
+
function rdoTokenStack(ip) {
|
|
1114
|
+
ip = Math.max(0, Math.min(100, ip || 0)); var op = 100 - ip;
|
|
1115
|
+
return '<div class="stack"><div class="seg" style="width:' + ip + '%;background:var(--echo-ink-primary);color:#fff">' + ip + '% in</div><div class="seg" style="width:' + op + '%;background:var(--echo-ink-accent);color:#0c3b37">' + op + '%</div></div>' +
|
|
1116
|
+
'<div class="rdo-legend"><span><i style="background:var(--echo-ink-primary)"></i>context loaded in</span><span><i style="background:var(--echo-ink-accent)"></i>answers written out</span></div>';
|
|
1117
|
+
}
|
|
1118
|
+
function rdoCostStack(c) {
|
|
1119
|
+
var t = c.total || 1; function s(col, v) { return '<div class="seg" style="width:' + Math.max(0, (v / t) * 100) + '%;background:' + col + '"></div>'; }
|
|
1120
|
+
return '<div class="stack">' + s("var(--echo-ink-seal)", c.cacheReadCost) + s("var(--echo-ink-mute)", c.coldInputCost) + s("var(--echo-night)", c.cacheWriteCost) + s("var(--echo-ink-accent)", c.outputCost) + '</div>' +
|
|
1121
|
+
'<div class="rdo-legend"><span><i style="background:var(--echo-ink-seal)"></i>cache read ' + money(c.cacheReadCost) + '</span><span><i style="background:var(--echo-ink-mute)"></i>cold input ' + money(c.coldInputCost) + '</span>' +
|
|
1122
|
+
'<span><i style="background:var(--echo-night)"></i>cache write ' + money(c.cacheWriteCost) + '</span><span><i style="background:var(--echo-ink-accent)"></i>output ' + money(c.outputCost) + '</span></div>';
|
|
1123
|
+
}
|
|
1124
|
+
function rdoModelRows(byModel) {
|
|
1125
|
+
return Object.keys(byModel || {}).map(function (k) { return [k, byModel[k]]; }).sort(function (a, b) { return b[1].cost - a[1].cost; })
|
|
1126
|
+
.map(function (e) { return '<div class="mrow2"><b>' + esc(e[0]) + '</b><span>' + big(e[1].total) + ' tok</span><strong>' + money(e[1].cost) + '</strong></div>'; }).join("");
|
|
1127
|
+
}
|
|
1128
|
+
function rdoEvidence(files) {
|
|
1129
|
+
var list = files || [];
|
|
1130
|
+
function rowHtml(f) {
|
|
1131
|
+
var tag = f.everChanged ? '' : '<span class="rdo-tag">never changed</span>';
|
|
1132
|
+
return '<div class="ex-row"><div><div class="ex-file">' + esc(f.file) + '</div><div class="ex-meta">' + esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + tag + '</div></div><div class="ex-reads">' + number(f.totalReads) + '×</div></div>';
|
|
1133
|
+
}
|
|
1134
|
+
var firstN = 5;
|
|
1135
|
+
var head = list.slice(0, firstN).map(rowHtml).join("");
|
|
1136
|
+
var rest = list.slice(firstN).map(rowHtml).join("");
|
|
1137
|
+
var moreBtn = list.length > firstN ? '<button class="moreBtn" data-rdo-toggle>Show all ' + number(list.length) + ' files ↓</button>' : '';
|
|
1138
|
+
return head + '<div class="collapse"><div class="collapse-inner">' + rest + '</div></div>' + moreBtn;
|
|
1139
|
+
}
|
|
1140
|
+
function rdoNotes() {
|
|
1141
|
+
var r = report || {}, s = r.scale || {}, c = r.cost || {}, cl = r.cleanliness || {}, rh = r.rhythm || {}, uwh = r.userWorkingHours || {};
|
|
1142
|
+
return {
|
|
1143
|
+
"repeated-reads": { k: "Cleanliness", t: "Repeated reads", b: '<p>' + number(cl.staleReadSharePct) + '% of file reads reopened a file already seen <b>unchanged</b> — ' + number(cl.staleRereads) + ' stale rereads across ' + number(s.repoCount) + ' repos.</p><p>Every reopen re-pays the cost of loading that file, though nothing changed. It is the biggest source of avoidable token spend.</p><div class="echo"><b>What Echo does:</b> remembers each file so your agent recalls it instead of reading it again.</div>' },
|
|
1144
|
+
"agent-attention": { k: "Activity", t: "Agent attention", b: '<p>' + number(uwh.userAttentionHours) + ' hours of active agent work across the scan. ' + rdoDec(uwh.averageDailyAttentionHours) + 'h/day average, peaking at ' + rdoDec(uwh.maxDailyAttentionHours) + 'h.</p>' },
|
|
1145
|
+
"tokens-moved": { k: "Scale", t: "Tokens moved", b: '<p>' + big(s.totalTokens) + ' tokens read and written. ' + number(s.inputPct) + '% was <b>input</b> — context fed in, not output generated.</p>' },
|
|
1146
|
+
"api-value": { k: "Cost", t: "API-equivalent", b: '<p>' + money(c.total) + ' at API list prices — a measure of scale, not a charge. Subscriptions were not billed this.</p>' },
|
|
1147
|
+
"when-you-work": { k: "Rhythm", t: "When you work", b: '<p>Prompts by hour of day. The soft-indigo bars are late-night hours (10pm–4am); ' + number(rh.lateNightShare) + '% of your prompts land there.</p>' },
|
|
1148
|
+
"time-by-repo": { k: "Focus", t: "Where your time goes", b: '<p>Agent attention hours split by repository — where your coding actually happened.</p>' },
|
|
1149
|
+
"cost-went": { k: "Cost", t: "Where the tokens & cost go", b: '<p>Most volume is input — reloading context each turn. In cost, <b>cache read</b> + <b>cold input</b> are re-fed context.</p><div class="echo">That re-fed-context tax shrinks when agents recall instead of re-read.</div>' },
|
|
1150
|
+
"rereads": { k: "Cleanliness", t: "Most re-read files", b: '<p>Files your agents kept reopening after seeing them unchanged. A <code>never changed</code> tag means many reads, zero edits.</p>' }
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function rdoOpenNote(key) {
|
|
1154
|
+
var d = rdoNotes()[key]; if (!d) return; var dlg = document.getElementById("echo-note"); if (!dlg) return;
|
|
1155
|
+
document.getElementById("echo-note-k").textContent = d.k;
|
|
1156
|
+
document.getElementById("echo-note-t").textContent = d.t;
|
|
1157
|
+
document.getElementById("echo-note-b").innerHTML = d.b;
|
|
1158
|
+
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute("open", "");
|
|
1159
|
+
}
|
|
1160
|
+
function rdoCountUp(el) {
|
|
1161
|
+
var target = parseFloat(el.getAttribute("data-count")), kind = el.getAttribute("data-fmt");
|
|
1162
|
+
if (rdoReduce || !isFinite(target)) { el.textContent = rdoFmt(target, kind); return; }
|
|
1163
|
+
var dur = 950, start = null;
|
|
1164
|
+
function tick(now) { if (start === null) start = now; var t = Math.min(1, (now - start) / dur), e = 1 - Math.pow(1 - t, 3); el.textContent = rdoFmt(target * e, kind); if (t < 1) requestAnimationFrame(tick); else el.textContent = rdoFmt(target, kind); }
|
|
1165
|
+
requestAnimationFrame(tick);
|
|
1166
|
+
}
|
|
1167
|
+
function rdoKpi(cls, value, kind, label, sub, key) {
|
|
1168
|
+
return '<div class="rdo-card rdo-kpi ' + cls + '"><button class="rdo-help" data-info="' + key + '">?</button>' +
|
|
1169
|
+
'<div class="num" data-count="' + value + '" data-fmt="' + kind + '">' + rdoFmt(value, kind) + '</div>' +
|
|
1170
|
+
'<p class="clab" style="margin-top:11px">' + esc(label) + '</p><div class="csub">' + sub + '</div></div>';
|
|
1171
|
+
}
|
|
1172
|
+
function rdoWire() {
|
|
1173
|
+
var root = document.getElementById("setup-details"); if (!root) return;
|
|
1174
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-count]"), function (el, i) { setTimeout(function () { rdoCountUp(el); }, rdoReduce ? 0 : 260 + i * 60); });
|
|
1175
|
+
Array.prototype.forEach.call(root.querySelectorAll(".rdo-help"), function (el) { el.onclick = function () { rdoOpenNote(el.getAttribute("data-info")); }; });
|
|
1176
|
+
var dlg = document.getElementById("echo-note");
|
|
1177
|
+
if (dlg) { var cl = dlg.querySelector(".nclose"); if (cl) cl.onclick = function () { dlg.close(); }; dlg.onclick = function (e) { if (e.target === dlg) dlg.close(); }; }
|
|
1178
|
+
Array.prototype.forEach.call(root.querySelectorAll("[data-rdo-toggle]"), function (mb) {
|
|
1179
|
+
var box = mb.previousElementSibling, tx = mb.textContent;
|
|
1180
|
+
if (!box || !box.classList.contains("collapse")) return;
|
|
1181
|
+
mb.onclick = function () { var o = !box.classList.contains("open"); box.classList.toggle("open", o); mb.textContent = o ? "Show less ↑" : tx; };
|
|
1182
|
+
});
|
|
1183
|
+
var donut = root.querySelector(".terr .donut");
|
|
1184
|
+
if (donut) {
|
|
1185
|
+
var segEls = donut.querySelectorAll(".seg");
|
|
1186
|
+
var tip = donut.querySelector(".donut-tip");
|
|
1187
|
+
function placeTip(e) { var rc = donut.getBoundingClientRect(); if (tip) { tip.style.left = (e.clientX - rc.left) + "px"; tip.style.top = (e.clientY - rc.top) + "px"; } }
|
|
1188
|
+
Array.prototype.forEach.call(segEls, function (s) {
|
|
1189
|
+
s.addEventListener("mouseenter", function (e) {
|
|
1190
|
+
donut.classList.add("hovering");
|
|
1191
|
+
Array.prototype.forEach.call(segEls, function (x) { x.classList.toggle("on", x === s); });
|
|
1192
|
+
if (tip) { tip.textContent = s.getAttribute("data-name") + " · " + s.getAttribute("data-pct"); tip.classList.add("show"); placeTip(e); }
|
|
1193
|
+
});
|
|
1194
|
+
s.addEventListener("mousemove", placeTip);
|
|
1195
|
+
s.addEventListener("mouseleave", function () {
|
|
1196
|
+
donut.classList.remove("hovering");
|
|
1197
|
+
s.classList.remove("on");
|
|
1198
|
+
if (tip) tip.classList.remove("show");
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
var histo = root.querySelector(".rdo-histo");
|
|
1203
|
+
if (histo) {
|
|
1204
|
+
var figbox = histo.closest(".figbox");
|
|
1205
|
+
var htip = figbox ? figbox.querySelector(".histo-tip") : null;
|
|
1206
|
+
function placeHTip(e) { if (!figbox || !htip) return; var rc = figbox.getBoundingClientRect(); htip.style.left = (e.clientX - rc.left) + "px"; htip.style.top = (e.clientY - rc.top) + "px"; }
|
|
1207
|
+
Array.prototype.forEach.call(histo.querySelectorAll(".bar"), function (b) {
|
|
1208
|
+
b.addEventListener("mouseenter", function (e) { if (htip) { htip.textContent = b.getAttribute("data-tip"); htip.classList.add("show"); placeHTip(e); } });
|
|
1209
|
+
b.addEventListener("mousemove", placeHTip);
|
|
1210
|
+
b.addEventListener("mouseleave", function () { if (htip) htip.classList.remove("show"); });
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function renderForensicReport(message) {
|
|
1215
|
+
var r = report;
|
|
1216
|
+
if (!r) { showReport(); return; }
|
|
1217
|
+
if (!reportMounted || !document.getElementById("setup-details")) mountReportShell();
|
|
1218
|
+
setCityMode(true);
|
|
1219
|
+
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 || {};
|
|
1220
|
+
var night = rf.nightExample;
|
|
1221
|
+
setHead("AI Coding City", connected ? "Signed in" : "Local");
|
|
1222
|
+
// The city iframe is already mounted by the shell and shows its own loading; just reveal the
|
|
1223
|
+
// details arrow and fill the sections, so the city never reloads from loading to full report.
|
|
1224
|
+
var dArrow = document.getElementById("details-arrow");
|
|
1225
|
+
if (dArrow) dArrow.classList.remove("hidden");
|
|
1226
|
+
var files = rf.topFiles || [], repos = r.repos || [];
|
|
1227
|
+
var topRepoHours = repos.reduce(function (t, x) { return t + (x.attentionHours || 0); }, 0);
|
|
1228
|
+
var weeks = (new Date(r.scanEndDate) - new Date(r.scanStartDate)) / (7 * 86400000);
|
|
1229
|
+
if (!isFinite(weeks) || weeks < 1) weeks = 1;
|
|
1230
|
+
var sharePct = cl.staleReadSharePct || 0;
|
|
1231
|
+
var savedCost = Math.round((c.total / weeks) * (sharePct / 100));
|
|
1232
|
+
var savedTokens = (s.totalTokens / weeks) * (sharePct / 100);
|
|
1233
|
+
var weeklyRereads = Math.round((cl.staleRereads || 0) / weeks);
|
|
1234
|
+
var targetShare = 10;
|
|
1235
|
+
document.getElementById("setup-details").innerHTML =
|
|
1236
|
+
'<div class="echo-rdo">' +
|
|
1237
|
+
(message ? '<div class="notice" style="margin-bottom:18px">' + esc(message) + '</div>' : '') +
|
|
1238
|
+
'<div class="rdo-head rise" style="--d:0ms">' +
|
|
1239
|
+
'<p class="rdo-kicker">EchoMem · Local Workspace Scan</p>' +
|
|
1240
|
+
'<h2 class="rdo-h1">Where your agents spent their attention</h2>' +
|
|
1241
|
+
'<p class="rdo-lead">A local scan of ' + number(s.sessionCount) + ' coding sessions across ' + number(s.repoCount) + ' repositories. Most of the load wasn't new thinking — it was <b>reloading context</b> your agents had already seen.</p>' +
|
|
1242
|
+
'<div class="rdo-meta"><span class="rdo-chip"><span class="live"></span>Filed locally</span><span class="rdo-chip"><b>' + number(s.sessionCount) + '</b> sessions</span><span class="rdo-chip"><b>' + number(s.repoCount) + '</b> repos</span><span class="rdo-chip">' + esc(r.scanStartDate) + ' – ' + esc(r.scanEndDate) + '</span></div>' +
|
|
1243
|
+
'</div>' +
|
|
1244
|
+
'<div class="rdo-kpis">' +
|
|
1245
|
+
'<div class="rise" style="--d:60ms">' + rdoKpi("hero", cl.staleReadSharePct, "pct", "repeated reads", number(cl.staleRereads) + ' stale rereads', "repeated-reads") + '</div>' +
|
|
1246
|
+
'<div class="rise" style="--d:110ms">' + rdoKpi("", uwh.userAttentionHours, "hours", "agent attention", rdoDec(uwh.averageDailyAttentionHours) + 'h/day avg', "agent-attention") + '</div>' +
|
|
1247
|
+
'<div class="rise" style="--d:160ms">' + rdoKpi("", s.totalTokens, "big", "tokens moved", number(s.inputPct) + '% input', "tokens-moved") + '</div>' +
|
|
1248
|
+
'<div class="rise" style="--d:210ms">' + rdoKpi("", c.total, "money", "API-equivalent", 'list price, not billed', "api-value") + '</div>' +
|
|
1249
|
+
'</div>' +
|
|
1250
|
+
'<div class="rdo-grid">' +
|
|
1251
|
+
'<div class="rdo-card rise" style="--d:260ms"><button class="rdo-help" data-info="when-you-work">?</button><p class="clab">When you work</p><div class="csub"><b>' + number(rh.lateNightShare) + '%</b> after 10pm · <b>' + rdoDec(uwh.averageDailyAttentionHours) + 'h</b> avg active day</div>' + rdoHisto(rh.hourHistogram) + '</div>' +
|
|
1252
|
+
'<div class="rdo-card rise" style="--d:310ms"><button class="rdo-help" data-info="cost-went">?</button><p class="clab">Where the tokens & cost go</p><div class="csub"><b>' + number(s.inputPct) + '%</b> input · <b>' + money(c.total) + '</b> API-equivalent</div>' +
|
|
1253
|
+
'<div class="ledgerCols"><div><div class="lblrow">Tokens — in vs. out</div>' + rdoTokenStack(s.inputPct) + '</div><div><div class="lblrow">Cost by token type</div>' + rdoCostStack(c) + '</div></div>' +
|
|
1254
|
+
'<div class="collapse"><div class="collapse-inner" style="padding-top:6px">' + rdoModelRows(c.byModel) + '</div></div>' +
|
|
1255
|
+
'<button class="moreBtn" data-rdo-toggle>Break down by model ↓</button>' +
|
|
1256
|
+
'</div>' +
|
|
1257
|
+
'<div class="rdo-card rise" style="--d:360ms"><button class="rdo-help" data-info="time-by-repo">?</button><p class="clab">Where your time goes</p><div class="csub"><b>' + rdoDec(topRepoHours) + 'h</b> across ' + number(repos.length) + ' repos</div>' + rdoTerritory(repos) + '</div>' +
|
|
1258
|
+
'<div class="rdo-card rise" style="--d:410ms"><button class="rdo-help" data-info="rereads">?</button><p class="clab">Most re-read files</p><div class="csub">top 5 of <b>' + number(files.length) + '</b> by total reads</div>' + rdoEvidence(files) + '</div>' +
|
|
1259
|
+
'</div>' +
|
|
1260
|
+
'<section class="outcome rise" style="--d:460ms">' +
|
|
1261
|
+
'<div class="oc-copy">' +
|
|
1262
|
+
'<p class="rdo-kicker">What changes next week</p>' +
|
|
1263
|
+
'<h2>Echo remembers, so your agents stop re-reading.</h2>' +
|
|
1264
|
+
'<p class="rdo-lead" data-signin-help>Right now ' + number(sharePct) + '% of reads are repeats. As Echo fills in, your agents <b>recall</b> what they have already seen instead of reloading it — and it compounds every session.</p>' +
|
|
1265
|
+
'<div class="targetbar"><div class="tb-track"><div class="tb-fill" style="width:' + sharePct + '%">' + number(sharePct) + '% repeats today</div><div class="tb-goal" style="left:' + targetShare + '%"></div></div><div class="tb-flag" style="left:' + targetShare + '%">target ≤ ' + targetShare + '%</div></div>' +
|
|
1266
|
+
'</div>' +
|
|
1267
|
+
'<div class="oc-targets">' +
|
|
1268
|
+
'<p class="ot-title">Next week’s target</p>' +
|
|
1269
|
+
'<p class="ot-note">What Echo saves you in a week, once it recalls instead of re-reading:</p>' +
|
|
1270
|
+
'<div class="ot-row"><div class="otk"><span class="k">Cost saved</span><span class="ks">API list-price value</span></div><span class="v" data-count="' + savedCost + '" data-fmt="money">' + money(savedCost) + '</span></div>' +
|
|
1271
|
+
'<div class="ot-row"><div class="otk"><span class="k">Tokens saved</span><span class="ks">context not reloaded</span></div><span class="v" data-count="' + savedTokens + '" data-fmt="big">' + big(savedTokens) + '</span></div>' +
|
|
1272
|
+
'<div class="ot-row"><div class="otk"><span class="k">Re-reads avoided</span><span class="ks">unchanged files reopened</span></div><span class="v" data-count="' + weeklyRereads + '" data-fmt="int">' + number(weeklyRereads) + '</span></div>' +
|
|
1273
|
+
'<button class="rdo-cta" data-connect-echo>' + (connected ? "Continue →" : "Connect Echo →") + '</button>' +
|
|
1274
|
+
'</div>' +
|
|
1275
|
+
'</section>' +
|
|
1276
|
+
'<div class="rdo-foot rise" style="--d:520ms">' +
|
|
1277
|
+
'<span>Compiled locally from <code>~/.codex/sessions</code> and <code>~/.claude/projects</code> · API-equivalent list pricing, not a subscription bill.</span>' +
|
|
1278
|
+
'<span class="priv">No transcripts leave this machine.</span>' +
|
|
1279
|
+
'</div>' +
|
|
1280
|
+
'</div>';
|
|
1281
|
+
rdoWire();
|
|
1282
|
+
bindConnect();
|
|
1283
|
+
bindDetailsArrow();
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/* ---------- dashboard (post-auth extraction) ---------- */
|
|
1287
|
+
function statusStrip(title, detail, kind) {
|
|
1288
|
+
return '<div class="status-strip ' + esc(kind || "") + '"><span class="dot"></span><div><strong>' + esc(title) + '</strong><p>' + esc(detail) + '</p></div></div>';
|
|
1289
|
+
}
|
|
1290
|
+
function renderBridgeIssue(error) {
|
|
1291
|
+
setCityMode(false);
|
|
1292
|
+
setHead("Local bridge issue", "Retrying");
|
|
1293
|
+
app.className = "loading-panel";
|
|
1294
|
+
app.innerHTML =
|
|
1295
|
+
'<div style="padding:24px"><h2>Still waiting for the local bridge</h2>' +
|
|
1296
|
+
'<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>' +
|
|
1297
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') + '</div>';
|
|
1298
|
+
}
|
|
1299
|
+
function renderLogoutPending() {
|
|
1300
|
+
setCityMode(false);
|
|
1301
|
+
setHead("Signing out locally", "Working");
|
|
1302
|
+
app.className = "loading-panel";
|
|
1303
|
+
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>';
|
|
1304
|
+
}
|
|
1305
|
+
function renderLogoutIssue(error) {
|
|
1306
|
+
setCityMode(false);
|
|
1307
|
+
setHead("Sign out did not finish", "Needs attention");
|
|
1308
|
+
app.className = "loading-panel";
|
|
1309
|
+
app.innerHTML =
|
|
1310
|
+
'<div style="padding:24px"><h2>The local bridge did not answer</h2>' +
|
|
1311
|
+
'<p class="helper">Keep the terminal open and retry. If the command exited, close this tab and run setup again.</p>' +
|
|
1312
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') +
|
|
1313
|
+
'<div class="actions"><button id="logout" class="secondary">Retry sign out</button></div></div>';
|
|
1314
|
+
var lo = document.getElementById("logout"); if (lo) lo.onclick = localLogout;
|
|
1315
|
+
}
|
|
1316
|
+
async function waitForConnection() {
|
|
1317
|
+
if (connectionPollStarted) return;
|
|
1318
|
+
connectionPollStarted = true;
|
|
1319
|
+
try {
|
|
1320
|
+
for (;;) {
|
|
1321
|
+
if (decisionMade) return;
|
|
1322
|
+
try {
|
|
1323
|
+
var cfg = await getJson("/config");
|
|
1324
|
+
authUrl = cfg.authUrl || authUrl;
|
|
1325
|
+
if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
|
|
1326
|
+
} catch (_) {}
|
|
1327
|
+
await sleep(900);
|
|
1328
|
+
}
|
|
1329
|
+
} finally { connectionPollStarted = false; }
|
|
1330
|
+
}
|
|
1331
|
+
function startConnectionPoll() { void waitForConnection(); }
|
|
402
1332
|
async function waitForStats() {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1333
|
+
if (statsPollStarted) return;
|
|
1334
|
+
statsPollStarted = true;
|
|
1335
|
+
setCityMode(false);
|
|
1336
|
+
setHead("Turn history into memory", "Working");
|
|
1337
|
+
app.className = "";
|
|
1338
|
+
app.innerHTML = '<div class="miniLoad"><h2>Preparing your conversations</h2><p>Sizing what is new since your last run.</p><div class="miniBar"><i></i></div></div>';
|
|
1339
|
+
try {
|
|
1340
|
+
var started = Date.now();
|
|
1341
|
+
var lastError = "";
|
|
1342
|
+
for (;;) {
|
|
1343
|
+
if (decisionMade) return;
|
|
1344
|
+
try {
|
|
1345
|
+
var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
|
|
1346
|
+
if (res.status === 200) {
|
|
1347
|
+
stats = await res.json();
|
|
1348
|
+
if (decisionMade) return;
|
|
1349
|
+
renderDashboard();
|
|
1350
|
+
if (stats && stats.partial) { await sleep(1200); continue; }
|
|
1351
|
+
return;
|
|
418
1352
|
}
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
statsSlow = true;
|
|
425
|
-
renderDashboard();
|
|
1353
|
+
if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
lastError = e && e.message ? e.message : String(e);
|
|
1356
|
+
if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
|
|
1357
|
+
if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
|
|
426
1358
|
}
|
|
1359
|
+
await sleep(statsSlow ? 1500 : 500);
|
|
427
1360
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
1361
|
+
} finally { statsPollStarted = false; }
|
|
1362
|
+
}
|
|
1363
|
+
// The plate slot: ONLY the WebGL clay city (echo-extraction-plate.html). It self-fetches /report
|
|
1364
|
+
// (your repos) and polls /progress (extraction state), growing as memories are created. The old SVG
|
|
1365
|
+
// fallback board was removed — it was bleeding through behind the transparent canvas (the double board).
|
|
1366
|
+
function plateBlock() {
|
|
1367
|
+
return '<div class="exPlate"><div class="plateStack">' +
|
|
1368
|
+
'<iframe class="plateGl" title="Extraction plate" src="/city/echo-extraction-plate.html?nonce=' + encodeURIComponent(nonce) + '"></iframe>' +
|
|
1369
|
+
'</div></div>';
|
|
434
1370
|
}
|
|
435
1371
|
function renderDashboard(error) {
|
|
1372
|
+
setCityMode(false);
|
|
436
1373
|
var migratable = stats && stats.migratable ? stats.migratable : {};
|
|
437
1374
|
var pending = stats && stats.migratable ? stats.migratable.pending : null;
|
|
438
1375
|
var hasPendingCount = typeof pending === "number";
|
|
439
|
-
var
|
|
440
|
-
|
|
1376
|
+
var discovery = stats && stats.discovery ? stats.discovery : {};
|
|
1377
|
+
var isPartial = !!(stats && stats.partial);
|
|
1378
|
+
var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
|
|
441
1379
|
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
442
|
-
var buckets = migrationBuckets(migratable);
|
|
443
|
-
var over1m = bucketCount(buckets, "huge") + bucketCount(buckets, "massive");
|
|
444
|
-
var processed = stats && stats.migratable ? stats.migratable.alreadyMigrated : null;
|
|
445
1380
|
var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
|
|
446
|
-
var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || (knownDone ? "complete" : "calculating");
|
|
447
1381
|
var sessions = stats && stats.sessions ? stats.sessions : {};
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
var
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
document.getElementById("
|
|
1382
|
+
// The "quick" phase counts only this device's local ledger; it does not know what the account
|
|
1383
|
+
// already imported elsewhere, so it over-counts (e.g. 10). The count is only trustworthy after the
|
|
1384
|
+
// account check (phase "account"/"exact"/"full"), which corrects it (e.g. 3). Until then we show a
|
|
1385
|
+
// "counting" state with no number, so the user never sees the count jump down.
|
|
1386
|
+
var pendingTrusted = hasPendingCount && !!discovery.phase && discovery.phase !== "quick";
|
|
1387
|
+
var knownDone = pendingTrusted && pending === 0;
|
|
1388
|
+
var canExtract = pendingTrusted && pending > 0;
|
|
1389
|
+
var pendN = hasPendingCount ? pending : 0;
|
|
1390
|
+
var alreadyN = Math.max(0, (sessions.total || 0) - pendN);
|
|
1391
|
+
var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || "a couple of minutes";
|
|
1392
|
+
setHead("Turn history into memory", localScanReady ? "Ready" : "Scanning");
|
|
1393
|
+
var headline = !pendingTrusted
|
|
1394
|
+
? "Counting your new conversations…"
|
|
1395
|
+
: (knownDone ? "You are all caught up." : (pendN === 1 ? "1 new conversation to turn into memory." : number(pendN) + " new conversations to turn into memory."));
|
|
1396
|
+
var sub = !pendingTrusted
|
|
1397
|
+
? "Echo is matching your local history against what is already in memory."
|
|
1398
|
+
: (knownDone
|
|
1399
|
+
? (skippedActive ? number(skippedActive) + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing — Echo will pick them up later." : "Your history is already in EchoMem. Nothing new to extract.")
|
|
1400
|
+
: "Already-processed conversations are skipped, so re-running stays incremental. About " + etaLabel + ".");
|
|
1401
|
+
var extractLabel = !pendingTrusted ? "Counting…" : (canExtract ? ("Extract " + number(pendN) + " conversation" + (pendN === 1 ? "" : "s")) : "All caught up");
|
|
1402
|
+
// Mount the dashboard shell ONCE so the WebGL plate iframe is never re-created across /stats polls.
|
|
1403
|
+
// (Rebuilding app.innerHTML on every partial-scan poll reloaded the iframe → the city flickered.)
|
|
1404
|
+
// Later polls only patch the text/counts in place; the iframe stays mounted.
|
|
1405
|
+
var live = dashMounted && document.getElementById("exHeadline");
|
|
1406
|
+
if (!live) {
|
|
1407
|
+
extractMounted = false;
|
|
1408
|
+
app.className = "";
|
|
1409
|
+
app.innerHTML =
|
|
1410
|
+
'<div id="exBanner"></div>' +
|
|
1411
|
+
'<section class="exHero">' +
|
|
1412
|
+
'<div class="exCopy">' +
|
|
1413
|
+
'<p class="kicker-mono" id="exKicker"></p>' +
|
|
1414
|
+
'<h2 id="exHeadline"></h2>' +
|
|
1415
|
+
'<p class="exSub" id="exSub"></p>' +
|
|
1416
|
+
'<div class="exLegend" id="exLegend"></div>' +
|
|
1417
|
+
'<div class="actions">' +
|
|
1418
|
+
'<button id="migrate" class="primary"></button>' +
|
|
1419
|
+
'<button id="skip" class="secondary"></button>' +
|
|
1420
|
+
'</div>' +
|
|
1421
|
+
'<p class="exHint">No transcripts leave this machine until you start extraction.</p>' +
|
|
1422
|
+
'</div>' +
|
|
1423
|
+
plateBlock() +
|
|
1424
|
+
'</section>';
|
|
1425
|
+
document.getElementById("migrate").onclick = startMigrate;
|
|
1426
|
+
document.getElementById("skip").onclick = skip;
|
|
1427
|
+
dashMounted = true;
|
|
1428
|
+
}
|
|
1429
|
+
document.getElementById("exBanner").innerHTML =
|
|
1430
|
+
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
|
|
1431
|
+
(error ? '<div class="error">' + esc(error) + '</div>' : '');
|
|
1432
|
+
document.getElementById("exKicker").textContent = "Local scan · " + number(sessions.total) + " sessions found";
|
|
1433
|
+
document.getElementById("exHeadline").textContent = headline;
|
|
1434
|
+
document.getElementById("exSub").textContent = sub;
|
|
1435
|
+
document.getElementById("exLegend").innerHTML = pendingTrusted
|
|
1436
|
+
? '<span class="lgDot lgNew"></span>new (' + esc(number(pendN)) + ')<span class="lgDot lgDim"></span>already in memory (' + esc(number(alreadyN)) + ')'
|
|
1437
|
+
: '<span class="lgDot lgDim"></span>matching your history against saved memory…';
|
|
1438
|
+
// While still counting, hide the primary button entirely (a disabled "Counting…" button looked
|
|
1439
|
+
// half-baked) — the headline + legend already say it is working; only "Not now" stays available.
|
|
1440
|
+
var migrateBtn = document.getElementById("migrate");
|
|
1441
|
+
if (pendingTrusted) { migrateBtn.style.display = ""; migrateBtn.textContent = extractLabel; migrateBtn.disabled = !canExtract; }
|
|
1442
|
+
else { migrateBtn.style.display = "none"; }
|
|
1443
|
+
document.getElementById("skip").textContent = knownDone ? "Finish setup" : "Not now";
|
|
496
1444
|
}
|
|
497
1445
|
async function startMigrate() {
|
|
498
1446
|
if (decisionMade) return;
|
|
499
1447
|
decisionMade = true;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
app.
|
|
1448
|
+
setCityMode(false);
|
|
1449
|
+
setHead("Extracting", "Starting");
|
|
1450
|
+
app.className = "";
|
|
1451
|
+
app.innerHTML = '<div class="miniLoad"><h2>Starting extraction</h2><p>Echo is sizing the remaining jobs, then imports automatically — keep this terminal open.</p><div class="miniBar"><i></i></div></div>';
|
|
503
1452
|
try {
|
|
504
1453
|
var started = await postJson("/migrate", {});
|
|
505
|
-
|
|
1454
|
+
extractMounted = false;
|
|
1455
|
+
renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
|
|
506
1456
|
await pollProgress();
|
|
507
1457
|
} catch (e) {
|
|
508
1458
|
decisionMade = false;
|
|
@@ -518,49 +1468,147 @@ export function renderSetupPage() {
|
|
|
518
1468
|
}
|
|
519
1469
|
}
|
|
520
1470
|
function renderProgress(progress) {
|
|
1471
|
+
setCityMode(false);
|
|
1472
|
+
var status = progress.status;
|
|
1473
|
+
var done = status === "completed";
|
|
1474
|
+
var stopped = status === "failed";
|
|
521
1475
|
var total = progress.total || progress.jobCount || 0;
|
|
522
1476
|
var completed = progress.completed || 0;
|
|
523
1477
|
var failed = progress.failed || 0;
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
app.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1478
|
+
var extracted = progress.extracted || 0;
|
|
1479
|
+
var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
|
|
1480
|
+
var preparing = (status === "starting" || status === "preparing");
|
|
1481
|
+
app.className = "";
|
|
1482
|
+
// Mount the shell ONCE so the WebGL plate iframe survives every poll (it self-polls /progress);
|
|
1483
|
+
// each poll only rebuilds the text column.
|
|
1484
|
+
if (!extractMounted) {
|
|
1485
|
+
app.innerHTML =
|
|
1486
|
+
'<section class="exHero"><div class="exCopy" id="exText"></div>' +
|
|
1487
|
+
plateBlock() +
|
|
1488
|
+
'</section>';
|
|
1489
|
+
extractMounted = true;
|
|
1490
|
+
}
|
|
1491
|
+
var t = document.getElementById("exText");
|
|
1492
|
+
if (!t) return;
|
|
1493
|
+
if (done || stopped) {
|
|
1494
|
+
setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
|
|
1495
|
+
t.innerHTML =
|
|
1496
|
+
'<p class="kicker-mono">' + (done ? "Extraction complete" : "Extraction stopped") + '</p>' +
|
|
1497
|
+
'<h2>' + (done ? esc(number(extracted)) + ' new ' + (extracted === 1 ? "memory" : "memories") + '.' : "Some conversations did not finish.") + '</h2>' +
|
|
1498
|
+
'<p class="exSub">' + (done
|
|
1499
|
+
? "From " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session recalls instead of re-reading — and it keeps compounding."
|
|
1500
|
+
: (String(progress.error) === "key-expired"
|
|
1501
|
+
? "Your encryption key was not accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
|
|
1502
|
+
: esc(progress.error || "Re-run extraction to finish the rest — already-done conversations are skipped."))) + '</p>' +
|
|
1503
|
+
'<div class="recon">✓ ' + esc(number(completed)) + ' extracted · ' + esc(number(extracted)) + ' memories · ' + esc(number(failed)) + ' error' + (failed === 1 ? "" : "s") + '</div>';
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
|
|
1507
|
+
var statusLine = progress.latest ? ("Now: " + progress.latest) : (preparing ? "Getting your sessions ready locally…" : "");
|
|
1508
|
+
t.innerHTML =
|
|
1509
|
+
'<p class="kicker-mono">' + (preparing ? "Preparing" : "Extraction progress") + '</p>' +
|
|
1510
|
+
'<h2>' + (preparing ? "Sizing your sessions…" : (total ? esc(number(completed)) + ' of ' + esc(number(total)) : "Starting…")) + '</h2>' +
|
|
1511
|
+
'<div class="track' + (preparing ? " indef" : "") + '"><div class="fill"' + (preparing ? "" : ' style="width:' + pct + '%"') + '></div></div>' +
|
|
1512
|
+
'<p class="exSub">' + esc(number(extracted)) + ' ' + (extracted === 1 ? "memory" : "memories") + ' captured' + (failed ? ' · ' + esc(number(failed)) + ' failed' : "") + '.' + (statusLine ? ' ' + esc(statusLine) : "") + '</p>' +
|
|
1513
|
+
(progress.capped ? '<p class="exHint">Importing the newest ' + esc(number(progress.capped)) + ' — re-run later for older sessions.</p>' : '<p class="exHint">Keep this terminal open — nothing leaves your machine beyond what you extract. You can use EchoMem in your editor now.</p>');
|
|
536
1514
|
}
|
|
537
1515
|
async function skip() {
|
|
538
1516
|
if (decisionMade) return;
|
|
539
1517
|
decisionMade = true;
|
|
540
1518
|
try {
|
|
541
1519
|
await postJson("/skip", {});
|
|
1520
|
+
setCityMode(false);
|
|
542
1521
|
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>';
|
|
1522
|
+
app.className = "notice";
|
|
1523
|
+
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
1524
|
} catch (e) {
|
|
546
1525
|
decisionMade = false;
|
|
547
1526
|
renderDashboard(e && e.message);
|
|
548
1527
|
}
|
|
549
1528
|
}
|
|
1529
|
+
async function localLogout() {
|
|
1530
|
+
if (decisionMade) return;
|
|
1531
|
+
decisionMade = true;
|
|
1532
|
+
renderLogoutPending();
|
|
1533
|
+
try {
|
|
1534
|
+
await postJson("/logout", {}, 12000);
|
|
1535
|
+
stats = null;
|
|
1536
|
+
statsSlow = false;
|
|
1537
|
+
connected = false;
|
|
1538
|
+
decisionMade = false;
|
|
1539
|
+
renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
decisionMade = false;
|
|
1542
|
+
renderLogoutIssue(e && e.message ? e.message : String(e));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/* ---------- scan-first poll for the forensic report ---------- */
|
|
1547
|
+
async function showReport() {
|
|
1548
|
+
if (reportPollStarted) return;
|
|
1549
|
+
reportPollStarted = true;
|
|
1550
|
+
if (!report) renderLoading();
|
|
1551
|
+
try {
|
|
1552
|
+
var fails = 0;
|
|
1553
|
+
for (;;) {
|
|
1554
|
+
if (decisionMade) return;
|
|
1555
|
+
try {
|
|
1556
|
+
var res = await fetchWithTimeout("/report?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 12000);
|
|
1557
|
+
if (res.status === 200) { report = await res.json(); renderForensicReport(); return; }
|
|
1558
|
+
if (res.status === 202) {
|
|
1559
|
+
fails = 0;
|
|
1560
|
+
renderLoading();
|
|
1561
|
+
} else {
|
|
1562
|
+
throw new Error(await res.text() || ("HTTP " + res.status));
|
|
1563
|
+
}
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
// Only a RUN of consecutive failures is a real bridge problem. A long scan that keeps
|
|
1566
|
+
// answering 202 must never trip this (that was the old false "Local bridge issue").
|
|
1567
|
+
fails++;
|
|
1568
|
+
if (!report && fails >= 8) renderBridgeIssue(e && e.message ? e.message : String(e));
|
|
1569
|
+
}
|
|
1570
|
+
await sleep(700);
|
|
1571
|
+
}
|
|
1572
|
+
} finally { reportPollStarted = false; }
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
window.addEventListener("message", function (event) {
|
|
1576
|
+
if (event.origin !== location.origin) return;
|
|
1577
|
+
var data = event.data || {};
|
|
1578
|
+
if (data.type === "echomem:connected" && data.nonce === nonce) {
|
|
1579
|
+
connected = true;
|
|
1580
|
+
closeAuthWindow();
|
|
1581
|
+
void waitForStats();
|
|
1582
|
+
} else if (data.type === "echomem:connect-click") {
|
|
1583
|
+
onConnectClick();
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
550
1586
|
async function init() {
|
|
551
1587
|
if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp setup.");
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1588
|
+
if (params.get("connected") === "1") {
|
|
1589
|
+
connected = true;
|
|
1590
|
+
if (notifyOpenerConnected()) {
|
|
1591
|
+
// Popup flow: hand the main tab back control, then this popup closes.
|
|
1592
|
+
setHead("EchoMem connected", "Done");
|
|
1593
|
+
app.className = "notice";
|
|
1594
|
+
app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
|
|
1595
|
+
// Fallback: if window.close() was blocked (auth opened as a tab, or the browser refused),
|
|
1596
|
+
// this window would otherwise hang on "Returning..." forever. After a beat, take over here
|
|
1597
|
+
// so the user always lands on the dashboard instead of a dead page.
|
|
1598
|
+
setTimeout(function () { if (!window.closed) void waitForStats(); }, 1500);
|
|
1599
|
+
} else {
|
|
1600
|
+
// Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
|
|
1601
|
+
void waitForStats();
|
|
560
1602
|
}
|
|
561
1603
|
return;
|
|
562
1604
|
}
|
|
563
|
-
|
|
1605
|
+
// Learn the auth URL + connection state, but don't block the local scan on it.
|
|
1606
|
+
getJson("/config").then(function (cfg) {
|
|
1607
|
+
authUrl = cfg.authUrl || authUrl;
|
|
1608
|
+
connected = !!cfg.connected;
|
|
1609
|
+
}).catch(function () {});
|
|
1610
|
+
// Scan-first: show the local forensic report before asking the user to connect.
|
|
1611
|
+
await showReport();
|
|
564
1612
|
}
|
|
565
1613
|
init().catch(function (e) {
|
|
566
1614
|
setHead("Connection issue", "Needs attention");
|