@sansynx/erroratlas 0.1.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.
@@ -0,0 +1,2295 @@
1
+ function escapeHtml(value) {
2
+ return String(value ?? "").replace(/[&<>"']/g, (char) => {
3
+ switch (char) {
4
+ case "&": return "&amp;";
5
+ case "<": return "&lt;";
6
+ case ">": return "&gt;";
7
+ case '"': return "&quot;";
8
+ case "'": return "&#039;";
9
+ default: return char;
10
+ }
11
+ });
12
+ }
13
+ function themeBootScript() {
14
+ return `<script>
15
+ (() => {
16
+ const key = "erroratlas-theme";
17
+ const root = document.documentElement;
18
+ let stored = null;
19
+ try { stored = localStorage.getItem(key); } catch (_error) {}
20
+ const initial = stored || "light";
21
+ root.dataset.theme = initial;
22
+ function apply(theme) {
23
+ root.dataset.theme = theme;
24
+ const toggle = document.getElementById("themeToggle");
25
+ if (toggle) {
26
+ const label = theme === "dark" ? "Switch to light theme" : "Switch to dark theme";
27
+ toggle.dataset.mode = theme;
28
+ toggle.setAttribute("aria-label", label);
29
+ toggle.setAttribute("title", label);
30
+ }
31
+ }
32
+ document.addEventListener("DOMContentLoaded", () => {
33
+ const toggle = document.getElementById("themeToggle");
34
+ const nav = document.querySelector(".nav");
35
+ const menu = document.getElementById("navMenu");
36
+ function closeMenu() {
37
+ if (!nav || !menu) return;
38
+ nav.classList.remove("open");
39
+ menu.setAttribute("aria-expanded", "false");
40
+ }
41
+ apply(root.dataset.theme || initial);
42
+ if (toggle) {
43
+ toggle.addEventListener("click", () => {
44
+ const next = root.dataset.theme === "dark" ? "light" : "dark";
45
+ try { localStorage.setItem(key, next); } catch (_error) {}
46
+ apply(next);
47
+ });
48
+ }
49
+ if (nav && menu) {
50
+ menu.addEventListener("click", () => {
51
+ const next = !nav.classList.contains("open");
52
+ nav.classList.toggle("open", next);
53
+ menu.setAttribute("aria-expanded", String(next));
54
+ });
55
+ nav.addEventListener("click", (event) => {
56
+ if (!(event.target instanceof HTMLElement)) return;
57
+ const control = event.target.closest(".nav-links a, .nav-links button");
58
+ if (control && control.id !== "themeToggle") closeMenu();
59
+ });
60
+ window.addEventListener("scroll", closeMenu, { passive: true });
61
+ document.addEventListener("click", (event) => {
62
+ if (event.target instanceof Node && !nav.contains(event.target)) closeMenu();
63
+ });
64
+ document.addEventListener("keydown", (event) => {
65
+ if (event.key === "Escape") closeMenu();
66
+ });
67
+ }
68
+ });
69
+ })();
70
+ </script>`;
71
+ }
72
+ function sharedCss() {
73
+ return `<style>
74
+ @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap");
75
+
76
+ :root {
77
+ color-scheme: light;
78
+ --spacing-1: 4px;
79
+ --spacing-2: 8px;
80
+ --spacing-3: 12px;
81
+ --spacing-4: 16px;
82
+ --spacing-6: 24px;
83
+ --spacing-8: 32px;
84
+ --spacing-10: 40px;
85
+ --spacing-16: 64px;
86
+ --spacing-24: 96px;
87
+ --background-100: #ffffff;
88
+ --background-200: #fafafa;
89
+ --background-300: #f2f2f2;
90
+ --gray-100: #f2f2f2;
91
+ --gray-200: #ebebeb;
92
+ --gray-300: #e6e6e6;
93
+ --gray-400: #eaeaea;
94
+ --gray-500: #c9c9c9;
95
+ --gray-600: #a8a8a8;
96
+ --gray-700: #8f8f8f;
97
+ --gray-800: #7d7d7d;
98
+ --gray-900: #4d4d4d;
99
+ --gray-1000: #171717;
100
+ --gray-alpha-100: #0000000d;
101
+ --gray-alpha-200: #00000015;
102
+ --gray-alpha-300: #0000001a;
103
+ --gray-alpha-400: #00000014;
104
+ --gray-alpha-500: #00000036;
105
+ --blue-100: #f0f7ff;
106
+ --blue-200: #e9f4ff;
107
+ --blue-300: #dfefff;
108
+ --blue-400: #cae7ff;
109
+ --blue-500: #94ccff;
110
+ --blue-600: #48aeff;
111
+ --blue-700: #006bff;
112
+ --blue-800: #0059ec;
113
+ --blue-900: #005ff2;
114
+ --blue-1000: #002359;
115
+ --red-100: #ffeeef;
116
+ --red-200: #ffe8ea;
117
+ --red-300: #ffe3e4;
118
+ --red-400: #ffd7d6;
119
+ --red-500: #ffb1b3;
120
+ --red-600: #ff676d;
121
+ --red-700: #fc0035;
122
+ --red-800: #ea001d;
123
+ --red-900: #d8001b;
124
+ --red-1000: #47000c;
125
+ --amber-100: #fff6de;
126
+ --amber-200: #fff4cf;
127
+ --amber-300: #fff1c1;
128
+ --amber-400: #ffdc73;
129
+ --amber-500: #ffc543;
130
+ --amber-600: #ffa600;
131
+ --amber-700: #ffae00;
132
+ --amber-800: #ff9300;
133
+ --amber-900: #aa4d00;
134
+ --amber-1000: #561900;
135
+ --green-100: #ecfdec;
136
+ --green-200: #e5fce7;
137
+ --green-300: #d3fad1;
138
+ --green-400: #b9f5bc;
139
+ --green-500: #82eb8d;
140
+ --green-600: #4ce15e;
141
+ --green-700: #28a948;
142
+ --green-800: #279141;
143
+ --green-900: #107d32;
144
+ --green-1000: #003a00;
145
+ --teal-100: #defffb;
146
+ --teal-200: #ddfef6;
147
+ --teal-300: #ccf9f1;
148
+ --teal-400: #b1f7ec;
149
+ --teal-500: #52f0db;
150
+ --teal-600: #00e3c4;
151
+ --teal-700: #00ac96;
152
+ --teal-800: #00927f;
153
+ --teal-900: #007f70;
154
+ --teal-1000: #003f34;
155
+ --purple-100: #faf0ff;
156
+ --purple-200: #f9f0ff;
157
+ --purple-300: #f6e8ff;
158
+ --purple-400: #f2d9ff;
159
+ --purple-500: #dfa7ff;
160
+ --purple-600: #c979ff;
161
+ --purple-700: #a000f8;
162
+ --purple-800: #8500d1;
163
+ --purple-900: #7d00cc;
164
+ --purple-1000: #2f004e;
165
+ --pink-100: #ffe8f6;
166
+ --pink-200: #ffe8f3;
167
+ --pink-300: #ffdfeb;
168
+ --pink-400: #ffd3e1;
169
+ --pink-500: #fdb3cc;
170
+ --pink-600: #f97ea7;
171
+ --pink-700: #f22782;
172
+ --pink-800: #e4106e;
173
+ --pink-900: #c41562;
174
+ --pink-1000: #460523;
175
+ --font-sans: "Geist Sans", "Inter", ui-sans-serif, system-ui, sans-serif;
176
+ --font-mono: "Geist Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
177
+ --easing-soft: cubic-bezier(0.175, 0.885, 0.32, 1.1);
178
+ --radius-sm: 6px;
179
+ --radius-md: 12px;
180
+ --radius-lg: 16px;
181
+ --radius-full: 9999px;
182
+ --page-width: 1200px;
183
+ --container-gap: 32px;
184
+ --bg: var(--background-100);
185
+ --surface: var(--background-100);
186
+ --surface-2: var(--background-200);
187
+ --fg: var(--gray-1000);
188
+ --body: var(--gray-900);
189
+ --muted: var(--gray-700);
190
+ --line: var(--gray-300);
191
+ --line-strong: var(--gray-400);
192
+ --accent: var(--blue-700);
193
+ --good: var(--green-700);
194
+ --warn: var(--amber-700);
195
+ --bad: var(--red-700);
196
+ --shadow: 0 2px 2px rgba(0, 0, 0, 0.04);
197
+ --shadow-popover: 0 1px 1px rgba(0, 0, 0, 0.02), 0 4px 8px -4px rgba(0, 0, 0, 0.04), 0 16px 24px -8px rgba(0, 0, 0, 0.06);
198
+ --shadow-modal: 0 1px 1px rgba(0, 0, 0, 0.02), 0 8px 16px -4px rgba(0, 0, 0, 0.04), 0 24px 32px -8px rgba(0, 0, 0, 0.06);
199
+ }
200
+
201
+ html[data-theme="dark"] {
202
+ color-scheme: dark;
203
+ --background-100: #0f0f0f;
204
+ --background-200: #171717;
205
+ --background-300: #262626;
206
+ --gray-100: #2a2a2a;
207
+ --gray-200: #333333;
208
+ --gray-300: #3f3f3f;
209
+ --gray-400: #555555;
210
+ --gray-500: #6f6f6f;
211
+ --gray-600: #8d8d8d;
212
+ --gray-700: #a5a5a5;
213
+ --gray-800: #c0c0c0;
214
+ --gray-900: #dadada;
215
+ --gray-1000: #f2f2f2;
216
+ --gray-alpha-100: #ffffff0d;
217
+ --gray-alpha-200: #ffffff15;
218
+ --gray-alpha-300: #ffffff1a;
219
+ --gray-alpha-400: #ffffff14;
220
+ --gray-alpha-500: #ffffff36;
221
+ --blue-100: #0f1f40;
222
+ --blue-200: #11305f;
223
+ --blue-300: #0f3e75;
224
+ --blue-400: #114e8f;
225
+ --blue-500: #1d6cc1;
226
+ --blue-600: #2f8ef5;
227
+ --blue-700: #4f9cff;
228
+ --blue-800: #7db8ff;
229
+ --blue-900: #8fc4ff;
230
+ --blue-1000: #dbeaff;
231
+ --red-100: #3a0f17;
232
+ --red-200: #56101d;
233
+ --red-300: #6f1022;
234
+ --red-400: #8d1528;
235
+ --red-500: #a21a2f;
236
+ --red-600: #bd2441;
237
+ --red-700: #ff5c5c;
238
+ --red-800: #ff7c8d;
239
+ --red-900: #ff9aa8;
240
+ --red-1000: #ffd3da;
241
+ --amber-100: #3a310a;
242
+ --amber-200: #4b3d0f;
243
+ --amber-300: #624915;
244
+ --amber-400: #7d5f1c;
245
+ --amber-500: #9f7e25;
246
+ --amber-600: #c29d2f;
247
+ --amber-700: #ffbb43;
248
+ --amber-800: #ffd37e;
249
+ --amber-900: #ffe4a8;
250
+ --amber-1000: #fff7df;
251
+ --green-100: #0c2e0f;
252
+ --green-200: #0f3d15;
253
+ --green-300: #13521e;
254
+ --green-400: #186626;
255
+ --green-500: #208231;
256
+ --green-600: #2ca341;
257
+ --green-700: #4dce74;
258
+ --green-800: #6de18a;
259
+ --green-900: #a7efae;
260
+ --green-1000: #dffce5;
261
+ --teal-100: #0b2e2c;
262
+ --teal-200: #0d3f3b;
263
+ --teal-300: #10524e;
264
+ --teal-400: #166760;
265
+ --teal-500: #1f7f79;
266
+ --teal-600: #29a29c;
267
+ --teal-700: #1ed3b3;
268
+ --teal-800: #33e0d5;
269
+ --teal-900: #5debe4;
270
+ --teal-1000: #c8f6f2;
271
+ --purple-100: #2a1240;
272
+ --purple-200: #33174f;
273
+ --purple-300: #411d60;
274
+ --purple-400: #5a277f;
275
+ --purple-500: #7a32a9;
276
+ --purple-600: #9f4ccc;
277
+ --purple-700: #bf5cff;
278
+ --purple-800: #d083ff;
279
+ --purple-900: #e8b4ff;
280
+ --purple-1000: #f8edff;
281
+ --pink-100: #3d1025;
282
+ --pink-200: #571333;
283
+ --pink-300: #7a1e4b;
284
+ --pink-400: #a02d63;
285
+ --pink-500: #c63f7d;
286
+ --pink-600: #e75b95;
287
+ --pink-700: #ff6cab;
288
+ --pink-800: #ff96c6;
289
+ --pink-900: #ffc4e2;
290
+ --pink-1000: #ffe8f5;
291
+ --bg: var(--background-100);
292
+ --surface: #131313;
293
+ --surface-2: var(--background-200);
294
+ --fg: var(--gray-1000);
295
+ --body: var(--gray-900);
296
+ --muted: var(--gray-700);
297
+ --line: var(--gray-300);
298
+ --line-strong: var(--gray-400);
299
+ --accent: var(--blue-700);
300
+ --good: var(--green-700);
301
+ --warn: var(--amber-700);
302
+ --bad: var(--red-700);
303
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
304
+ }
305
+
306
+ * { box-sizing: border-box; }
307
+ html {
308
+ min-width: 320px;
309
+ scroll-behavior: smooth;
310
+ }
311
+ body {
312
+ margin: 0;
313
+ overflow-x: hidden;
314
+ min-height: 100%;
315
+ background-color: var(--bg);
316
+ color: var(--fg);
317
+ font-family: var(--font-sans);
318
+ font-size: 14px;
319
+ line-height: 1.5;
320
+ font-weight: 400;
321
+ animation: pageIn 420ms ease-out both;
322
+ transition: background-color 180ms ease, color 180ms ease;
323
+ -webkit-font-smoothing: antialiased;
324
+ }
325
+ a { color: inherit; text-decoration: none; }
326
+ button, input, textarea, select { font: inherit; }
327
+ pre, code, kbd, samp { font-family: var(--font-mono); }
328
+ [hidden] { display: none !important; }
329
+ button { cursor: pointer; }
330
+ .sr-only {
331
+ position: absolute;
332
+ width: 1px;
333
+ height: 1px;
334
+ padding: 0;
335
+ margin: -1px;
336
+ overflow: hidden;
337
+ clip: rect(0, 0, 0, 0);
338
+ white-space: nowrap;
339
+ border: 0;
340
+ }
341
+ .content-shell {
342
+ width: min(var(--page-width), calc(100vw - 40px));
343
+ margin-left: auto;
344
+ margin-right: auto;
345
+ }
346
+ .shell {
347
+ width: min(var(--page-width), calc(100vw - 40px));
348
+ margin: 0 auto;
349
+ }
350
+ .shell.narrow { width: min(1060px, calc(100vw - 48px)); }
351
+ .nav {
352
+ min-height: 72px;
353
+ display: flex;
354
+ align-items: center;
355
+ justify-content: space-between;
356
+ gap: 14px;
357
+ padding: 8px 0;
358
+ position: relative;
359
+ transition: border-radius 180ms ease, background-color 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
360
+ }
361
+ .brand {
362
+ display: inline-flex;
363
+ align-items: center;
364
+ font-weight: 600;
365
+ font-size: 16px;
366
+ letter-spacing: -0.02em;
367
+ white-space: nowrap;
368
+ }
369
+ .nav-links { display: flex; align-items: center; gap: 10px; color: var(--body); }
370
+ .nav-links a,
371
+ .nav-links button { display: inline-flex; align-items: center; justify-content: center; }
372
+ .nav-menu {
373
+ display: none;
374
+ min-height: 38px;
375
+ min-width: 44px;
376
+ border: 1px solid transparent;
377
+ border-radius: var(--radius-full);
378
+ padding: 0;
379
+ background: transparent;
380
+ color: var(--fg);
381
+ line-height: 1;
382
+ transition: background-color 180ms ease, color 180ms ease, transform 180ms ease;
383
+ }
384
+ .button, .theme-toggle {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+ min-height: 40px;
389
+ min-width: 0;
390
+ height: 40px;
391
+ border: 1px solid var(--line);
392
+ border-radius: var(--radius-sm);
393
+ padding: 0 12px;
394
+ background: transparent;
395
+ color: var(--fg);
396
+ font-size: 14px;
397
+ font-weight: 500;
398
+ line-height: 20px;
399
+ white-space: nowrap;
400
+ text-wrap: nowrap;
401
+ transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
402
+ }
403
+ .theme-toggle {
404
+ width: 40px;
405
+ min-width: 40px;
406
+ padding: 0;
407
+ border-radius: var(--radius-full);
408
+ background: var(--surface);
409
+ overflow: hidden;
410
+ }
411
+ .theme-toggle-icon {
412
+ position: relative;
413
+ display: block;
414
+ width: 16px;
415
+ height: 16px;
416
+ border-radius: var(--radius-full);
417
+ background: currentColor;
418
+ transform: rotate(-18deg);
419
+ transition: background-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
420
+ }
421
+ .theme-toggle-icon::after {
422
+ content: "";
423
+ position: absolute;
424
+ inset: -1px -4px 1px 5px;
425
+ border-radius: inherit;
426
+ background: var(--surface);
427
+ transition: opacity 180ms ease, transform 180ms ease, background-color 180ms ease;
428
+ }
429
+ html[data-theme="dark"] .theme-toggle-icon {
430
+ width: 14px;
431
+ height: 14px;
432
+ background: currentColor;
433
+ box-shadow:
434
+ 0 -7px 0 -5px currentColor,
435
+ 0 7px 0 -5px currentColor,
436
+ 7px 0 0 -5px currentColor,
437
+ -7px 0 0 -5px currentColor,
438
+ 5px 5px 0 -5px currentColor,
439
+ -5px 5px 0 -5px currentColor,
440
+ 5px -5px 0 -5px currentColor,
441
+ -5px -5px 0 -5px currentColor;
442
+ transform: rotate(0deg);
443
+ }
444
+ html[data-theme="dark"] .theme-toggle-icon::after {
445
+ opacity: 0;
446
+ transform: scale(0.4);
447
+ }
448
+ .nav-menu-lines {
449
+ position: relative;
450
+ display: block;
451
+ width: 18px;
452
+ height: 14px;
453
+ }
454
+ .nav-menu-line {
455
+ display: block;
456
+ position: absolute;
457
+ left: 1px;
458
+ width: 15px;
459
+ height: 1px;
460
+ border-radius: 999px;
461
+ background: currentColor;
462
+ transform-origin: center;
463
+ transition: opacity 180ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1), width 180ms ease;
464
+ }
465
+ .nav-menu-line:nth-child(1) { top: 1px; }
466
+ .nav-menu-line:nth-child(2) { top: 6.5px; }
467
+ .nav-menu-line:nth-child(3) { top: 12px; }
468
+ .button:hover {
469
+ background: var(--background-200);
470
+ transform: translateY(-1px);
471
+ }
472
+ .button:active {
473
+ background: var(--background-300);
474
+ transform: translateY(0);
475
+ }
476
+ .theme-toggle:hover {
477
+ background: var(--background-200);
478
+ transform: translateY(-1px);
479
+ }
480
+ .theme-toggle:active {
481
+ transform: translateY(0);
482
+ }
483
+ .nav-menu:hover,
484
+ .nav-menu:active {
485
+ background: transparent;
486
+ transform: none;
487
+ }
488
+ .button.primary {
489
+ background: var(--gray-1000);
490
+ color: var(--background-100);
491
+ border-color: var(--gray-1000);
492
+ font-weight: 500;
493
+ }
494
+ .button.primary:hover {
495
+ background: var(--gray-900);
496
+ border-color: var(--gray-900);
497
+ }
498
+ .button.secondary {
499
+ background: var(--surface);
500
+ color: var(--fg);
501
+ border-color: var(--gray-alpha-500);
502
+ }
503
+ .button.secondary:hover {
504
+ background: var(--background-200);
505
+ }
506
+ .button.tertiary {
507
+ background: transparent;
508
+ color: var(--fg);
509
+ }
510
+ .button.subtle { border-color: var(--line); color: var(--body); }
511
+ .button:disabled { opacity: 0.58; cursor: not-allowed; }
512
+ .button:focus-visible,
513
+ input:focus-visible,
514
+ pre:focus-visible,
515
+ button:focus-visible,
516
+ a:focus-visible {
517
+ outline: none;
518
+ box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--blue-700);
519
+ }
520
+ .code, code {
521
+ background: var(--surface-2);
522
+ border: 1px solid var(--line);
523
+ border-radius: var(--radius-sm);
524
+ font-size: 13px;
525
+ }
526
+ code { padding: 1px 5px; }
527
+ .code {
528
+ display: block;
529
+ padding: 14px;
530
+ white-space: pre-wrap;
531
+ overflow-wrap: anywhere;
532
+ word-break: break-word;
533
+ line-height: 1.45;
534
+ color: var(--fg);
535
+ box-shadow: var(--shadow);
536
+ }
537
+ .panel {
538
+ border: 1px solid var(--line);
539
+ border-radius: var(--radius-lg);
540
+ background: var(--surface);
541
+ box-shadow: var(--shadow);
542
+ animation: surfaceIn 360ms ease-out both;
543
+ transition: background-color 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
544
+ }
545
+ .section-label {
546
+ display: inline-flex;
547
+ margin-bottom: 16px;
548
+ border-radius: var(--radius-sm);
549
+ border: 1px solid var(--line);
550
+ color: var(--body);
551
+ background: var(--background-200);
552
+ padding: 6px 10px;
553
+ font-size: 12px;
554
+ font-weight: 600;
555
+ letter-spacing: 0.05em;
556
+ text-transform: uppercase;
557
+ }
558
+ .muted { color: var(--muted); }
559
+ .good { color: var(--good); }
560
+ .warn { color: var(--warn); }
561
+ .bad { color: var(--bad); }
562
+ .notice {
563
+ border: 1px solid var(--line);
564
+ background: var(--surface-2);
565
+ padding: 12px 14px;
566
+ margin: 0 0 16px;
567
+ border-left: 3px solid var(--blue-700);
568
+ }
569
+ .notice.error { border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); }
570
+ .notice.success { border-color: color-mix(in srgb, var(--good) 45%, var(--line)); }
571
+ .notice strong { color: var(--fg); }
572
+ .loader-card {
573
+ min-height: 240px;
574
+ display: grid;
575
+ place-items: center;
576
+ text-align: center;
577
+ }
578
+ .dots {
579
+ display: inline-flex;
580
+ gap: 7px;
581
+ margin-bottom: 14px;
582
+ }
583
+ .dots span {
584
+ width: 8px;
585
+ height: 8px;
586
+ border-radius: 99px;
587
+ background: var(--blue-700);
588
+ animation: dotPulse 880ms ease-in-out infinite;
589
+ }
590
+ .dots span:nth-child(2) { animation-delay: 120ms; }
591
+ .dots span:nth-child(3) { animation-delay: 240ms; }
592
+ @keyframes dotPulse {
593
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.34; }
594
+ 40% { transform: translateY(-7px); opacity: 1; }
595
+ }
596
+ @keyframes pageIn {
597
+ from { opacity: 0; }
598
+ to { opacity: 1; }
599
+ }
600
+ @keyframes surfaceIn {
601
+ from { opacity: 0; transform: translateY(8px); }
602
+ to { opacity: 1; transform: translateY(0); }
603
+ }
604
+ .copy-wrap { position: relative; }
605
+ .copy-wrap .code { padding-right: 74px; }
606
+ .copy-button {
607
+ position: absolute;
608
+ top: 10px;
609
+ right: 12px;
610
+ display: inline-flex;
611
+ align-items: center;
612
+ justify-content: center;
613
+ gap: 5px;
614
+ min-height: 26px;
615
+ height: 26px;
616
+ border: 1px solid var(--line);
617
+ border-radius: 999px;
618
+ padding: 0 8px;
619
+ background: var(--surface);
620
+ color: var(--fg);
621
+ font-size: 12px;
622
+ line-height: 1;
623
+ box-shadow: none;
624
+ }
625
+ .copy-button::before {
626
+ content: "";
627
+ width: 8px;
628
+ height: 10px;
629
+ border: 1px solid currentColor;
630
+ box-shadow: 3px -3px 0 -1px var(--surface), 3px -3px 0 0 currentColor;
631
+ }
632
+ h1, h2, h3, h4, h5 {
633
+ margin: 0;
634
+ font-weight: 600;
635
+ letter-spacing: -0.022em;
636
+ line-height: 1.15;
637
+ color: var(--fg);
638
+ }
639
+ h1 { font-size: 48px; }
640
+ h2 { font-size: 32px; }
641
+ h3 { font-size: 20px; }
642
+ p { margin: 0; color: var(--body); }
643
+ ul, ol { margin: 0; padding-left: 20px; color: var(--body); }
644
+
645
+ @media (max-width: 760px) {
646
+ .shell, .shell.narrow, .content-shell { width: min(100% - 28px, var(--page-width)); }
647
+ .nav {
648
+ position: sticky;
649
+ top: 10px;
650
+ z-index: 30;
651
+ display: grid;
652
+ grid-template-columns: minmax(0, 1fr) auto;
653
+ min-height: 56px;
654
+ margin-top: 12px;
655
+ padding: 7px;
656
+ gap: 8px;
657
+ border: 1px solid var(--line);
658
+ border-radius: 999px;
659
+ background: var(--bg);
660
+ backdrop-filter: blur(8px);
661
+ box-shadow: 0 18px 60px var(--shadow);
662
+ overflow: visible;
663
+ }
664
+ .nav.open {
665
+ border-radius: var(--radius-md);
666
+ }
667
+ .brand {
668
+ min-width: 0;
669
+ padding-left: 8px;
670
+ font-size: 13px;
671
+ white-space: nowrap;
672
+ overflow: hidden;
673
+ text-overflow: ellipsis;
674
+ }
675
+ .nav-menu {
676
+ display: inline-flex;
677
+ justify-self: end;
678
+ width: 40px;
679
+ height: 40px;
680
+ padding: 0;
681
+ justify-content: center;
682
+ align-items: center;
683
+ border: 0;
684
+ border-radius: var(--radius-full);
685
+ background: transparent;
686
+ }
687
+ .nav.open .nav-menu-line:nth-child(1) {
688
+ transform: translateY(5.5px) rotate(45deg);
689
+ }
690
+ .nav.open .nav-menu-line:nth-child(2) {
691
+ opacity: 0;
692
+ transform: translateX(2px) scaleX(0.1);
693
+ }
694
+ .nav.open .nav-menu-line:nth-child(3) {
695
+ transform: translateY(-5.5px) rotate(-45deg);
696
+ }
697
+ .nav-links {
698
+ grid-column: 1 / -1;
699
+ display: flex;
700
+ width: 100%;
701
+ flex-direction: column;
702
+ align-items: stretch;
703
+ gap: 8px;
704
+ font-size: 12px;
705
+ max-height: 0;
706
+ margin: 0;
707
+ padding: 0;
708
+ border-top: 1px solid transparent;
709
+ opacity: 0;
710
+ overflow: hidden;
711
+ pointer-events: none;
712
+ transform: translateY(-4px);
713
+ transition: max-height 240ms ease, opacity 180ms ease, transform 220ms ease, margin 220ms ease, padding 220ms ease, border-color 180ms ease;
714
+ }
715
+ .nav.open .nav-links {
716
+ max-height: 260px;
717
+ margin: 6px 0 0;
718
+ padding: 8px 0 0;
719
+ border-top-color: var(--line);
720
+ opacity: 1;
721
+ pointer-events: auto;
722
+ transform: translateY(0);
723
+ }
724
+ .nav-links .button,
725
+ .nav-links .theme-toggle,
726
+ .nav-links > a:not(.button) {
727
+ width: 100%;
728
+ min-height: 40px;
729
+ padding: 7px 12px;
730
+ text-align: center;
731
+ white-space: nowrap;
732
+ }
733
+ .nav-links .theme-toggle {
734
+ width: 40px;
735
+ min-width: 40px;
736
+ padding: 0;
737
+ align-self: center;
738
+ }
739
+ .nav-links > a:not(.button) {
740
+ display: inline-flex;
741
+ border: 1px solid var(--line);
742
+ border-radius: var(--radius-sm);
743
+ background: var(--surface);
744
+ }
745
+ }
746
+ @media (prefers-reduced-motion: reduce) {
747
+ *,
748
+ *::before,
749
+ *::after {
750
+ scroll-behavior: auto !important;
751
+ animation-duration: 0.01ms !important;
752
+ animation-iteration-count: 1 !important;
753
+ transition-duration: 0.01ms !important;
754
+ }
755
+ }
756
+ @media (max-width: 760px) {
757
+ .nav {
758
+ isolation: isolate;
759
+ }
760
+ .nav.open {
761
+ overflow: hidden;
762
+ border-radius: 22px;
763
+ background: var(--surface);
764
+ border-color: var(--line-strong);
765
+ box-shadow: 0 20px 70px rgba(0, 0, 0, 0.18);
766
+ backdrop-filter: none;
767
+ }
768
+ .nav.open::before,
769
+ .nav.open::after {
770
+ display: none !important;
771
+ content: none !important;
772
+ }
773
+ .nav-menu {
774
+ box-shadow: none;
775
+ }
776
+ .nav.open .nav-menu {
777
+ background: var(--surface-2);
778
+ }
779
+ .nav.open .nav-links {
780
+ max-height: 320px;
781
+ margin: 10px 0 0;
782
+ padding: 12px 0 0;
783
+ gap: 0;
784
+ border-top: 1px solid var(--line);
785
+ }
786
+ .nav.open .nav-links .button,
787
+ .nav.open .nav-links > a:not(.button) {
788
+ width: 100%;
789
+ height: 46px;
790
+ min-height: 46px;
791
+ justify-content: flex-start;
792
+ padding: 0 4px;
793
+ border: 0;
794
+ border-bottom: 1px solid var(--line);
795
+ border-radius: 0;
796
+ background: transparent;
797
+ color: var(--fg);
798
+ box-shadow: none;
799
+ transform: none;
800
+ font-size: 13px;
801
+ font-weight: 500;
802
+ }
803
+ .nav.open .nav-links .button.primary,
804
+ .nav.open .nav-links .button.secondary {
805
+ background: transparent;
806
+ color: var(--fg);
807
+ border-color: var(--line);
808
+ }
809
+ .nav.open .nav-links .button:hover,
810
+ .nav.open .nav-links > a:not(.button):hover {
811
+ background: var(--surface-2);
812
+ padding-left: 12px;
813
+ transform: none;
814
+ }
815
+ .nav.open .theme-toggle {
816
+ width: 100%;
817
+ height: 46px;
818
+ min-height: 46px;
819
+ min-width: 0;
820
+ justify-content: flex-start;
821
+ gap: 10px;
822
+ padding: 0 4px;
823
+ border: 0;
824
+ border-bottom: 1px solid var(--line);
825
+ border-radius: 0;
826
+ background: transparent;
827
+ color: var(--fg);
828
+ box-shadow: none;
829
+ }
830
+ .nav.open .theme-toggle:hover {
831
+ background: var(--surface-2);
832
+ padding-left: 12px;
833
+ transform: none;
834
+ }
835
+ .nav.open .theme-toggle::after {
836
+ content: "Theme";
837
+ font-size: 13px;
838
+ font-weight: 500;
839
+ }
840
+ .nav.open .nav-links > *:last-child {
841
+ border-bottom: 0;
842
+ }
843
+ }
844
+ </style>`;
845
+ }
846
+ export function renderLandingUi(env) {
847
+ const appName = escapeHtml(env.APP_NAME || "ErrorAtlas");
848
+ return `<!doctype html>
849
+ <html lang="en">
850
+ <head>
851
+ <meta charset="utf-8">
852
+ <meta name="viewport" content="width=device-width, initial-scale=1">
853
+ <title>${appName}</title>
854
+ ${themeBootScript()}
855
+ ${sharedCss()}
856
+ <style>
857
+ .landing-main {
858
+ padding: 0 0 64px;
859
+ }
860
+ .hero {
861
+ position: relative;
862
+ display: grid;
863
+ justify-items: center;
864
+ text-align: center;
865
+ gap: 14px;
866
+ padding: clamp(56px, 11vw, 104px) 0 clamp(44px, 8vw, 72px);
867
+ }
868
+ .eyebrow {
869
+ display: inline-flex;
870
+ align-items: center;
871
+ justify-content: center;
872
+ color: var(--body);
873
+ font-size: 12px;
874
+ letter-spacing: 0.08em;
875
+ text-transform: uppercase;
876
+ height: 24px;
877
+ }
878
+ .hero-copy {
879
+ max-width: 720px;
880
+ margin: 16px auto 0;
881
+ color: var(--muted);
882
+ font-size: 18px;
883
+ line-height: 1.65;
884
+ }
885
+ .hero .cta-row {
886
+ display: grid;
887
+ grid-template-columns: auto auto;
888
+ justify-content: center;
889
+ gap: 12px;
890
+ margin-top: 8px;
891
+ }
892
+ .hero h1 {
893
+ max-width: 980px;
894
+ margin: 0;
895
+ text-wrap: balance;
896
+ font-size: clamp(36px, 6vw, 60px);
897
+ line-height: 1.08;
898
+ letter-spacing: -0.038em;
899
+ }
900
+ .hero .button.primary {
901
+ box-shadow: 0 2px 0 rgba(0, 0, 0, 0.06);
902
+ }
903
+ .hero .button {
904
+ min-width: 192px;
905
+ justify-content: center;
906
+ }
907
+ .product-strip {
908
+ padding-bottom: 56px;
909
+ }
910
+ .flow-panel {
911
+ margin-top: 26px;
912
+ display: grid;
913
+ grid-template-columns: minmax(280px, 0.95fr) minmax(0, 1.05fr);
914
+ gap: 16px;
915
+ padding: clamp(20px, 3vw, 32px);
916
+ align-items: stretch;
917
+ }
918
+ .flow-copy {
919
+ display: grid;
920
+ align-content: start;
921
+ gap: 12px;
922
+ }
923
+ .flow-copy h2 {
924
+ max-width: 520px;
925
+ font-size: clamp(30px, 4vw, 40px);
926
+ line-height: 1.14;
927
+ }
928
+ .flow-copy p {
929
+ max-width: 470px;
930
+ color: var(--body);
931
+ }
932
+ .flow-steps {
933
+ display: grid;
934
+ gap: 10px;
935
+ }
936
+ .flow-card {
937
+ display: grid;
938
+ grid-template-columns: auto minmax(0, 1fr);
939
+ gap: 14px;
940
+ align-items: start;
941
+ border: 1px solid var(--line);
942
+ border-radius: var(--radius-md);
943
+ padding: 16px;
944
+ background: var(--surface);
945
+ }
946
+ .flow-number {
947
+ display: grid;
948
+ place-items: center;
949
+ width: 32px;
950
+ height: 32px;
951
+ border-radius: 999px;
952
+ background: var(--background-200);
953
+ border: 1px solid var(--line);
954
+ color: var(--body);
955
+ font-weight: 600;
956
+ }
957
+ .flow-card h3 {
958
+ margin: 0 0 6px;
959
+ font-size: 19px;
960
+ }
961
+ .flow-card p {
962
+ margin: 0;
963
+ color: var(--body);
964
+ line-height: 1.45;
965
+ }
966
+ .footer {
967
+ border-top: 1px solid var(--line);
968
+ padding: 22px 0 30px;
969
+ color: var(--muted);
970
+ display: flex;
971
+ justify-content: center;
972
+ gap: 16px;
973
+ flex-wrap: wrap;
974
+ }
975
+ @media (max-width: 980px) {
976
+ .hero, .flow-panel {
977
+ grid-template-columns: 1fr;
978
+ }
979
+ .hero, .product-strip {
980
+ padding-left: 0;
981
+ padding-right: 0;
982
+ }
983
+ .flow-copy h2 {
984
+ font-size: 30px;
985
+ max-width: none;
986
+ }
987
+ }
988
+ @media (max-width: 640px) {
989
+ .landing-main {
990
+ padding-bottom: 40px;
991
+ }
992
+ .hero {
993
+ padding: 40px 0 34px;
994
+ gap: 20px;
995
+ }
996
+ .hero h1 {
997
+ font-size: clamp(34px, 10vw, 46px);
998
+ line-height: 1.12;
999
+ letter-spacing: -0.032em;
1000
+ }
1001
+ .hero-copy {
1002
+ font-size: 16px;
1003
+ margin-top: 14px;
1004
+ line-height: 1.6;
1005
+ }
1006
+ .hero .cta-row {
1007
+ width: 100%;
1008
+ grid-template-columns: 1fr;
1009
+ max-width: 380px;
1010
+ margin-left: auto;
1011
+ margin-right: auto;
1012
+ }
1013
+ .hero .button {
1014
+ width: 100%;
1015
+ min-width: 0;
1016
+ justify-content: center;
1017
+ }
1018
+ .footer {
1019
+ flex-direction: column;
1020
+ }
1021
+ }
1022
+ </style>
1023
+ </head>
1024
+ <body class="landing-page">
1025
+ <div class="shell narrow">
1026
+ <nav class="nav">
1027
+ <a href="/" class="brand">ErrorAtlas</a>
1028
+ <button class="nav-menu" id="navMenu" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Open navigation"><span class="nav-menu-lines" aria-hidden="true"><span class="nav-menu-line"></span><span class="nav-menu-line"></span><span class="nav-menu-line"></span></span></button>
1029
+ <div class="nav-links" id="navLinks">
1030
+ <a href="/setup">Setup</a>
1031
+ <button class="theme-toggle" id="themeToggle" type="button" aria-label="Switch theme" title="Switch theme"><span class="theme-toggle-icon" aria-hidden="true"></span></button>
1032
+ <a class="button primary" href="/dashboard">Dashboard</a>
1033
+ </div>
1034
+ </nav>
1035
+
1036
+ <main>
1037
+ <section class="hero">
1038
+ <div>
1039
+ <span class="eyebrow">Error memory for coding agents</span>
1040
+ <h1>Your agent remembers the bugs you beat.</h1>
1041
+ <p class="hero-copy">ErrorAtlas turns past failures into reusable resolutions your local agents can search before they burn another context window.</p>
1042
+ <div class="cta-row">
1043
+ <a class="button primary" href="/dashboard">Enter console</a>
1044
+ <a class="button" href="/setup">Connect an agent</a>
1045
+ </div>
1046
+ </div>
1047
+ </section>
1048
+
1049
+ <section class="product-strip">
1050
+ <div class="panel flow-panel">
1051
+ <div class="flow-copy">
1052
+ <span class="section-label">How it works</span>
1053
+ <h2>Reusable memory your agent can actually use.</h2>
1054
+ <p>ErrorAtlas sits between your local agent and your stored fixes. The agent searches before guessing, captures what failed, and only promotes fixes after validation.</p>
1055
+ </div>
1056
+ <div class="flow-steps">
1057
+ <article class="flow-card">
1058
+ <div class="flow-number">1</div>
1059
+ <div><h3>Search before spending context</h3><p>Your agent asks ErrorAtlas for similar errors before trying another random fix.</p></div>
1060
+ </article>
1061
+ <article class="flow-card">
1062
+ <div class="flow-number">2</div>
1063
+ <div><h3>Capture the useful signal</h3><p>Error, command, package manager, and failed attempts become structured memory.</p></div>
1064
+ </article>
1065
+ <article class="flow-card">
1066
+ <div class="flow-number">3</div>
1067
+ <div><h3>Capture once, reuse forever</h3><p>Working fixes become searchable playbooks after they are validated.</p></div>
1068
+ </article>
1069
+ </div>
1070
+ </div>
1071
+ </section>
1072
+ </main>
1073
+
1074
+ <footer class="footer">
1075
+ <span>ErrorAtlas 2026</span>
1076
+ </footer>
1077
+ </div>
1078
+ </body>
1079
+ </html>`;
1080
+ }
1081
+ export function renderSetupGuideUi(env, requestOrigin) {
1082
+ const appName = escapeHtml(env.APP_NAME || "ErrorAtlas");
1083
+ const origin = escapeHtml(requestOrigin || env.APP_ORIGIN || "https://erroratlas.your-workers-subdomain.workers.dev");
1084
+ return `<!doctype html>
1085
+ <html lang="en">
1086
+ <head>
1087
+ <meta charset="utf-8">
1088
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1089
+ <title>${appName} Setup</title>
1090
+ ${themeBootScript()}
1091
+ ${sharedCss()}
1092
+ <style>
1093
+ .hero {
1094
+ padding: 52px 0 24px;
1095
+ }
1096
+ .eyebrow {
1097
+ color: var(--muted);
1098
+ letter-spacing: 0.08em;
1099
+ text-transform: uppercase;
1100
+ }
1101
+ h1 {
1102
+ max-width: 860px;
1103
+ margin: 6px 0;
1104
+ font-size: clamp(34px, 4vw, 52px);
1105
+ line-height: 1.08;
1106
+ letter-spacing: -0.04em;
1107
+ }
1108
+ .intro {
1109
+ max-width: 800px;
1110
+ margin: 0;
1111
+ color: var(--muted);
1112
+ }
1113
+ .section {
1114
+ padding: 14px 0 64px;
1115
+ }
1116
+ .steps {
1117
+ display: grid;
1118
+ gap: 12px;
1119
+ margin-top: 16px;
1120
+ }
1121
+ .step {
1122
+ display: grid;
1123
+ grid-template-columns: 42px minmax(0, 1fr);
1124
+ gap: 16px;
1125
+ border: 1px solid var(--line);
1126
+ border-radius: var(--radius-md);
1127
+ padding: 18px;
1128
+ background: var(--surface);
1129
+ }
1130
+ .step h2 {
1131
+ margin: 0;
1132
+ font-size: 18px;
1133
+ }
1134
+ .step p {
1135
+ margin: 0 0 14px;
1136
+ color: var(--body);
1137
+ max-width: 700px;
1138
+ }
1139
+ .step .code {
1140
+ margin-top: 10px;
1141
+ }
1142
+ .copy-wrap { position: relative; }
1143
+ .copy-wrap .code { padding-right: 60px; }
1144
+ .copy-button {
1145
+ position: absolute;
1146
+ top: 12px;
1147
+ right: 12px;
1148
+ display: inline-flex;
1149
+ align-items: center;
1150
+ justify-content: center;
1151
+ width: 34px;
1152
+ min-width: 34px;
1153
+ height: 30px;
1154
+ min-height: 30px;
1155
+ border: 1px solid var(--line);
1156
+ border-radius: var(--radius-sm);
1157
+ padding: 0;
1158
+ background: var(--surface);
1159
+ color: var(--fg);
1160
+ font-size: 0;
1161
+ transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
1162
+ }
1163
+ .copy-button:hover {
1164
+ background: var(--background-200);
1165
+ transform: translateY(-1px);
1166
+ }
1167
+ .copy-button:active {
1168
+ transform: translateY(0);
1169
+ }
1170
+ .copy-button::before {
1171
+ content: "";
1172
+ width: 10px;
1173
+ height: 12px;
1174
+ border: 1px solid currentColor;
1175
+ box-shadow: 3px -3px 0 -1px var(--surface), 3px -3px 0 0 currentColor;
1176
+ }
1177
+ .copy-button[data-state="copied"]::before {
1178
+ width: 12px;
1179
+ height: 7px;
1180
+ border-top: 0;
1181
+ border-right: 0;
1182
+ box-shadow: none;
1183
+ transform: rotate(-45deg);
1184
+ }
1185
+ .step-number {
1186
+ width: 34px;
1187
+ height: 34px;
1188
+ border-radius: 999px;
1189
+ border: 1px solid var(--line);
1190
+ background: var(--surface-2);
1191
+ display: inline-flex;
1192
+ align-items: center;
1193
+ justify-content: center;
1194
+ color: var(--muted);
1195
+ font-weight: 600;
1196
+ }
1197
+ @media (max-width: 760px) {
1198
+ .setup-page .nav {
1199
+ border-radius: 999px;
1200
+ }
1201
+ .setup-page .nav-links {
1202
+ border-top: 1px solid var(--line);
1203
+ }
1204
+ .hero {
1205
+ padding: 40px 0 18px;
1206
+ }
1207
+ .step {
1208
+ grid-template-columns: 1fr;
1209
+ gap: 8px;
1210
+ padding: 16px;
1211
+ }
1212
+ .copy-wrap .code {
1213
+ padding: 46px 14px 14px;
1214
+ }
1215
+ .copy-button {
1216
+ top: 10px;
1217
+ right: 10px;
1218
+ }
1219
+ .step-number {
1220
+ width: 24px;
1221
+ height: 24px;
1222
+ }
1223
+ }
1224
+ </style>
1225
+ </head>
1226
+ <body class="setup-page">
1227
+ <div class="shell narrow">
1228
+ <nav class="nav">
1229
+ <a href="/" class="brand">ErrorAtlas</a>
1230
+ <button class="nav-menu" id="navMenu" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Open navigation"><span class="nav-menu-lines" aria-hidden="true"><span class="nav-menu-line"></span><span class="nav-menu-line"></span><span class="nav-menu-line"></span></span></button>
1231
+ <div class="nav-links" id="navLinks">
1232
+ <a href="/dashboard">Dashboard</a>
1233
+ <button class="theme-toggle" id="themeToggle" type="button" aria-label="Switch theme" title="Switch theme"><span class="theme-toggle-icon" aria-hidden="true"></span></button>
1234
+ <a class="button primary" href="/">Home</a>
1235
+ </div>
1236
+ </nav>
1237
+
1238
+ <main>
1239
+ <section class="hero">
1240
+ <span class="eyebrow">Setup</span>
1241
+ <h1>Connect ErrorAtlas to your coding agent.</h1>
1242
+ <p class="intro">Generate one hosted MCP secret, run Supermemory locally, then point your coding agent at the ErrorAtlas MCP server. No repo clone, database setup, or Worker deploy needed.</p>
1243
+ </section>
1244
+
1245
+ <section class="section">
1246
+ <div class="section-label">Developer setup</div>
1247
+ <div class="steps">
1248
+ <article class="step">
1249
+ <span class="step-number">1</span>
1250
+ <div>
1251
+ <h2>Generate your MCP secret</h2>
1252
+ <p>Open the hosted dashboard, sign in, click <strong>Generate secret</strong>, then copy it immediately. Existing raw secrets cannot be recovered later.</p>
1253
+ <pre class="code">${origin}/dashboard</pre>
1254
+ </div>
1255
+ </article>
1256
+ <article class="step">
1257
+ <span class="step-number">2</span>
1258
+ <div>
1259
+ <h2>Install package</h2>
1260
+ <p>Install ErrorAtlas in the project where your coding agent works.</p>
1261
+ <pre class="code">npm install -D @sansynx/erroratlas
1262
+ npx erroratlas init</pre>
1263
+ </div>
1264
+ </article>
1265
+ <article class="step">
1266
+ <span class="step-number">3</span>
1267
+ <div>
1268
+ <h2>Run local memory</h2>
1269
+ <p>Supermemory stores private debugging context on the developer machine. It needs one model-provider key such as <code>OPENAI_API_KEY</code>, <code>ANTHROPIC_API_KEY</code>, <code>GEMINI_API_KEY</code>, or <code>GROQ_API_KEY</code>.</p>
1270
+ <pre class="code">OPENAI_API_KEY=your_model_provider_key
1271
+ npx supermemory local --port 6767
1272
+
1273
+ # if installed globally
1274
+ supermemory local --port 6767</pre>
1275
+ <p>Keep this terminal running while your coding agent works. Local memories are stored in the project's <code>.supermemory</code> folder.</p>
1276
+ <p>Windows note: if the npm wrapper says the server binary was not found, open bash or WSL in the project and run <code>PORT=6767 ~/.supermemory/bin/supermemory-server</code> after exporting your model-provider key.</p>
1277
+ </div>
1278
+ </article>
1279
+ <article class="step">
1280
+ <span class="step-number">4</span>
1281
+ <div>
1282
+ <h2>Add MCP server</h2>
1283
+ <p>Paste this into your agent client's MCP config. Replace <code>ERRORATLAS_API_KEY</code> with the secret copied from the dashboard.</p>
1284
+ <pre class="code">{
1285
+ "mcpServers": {
1286
+ "erroratlas": {
1287
+ "command": "npx",
1288
+ "args": ["-y", "--package", "@sansynx/erroratlas", "erroratlas", "mcp"],
1289
+ "env": {
1290
+ "ERRORATLAS_API_URL": "${origin}",
1291
+ "ERRORATLAS_API_KEY": "ea_live_from_dashboard",
1292
+ "SUPERMEMORY_URL": "http://localhost:6767"
1293
+ }
1294
+ }
1295
+ }
1296
+ }</pre>
1297
+ </div>
1298
+ </article>
1299
+ <article class="step">
1300
+ <span class="step-number">5</span>
1301
+ <div>
1302
+ <h2>Confirm the environment</h2>
1303
+ <p>Your MCP server must receive these values. If Supermemory prints a local API key, add <code>SUPERMEMORY_API_KEY</code>; local unauthenticated requests may work without it.</p>
1304
+ <pre class="code">ERRORATLAS_API_URL=${origin}
1305
+ ERRORATLAS_API_KEY=ea_live_from_dashboard
1306
+ SUPERMEMORY_URL=http://localhost:6767
1307
+ SUPERMEMORY_API_KEY=optional_local_supermemory_key</pre>
1308
+ </div>
1309
+ </article>
1310
+ <article class="step">
1311
+ <span class="step-number">6</span>
1312
+ <div>
1313
+ <h2>Verify with your agent</h2>
1314
+ <p>Ask the agent to search before fixing, capture meaningful failures, and publish a playbook only after the fix is verified.</p>
1315
+ <pre class="code">- Before changing code, search ErrorAtlas.
1316
+ - If the error is useful, call capture_error_signal.
1317
+ - If the fix works, call publish_resolution with root cause, fix, and verification.</pre>
1318
+ </div>
1319
+ </article>
1320
+ </div>
1321
+ </section>
1322
+
1323
+ </main>
1324
+ </div>
1325
+ <script>
1326
+ document.addEventListener("DOMContentLoaded", function () {
1327
+ document.querySelectorAll("pre.code").forEach(function (pre) {
1328
+ if (pre.parentElement && pre.parentElement.classList.contains("copy-wrap")) return;
1329
+ const wrapper = document.createElement("div");
1330
+ wrapper.className = "copy-wrap";
1331
+ pre.parentNode.insertBefore(wrapper, pre);
1332
+ wrapper.appendChild(pre);
1333
+ const button = document.createElement("button");
1334
+ button.type = "button";
1335
+ button.className = "copy-button";
1336
+ button.innerHTML = '<span class="sr-only">Copy snippet</span>';
1337
+ button.title = "Copy snippet";
1338
+ button.setAttribute("aria-label", "Copy snippet");
1339
+ button.addEventListener("click", async function () {
1340
+ const value = pre.textContent || "";
1341
+ try {
1342
+ await navigator.clipboard.writeText(value);
1343
+ button.dataset.state = "copied";
1344
+ button.title = "Copied";
1345
+ button.setAttribute("aria-label", "Copied");
1346
+ setTimeout(function () {
1347
+ button.dataset.state = "";
1348
+ button.title = "Copy snippet";
1349
+ button.setAttribute("aria-label", "Copy snippet");
1350
+ }, 1400);
1351
+ } catch (_error) {
1352
+ button.title = "Select the snippet";
1353
+ button.setAttribute("aria-label", "Select the snippet");
1354
+ setTimeout(function () {
1355
+ button.title = "Copy snippet";
1356
+ button.setAttribute("aria-label", "Copy snippet");
1357
+ }, 1400);
1358
+ }
1359
+ });
1360
+ wrapper.appendChild(button);
1361
+ });
1362
+ });
1363
+ </script>
1364
+ </body>
1365
+ </html>`;
1366
+ }
1367
+ export function renderDashboardUi(env) {
1368
+ const appName = escapeHtml(env.APP_NAME || "ErrorAtlas");
1369
+ return String.raw `<!doctype html>
1370
+ <html lang="en">
1371
+ <head>
1372
+ <meta charset="utf-8">
1373
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1374
+ <title>${appName} Dashboard</title>
1375
+ ${themeBootScript()}
1376
+ ${sharedCss()}
1377
+ <style>
1378
+ .dashboard-page .shell {
1379
+ min-height: 100vh;
1380
+ }
1381
+ .app-frame {
1382
+ display: block;
1383
+ margin: 10px 0 28px;
1384
+ background: transparent;
1385
+ }
1386
+ .main { min-width: 0; }
1387
+ .topbar {
1388
+ min-height: 90px;
1389
+ padding: 22px 0;
1390
+ border-bottom: 1px solid var(--line);
1391
+ display: grid;
1392
+ grid-template-columns: minmax(220px, 1fr) auto;
1393
+ align-items: center;
1394
+ gap: 16px;
1395
+ }
1396
+ .topbar-main { min-width: 0; }
1397
+ .topbar h1 {
1398
+ margin: 0;
1399
+ font-size: 27px;
1400
+ line-height: 1.16;
1401
+ }
1402
+ .topbar p {
1403
+ margin: 6px 0 0;
1404
+ color: var(--muted);
1405
+ }
1406
+ .auth-actions {
1407
+ display: flex;
1408
+ flex-wrap: wrap;
1409
+ justify-content: flex-end;
1410
+ gap: 10px;
1411
+ }
1412
+ .content {
1413
+ padding: 22px 0 24px;
1414
+ }
1415
+ .view-space { min-width: 0; }
1416
+ .panel {
1417
+ padding: 20px;
1418
+ border: 1px solid var(--line);
1419
+ }
1420
+ .panel h3 {
1421
+ margin: 0 0 10px;
1422
+ font-size: 18px;
1423
+ font-weight: 600;
1424
+ }
1425
+ .panel p { color: var(--body); }
1426
+ .auth-open-card {
1427
+ min-height: 164px;
1428
+ display: grid;
1429
+ align-content: center;
1430
+ justify-items: start;
1431
+ gap: 10px;
1432
+ }
1433
+ .auth-modal {
1434
+ position: fixed;
1435
+ inset: 0;
1436
+ z-index: 50;
1437
+ display: grid;
1438
+ place-items: center;
1439
+ padding: 20px;
1440
+ background: color-mix(in srgb, var(--gray-1000) 15%, transparent);
1441
+ backdrop-filter: blur(8px);
1442
+ }
1443
+ .auth-modal[hidden] { display: none; }
1444
+ .auth-dialog {
1445
+ width: min(460px, 100%);
1446
+ border: 1px solid var(--line);
1447
+ border-radius: var(--radius-lg);
1448
+ padding: 24px;
1449
+ background: var(--surface);
1450
+ box-shadow: var(--shadow-modal);
1451
+ }
1452
+ .auth-dialog-head {
1453
+ display: flex;
1454
+ align-items: flex-start;
1455
+ justify-content: space-between;
1456
+ gap: 16px;
1457
+ margin-bottom: 18px;
1458
+ }
1459
+ .auth-dialog h3 { margin: 0 0 6px; font-size: 24px; }
1460
+ .auth-dialog p { margin: 0; color: var(--muted); }
1461
+ .icon-button {
1462
+ width: 34px;
1463
+ height: 34px;
1464
+ border: 1px solid var(--line);
1465
+ border-radius: 999px;
1466
+ background: var(--surface-2);
1467
+ color: var(--fg);
1468
+ }
1469
+ .auth-methods { display: grid; gap: 10px; }
1470
+ .auth-methods input {
1471
+ width: 100%;
1472
+ border-radius: var(--radius-sm);
1473
+ min-height: 40px;
1474
+ border: 1px solid var(--line);
1475
+ background: var(--surface-2);
1476
+ padding: 0 12px;
1477
+ color: var(--fg);
1478
+ }
1479
+ .auth-divider {
1480
+ display: flex;
1481
+ align-items: center;
1482
+ gap: 12px;
1483
+ color: var(--muted);
1484
+ font-size: 12px;
1485
+ margin: 4px 0;
1486
+ }
1487
+ .auth-divider::before,
1488
+ .auth-divider::after {
1489
+ content: "";
1490
+ height: 1px;
1491
+ flex: 1;
1492
+ background: var(--line);
1493
+ }
1494
+ .empty {
1495
+ padding: 14px 0;
1496
+ color: var(--muted);
1497
+ }
1498
+ .agent-note {
1499
+ margin: 10px 0 0;
1500
+ color: var(--muted);
1501
+ font-size: 13px;
1502
+ }
1503
+ .env-row {
1504
+ display: grid;
1505
+ grid-template-columns: minmax(0, 1fr) auto;
1506
+ gap: 12px;
1507
+ align-items: center;
1508
+ border-top: 1px solid var(--line);
1509
+ padding: 13px 0;
1510
+ background: transparent;
1511
+ }
1512
+ .env-row:last-child { border-bottom: 1px solid var(--line); }
1513
+ .env-name {
1514
+ color: var(--muted);
1515
+ font-size: 12px;
1516
+ margin-bottom: 5px;
1517
+ letter-spacing: 0.01em;
1518
+ }
1519
+ .env-value {
1520
+ min-width: 0;
1521
+ overflow: hidden;
1522
+ text-overflow: ellipsis;
1523
+ white-space: nowrap;
1524
+ color: var(--fg);
1525
+ font-size: 13px;
1526
+ font-family: var(--font-mono);
1527
+ }
1528
+ .env-value.is-secret {
1529
+ letter-spacing: 0.03em;
1530
+ }
1531
+ .tiny-copy {
1532
+ display: inline-flex;
1533
+ align-items: center;
1534
+ justify-content: center;
1535
+ min-height: 32px;
1536
+ border: 1px solid var(--line);
1537
+ border-radius: var(--radius-sm);
1538
+ padding: 0 10px;
1539
+ background: var(--surface);
1540
+ color: var(--fg);
1541
+ font-size: 12px;
1542
+ line-height: 1;
1543
+ box-shadow: none;
1544
+ }
1545
+ .key-console {
1546
+ display: grid;
1547
+ grid-template-columns: minmax(0, 1fr) minmax(340px, 0.86fr);
1548
+ border: 1px solid var(--line);
1549
+ border-radius: var(--radius-lg);
1550
+ background: color-mix(in srgb, var(--surface) 92%, var(--background-200));
1551
+ box-shadow: var(--shadow);
1552
+ overflow: hidden;
1553
+ }
1554
+ .key-pane {
1555
+ min-width: 0;
1556
+ padding: clamp(20px, 3vw, 30px);
1557
+ }
1558
+ .key-pane + .key-pane {
1559
+ border-left: 1px solid var(--line);
1560
+ }
1561
+ .key-head {
1562
+ display: flex;
1563
+ justify-content: space-between;
1564
+ align-items: flex-start;
1565
+ gap: 18px;
1566
+ margin-bottom: 24px;
1567
+ }
1568
+ .key-kicker {
1569
+ color: var(--muted);
1570
+ font-size: 12px;
1571
+ letter-spacing: 0.06em;
1572
+ text-transform: uppercase;
1573
+ margin-bottom: 8px;
1574
+ }
1575
+ .key-title {
1576
+ margin: 0;
1577
+ font-size: clamp(24px, 3vw, 34px);
1578
+ line-height: 1.05;
1579
+ }
1580
+ .key-desc {
1581
+ max-width: 560px;
1582
+ margin-top: 10px;
1583
+ color: var(--body);
1584
+ }
1585
+ .key-actions {
1586
+ display: flex;
1587
+ flex-wrap: wrap;
1588
+ align-items: center;
1589
+ gap: 10px;
1590
+ }
1591
+ .key-note {
1592
+ margin-top: 12px;
1593
+ color: var(--muted);
1594
+ font-size: 13px;
1595
+ }
1596
+ .key-status {
1597
+ border-top: 1px solid var(--line);
1598
+ margin-top: 22px;
1599
+ padding-top: 10px;
1600
+ }
1601
+ .status-line {
1602
+ display: grid;
1603
+ grid-template-columns: 130px minmax(0, 1fr);
1604
+ gap: 14px;
1605
+ padding: 12px 0;
1606
+ border-bottom: 1px solid var(--line);
1607
+ }
1608
+ .status-label {
1609
+ color: var(--muted);
1610
+ font-size: 12px;
1611
+ }
1612
+ .status-value {
1613
+ min-width: 0;
1614
+ color: var(--fg);
1615
+ font-weight: 500;
1616
+ }
1617
+ .secret-strip {
1618
+ display: grid;
1619
+ gap: 12px;
1620
+ margin: 18px 0 22px;
1621
+ padding: 16px;
1622
+ border: 1px solid var(--line);
1623
+ border-radius: var(--radius-md);
1624
+ background: var(--surface-2);
1625
+ }
1626
+ .secret-strip.is-ready {
1627
+ border-color: color-mix(in srgb, var(--good) 42%, var(--line));
1628
+ background: color-mix(in srgb, var(--green-700) 7%, var(--surface));
1629
+ }
1630
+ .secret-strip strong {
1631
+ display: block;
1632
+ margin-bottom: 4px;
1633
+ }
1634
+ .secret-strip span {
1635
+ display: block;
1636
+ color: var(--muted);
1637
+ font-size: 13px;
1638
+ }
1639
+ .secret-copy-row {
1640
+ display: grid;
1641
+ grid-template-columns: minmax(0, 1fr) auto;
1642
+ gap: 10px;
1643
+ align-items: center;
1644
+ }
1645
+ .secret-token {
1646
+ display: flex;
1647
+ align-items: center;
1648
+ min-height: 40px;
1649
+ min-width: 0;
1650
+ overflow: hidden;
1651
+ text-overflow: ellipsis;
1652
+ white-space: nowrap;
1653
+ border: 1px solid var(--line);
1654
+ border-radius: var(--radius-sm);
1655
+ padding: 0 12px;
1656
+ background: var(--surface);
1657
+ font-family: var(--font-mono);
1658
+ font-size: 13px;
1659
+ color: var(--fg);
1660
+ }
1661
+ .env-table {
1662
+ display: grid;
1663
+ border-top: 1px solid var(--line);
1664
+ }
1665
+ .loader-card {
1666
+ min-height: 250px;
1667
+ display: grid;
1668
+ place-items: center;
1669
+ text-align: center;
1670
+ }
1671
+ .dots {
1672
+ display: inline-flex;
1673
+ gap: 8px;
1674
+ margin-bottom: 12px;
1675
+ }
1676
+ .dots span {
1677
+ width: 8px;
1678
+ aspect-ratio: 1;
1679
+ border-radius: 99px;
1680
+ background: var(--blue-700);
1681
+ animation: pulse 1s ease-in-out infinite;
1682
+ }
1683
+ .dots span:nth-child(2) { animation-delay: 0.15s; }
1684
+ .dots span:nth-child(3) { animation-delay: 0.3s; }
1685
+ @keyframes pulse {
1686
+ 0%, 100% { transform: scale(0.8); opacity: 0.45; }
1687
+ 50% { transform: scale(1); opacity: 1; }
1688
+ }
1689
+ @media (max-width: 980px) {
1690
+ .app-frame,
1691
+ .key-console { grid-template-columns: 1fr; }
1692
+ .key-pane + .key-pane {
1693
+ border-left: 0;
1694
+ border-top: 1px solid var(--line);
1695
+ }
1696
+ .topbar { grid-template-columns: 1fr; align-items: start; }
1697
+ }
1698
+ @media (max-width: 640px) {
1699
+ .dashboard-page .shell.narrow { width: min(100% - 18px, 1200px); }
1700
+ .dashboard-page .shell { width: min(100% - 28px, 1200px); }
1701
+ .topbar { padding: 16px 0; gap: 12px; }
1702
+ .content { padding: 16px 0 20px; }
1703
+ .panel { padding: 16px; }
1704
+ .key-pane { padding: 18px; }
1705
+ .key-head,
1706
+ .secret-copy-row {
1707
+ grid-template-columns: 1fr;
1708
+ }
1709
+ .key-actions .button,
1710
+ .secret-strip .button { width: 100%; }
1711
+ .status-line { grid-template-columns: 1fr; gap: 4px; }
1712
+ .env-row { grid-template-columns: 1fr; }
1713
+ .tiny-copy { width: 100%; }
1714
+ .auth-actions .button { width: 100%; }
1715
+ input { min-width: 0; width: 100%; }
1716
+ }
1717
+ </style>
1718
+ </head>
1719
+ <body class="dashboard-page">
1720
+ <div class="shell">
1721
+ <nav class="nav">
1722
+ <a href="/" class="brand">ErrorAtlas</a>
1723
+ <button class="nav-menu" id="navMenu" type="button" aria-expanded="false" aria-controls="navLinks" aria-label="Open navigation"><span class="nav-menu-lines" aria-hidden="true"><span class="nav-menu-line"></span><span class="nav-menu-line"></span><span class="nav-menu-line"></span></span></button>
1724
+ <div class="nav-links" id="navLinks">
1725
+ <a href="/setup">Setup</a>
1726
+ <button class="theme-toggle" id="themeToggle" type="button" aria-label="Switch theme" title="Switch theme"><span class="theme-toggle-icon" aria-hidden="true"></span></button>
1727
+ <a class="button primary" href="/">Home</a>
1728
+ </div>
1729
+ </nav>
1730
+
1731
+ <section class="app-frame" aria-label="ErrorAtlas MCP key console">
1732
+ <main class="main">
1733
+ <header class="topbar">
1734
+ <div class="topbar-main">
1735
+ <h1 id="viewTitle">Dashboard</h1>
1736
+ <p id="sessionStatus">Checking workspace session</p>
1737
+ </div>
1738
+ <div class="auth-actions" id="topActions"></div>
1739
+ </header>
1740
+ <div class="content">
1741
+ <div id="message"></div>
1742
+ <div id="appContent" class="view-space">
1743
+ <div class="panel loader-card">
1744
+ <div>
1745
+ <div class="dots"><span></span><span></span><span></span></div>
1746
+ <h2>Warming up ErrorAtlas</h2>
1747
+ <p class="muted">Preparing dashboard and workspace state.</p>
1748
+ </div>
1749
+ </div>
1750
+ </div>
1751
+ </div>
1752
+ </main>
1753
+ </section>
1754
+ </div>
1755
+
1756
+ <script>
1757
+ (function () {
1758
+ const sessionKey = "erroratlas.session";
1759
+ const state = {
1760
+ config: null,
1761
+ session: null,
1762
+ data: null,
1763
+ generatedKey: null,
1764
+ message: "",
1765
+ messageType: ""
1766
+ };
1767
+
1768
+ const content = document.getElementById("appContent");
1769
+ const message = document.getElementById("message");
1770
+ const status = document.getElementById("sessionStatus");
1771
+ const title = document.getElementById("viewTitle");
1772
+ const actions = document.getElementById("topActions");
1773
+
1774
+ function esc(value) {
1775
+ return String(value == null ? "" : value).replace(/[&<>\"']/g, function (char) {
1776
+ if (char === "&") return "&amp;";
1777
+ if (char === "<") return "&lt;";
1778
+ if (char === ">") return "&gt;";
1779
+ if (char === '"') return "&quot;";
1780
+ if (char === "'") return "&#039;";
1781
+ return char;
1782
+ });
1783
+ }
1784
+
1785
+ function setMessage(text, type) {
1786
+ state.message = text || "";
1787
+ state.messageType = type || "error";
1788
+ renderMessage();
1789
+ }
1790
+
1791
+ function renderMessage() {
1792
+ message.innerHTML = state.message ? '<div class="notice ' + esc(state.messageType) + '">' + esc(state.message) + "</div>" : "";
1793
+ }
1794
+
1795
+ function supabaseBase() {
1796
+ const url = state.config && state.config.supabaseUrl ? state.config.supabaseUrl : "";
1797
+ return url.endsWith("/") ? url.slice(0, -1) : url;
1798
+ }
1799
+
1800
+ function authHeaders(token) {
1801
+ return {
1802
+ apikey: state.config.supabaseAnonKey,
1803
+ Authorization: "Bearer " + (token || state.config.supabaseAnonKey),
1804
+ "Content-Type": "application/json"
1805
+ };
1806
+ }
1807
+
1808
+ function readSession() {
1809
+ try {
1810
+ const raw = localStorage.getItem(sessionKey);
1811
+ return raw ? JSON.parse(raw) : null;
1812
+ } catch (_error) {
1813
+ return null;
1814
+ }
1815
+ }
1816
+
1817
+ function saveSession(session) {
1818
+ try {
1819
+ if (session && session.access_token) localStorage.setItem(sessionKey, JSON.stringify(session));
1820
+ else localStorage.removeItem(sessionKey);
1821
+ } catch (_error) {}
1822
+ }
1823
+
1824
+ function sessionFromHash() {
1825
+ const hash = new URLSearchParams(location.hash.replace(/^#/, ""));
1826
+ const token = hash.get("access_token");
1827
+ if (!token) return null;
1828
+
1829
+ const expiresIn = Number(hash.get("expires_in") || "3600");
1830
+ const session = {
1831
+ access_token: token,
1832
+ refresh_token: hash.get("refresh_token") || "",
1833
+ expires_at: Math.floor(Date.now() / 1000) + expiresIn
1834
+ };
1835
+
1836
+ history.replaceState(null, "", location.pathname);
1837
+ saveSession(session);
1838
+ return session;
1839
+ }
1840
+
1841
+ async function loadConfig() {
1842
+ const response = await fetch("/api/config", { headers: { Accept: "application/json" } });
1843
+ if (!response.ok) throw new Error("Worker config endpoint failed.");
1844
+ state.config = await response.json();
1845
+ }
1846
+
1847
+ async function loadUser() {
1848
+ const response = await fetch(supabaseBase() + "/auth/v1/user", { headers: authHeaders(state.session.access_token) });
1849
+ if (!response.ok) throw new Error("Your saved session expired. Sign in again.");
1850
+ await response.json();
1851
+ saveSession(state.session);
1852
+ }
1853
+
1854
+ async function loadDashboard() {
1855
+ state.data = await api("/api/dashboard");
1856
+ }
1857
+
1858
+ async function api(path, options) {
1859
+ const init = options || {};
1860
+ const headers = Object.assign({ "Content-Type": "application/json" }, init.headers || {});
1861
+ if (state.session && state.session.access_token) headers.Authorization = "Bearer " + state.session.access_token;
1862
+
1863
+ const response = await fetch(path, Object.assign({}, init, { headers: headers }));
1864
+ const text = await response.text();
1865
+ let parsed = null;
1866
+ try { parsed = text ? JSON.parse(text) : null; } catch (_error) { parsed = { message: text }; }
1867
+ if (!response.ok) throw new Error((parsed && (parsed.message || parsed.error)) || "Request failed.");
1868
+ return parsed;
1869
+ }
1870
+
1871
+ function renderTopActions() {
1872
+ if (!state.config || !state.config.supabaseUrl || !state.config.supabaseAnonKey) {
1873
+ actions.innerHTML = '<a class="button primary" href="/setup">Setup guide</a>';
1874
+ return;
1875
+ }
1876
+ if (!state.session) {
1877
+ actions.innerHTML = '<button class="button primary" id="openAuthTop" type="button">Sign in</button>';
1878
+ return;
1879
+ }
1880
+ actions.innerHTML = '<button class="button" id="logout" type="button">Sign out</button>';
1881
+ }
1882
+
1883
+ function render() {
1884
+ if (!state.config || !state.config.supabaseUrl || !state.config.supabaseAnonKey) {
1885
+ renderSetupMissing();
1886
+ return;
1887
+ }
1888
+
1889
+ if (!state.session) {
1890
+ renderAuth();
1891
+ return;
1892
+ }
1893
+
1894
+ if (!state.data) {
1895
+ title.textContent = "Loading";
1896
+ status.textContent = "Fetching MCP workspace";
1897
+ content.innerHTML = '<div class="panel loader-card"><div><div class="dots"><span></span><span></span><span></span></div><h3>Loading data</h3><p class="muted">Loading key inventory.</p></div></div>';
1898
+ return;
1899
+ }
1900
+
1901
+ renderDashboard();
1902
+ renderMessage();
1903
+ }
1904
+
1905
+ function renderTopBar() {
1906
+ renderTopActions();
1907
+ renderMessage();
1908
+ }
1909
+
1910
+ function schemaWarning() {
1911
+ if (!state.data || !state.data.setupWarning) return "";
1912
+ return '<div class="notice error"><strong>Workspace setup required.</strong><br>' + esc(state.data.setupWarning) + "</div>";
1913
+ }
1914
+
1915
+ function maskSecret(value) {
1916
+ if (!value) return "secret hidden";
1917
+ if (String(value).startsWith("ea_live_")) return "ea_live_[hidden]";
1918
+ return "secret_[hidden]";
1919
+ }
1920
+
1921
+ function envRow(name, value, copyable, sensitive) {
1922
+ const visibleValue = sensitive ? maskSecret(value) : value;
1923
+ const title = sensitive ? "" : ' title="' + esc(value) + '"';
1924
+ const valueClass = sensitive ? "env-value is-secret" : "env-value";
1925
+ const copyButton = copyable
1926
+ ? sensitive
1927
+ ? '<button class="tiny-copy" type="button" data-copy-secret="api-key">Copy</button>'
1928
+ : '<button class="tiny-copy" type="button" data-copy-value="' + esc(value) + '">Copy</button>'
1929
+ : "";
1930
+ return '<div class="env-row' + (sensitive ? ' secret-row' : '') + '"><div><div class="env-name">' + esc(name) + '</div><div class="' + valueClass + '"' + title + '>' + esc(visibleValue) + '</div></div>' + copyButton + '</div>';
1931
+ }
1932
+
1933
+ function renderGeneratedEnv(hasKey) {
1934
+ return envRow("ERRORATLAS_API_URL", location.origin, false, false)
1935
+ + (state.generatedKey && state.generatedKey.key
1936
+ ? '<div class="env-row secret-row"><div><div class="env-name">ERRORATLAS_API_KEY</div><div class="env-value is-secret">copied_from_secret_button</div></div></div>'
1937
+ : '<div class="env-row secret-row"><div><div class="env-name">ERRORATLAS_API_KEY</div><div class="env-value is-secret">' + (hasKey ? 'rotate_to_copy_new_secret' : 'generate_secret_first') + '</div></div></div>')
1938
+ + envRow("SUPERMEMORY_URL", "http://localhost:6767", false, false);
1939
+ }
1940
+
1941
+ function renderAgentKeys(keys) {
1942
+ if (!keys || !keys.length) {
1943
+ return '<div class="key-status"><div class="status-line"><div class="status-label">Status</div><div class="status-value">No key generated</div></div><div class="status-line"><div class="status-label">Secret</div><div class="status-value">Ready after generation</div></div></div>';
1944
+ }
1945
+ const key = keys[0];
1946
+ const created = key.created_at ? new Date(key.created_at).toLocaleDateString() : "unknown date";
1947
+ return '<div class="key-status"><div class="status-line"><div class="status-label">Status</div><div class="status-value">Active</div></div><div class="status-line"><div class="status-label">Created</div><div class="status-value">' + esc(created) + '</div></div><div class="status-line"><div class="status-label">Secret</div><div class="status-value">Hidden after creation</div></div></div>';
1948
+ }
1949
+
1950
+ function renderSecretStrip(hasKey) {
1951
+ if (state.generatedKey && state.generatedKey.key) {
1952
+ return '<div class="secret-strip is-ready"><div><strong>Secret generated</strong><span>Copy it now. The raw key is held only in this browser session.</span></div><div class="secret-copy-row"><span class="secret-token">ea_live_[hidden]</span><button class="button primary" type="button" data-copy-secret="api-key">Copy secret</button></div></div>';
1953
+ }
1954
+
1955
+ if (hasKey) {
1956
+ return '<div class="secret-strip"><div><strong>Secret already exists</strong><span>Existing raw keys are not recoverable. Rotate the secret to generate a new one-time copy.</span></div></div>';
1957
+ }
1958
+
1959
+ return '<div class="secret-strip"><div><strong>No secret yet</strong><span>Generate your first MCP secret. The copy button appears immediately after generation.</span></div></div>';
1960
+ }
1961
+
1962
+ function renderDashboard() {
1963
+ title.textContent = "MCP access";
1964
+ status.textContent = "One workspace secret";
1965
+ const hasKey = !!(state.data.agentKeys && state.data.agentKeys.length);
1966
+ const actionLabel = hasKey ? "Rotate secret" : "Generate secret";
1967
+ const note = state.generatedKey
1968
+ ? "Secret is ready for clipboard copy."
1969
+ : hasKey
1970
+ ? "You already have one active key. Rotate only when you need a new secret."
1971
+ : "First-time setup starts here. Generate one secret, then paste it into your MCP config.";
1972
+
1973
+ content.innerHTML = schemaWarning() +
1974
+ '<div class="key-console">' +
1975
+ '<section class="key-pane"><div class="key-head"><div><div class="key-kicker">Workspace key</div><h2 class="key-title">Connect one agent secret.</h2><p class="key-desc">ErrorAtlas keeps one active MCP secret per workspace. Generating a new secret revokes the old one immediately.</p></div></div>' +
1976
+ '<div class="key-actions"><button class="button primary" id="createKey" type="button">' + actionLabel + '</button></div>' +
1977
+ '<p id="newKey" class="key-note">' + esc(note) + '</p>' +
1978
+ '<div id="agentKeyList">' + renderAgentKeys(state.data.agentKeys) + '</div></section>' +
1979
+ '<section class="key-pane"><div class="key-kicker">MCP environment</div><h2 class="key-title">Paste into your agent.</h2><p class="key-desc">The API key stays masked in the UI. Copy is only available right after generation or rotation.</p>' +
1980
+ renderSecretStrip(hasKey) +
1981
+ '<div id="mcpEnv" class="env-table">' + renderGeneratedEnv(hasKey) + '</div></section>' +
1982
+ '</div>';
1983
+ }
1984
+
1985
+ function renderAuth() {
1986
+ title.textContent = "Sign in";
1987
+ status.textContent = "Protected console";
1988
+ content.innerHTML = '<div class="panel auth-open-card"><span class="muted">Sign in to mint MCP keys</span><h3>Console access</h3><p>Use GitHub or a one-time email link. New operators are created automatically.</p></div><div class="auth-modal" id="authModal" hidden><div class="auth-dialog" role="dialog" aria-modal="true" aria-labelledby="authTitle"><div class="auth-dialog-head"><div><h3 id="authTitle">Enter ErrorAtlas</h3><p>Pick an auth method.</p></div><button class="icon-button" id="closeAuthModal" type="button" aria-label="Close sign in dialog">×</button></div><div class="auth-methods"><button class="button primary" id="githubSignIn" type="button">Continue with GitHub</button><div class="auth-divider">or</div><input id="authEmail" type="email" placeholder="you@example.com" aria-label="Email address"><button class="button" id="sendEmail" type="button">Send magic link</button></div></div></div>';
1989
+ }
1990
+
1991
+ function renderSetupMissing() {
1992
+ title.textContent = "Setup";
1993
+ status.textContent = "Worker setup required";
1994
+ const missing = state.config && state.config.missing && state.config.missing.length ? "Workspace configuration is incomplete." : "Workspace configuration is unavailable.";
1995
+ content.innerHTML = '<div class="panel"><h3>Workspace is not ready</h3><p>The dashboard is online, but workspace config is not complete.</p><div class="notice error">' + esc(missing) + '</div><a class="button primary" href="/setup">Open setup guide</a></div>';
1996
+ }
1997
+
1998
+ async function signInWithGitHub() {
1999
+ if (!state.config || !state.config.supabaseUrl) return setMessage("Supabase URL is missing.");
2000
+ const redirectTo = location.origin + "/dashboard";
2001
+ location.assign(supabaseBase() + "/auth/v1/authorize?provider=github&redirect_to=" + encodeURIComponent(redirectTo));
2002
+ }
2003
+
2004
+ async function sendMagicLink(email) {
2005
+ if (!email) return setMessage("Enter an email address first.");
2006
+ const redirectTo = location.origin + "/dashboard";
2007
+ const response = await fetch(supabaseBase() + "/auth/v1/otp?redirect_to=" + encodeURIComponent(redirectTo), {
2008
+ method: "POST",
2009
+ headers: authHeaders(),
2010
+ body: JSON.stringify({ email: email, create_user: true })
2011
+ });
2012
+ if (!response.ok) throw new Error(await response.text());
2013
+ setMessage("Magic link sent. Check your inbox for " + email + ".", "success");
2014
+ }
2015
+
2016
+ function openAuthModal() {
2017
+ const modal = document.getElementById("authModal");
2018
+ if (!modal) return;
2019
+ modal.hidden = false;
2020
+ const input = document.getElementById("authEmail");
2021
+ if (input && input.focus) input.focus();
2022
+ }
2023
+
2024
+ function closeAuthModal() {
2025
+ const modal = document.getElementById("authModal");
2026
+ if (modal) modal.hidden = true;
2027
+ }
2028
+
2029
+ async function createKey(button) {
2030
+ button.disabled = true;
2031
+ button.textContent = "Working";
2032
+ try {
2033
+ const result = await api("/api/agent-keys", { method: "POST", body: JSON.stringify({}) });
2034
+ const key = result && result.key ? result.key : "";
2035
+ if (!key) throw new Error("API did not return the raw key.");
2036
+ state.generatedKey = { key };
2037
+ state.message = "";
2038
+ await loadDashboard();
2039
+ render();
2040
+ renderTopBar();
2041
+ } catch (error) {
2042
+ setMessage("Agent key creation failed: " + (error instanceof Error ? error.message : "Unknown error."));
2043
+ } finally {
2044
+ button.disabled = false;
2045
+ button.textContent = state.data && state.data.agentKeys && state.data.agentKeys.length ? "Rotate secret" : "Generate secret";
2046
+ }
2047
+ }
2048
+
2049
+ async function rotateKey(id, button) {
2050
+ if (!confirm("Rotate this key? The old secret stops working immediately.")) return;
2051
+ button.disabled = true;
2052
+ button.textContent = "Rotating";
2053
+ try {
2054
+ const result = await api("/api/agent-keys/" + encodeURIComponent(id) + "/rotate", { method: "POST" });
2055
+ if (!result || !result.key) throw new Error("API did not return the new raw key.");
2056
+ state.generatedKey = { key: result.key };
2057
+ state.message = "";
2058
+ await loadDashboard();
2059
+ render();
2060
+ renderTopBar();
2061
+ } catch (error) {
2062
+ setMessage("Key rotation failed: " + (error instanceof Error ? error.message : "Unknown error."));
2063
+ } finally {
2064
+ button.disabled = false;
2065
+ button.textContent = "Rotate secret";
2066
+ }
2067
+ }
2068
+
2069
+ document.addEventListener("click", async function (event) {
2070
+ let target = event.target;
2071
+ if (!(target instanceof HTMLElement)) return;
2072
+ const control = target.closest("button, a");
2073
+ if (control instanceof HTMLElement) target = control;
2074
+
2075
+ if (target.id === "openAuthTop") {
2076
+ openAuthModal();
2077
+ return;
2078
+ }
2079
+
2080
+ if (target.id === "closeAuthModal" || target.id === "authModal") {
2081
+ closeAuthModal();
2082
+ return;
2083
+ }
2084
+
2085
+ if (target.id === "githubSignIn") {
2086
+ await signInWithGitHub();
2087
+ return;
2088
+ }
2089
+
2090
+ if (target.id === "sendEmail") {
2091
+ const input = document.getElementById("authEmail");
2092
+ const email = input && input.value ? input.value.trim() : "";
2093
+ try { await sendMagicLink(email); } catch (error) { setMessage("Email sign-in failed: " + (error instanceof Error ? error.message : "Unknown error.")); }
2094
+ return;
2095
+ }
2096
+
2097
+ if (target.id === "logout") {
2098
+ saveSession(null);
2099
+ state.session = null;
2100
+ state.data = null;
2101
+ setMessage("Signed out.", "success");
2102
+ render();
2103
+ renderTopBar();
2104
+ return;
2105
+ }
2106
+
2107
+ const copySecret = target.getAttribute("data-copy-secret");
2108
+ if (copySecret === "api-key") {
2109
+ const secret = state.generatedKey && state.generatedKey.key ? state.generatedKey.key : "";
2110
+ if (!secret) {
2111
+ setMessage("Generate or rotate a secret first. Existing raw secrets cannot be recovered.");
2112
+ return;
2113
+ }
2114
+ const idleLabel = target.textContent || "Copy secret";
2115
+ try {
2116
+ await navigator.clipboard.writeText(secret);
2117
+ target.textContent = "Copied";
2118
+ setTimeout(function () { target.textContent = idleLabel; }, 1400);
2119
+ } catch (_error) {
2120
+ target.textContent = "Select";
2121
+ setTimeout(function () { target.textContent = idleLabel; }, 1400);
2122
+ }
2123
+ return;
2124
+ }
2125
+
2126
+ const copyValue = target.getAttribute("data-copy-value");
2127
+ if (copyValue) {
2128
+ const idleLabel = target.textContent || "Copy value";
2129
+ try {
2130
+ await navigator.clipboard.writeText(copyValue);
2131
+ target.textContent = "Copied";
2132
+ setTimeout(function () { target.textContent = idleLabel; }, 1400);
2133
+ } catch (_error) {
2134
+ target.textContent = "Select";
2135
+ setTimeout(function () { target.textContent = idleLabel; }, 1400);
2136
+ }
2137
+ return;
2138
+ }
2139
+
2140
+ if (target.id === "createKey") {
2141
+ await createKey(target);
2142
+ renderTopBar();
2143
+ return;
2144
+ }
2145
+
2146
+ const rotateId = target.getAttribute("data-rotate-key");
2147
+ if (rotateId) {
2148
+ await rotateKey(rotateId, target);
2149
+ renderTopBar();
2150
+ return;
2151
+ }
2152
+ });
2153
+
2154
+ document.addEventListener("keydown", async function (event) {
2155
+ const target = event.target;
2156
+ if (!(target instanceof HTMLElement)) return;
2157
+ if (event.key === "Escape") closeAuthModal();
2158
+ if (event.key === "Enter" && target.id === "authEmail") {
2159
+ const input = document.getElementById("authEmail");
2160
+ const email = input && input.value ? input.value.trim() : "";
2161
+ try { await sendMagicLink(email); } catch (error) { setMessage("Email sign-in failed: " + (error instanceof Error ? error.message : "Unknown error.")); }
2162
+ }
2163
+ });
2164
+
2165
+ async function boot() {
2166
+ try {
2167
+ await loadConfig();
2168
+ } catch (error) {
2169
+ state.config = { missing: ["PUBLIC_SUPABASE_URL", "PUBLIC_SUPABASE_ANON_KEY"] };
2170
+ setMessage(error instanceof Error ? error.message : "Unable to load config.");
2171
+ render();
2172
+ renderTopBar();
2173
+ return;
2174
+ }
2175
+
2176
+ state.session = sessionFromHash() || readSession();
2177
+ if (!state.config.supabaseUrl || !state.config.supabaseAnonKey || !state.session) {
2178
+ renderTopBar();
2179
+ render();
2180
+ return;
2181
+ }
2182
+
2183
+ try {
2184
+ await loadUser();
2185
+ await loadDashboard();
2186
+ renderTopBar();
2187
+ render();
2188
+ } catch (error) {
2189
+ saveSession(null);
2190
+ state.session = null;
2191
+ state.data = null;
2192
+ setMessage(error instanceof Error ? error.message : "Dashboard failed to load.");
2193
+ renderTopBar();
2194
+ render();
2195
+ }
2196
+ }
2197
+
2198
+ renderTopBar();
2199
+ render();
2200
+ boot();
2201
+ })();
2202
+ </script>
2203
+ </body>
2204
+ </html>`;
2205
+ }
2206
+ export function renderLlmsTxt(origin) {
2207
+ return `# ErrorAtlas
2208
+
2209
+ ErrorAtlas is a debugging memory layer for coding agents. Agents use the MCP server, not the dashboard UI.
2210
+
2211
+ ## Product Boundary
2212
+
2213
+ - Humans use ${origin}/dashboard only to sign in and generate or rotate one MCP secret.
2214
+ - Agents use the local \`erroratlas\` MCP server.
2215
+ - Supermemory local stores private project memory on the developer machine.
2216
+ - ErrorAtlas remote stores sanitized searchable incidents, playbooks, audit events, hashed keys, and encrypted payload snapshots.
2217
+ - Developers do not clone the ErrorAtlas repo, create Supabase projects, deploy Workers, or manage service-owner infrastructure.
2218
+
2219
+ ## Public Endpoints
2220
+
2221
+ - ${origin}/setup
2222
+ - ${origin}/api/search
2223
+ - ${origin}/api/playbooks
2224
+ - ${origin}/llms.txt
2225
+
2226
+ ## Beginner Setup
2227
+
2228
+ 1. Open the dashboard and generate one MCP secret:
2229
+
2230
+ ${origin}/dashboard
2231
+
2232
+ 2. Install ErrorAtlas in the project:
2233
+
2234
+ npm install -D @sansynx/erroratlas
2235
+ npx erroratlas init
2236
+
2237
+ 3. Run Supermemory local before starting the agent. Supermemory requires one model-provider key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, or GROQ_API_KEY:
2238
+
2239
+ OPENAI_API_KEY=your_model_provider_key
2240
+ npx supermemory local --port 6767
2241
+
2242
+ Windows/WSL note: if the npm wrapper installs the binary under bash home but cannot find it from Windows, start it from bash inside the project:
2243
+
2244
+ export OPENAI_API_KEY=your_model_provider_key
2245
+ PORT=6767 ~/.supermemory/bin/supermemory-server
2246
+
2247
+ 4. Configure the agent MCP server:
2248
+
2249
+ {
2250
+ "mcpServers": {
2251
+ "erroratlas": {
2252
+ "command": "npx",
2253
+ "args": ["-y", "--package", "@sansynx/erroratlas", "erroratlas", "mcp"],
2254
+ "env": {
2255
+ "ERRORATLAS_API_URL": "${origin}",
2256
+ "ERRORATLAS_API_KEY": "ea_live_from_dashboard",
2257
+ "SUPERMEMORY_URL": "http://localhost:6767"
2258
+ }
2259
+ }
2260
+ }
2261
+ }
2262
+
2263
+ 5. If Supermemory prints a local API key, also pass:
2264
+
2265
+ SUPERMEMORY_API_KEY=optional_local_supermemory_key
2266
+
2267
+ ## MCP Tools
2268
+
2269
+ - erroratlas.search_error
2270
+ - erroratlas.capture_error_signal
2271
+ - erroratlas.record_incident
2272
+ - erroratlas.publish_resolution
2273
+ - erroratlas.get_playbook
2274
+
2275
+ ## Agent Protocol
2276
+
2277
+ - Before fixing a non-trivial error, call \`search_error\` with exact error text, stack summary, command, exit code, language, framework, package manager, and dependency versions when available.
2278
+ - If the failure is useful debugging signal, call \`capture_error_signal\`.
2279
+ - After a verified fix, call \`publish_resolution\` with root cause, failed attempts, final fix, verification, risk, and confidence.
2280
+ - Do not send secrets, customer data, private URLs, full source files, or unnecessary absolute local paths.
2281
+ - Respect repository instructions first. ErrorAtlas does not override AGENTS.md, CLAUDE.md, Cursor rules, Windsurf rules, or project policy.
2282
+
2283
+ ## Tested Local Endpoints
2284
+
2285
+ - MCP writes local memory to Supermemory with \`POST /v3/documents\`.
2286
+ - MCP searches local memory with \`POST /v3/search\`.
2287
+ - MCP calls ErrorAtlas remote search with \`POST /api/search\`.
2288
+ - MCP records remote incidents with \`POST /api/incidents\`.
2289
+ - MCP publishes remote playbooks with \`POST /api/resolutions\`.
2290
+
2291
+ ## Storage
2292
+
2293
+ Local Supermemory data is stored in the developer project's \`.supermemory\` directory. Keep that directory out of git.
2294
+ `;
2295
+ }