vault-go 0.11.0 → 0.13.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.
@@ -1,8 +1,11 @@
1
1
  /** Local monitor: no secrets or cloud content are interpolated into this document. */
2
2
  export function dashboardHtml() {
3
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>Vault · Local</title><style>
4
- :root{font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eaf0f1;background:#080b10;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(ellipse at 80% 0%,#17363c66,transparent 50%),#080b10}button,input,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid #88ebd4;outline-offset:4px}button:disabled{opacity:.5;cursor:wait}.shell{max-width:1440px;margin:auto;display:grid;grid-template-columns:228px 1fr;min-height:100vh}.rail{border-right:1px solid #ffffff0c;padding:34px 22px;background:#0b1018a6}.brand{display:flex;align-items:center;gap:12px;font-size:24px;font-weight:660;letter-spacing:-1px}.brand svg{width:34px;height:34px}.badge{font-size:10px;letter-spacing:1.4px;color:#8edbc8;border:1px solid #91dbc32b;border-radius:5px;padding:4px 6px;margin-left:2px}.rail-label{margin:52px 12px 15px;font-size:10px;color:#6f7f91;letter-spacing:1.7px}.nav{display:flex;flex-direction:column;gap:7px}.nav a{display:flex;align-items:center;gap:12px;color:#8c9caf;text-decoration:none;padding:12px;border-radius:9px;font-size:13px}.nav a:hover,.nav a.active{background:#90e4cf0c;color:#a2eedb}.nav .icon{width:20px;font-size:18px}.rail-footer{margin-top:70px;color:#637388;font-size:11px;line-height:1.8;padding:12px}.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#7ce0b8;margin-right:7px}.main{padding:29px 42px 50px;min-width:0}.top{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #ffffff0c;padding-bottom:24px;gap:18px}.breadcrumb{font-size:12px;color:#738398}.breadcrumb b{font-weight:400;color:#c2ccd7}.controls{display:flex;align-items:center;gap:12px}select,.button{color:#c7d4df;border:1px solid #ffffff17;background:#141d28;border-radius:8px;padding:9px 12px;font-size:12px}.button{display:inline-flex;align-items:center;gap:8px}.button:hover{border-color:#79d8c05c}.welcome{display:flex;justify-content:space-between;align-items:center;margin:36px 0 26px;gap:20px}.eyebrow{font-size:10px;letter-spacing:2px;color:#79cbb6;margin-bottom:12px}h1{font-size:30px;letter-spacing:-1.1px;font-weight:550;margin:0 0 10px}p{color:#8494a7;font-size:13px;line-height:1.7;margin:0}.pulse{width:48px;height:48px;display:grid;place-items:center;background:#9af1d30a;border:1px solid #99eace20;border-radius:15px;color:#94e6ce;font-size:23px;animation:arrive .7s ease both}.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.card,.panel{border:1px solid #ffffff11;background:linear-gradient(140deg,#1a263172,#101721aa);border-radius:14px;box-shadow:0 15px 40px #0000000a}.card{padding:22px}.card-label{font-size:11px;color:#8e9eb0;display:flex;justify-content:space-between}.card-value{font-size:23px;letter-spacing:-.6px;margin:17px 0 9px;font-weight:550;overflow-wrap:anywhere}.card-note{font-size:11px;color:#708297}.good{color:#9ce9cc}.warning{color:#efc489}.layout{display:grid;grid-template-columns:1.3fr 1fr;gap:20px;margin-top:24px}.panel{padding:23px;min-width:0;scroll-margin-top:20px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}h2{font-size:14px;font-weight:550;margin:0;letter-spacing:-.2px}.count{color:#76899e;font-size:11px;border:1px solid #ffffff13;padding:3px 7px;border-radius:5px}.search{width:100%;background:#070e1680;border:1px solid #ffffff12;color:#d8e3ec;border-radius:8px;padding:10px 12px;margin-bottom:14px;font-size:12px}.empty{min-height:162px;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:22px 10px;gap:10px}.empty-symbol{width:34px;height:34px;border:1px solid #ffffff15;border-radius:10px;display:grid;place-items:center;color:#698a96}.empty p{font-size:12px;max-width:280px}.row{padding:13px 0;border-top:1px solid #ffffff0b;display:flex;gap:12px;align-items:flex-start}.row:first-child{border-top:0}.row-marker{width:7px;height:7px;background:#8bddbd;border-radius:50%;margin-top:6px;flex-shrink:0}.row-marker.failed{background:#dfb483}.row-body{flex:1;min-width:0}.row-title{font-size:12px;color:#cbd8e4;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.row-meta{font-size:10px;color:#6f839a;margin-top:4px;overflow-wrap:anywhere}.row-time{font-size:10px;color:#7d8ea1;white-space:nowrap;margin-top:4px}.wide{grid-column:1/-1}.footer{display:flex;justify-content:space-between;margin-top:23px;gap:15px;font-size:10px;color:#586c82;line-height:1.6}.notice{padding:17px 20px;background:#172c31;border:1px solid #93e3ce30;border-radius:10px;margin-bottom:22px}.notice p{color:#c0d5d3}.notice code{display:block;color:#a2efcf;font-size:12px;margin-top:9px;overflow-wrap:anywhere}.notice[hidden]{display:none}.error{color:#e7bd8a;font-size:12px;line-height:1.6;margin:10px 0;overflow-wrap:anywhere}.error:empty{display:none}@keyframes arrive{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.cards,.layout{animation:arrive .6s ease both}.layout{animation-delay:.08s}@media(prefers-reduced-motion:reduce){*{animation:none!important;scroll-behavior:auto!important}}@media(max-width:1000px){.shell{grid-template-columns:180px 1fr}.rail{padding:28px 15px}.main{padding:28px 25px}.cards{gap:10px}.card{padding:17px}.card-value{font-size:20px}.layout{grid-template-columns:1fr}.wide{grid-column:auto}}@media(max-width:680px){.shell{display:block}.rail{padding:18px 20px;border-right:0;border-bottom:1px solid #ffffff0c}.brand{font-size:21px}.rail-label,.rail-footer{display:none}.nav{flex-direction:row;margin-top:18px;gap:3px;overflow:auto}.nav a{padding:8px;font-size:11px;white-space:nowrap;gap:5px}.nav .icon{font-size:14px;width:15px}.main{padding:20px}.top{padding-bottom:18px}.breadcrumb{display:none}.controls{width:100%;justify-content:space-between}.welcome{margin-top:28px}h1{font-size:26px}.cards{grid-template-columns:1fr}.card{padding:18px;display:grid;grid-template-columns:1fr 1fr;align-items:center}.card-value{margin:0;text-align:right;font-size:20px}.card-note{margin-top:9px;grid-column:1/-1}.panel{padding:18px}.footer{flex-direction:column;gap:4px}}
5
- </style><script src="/app.js" defer></script></head><body><div class="shell"><aside class="rail"><div class="brand"><svg viewBox="0 0 40 40" aria-hidden="true"><defs><linearGradient id="v" x2="1" y2="1"><stop stop-color="#ccfbea"/><stop offset="1" stop-color="#65b9a8"/></linearGradient></defs><path d="M5 8h8l7 18L27 8h8L23 34h-6z" fill="url(#v)"/><path d="m17 8 3 8 3-8" fill="#82baac" opacity=".4"/></svg>vault<span class="badge">LOCAL</span></div><div class="rail-label" data-i18n="workspace">WORKSPACE</div><nav class="nav" aria-label="Navigation"><a class="active" href="#overview"><span class="icon">◈</span><span data-i18n="overview">Overview</span></a><a href="#activity"><span class="icon">⌁</span><span data-i18n="activity">Activity</span></a><a href="#memories"><span class="icon">▤</span><span data-i18n="memories">Memories</span></a><a href="#projects"><span class="icon">▱</span><span data-i18n="projects">Projects</span></a></nav><div class="rail-footer"><span class="dot"></span><span data-i18n="localWorkspace">Your local workspace</span><br><span data-i18n="privacy">Activity metadata stays local.</span></div></aside><main class="main" id="overview"><header class="top"><div class="breadcrumb">Vault <span aria-hidden="true"> / </span><b data-i18n="localMonitor">Local monitor</b></div><div class="controls"><select id="locale" aria-label="Language"><option value="en">English</option><option value="pt">Português</option><option value="es">Español</option></select><button class="button" id="refresh" type="button"><span aria-hidden="true">↻</span><span data-i18n="refresh">Refresh cloud</span></button></div></header><div class="welcome"><div><div class="eyebrow" data-i18n="eyebrow">YOUR KNOWLEDGE, CONNECTED</div><h1 data-i18n="title">A little more context.</h1><p data-i18n="subtitle">A clear view of your local Vault and cloud connection.</p></div><div class="pulse" aria-hidden="true">⌁</div></div><div id="session-notice" class="notice" hidden><p data-i18n="session">Open a secure local session from your terminal:</p><code>bunx --bun vault-go@latest local open</code></div><div id="error" class="error" role="status" aria-live="polite"></div><section class="cards" aria-label="Status"><article class="card"><div class="card-label"><span data-i18n="localService">Local service</span><span aria-hidden="true">◈</span></div><div class="card-value" id="local-status">—</div><div class="card-note" id="local-note">localhost:38850</div></article><article class="card"><div class="card-label"><span data-i18n="cloudConnection">Cloud connection</span><span aria-hidden="true">↗</span></div><div class="card-value" id="cloud-status">—</div><div class="card-note" id="cloud-note">—</div></article><article class="card"><div class="card-label"><span data-i18n="recentOperations">Recent operations</span><span aria-hidden="true">⌁</span></div><div class="card-value" id="activity-total">—</div><div class="card-note" data-i18n="operationsNote">Recent MCP request metadata</div></article></section><div class="layout"><section class="panel" id="activity"><div class="panel-head"><h2 data-i18n="activity">Activity</h2><span class="count" id="activity-count">0</span></div><div id="activity-list"></div></section><section class="panel" id="memories"><div class="panel-head"><h2 data-i18n="memories">Memories</h2><span class="count" id="memory-count">0</span></div><input class="search" id="memory-search" type="search" data-placeholder="searchMemories" aria-label="Search memories"><div id="memory-list"></div></section><section class="panel wide" id="projects"><div class="panel-head"><h2 data-i18n="projects">Projects</h2><span class="count" id="project-count">0</span></div><input class="search" id="project-search" type="search" data-placeholder="searchProjects" aria-label="Search projects"><div id="project-list"></div></section></div><footer class="footer"><span data-i18n="footer">Vault Local · Context with a place to call home.</span><span id="updated" aria-live="off">—</span></footer></main></div></body></html>`;
3
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>AI Vault Memory · Local</title><style>
4
+ :root{font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eaf0f1;background:#080b10;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(ellipse at 80% 0%,#17363c66,transparent 50%),#080b10}button,input,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid #88ebd4;outline-offset:4px}button:disabled{opacity:.5;cursor:wait}.shell{max-width:1440px;margin:auto;display:grid;grid-template-columns:228px 1fr;min-height:100vh}.rail{border-right:1px solid #ffffff0c;padding:34px 22px;background:#0b1018a6}.brand{display:flex;align-items:center;gap:12px;font-size:24px;font-weight:660;letter-spacing:-1px}.brand svg{width:34px;height:34px}.badge{font-size:10px;letter-spacing:1.4px;color:#8edbc8;border:1px solid #91dbc32b;border-radius:5px;padding:4px 6px;margin-left:2px}.rail-label{margin:52px 12px 15px;font-size:10px;color:#6f7f91;letter-spacing:1.7px}.nav{display:flex;flex-direction:column;gap:7px}.nav a{display:flex;align-items:center;gap:12px;color:#8c9caf;text-decoration:none;padding:12px;border-radius:9px;font-size:13px}.nav a:hover,.nav a.active{background:#90e4cf0c;color:#a2eedb}.nav .icon{width:20px;font-size:18px}.rail-footer{margin-top:70px;color:#637388;font-size:11px;line-height:1.8;padding:12px}.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#7ce0b8;margin-right:7px}.main{padding:29px 42px 50px;min-width:0}.top{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #ffffff0c;padding-bottom:24px;gap:18px}.breadcrumb{font-size:12px;color:#738398}.breadcrumb b{font-weight:400;color:#c2ccd7}.controls{display:flex;align-items:center;gap:12px}select,.button{color:#c7d4df;border:1px solid #ffffff17;background:#141d28;border-radius:8px;padding:9px 12px;font-size:12px}.button{display:inline-flex;align-items:center;gap:8px}.button:hover{border-color:#79d8c05c}.welcome{display:flex;justify-content:space-between;align-items:center;margin:36px 0 26px;gap:20px}.eyebrow{font-size:10px;letter-spacing:2px;color:#79cbb6;margin-bottom:12px}h1{font-size:30px;letter-spacing:-1.1px;font-weight:550;margin:0 0 10px}p{color:#8494a7;font-size:13px;line-height:1.7;margin:0}.pulse{width:48px;height:48px;display:grid;place-items:center;background:#9af1d30a;border:1px solid #99eace20;border-radius:15px;color:#94e6ce;font-size:23px;animation:arrive .7s ease both}.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}.card,.panel{border:1px solid #ffffff11;background:linear-gradient(140deg,#1a263172,#101721aa);border-radius:14px;box-shadow:0 15px 40px #0000000a}.card{padding:22px}.card-label{font-size:11px;color:#8e9eb0;display:flex;justify-content:space-between}.card-value{font-size:23px;letter-spacing:-.6px;margin:17px 0 9px;font-weight:550;overflow-wrap:anywhere}.card-note{font-size:11px;color:#708297}.good{color:#9ce9cc}.warning{color:#efc489}.layout{display:grid;grid-template-columns:1.3fr 1fr;gap:20px;margin-top:24px}.panel{padding:23px;min-width:0;scroll-margin-top:20px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}h2{font-size:14px;font-weight:550;margin:0;letter-spacing:-.2px}.count{color:#76899e;font-size:11px;border:1px solid #ffffff13;padding:3px 7px;border-radius:5px}.search{width:100%;background:#070e1680;border:1px solid #ffffff12;color:#d8e3ec;border-radius:8px;padding:10px 12px;margin-bottom:14px;font-size:12px}.empty{min-height:162px;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:22px 10px;gap:10px}.empty-symbol{width:34px;height:34px;border:1px solid #ffffff15;border-radius:10px;display:grid;place-items:center;color:#698a96}.empty p{font-size:12px;max-width:280px}.row{padding:13px 0;border-top:1px solid #ffffff0b;display:flex;gap:12px;align-items:flex-start}.row:first-child{border-top:0}.row-marker{width:7px;height:7px;background:#8bddbd;border-radius:50%;margin-top:6px;flex-shrink:0}.row-marker.failed{background:#dfb483}.row-body{flex:1;min-width:0}.row-title{font-size:12px;color:#cbd8e4;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.row-meta{font-size:10px;color:#6f839a;margin-top:4px;overflow-wrap:anywhere}.row-time{font-size:10px;color:#7d8ea1;white-space:nowrap;margin-top:4px}.wide{grid-column:1/-1}.footer{display:flex;justify-content:space-between;margin-top:23px;gap:15px;font-size:10px;color:#586c82;line-height:1.6}.notice{padding:17px 20px;background:#172c31;border:1px solid #93e3ce30;border-radius:10px;margin-bottom:22px}.notice p{color:#c0d5d3}.notice code{display:block;color:#a2efcf;font-size:12px;margin-top:9px;overflow-wrap:anywhere}.notice[hidden]{display:none}.error{color:#e7bd8a;font-size:12px;line-height:1.6;margin:10px 0;overflow-wrap:anywhere}.error:empty{display:none}@keyframes arrive{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.cards,.layout{animation:arrive .6s ease both}.layout{animation-delay:.08s}@media(prefers-reduced-motion:reduce){*{animation:none!important;scroll-behavior:auto!important}}@media(max-width:1000px){.shell{grid-template-columns:180px 1fr}.rail{padding:28px 15px}.main{padding:28px 25px}.cards{gap:10px}.card{padding:17px}.card-value{font-size:20px}.layout{grid-template-columns:1fr}.wide{grid-column:auto}}@media(max-width:680px){.shell{display:block}.rail{padding:18px 20px;border-right:0;border-bottom:1px solid #ffffff0c}.brand{font-size:21px}.rail-label,.rail-footer{display:none}.nav{flex-direction:row;margin-top:18px;gap:3px;overflow:auto}.nav a{padding:8px;font-size:11px;white-space:nowrap;gap:5px}.nav .icon{font-size:14px;width:15px}.main{padding:20px}.top{padding-bottom:18px}.breadcrumb{display:none}.controls{width:100%;justify-content:space-between;flex-wrap:wrap}.welcome{margin-top:28px}h1{font-size:26px}.cards{grid-template-columns:1fr}.card{padding:18px;display:grid;grid-template-columns:1fr 1fr;align-items:center}.card-value{margin:0;text-align:right;font-size:20px}.card-note{margin-top:9px;grid-column:1/-1}.panel{padding:18px}.footer{flex-direction:column;gap:4px}}
5
+
6
+ /* Same identity tokens and glass surfaces as the Vault portal. */
7
+ :root{--c-canvas:8 11 18;--c-surface:24 29 45;--c-surface-2:32 38 56;--c-line:53 61 81;--c-fg:241 243 252;--c-muted:172 180 203;--c-faint:148 159 187;--c-accent:185 182 255;color:rgb(var(--c-fg));background:rgb(var(--c-canvas))}body{background:radial-gradient(ellipse at 85% 8%,#534ca02e,transparent 45%),linear-gradient(145deg,#080b12,#101321 58%,#080b12)}[hidden]{display:none!important}.shell{grid-template-columns:258px 1fr;max-width:1600px}.rail{background:linear-gradient(160deg,#1c2135f5,#101522f5);border-color:#cdd8ff26}.brand{font-size:15px;letter-spacing:-.3px;gap:10px;white-space:nowrap}.brand img,.onboarding-logo{object-fit:contain;filter:drop-shadow(0 5px 12px #7775c533)}.brand-sub{display:block;font-size:9px;letter-spacing:1.7px;color:#acb4cb;margin-top:7px}.card,.panel,.onboarding,.step,.notice{background:linear-gradient(135deg,#272c3ed1,#131724e0);border-color:#cdd8ff26;box-shadow:0 24px 70px -30px #0009,inset 0 1px 0 #ffffff0c;backdrop-filter:blur(28px) saturate(135%)}.nav a.active,.nav a:hover{background:#b9b6ff20;color:#d3d1ff}.eyebrow,.step-number,.empty-symbol{color:#b9b6ff}p,.card-label{color:#acb4cb}.card-note,.row-meta,.rail-footer,.rail-label,.footer{color:#949fbb}.pulse{color:#b9b6ff;border-color:#b9b6ff30;background:#b9b6ff0d}.button{background:#202638;color:#f1f3fc;border-color:#cdd8ff26;text-decoration:none;justify-content:center}.button:hover{border-color:#b9b6ff88}.primary{background:linear-gradient(135deg,#c8c5ff,#9d9bed);color:#15152f;border-color:transparent;font-weight:650;padding:13px 20px;font-size:14px}.primary:hover{background:#d3d1ff}.onboarding{border:1px solid #cdd8ff26;border-radius:20px;overflow:hidden;margin-top:30px;animation:arrive .6s ease both}.onboarding-hero{position:relative;padding:38px;background:radial-gradient(ellipse at 100% 0%,#7370d02c,transparent 62%)}.onboarding-logo{width:86px;height:86px;margin-bottom:24px}.onboarding h1{font-size:36px;max-width:600px;line-height:1.12;margin:12px 0 18px}.onboarding-hero>p{max-width:570px;font-size:14px;line-height:1.8}.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:25px}.link{color:#d3d1ff;text-decoration:none;font-size:12px}.link:hover{text-decoration:underline}.onboarding-hint{font-size:11px!important;color:#949fbb;margin-top:15px!important}.steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-top:1px solid #cdd8ff26}.step{padding:25px;background:#080b122b;box-shadow:none;border-right:1px solid #cdd8ff16}.step:last-child{border:0}.step-number{font-family:monospace;font-size:11px;margin-bottom:13px}.step h2{margin-bottom:9px}.step p{font-size:12px}.terminal-fallback{padding:22px 30px;border-top:1px solid #cdd8ff26;background:#080b1233}.terminal-fallback p{font-size:12px}.terminal-fallback code{display:block;margin-top:10px;font-size:12px;color:#d3d1ff;overflow-wrap:anywhere}.connection-message{margin-top:18px;font-size:12px;color:#d3d1ff;min-height:18px}.next-step{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-top:22px;padding:23px;border:1px solid #b9b6ff30;border-radius:14px;background:#b9b6ff09}.next-step h2{margin-bottom:8px}.next-step p{font-size:12px}.next-step .actions{margin:0;flex-shrink:0}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline-color:#b9b6ff}.notice code{color:#d3d1ff}.search{border-color:#cdd8ff26;background:#0307117a}.breadcrumb b{color:#f1f3fc}.rail-footer .dot{background:#b9b6ff}.footer{margin-top:28px}@media(max-width:1100px){.shell{grid-template-columns:225px 1fr}.brand{font-size:13px}.main{padding:25px}.onboarding-hero{padding:30px}.next-step{align-items:flex-start;flex-direction:column}}@media(max-width:680px){.shell{display:block}.brand{font-size:16px}.onboarding{margin-top:22px}.onboarding-hero{padding:26px}.onboarding h1{font-size:30px}.onboarding-logo{width:65px;height:65px;margin-bottom:15px}.steps{grid-template-columns:1fr}.step{padding:20px 26px;border-right:0;border-bottom:1px solid #cdd8ff16}.step-number{margin-bottom:8px}.terminal-fallback{padding:22px 26px}.main{padding:20px}.next-step .actions{flex-shrink:1}.onboarding .actions .button{width:100%}.onboarding .actions .link{padding:8px 0}.card-value{overflow-wrap:anywhere}.nav{padding-bottom:2px}}
8
+ .danger{border-color:#ed9a9a66;color:#ffb5b5;background:#53272d55}.danger:hover{border-color:#ffb5b5}.danger-title{color:#ffb5b5}.pagination{display:flex;gap:9px;align-items:center;flex-wrap:wrap;border-top:1px solid #cdd8ff20;margin-top:15px;padding-top:15px;color:#acb4cb;font-size:11px}.pagination label{display:flex;align-items:center;gap:7px}.pagination .page-summary{flex:1;min-width:95px}.pagination select{padding:7px}.pagination .button{padding:7px 10px}.pagination button:disabled{cursor:default}.row-open{background:transparent;border:0;text-align:left;padding:0;color:#d3d1ff;cursor:pointer;font:inherit}.row-open:hover{text-decoration:underline}.row-actions{padding-top:5px}.detail-dialog{color:#f1f3fc;background:linear-gradient(135deg,#272c3e,#131724);border:1px solid #cdd8ff40;border-radius:18px;max-width:800px;width:calc(100% - 32px);max-height:85vh;padding:26px;box-shadow:0 30px 100px #0009}.detail-dialog::backdrop{background:#050710bd;backdrop-filter:blur(5px)}.detail-dialog h2{font-size:20px;overflow-wrap:anywhere}.detail-fields{margin:0}.detail-fields dt{color:#acb4cb;font-size:11px;margin:20px 0 8px}.detail-fields dd{margin:0;font-size:13px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere}.detail-fields pre{font:inherit;white-space:pre-wrap;margin:0}.engine-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.engine-keys{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;margin-top:16px}.engine-keys label{display:block;font-size:12px;margin-bottom:8px}.engine-keys input{width:100%;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:10px 12px;font:inherit;font-size:13px;margin-bottom:8px}.engine-option{display:block;padding:17px;border:1px solid #cdd8ff26;border-radius:10px;background:#080b1233;cursor:pointer}.engine-option:has(input:checked){border-color:#b9b6ff;background:#b9b6ff12}.engine-option:has(input:disabled){opacity:.7;cursor:default}.engine-option input{accent-color:#b9b6ff;margin-right:8px}.engine-option strong{font-size:12px}.engine-option p{font-size:11px;margin-top:9px}.engine-message{margin-top:13px;font-size:12px;color:#d3d1ff}.context-editor{margin-top:20px}.context-editor label{display:block;font-size:12px;margin-bottom:8px}.context-editor textarea{width:100%;min-height:130px;resize:vertical;background:#0307117a;color:#f1f3fc;border:1px solid #cdd8ff26;border-radius:9px;padding:12px;font:inherit;font-size:13px}.context-editor textarea:focus-visible{outline:2px solid #b9b6ff;outline-offset:3px}@media(max-width:800px){.engine-options{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){.detail-dialog::backdrop{backdrop-filter:none}}</style><script src="/app.js" defer></script></head><body><div class="shell"><aside class="rail"><div class="brand"><img src="/logo.png" alt="" width="46" height="46"><div>AI Vault Memory<span class="brand-sub">LOCAL AGENT</span></div></div><div class="rail-label" data-i18n="workspace">WORKSPACE</div><nav class="nav" aria-label="Navigation"><a class="active" href="#overview"><span class="icon">◈</span><span data-i18n="overview">Overview</span></a><a href="#activity"><span class="icon">⌁</span><span data-i18n="activity">Activity</span></a><a href="#memories"><span class="icon">▤</span><span data-i18n="memories">Memories</span></a><a href="#context"><span class="icon">✧</span><span data-i18n="contextEngine">Context engine</span></a><a href="#projects"><span class="icon">▱</span><span data-i18n="projects">Projects</span></a></nav><div class="rail-footer"><span class="dot"></span><span data-i18n="localWorkspace">Your local workspace</span><br><span data-i18n="privacy">Activity metadata stays local.</span></div></aside><main class="main" id="overview"><header class="top"><div class="breadcrumb">Vault <span aria-hidden="true"> / </span><b data-i18n="localMonitor">Local monitor</b></div><div class="controls"><select id="locale" aria-label="Language"><option value="en">English</option><option value="pt">Português</option><option value="es">Español</option></select><button class="button" id="logout" type="button" hidden data-i18n="logout">Sign out of dashboard</button><button class="button" id="reconnect" type="button" hidden data-i18n="reconnect">Reconnect account</button><button class="button" id="refresh" type="button"><span aria-hidden="true">↻</span><span data-i18n="refresh">Refresh cloud</span></button></div></header><div id="error" class="error" role="status" aria-live="polite"></div><section class="onboarding" id="onboarding"><div class="onboarding-hero"><img class="onboarding-logo" src="/logo.png" alt="" width="86" height="86"><div class="eyebrow" data-i18n="connectEyebrow">AI VAULT MEMORY · LOCAL</div><h1 data-i18n="connectTitle">Your memory. Connected to this machine.</h1><p data-i18n="connectIntro">Connect your Vault account to see your memories, projects and recent activity in one place.</p><div class="actions"><button type="button" class="button primary" id="connect" data-i18n="connect">Connect to Vault</button><a class="link" href="https://vault.resolveup.com.br/app" target="_blank" rel="noopener noreferrer" data-i18n="openPortal">Open Vault portal ↗</a></div><p class="onboarding-hint" data-i18n="connectHint">Sign in securely in a separate window. Your password stays with the Vault portal.</p><div class="connection-message" id="connection-message" role="status" aria-live="polite"></div><a id="auth-fallback" class="button" hidden target="_blank" rel="noopener noreferrer" data-i18n="continueAuth">Continue sign-in ↗</a></div><div class="steps"><article class="step"><div class="step-number">01 / VAULT</div><h2 data-i18n="stepAccount">Connect your account</h2><p data-i18n="stepAccountBody">Sign in to Vault and authorize this local agent.</p></article><article class="step"><div class="step-number">02 / MCP</div><h2 data-i18n="stepClient">Connect your AI client</h2><p data-i18n="stepClientBody">Configure Vault MCP in Codex, Cursor or another compatible client.</p></article><article class="step"><div class="step-number">03 / MEMORY</div><h2 data-i18n="stepMemory">Build your first memory</h2><p data-i18n="stepMemoryBody">Ask your connected client to save useful context, then follow its activity here.</p></article></div><div class="terminal-fallback"><button class="button" id="verify-connection" type="button" data-i18n="verifyConnection">Already signed in? Check connection</button><p style="margin-top:16px" data-i18n="terminalFallback">Already connected through the CLI? Open a secure local session:</p><code>bunx --bun vault-go@latest local open</code></div></section><div id="dashboard" hidden><div class="welcome"><div><div class="eyebrow" data-i18n="eyebrow">YOUR KNOWLEDGE, CONNECTED</div><h1 data-i18n="title">Your local workspace.</h1><p data-i18n="subtitle">Your Vault account, memories and activity on this machine.</p></div><img src="/logo.png" alt="" width="56" height="56"></div><div id="session-notice" hidden></div><section class="cards" aria-label="Status"><article class="card"><div class="card-label"><span data-i18n="localService">Local service</span><span aria-hidden="true">◈</span></div><div class="card-value" id="local-status">—</div><div class="card-note" id="local-note">localhost:38850</div></article><article class="card"><div class="card-label"><span data-i18n="cloudConnection">Cloud connection</span><span aria-hidden="true">↗</span></div><div class="card-value" id="cloud-status">—</div><div class="card-note" id="cloud-note">—</div></article><article class="card"><div class="card-label"><span data-i18n="recentOperations">Recent operations</span><span aria-hidden="true">⌁</span></div><div class="card-value" id="activity-total">—</div><div class="card-note" data-i18n="operationsNote">Recent MCP request metadata</div></article></section><section class="next-step" id="first-memory"><div><h2 data-i18n="firstTitle">Create your first memory</h2><p data-i18n="firstBody">Connect Vault MCP to your AI client, then ask it to save context. Your memories will appear here after a cloud refresh.</p><code id="mcp-command" style="display:block;margin-top:12px;font-size:12px;overflow-wrap:anywhere"></code><p data-i18n="rememberPrompt" style="margin-top:10px"></p></div><div class="actions"><a class="button" href="https://vault.resolveup.com.br/mcp" target="_blank" rel="noopener noreferrer" data-i18n="setupMcp">Set up MCP ↗</a><a class="link" href="https://vault.resolveup.com.br/app/memory" target="_blank" rel="noopener noreferrer" data-i18n="openMemories">Open memories ↗</a></div></section><div class="layout"><section class="panel wide" id="security"><div class="panel-head"><h2 data-i18n="securityTitle">Security on this machine</h2></div><div id="security-facts"></div><div class="actions"><button class="button danger" id="disconnect-device" type="button" data-i18n="disconnectDevice">Disconnect device</button></div></section><section class="panel wide" id="context"><div class="panel-head"><h2 data-i18n="contextEngine">Context engine</h2></div><p data-i18n="engineIntro">Choose how Vault prepares your context.</p><div class="engine-options" id="engine-options" role="radiogroup" aria-label="Context engine"></div><div class="engine-keys" id="engine-keys"></div><div class="engine-message" id="engine-message" role="status" aria-live="polite"></div><div class="context-editor"><label for="context-text" data-i18n="contextText">Text to prepare</label><textarea id="context-text" maxlength="16000" aria-describedby="context-note"></textarea><p id="context-note" data-i18n="contextNote"></p><div class="actions"><button class="button primary" id="context-preview" type="button" data-i18n="contextPreview"></button><button class="button" id="context-stop" type="button" data-i18n="cancel" hidden></button></div><p id="context-progress" role="status" aria-live="polite"></p></div></section><section class="panel" id="activity"><div class="panel-head"><h2 data-i18n="activity">Activity</h2><span class="count" id="activity-count">0</span></div><input class="search" id="activity-search" type="search" data-placeholder="searchActivity" aria-label="Search activity"><div id="activity-list"></div><div class="pagination" id="activity-pagination"></div></section><section class="panel" id="memories"><div class="panel-head"><h2 data-i18n="memories">Memories</h2><span class="count" id="memory-count">0</span></div><input class="search" id="memory-search" type="search" data-placeholder="searchMemories" aria-label="Search memories"><div id="memory-list"></div><div class="pagination" id="memory-pagination"></div></section><section class="panel wide" id="projects"><div class="panel-head"><h2 data-i18n="projects">Projects</h2><span class="count" id="project-count">0</span></div><input class="search" id="project-search" type="search" data-placeholder="searchProjects" aria-label="Search projects"><div id="project-list"></div><div class="pagination" id="project-pagination"></div></section></div></div><footer class="footer"><span data-i18n="footer">Vault Local · Context with a place to call home.</span><span id="updated" aria-live="off">—</span></footer></main></div><dialog class="detail-dialog" id="detail-dialog" aria-labelledby="detail-title"><div class="panel-head"><h2 id="detail-title"></h2><button class="button" type="button" id="detail-copy" hidden data-i18n="copyResult">Copy result</button><button class="button" type="button" id="detail-close" data-i18n="close">Close</button></div><dl class="detail-fields" id="detail-fields"></dl></dialog><dialog class="detail-dialog" id="logout-dialog" aria-labelledby="logout-title" aria-describedby="logout-description"><h2 id="logout-title" data-i18n="logout"></h2><p id="logout-description" data-i18n="logoutDescription" style="margin-top:16px"></p><div class="actions"><button class="button" type="button" id="logout-cancel" data-i18n="cancel"></button><button class="button primary" type="button" id="logout-confirm" data-i18n="logoutConfirm"></button></div></dialog><dialog class="detail-dialog" id="context-confirm-dialog" aria-labelledby="context-confirm-title" aria-describedby="context-confirm-note"><h2 id="context-confirm-title" data-i18n="contextPreview"></h2><p id="context-confirm-engine" style="margin-top:16px"></p><p id="context-confirm-billing"></p><p id="context-confirm-note" data-i18n="contextConsentNote" style="margin-top:16px"></p><pre id="context-confirm-text" style="white-space:pre-wrap;overflow-wrap:anywhere;max-height:32vh;overflow:auto;font-size:12px;line-height:1.7"></pre><label style="display:flex;gap:10px;align-items:flex-start;font-size:12px"><input id="context-consent" type="checkbox"><span data-i18n="contextConsent"></span></label><div class="actions"><button class="button" id="context-cancel" type="button" data-i18n="cancel"></button><button class="button primary" id="context-generate" type="button" disabled data-i18n="contextGenerate"></button></div></dialog><dialog class="detail-dialog" id="disconnect-dialog" aria-labelledby="disconnect-title" aria-describedby="disconnect-description"><h2 class="danger-title" id="disconnect-title" data-i18n="disconnectDevice"></h2><p id="disconnect-description" data-i18n="disconnectDescription" style="margin-top:16px"></p><p id="disconnect-error" class="error" role="alert"></p><div class="actions"><button class="button" type="button" id="disconnect-cancel" data-i18n="cancel"></button><button class="button danger" type="button" id="disconnect-confirm" data-i18n="disconnectConfirm"></button></div></dialog></body></html>`;
6
9
  }
7
10
  export function dashboardScript() {
8
11
  return `"use strict";
@@ -15,10 +18,26 @@ export function dashboardScript() {
15
18
  pt: {workspace:'ESPAÇO DE TRABALHO',overview:'Visão geral',activity:'Atividade',memories:'Memórias',projects:'Projetos',localWorkspace:'Seu espaço local',privacy:'Metadados de atividade ficam locais.',localMonitor:'Monitor local',refresh:'Atualizar nuvem',eyebrow:'SEU CONHECIMENTO, CONECTADO',title:'Um pouco mais de contexto.',subtitle:'Uma visão clara do seu Vault local e da conexão com a nuvem.',session:'Abra uma sessão local segura pelo terminal:',localService:'Serviço local',cloudConnection:'Conexão com a nuvem',recentOperations:'Operações recentes',operationsNote:'Metadados recentes das chamadas MCP',searchMemories:'Buscar memórias…',searchProjects:'Buscar projetos…',footer:'Vault Local · Um lugar para o seu contexto.',online:'Em execução',offline:'Indisponível',connected:'Conectada',disconnected:'Desconectada',checking:'Verificando…',signedOut:'Entre pela CLI para conectar.',activityEmpty:'Nenhuma operação registrada. A atividade aparecerá conforme este serviço local for usado.',memoriesEmpty:'Nenhuma memória disponível. As memórias retornadas pela sua conta na nuvem aparecerão aqui.',projectsEmpty:'Nenhum projeto disponível. Os projetos retornados pela sua conta na nuvem aparecerão aqui.',noResults:'Nenhum resultado encontrado.',updated:'Atualizado',waiting:'Aguardando uma sessão segura',fetchError:'Não foi possível acessar o serviço local. Verifique se o Vault Local está em execução.',syncError:'A atualização da nuvem falhou. Tente novamente em instantes.',success:'Concluída',failed:'Falhou',untitled:'Sem título',unknown:'Desconhecido',paused:'Atualizações pausadas enquanto esta aba está oculta'},
16
19
  es: {workspace:'ESPACIO DE TRABAJO',overview:'Resumen',activity:'Actividad',memories:'Memorias',projects:'Proyectos',localWorkspace:'Tu espacio local',privacy:'Los metadatos de actividad quedan locales.',localMonitor:'Monitor local',refresh:'Actualizar nube',eyebrow:'TU CONOCIMIENTO, CONECTADO',title:'Un poco más de contexto.',subtitle:'Una vista clara de tu Vault local y de la conexión con la nube.',session:'Abre una sesión local segura desde tu terminal:',localService:'Servicio local',cloudConnection:'Conexión con la nube',recentOperations:'Operaciones recientes',operationsNote:'Metadatos recientes de llamadas MCP',searchMemories:'Buscar memorias…',searchProjects:'Buscar proyectos…',footer:'Vault Local · Un hogar para tu contexto.',online:'En ejecución',offline:'No disponible',connected:'Conectada',disconnected:'Desconectada',checking:'Comprobando…',signedOut:'Inicia sesión desde la CLI para conectar.',activityEmpty:'Aún no hay operaciones registradas. La actividad aparecerá al usar este servicio local.',memoriesEmpty:'No hay memorias disponibles. Las memorias de tu cuenta en la nube aparecerán aquí.',projectsEmpty:'No hay proyectos disponibles. Los proyectos de tu cuenta en la nube aparecerán aquí.',noResults:'No se encontraron resultados.',updated:'Actualizado',waiting:'Esperando una sesión segura',fetchError:'No se pudo acceder al servicio local. Comprueba que Vault Local esté en ejecución.',syncError:'La actualización de la nube falló. Inténtalo de nuevo en unos momentos.',success:'Completada',failed:'Falló',untitled:'Sin título',unknown:'Desconocido',paused:'Actualizaciones pausadas mientras esta pestaña está oculta'}
17
20
  };
21
+ const onboardingCopy = {
22
+ en: {reconnect:'Reconnect account',verifyConnection:'Already signed in? Check connection',rememberPrompt:'Then ask your assistant: “Use vault_go_remember to remember that…”',connectEyebrow:'AI VAULT MEMORY · LOCAL',connectTitle:'Your memory. Connected to this machine.',connectIntro:'Connect your Vault account to see your memories, projects and recent activity in one place.',connect:'Connect to Vault',openPortal:'Open Vault portal ↗',connectHint:'Sign in securely in a separate window. Your password stays with the Vault portal.',continueAuth:'Continue sign-in ↗',stepAccount:'Connect your account',stepAccountBody:'Sign in to Vault and authorize this local agent.',stepClient:'Connect your AI client',stepClientBody:'Configure Vault MCP in Codex, Cursor or another compatible client.',stepMemory:'Build your first memory',stepMemoryBody:'Ask your connected client to save useful context, then follow its activity here.',terminalFallback:'Already connected through the CLI? Open a secure local session:',firstTitle:'Create your first memory',firstBody:'Connect Vault MCP to your AI client, then ask it to save context. Your memories will appear here after a cloud refresh.',setupMcp:'Set up MCP ↗',openMemories:'Open memories ↗',connecting:'Opening secure sign-in…',awaitingAuth:'Complete sign-in in the Vault window. This page will update automatically.',popupBlocked:'Your browser blocked the window. Use the sign-in link below to continue.',connectFailed:'Could not connect to Vault. Try again or open a local session through the terminal.',connectExpired:'Sign-in was not completed. Click Connect to Vault to try again.',title:'Your local workspace.',subtitle:'Your Vault account, memories and activity on this machine.',memoriesEmpty:'No memories yet. Set up Vault MCP and ask your AI client to save your first memory.',projectsEmpty:'No projects yet. Projects returned by your Vault account will appear here.',footer:'AI Vault Memory · Local agent'},
23
+ pt: {reconnect:'Reconectar conta',verifyConnection:'Já entrei: verificar conexão',rememberPrompt:'Depois peça ao assistente: “Use vault_go_remember para lembrar que…”',connectEyebrow:'AI VAULT MEMORY · LOCAL',connectTitle:'Sua memória. Conectada a esta máquina.',connectIntro:'Conecte sua conta Vault para acompanhar memórias, projetos e atividades recentes em um só lugar.',connect:'Conectar ao Vault',openPortal:'Abrir portal Vault ↗',connectHint:'Entre com segurança em uma janela separada. Sua senha fica no portal Vault.',continueAuth:'Continuar login ↗',stepAccount:'Conecte sua conta',stepAccountBody:'Entre no Vault e autorize este agente local.',stepClient:'Conecte seu cliente de IA',stepClientBody:'Configure o Vault MCP no Codex, Cursor ou outro cliente compatível.',stepMemory:'Crie sua primeira memória',stepMemoryBody:'Peça ao cliente conectado para salvar um contexto útil e acompanhe a atividade aqui.',terminalFallback:'Já conectou pela CLI? Abra uma sessão local segura:',firstTitle:'Crie sua primeira memória',firstBody:'Conecte o Vault MCP ao seu cliente de IA e peça para salvar um contexto. As memórias aparecerão aqui após atualizar a nuvem.',setupMcp:'Configurar MCP ↗',openMemories:'Abrir memórias ↗',connecting:'Abrindo login seguro…',awaitingAuth:'Conclua o login na janela do Vault. Esta página será atualizada automaticamente.',popupBlocked:'O navegador bloqueou a janela. Use o link de login abaixo para continuar.',connectFailed:'Não foi possível conectar ao Vault. Tente novamente ou abra uma sessão local pelo terminal.',connectExpired:'O login não foi concluído. Clique em Conectar ao Vault para tentar novamente.',title:'Seu espaço de trabalho local.',subtitle:'Sua conta Vault, memórias e atividades nesta máquina.',memoriesEmpty:'Ainda não há memórias. Configure o Vault MCP e peça ao seu cliente de IA para salvar a primeira.',projectsEmpty:'Ainda não há projetos. Os projetos retornados pela sua conta Vault aparecerão aqui.',footer:'AI Vault Memory · Agente local'},
24
+ es: {reconnect:'Reconectar cuenta',verifyConnection:'Ya inicié sesión: comprobar conexión',rememberPrompt:'Después pide a tu asistente: “Usa vault_go_remember para recordar que…”',connectEyebrow:'AI VAULT MEMORY · LOCAL',connectTitle:'Tu memoria. Conectada a esta máquina.',connectIntro:'Conecta tu cuenta Vault para consultar memorias, proyectos y actividad reciente en un solo lugar.',connect:'Conectar a Vault',openPortal:'Abrir portal Vault ↗',connectHint:'Inicia sesión de forma segura en otra ventana. Tu contraseña permanece en el portal Vault.',continueAuth:'Continuar inicio de sesión ↗',stepAccount:'Conecta tu cuenta',stepAccountBody:'Inicia sesión en Vault y autoriza este agente local.',stepClient:'Conecta tu cliente de IA',stepClientBody:'Configura Vault MCP en Codex, Cursor u otro cliente compatible.',stepMemory:'Crea tu primera memoria',stepMemoryBody:'Pide al cliente conectado que guarde contexto útil y sigue su actividad aquí.',terminalFallback:'¿Ya te conectaste mediante la CLI? Abre una sesión local segura:',firstTitle:'Crea tu primera memoria',firstBody:'Conecta Vault MCP a tu cliente de IA y pídele que guarde contexto. Tus memorias aparecerán aquí al actualizar la nube.',setupMcp:'Configurar MCP ↗',openMemories:'Abrir memorias ↗',connecting:'Abriendo inicio de sesión seguro…',awaitingAuth:'Completa el inicio de sesión en la ventana de Vault. Esta página se actualizará automáticamente.',popupBlocked:'El navegador bloqueó la ventana. Usa el enlace de abajo para continuar.',connectFailed:'No se pudo conectar a Vault. Inténtalo de nuevo o abre una sesión local desde el terminal.',connectExpired:'No se completó el inicio de sesión. Pulsa Conectar a Vault para intentarlo de nuevo.',title:'Tu espacio de trabajo local.',subtitle:'Tu cuenta Vault, memorias y actividad en esta máquina.',memoriesEmpty:'Aún no hay memorias. Configura Vault MCP y pide a tu cliente de IA que guarde la primera.',projectsEmpty:'Aún no hay proyectos. Los proyectos de tu cuenta Vault aparecerán aquí.',footer:'AI Vault Memory · Agente local'}
25
+ };
26
+ const detailCopy = {
27
+ en:{disconnectDevice:'Disconnect device',disconnectConfirm:'Revoke access and disconnect',disconnectDescription:'This revokes this agent’s cloud access and removes its local credentials and cache. Your files are preserved. MCP clients stop using this account until you sign in again.',disconnectFailed:'Could not confirm device disconnection. Nothing was cleared from this panel. Check the connection and try again.',disconnectLoginBusy:'Sign-in is in progress. Finish signing in before disconnecting this device.',contextCanceling:'Requesting cancellation…',contextCancelFailed:'Cancellation could not be confirmed. The provider may still be running and usage charges may apply.',securityTitle:'Security on this machine',cacheEncrypted:'Local cache encrypted at rest',cacheUnverified:'Local cache protection not confirmed',credentialProtection:'Account credentials protected by local file permissions',transportProtection:'Local connection: loopback HTTP. Vault cloud connection: HTTPS.',noEndToEnd:'This is not end-to-end encryption. The selected provider processes text you authorize.',device:'Device',deviceOnline:'Online',deviceOffline:'Offline',deviceRevoked:'Revoked',deviceUnregistered:'Not registered',lastSeen:'Last seen',keyStorage:'Encryption key storage',contextCanceled:'Generation canceled. Provider usage already consumed may still be charged.',contextText:'Text to prepare',contextNote:'Up to 16,000 characters. Only this text is sent; no memory is saved automatically.',contextPreview:'Review generation',contextConsentNote:'The following text will be sent to the selected provider. Review it before continuing.',contextConsent:'I authorize sending this text and using the selected engine allowance or applicable credits.',contextGenerate:'Generate context',contextRunning:'Preparing context…',contextComplete:'Context ready. Review or copy the result.',contextFailed:'Could not generate context. Check your engine login and usage limits.',contextTimedOut:'The local wait limit was reached. No further polling will occur.',contextInvalid:'Enter 1–16,000 characters and choose an available engine.',contextBilling:'Provider subscription limits or usage credits may apply.',copyResult:'Copy result',copied:'Result copied.',copyFailed:'Could not copy. Select the result text manually.',logout:'Sign out of dashboard',logoutDescription:'This ends the session in this browser. The local agent remains connected to your Vault account and keeps working.',logoutConfirm:'Sign out',cancel:'Cancel',logoutFailed:'Could not end this session. Please try again.',searchActivity:'Search activity…',previous:'Previous',next:'Next',perPage:'Per page',of:'of',page:'Page',view:'View details',close:'Close',details:'Details',contextEngine:'Context engine',engineIntro:'Choose how Vault prepares your context.',engineLoading:'Checking available engines…',engineUnavailable:'Unavailable on this machine',engineAvailable:'Available',engineSaved:'Context engine updated.',engineFailed:'Could not update the context engine. Try again.',engineLoadFailed:'Could not load context engines. Refresh to retry.'},
28
+ pt:{disconnectDevice:'Desconectar dispositivo',disconnectConfirm:'Revogar acesso e desconectar',disconnectDescription:'Isso revoga o acesso deste agente à nuvem e remove suas credenciais e seu cache locais. Seus arquivos serão preservados. Os clientes MCP param de usar esta conta até você entrar novamente.',disconnectFailed:'Não foi possível confirmar a desconexão. Nada foi apagado deste painel. Verifique a conexão e tente novamente.',disconnectLoginBusy:'Há um login em andamento. Conclua o login antes de desconectar este dispositivo.',contextCanceling:'Solicitando cancelamento…',contextCancelFailed:'Não foi possível confirmar o cancelamento. O provedor ainda pode estar executando e o uso pode ser cobrado.',securityTitle:'Segurança nesta máquina',cacheEncrypted:'Cache local criptografado em repouso',cacheUnverified:'Proteção do cache local não confirmada',credentialProtection:'Credenciais da conta protegidas por permissões de arquivo locais',transportProtection:'Conexão local: HTTP em loopback. Conexão com a nuvem Vault: HTTPS.',noEndToEnd:'Isso não é criptografia de ponta a ponta. O provedor escolhido processa o texto que você autorizar.',device:'Dispositivo',deviceOnline:'Online',deviceOffline:'Offline',deviceRevoked:'Revogado',deviceUnregistered:'Não registrado',lastSeen:'Última atividade',keyStorage:'Armazenamento da chave',contextCanceled:'Geração cancelada. O uso já consumido no provedor ainda pode ser cobrado.',contextText:'Texto para preparar',contextNote:'Até 16.000 caracteres. Só este texto será enviado; nenhuma memória será salva automaticamente.',contextPreview:'Revisar geração',contextConsentNote:'O texto abaixo será enviado ao provedor selecionado. Confira antes de continuar.',contextConsent:'Autorizo o envio deste texto e o uso da franquia ou dos créditos aplicáveis ao motor selecionado.',contextGenerate:'Gerar contexto',contextRunning:'Preparando contexto…',contextComplete:'Contexto pronto. Confira ou copie o resultado.',contextFailed:'Não foi possível gerar contexto. Verifique o login e os limites do motor.',contextTimedOut:'O limite de espera local foi atingido. Não haverá novas consultas.',contextInvalid:'Digite de 1 a 16.000 caracteres e escolha um motor disponível.',contextBilling:'Podem ser aplicados limites da assinatura ou créditos de uso do provedor.',copyResult:'Copiar resultado',copied:'Resultado copiado.',copyFailed:'Não foi possível copiar. Selecione o texto do resultado manualmente.',logout:'Sair do painel',logoutDescription:'Isso encerra a sessão neste navegador. O agente local continua conectado à sua conta Vault e funcionando.',logoutConfirm:'Sair do painel',cancel:'Cancelar',logoutFailed:'Não foi possível encerrar esta sessão. Tente novamente.',searchActivity:'Buscar atividades…',previous:'Anterior',next:'Próxima',perPage:'Por página',of:'de',page:'Página',view:'Ver detalhes',close:'Fechar',details:'Detalhes',contextEngine:'Motor de contexto',engineIntro:'Escolha como o Vault prepara seu contexto.',engineLoading:'Verificando motores disponíveis…',engineUnavailable:'Indisponível nesta máquina',engineAvailable:'Disponível',engineSaved:'Motor de contexto atualizado.',engineFailed:'Não foi possível alterar o motor de contexto. Tente novamente.',engineLoadFailed:'Não foi possível carregar os motores. Atualize para tentar novamente.'},
29
+ es:{disconnectDevice:'Desconectar dispositivo',disconnectConfirm:'Revocar acceso y desconectar',disconnectDescription:'Esto revoca el acceso de este agente a la nube y elimina sus credenciales y caché locales. Tus archivos se conservan. Los clientes MCP dejan de usar esta cuenta hasta que inicies sesión nuevamente.',disconnectFailed:'No se pudo confirmar la desconexión. No se ha borrado nada de este panel. Comprueba la conexión e inténtalo de nuevo.',disconnectLoginBusy:'Hay un inicio de sesión en curso. Complétalo antes de desconectar este dispositivo.',contextCanceling:'Solicitando cancelación…',contextCancelFailed:'No se pudo confirmar la cancelación. El proveedor puede seguir ejecutándose y cobrar el uso.',securityTitle:'Seguridad en este equipo',cacheEncrypted:'Caché local cifrada en reposo',cacheUnverified:'Protección de la caché local no confirmada',credentialProtection:'Credenciales protegidas por permisos de archivos locales',transportProtection:'Conexión local: HTTP de loopback. Conexión con Vault en la nube: HTTPS.',noEndToEnd:'No es cifrado de extremo a extremo. El proveedor seleccionado procesa el texto que autorices.',device:'Dispositivo',deviceOnline:'En línea',deviceOffline:'Sin conexión',deviceRevoked:'Revocado',deviceUnregistered:'No registrado',lastSeen:'Última actividad',keyStorage:'Almacenamiento de la clave',contextCanceled:'Generación cancelada. El uso ya consumido en el proveedor puede cobrarse.',contextText:'Texto para preparar',contextNote:'Hasta 16.000 caracteres. Solo se envía este texto; no se guarda ninguna memoria automáticamente.',contextPreview:'Revisar generación',contextConsentNote:'El siguiente texto se enviará al proveedor seleccionado. Revísalo antes de continuar.',contextConsent:'Autorizo el envío de este texto y el uso de la cuota o créditos aplicables al motor seleccionado.',contextGenerate:'Generar contexto',contextRunning:'Preparando contexto…',contextComplete:'Contexto listo. Revisa o copia el resultado.',contextFailed:'No se pudo generar contexto. Comprueba el inicio de sesión y los límites del motor.',contextTimedOut:'Se alcanzó el límite de espera local. No habrá más consultas.',contextInvalid:'Introduce de 1 a 16.000 caracteres y elige un motor disponible.',contextBilling:'Pueden aplicarse límites de suscripción o créditos del proveedor.',copyResult:'Copiar resultado',copied:'Resultado copiado.',copyFailed:'No se pudo copiar. Selecciona el texto del resultado manualmente.',logout:'Salir del panel',logoutDescription:'Esto cierra la sesión en este navegador. El agente local sigue conectado a tu cuenta Vault y funcionando.',logoutConfirm:'Cerrar sesión',cancel:'Cancelar',logoutFailed:'No se pudo cerrar esta sesión. Inténtalo de nuevo.',searchActivity:'Buscar actividad…',previous:'Anterior',next:'Siguiente',perPage:'Por página',of:'de',page:'Página',view:'Ver detalles',close:'Cerrar',details:'Detalles',contextEngine:'Motor de contexto',engineIntro:'Elige cómo Vault prepara tu contexto.',engineLoading:'Comprobando motores disponibles…',engineUnavailable:'No disponible en este equipo',engineAvailable:'Disponible',engineSaved:'Motor de contexto actualizado.',engineFailed:'No se pudo cambiar el motor de contexto. Inténtalo de nuevo.',engineLoadFailed:'No se pudieron cargar los motores. Actualiza para reintentar.'}
30
+ };
31
+ const engineCopy={
32
+ pt:{engineUpdateCli:'Atualize o CLI oficial para uma versão compatível com geração isolada de contexto.',engineClaudeLogin:'Entre com sua assinatura Claude usando claude auth login.',engineCodexLogin:'Entre com sua conta ChatGPT usando codex login.',engineCliUnavailable:'CLI oficial ou login da assinatura indisponível. Instale o CLI do provedor ou entre novamente por ele.',engineVaultUnavailable:'O Vault AI Resume não está configurado ou não está disponível para esta conta.',engineProviderCheck:'A disponibilidade do provedor será verificada ao gerar.',engineObserverMissing:'O observador gerenciado não está configurado.',engineOpenrouterKey:'Configure uma chave OpenRouter com vault-go engine key openrouter.',engineGeminiKey:'Configure uma chave Gemini com vault-go engine key gemini.',billingVault:'Usa a franquia do observador do Vault.',billingClaude:'Usa a franquia do provedor para execuções não interativas do Claude Code; podem ser aplicados limites e cobranças adicionais.',billingOpenai:'Usa sua assinatura ChatGPT pelo Codex. Podem ser aplicados limites ou créditos de uso do provedor.',billingOpenrouter:'Usa sua chave e créditos OpenRouter. Podem ser aplicados limites do provedor.',billingGemini:'Usa sua chave da API Gemini. Podem ser aplicados limites ou cobrança do Google.',openrouterKey:'Chave OpenRouter',geminiKey:'Chave Gemini',saveKey:'Salvar chave',keyConfigured:'Chave configurada nesta máquina',keyMissing:'Cole a chave e salve',keySaved:'Chave gravada com permissão privada.',keyFailed:'Não foi possível gravar a chave. Tente novamente.',keyOs:'Cofre de chaves do sistema operacional',keyFile:'Arquivo local protegido por permissões',keyEphemeral:'Somente na memória desta execução',keyUnavailable:'Indisponível'},
33
+ en:{engineUpdateCli:'Update the official CLI to a version supporting isolated context generation.',engineClaudeLogin:'Sign in with a Claude subscription using claude auth login.',engineCodexLogin:'Sign in with ChatGPT using codex login.',engineCliUnavailable:'Official CLI or subscription login unavailable. Install or sign in using the provider CLI.',engineVaultUnavailable:'Vault AI Resume is not configured or unavailable for this account.',engineProviderCheck:'Provider readiness is checked when generating.',engineObserverMissing:'Managed observer is not configured.',engineOpenrouterKey:'Configure an OpenRouter API key with vault-go engine key openrouter.',engineGeminiKey:'Configure a Gemini API key with vault-go engine key gemini.',billingVault:'Uses the Vault observer allowance.',billingClaude:'Uses the provider allowance for Claude Code non-interactive runs; limits/additional charges may apply.',billingOpenai:'Uses your ChatGPT subscription through Codex. Provider limits or credits may apply.',billingOpenrouter:'Uses your OpenRouter key and credits. Provider limits may apply.',billingGemini:'Uses your Gemini API key. Google rate limits or billing may apply.',openrouterKey:'OpenRouter API key',geminiKey:'Gemini API key',saveKey:'Save key',keyConfigured:'Key configured on this machine',keyMissing:'Paste the key and save',keySaved:'Key stored with private file permissions.',keyFailed:'Could not store the key. Try again.',keyOs:'Operating system keychain',keyFile:'Local file protected by permissions',keyEphemeral:'In memory for this run only',keyUnavailable:'Unavailable'},
34
+ es:{engineUpdateCli:'Actualiza el CLI oficial a una versión compatible con la generación aislada de contexto.',engineClaudeLogin:'Inicia sesión con tu suscripción Claude usando claude auth login.',engineCodexLogin:'Inicia sesión con tu cuenta ChatGPT usando codex login.',engineCliUnavailable:'CLI oficial o sesión de la suscripción no disponible. Instala el CLI del proveedor o inicia sesión mediante él.',engineVaultUnavailable:'Vault AI Resume no está configurado o no está disponible para esta cuenta.',engineProviderCheck:'La disponibilidad del proveedor se comprobará al generar.',engineObserverMissing:'El observador administrado no está configurado.',engineOpenrouterKey:'Configura una clave OpenRouter con vault-go engine key openrouter.',engineGeminiKey:'Configura una clave Gemini con vault-go engine key gemini.',billingVault:'Usa la cuota del observador de Vault.',billingClaude:'Usa la cuota del proveedor para ejecuciones no interactivas de Claude Code; pueden aplicarse límites y cargos adicionales.',billingOpenai:'Usa tu suscripción ChatGPT mediante Codex. Pueden aplicarse límites o créditos de uso del proveedor.',billingOpenrouter:'Usa tu clave y créditos de OpenRouter. Pueden aplicarse límites del proveedor.',billingGemini:'Usa tu clave de la API Gemini. Pueden aplicarse límites o facturación de Google.',openrouterKey:'Clave OpenRouter',geminiKey:'Clave Gemini',saveKey:'Guardar clave',keyConfigured:'Clave configurada en este equipo',keyMissing:'Pega la clave y guarda',keySaved:'Clave guardada con permisos privados.',keyFailed:'No se pudo guardar la clave. Inténtalo de nuevo.',keyOs:'Llavero del sistema operativo',keyFile:'Archivo local protegido por permisos',keyEphemeral:'Solo en memoria durante esta ejecución',keyUnavailable:'No disponible'}
35
+ };
36
+ for (const language of Object.keys(strings)) Object.assign(strings[language],onboardingCopy[language],detailCopy[language],engineCopy[language]);
18
37
  let locale = navigator.language.slice(0,2);
19
38
  try { locale = localStorage.getItem('vault-local-locale') || locale; } catch {}
20
39
  if (!strings[locale]) locale = 'en';
21
- let state = null, busy = false, sessionRequired = false, failure = '', pendingRefresh = false;
40
+ let state = null, busy = false, sessionRequired = false, failure = '', pendingRefresh = false, connecting = false, connectionMessage = '', connectionTimer = null, connectionStarted = 0;
22
41
  const el = id => document.getElementById(id);
23
42
  const t = key => strings[locale][key] || key;
24
43
  const object = value => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -29,21 +48,157 @@ export function dashboardScript() {
29
48
  const text = (id, value) => { el(id).textContent = value; };
30
49
  const node = (tag, className, value) => { const n = document.createElement(tag); n.className = className; if (value !== undefined) n.textContent = value; return n; };
31
50
  function empty(target, message) { const box = node('div','empty'); box.append(node('span','empty-symbol','◇'),node('p','',message)); target.append(box); }
51
+ const pagination = {activity:{page:1,size:10},memory:{page:1,size:10},project:{page:1,size:10}};
52
+ let detailFocus = '', engines = null, engineBusy = false, engineMessage = '', sessionEpoch = 0, enginesChecked = 0, disconnecting = false;
53
+ let contextPreview=null, contextJob=null, contextTimer=null, contextStarted=0, contextMessage='', contextResult=null, contextStarting=false;
54
+ function showDetails(title,item,trigger) {
55
+ contextResult=null;el('detail-copy').hidden=true;
56
+ detailFocus=trigger.id; text('detail-title',title);
57
+ const fields=el('detail-fields'); fields.replaceChildren();
58
+ const entries=Object.entries(object(item));
59
+ for(const [key,value] of entries.length ? entries : [[t('details'),item]]) {
60
+ const term=node('dt','',key),definition=node('dd','');
61
+ definition.textContent=typeof value==='string' ? value : JSON.stringify(value,null,2);
62
+ fields.append(term,definition);
63
+ }
64
+ el('detail-dialog').showModal(); el('detail-close').focus();
65
+ }
66
+ function viewButton(title,item,kind,index) {
67
+ const button=node('button','button',t('view'));button.type='button';button.id=kind+'-view-'+index;
68
+ button.setAttribute('aria-label',t('view')+': '+title.slice(0,100));
69
+ button.addEventListener('click',()=>showDetails(title,item,button));return button;
70
+ }
71
+ function pageRows(kind,rows,total) {
72
+ const paging=pagination[kind],pages=Math.max(1,Math.ceil(rows.length/paging.size));
73
+ paging.page=Math.min(paging.page,pages);
74
+ const start=(paging.page-1)*paging.size, target=el(kind+'-pagination');target.replaceChildren();
75
+ const label=node('label','',t('perPage')),size=node('select','');
76
+ size.setAttribute('aria-label',t('perPage')+' · '+t(kind==='memory'?'memories':kind==='project'?'projects':'activity'));
77
+ for(const amount of [10,20,50]){const option=node('option','',String(amount));option.value=String(amount);option.selected=amount===paging.size;size.append(option);}
78
+ size.addEventListener('change',()=>{paging.size=Number(size.value);paging.page=1;list(kind);el(kind+'-pagination').querySelector('select').focus();});label.append(size);
79
+ const summary=node('span','page-summary',(rows.length?start+1:0)+'–'+Math.min(start+paging.size,rows.length)+' '+t('of')+' '+rows.length+' · '+t('page')+' '+paging.page+'/'+pages);
80
+ summary.setAttribute('role','status');summary.setAttribute('aria-live','polite');
81
+ const previous=node('button','button',t('previous')),next=node('button','button',t('next'));previous.type=next.type='button';
82
+ previous.disabled=paging.page===1;next.disabled=paging.page===pages;
83
+ const restorePageFocus=index=>{const buttons=el(kind+'-pagination').querySelectorAll('button');(buttons[index].disabled?buttons[1-index]:buttons[index]).focus();};
84
+ previous.addEventListener('click',()=>{paging.page--;list(kind);restorePageFocus(0);});
85
+ next.addEventListener('click',()=>{paging.page++;list(kind);restorePageFocus(1);});
86
+ target.append(label,summary,previous,next);text(kind+'-count',rows.length===total?String(total):rows.length+' / '+total);
87
+ return rows.slice(start,start+paging.size);
88
+ }
32
89
  function list(kind) {
33
- const source = array(state && state[kind === 'memory' ? 'memories' : 'projects']);
34
- const search = el(kind + '-search').value.toLocaleLowerCase(locale).trim();
35
- const rows = source.map(item => ({title:first(item,['title','name','summary','content','text','id']) || t('untitled'),detail:first(item,['project','project_name','description','created_at','createdAt'])}));
36
- const filtered = rows.filter(row => (row.title + ' ' + row.detail).toLocaleLowerCase(locale).includes(search));
37
- const target = el(kind + '-list'); target.replaceChildren(); text(kind + '-count',String(source.length));
38
- if (!filtered.length) return empty(target,t(source.length ? 'noResults' : kind === 'memory' ? 'memoriesEmpty' : 'projectsEmpty'));
39
- for (const row of filtered.slice(0,100)) { const wrap=node('div','row'),body=node('div','row-body'); body.append(node('div','row-title',row.title.slice(0,600))); if (row.detail) body.append(node('div','row-meta',row.detail.slice(0,200))); wrap.append(body); target.append(wrap); }
90
+ const source=array(state && state[kind==='memory'?'memories':kind==='project'?'projects':'activity']);
91
+ const search=el(kind+'-search').value.toLocaleLowerCase(locale).trim();
92
+ const rows=source.map((item,index)=>({item,index,title:first(item,kind==='activity'?['operation']:['title','name','summary','content','text','id'])||t('untitled'),detail:first(item,['project','project_name','description','created_at','createdAt'])}));
93
+ const filtered=rows.filter(row=>JSON.stringify(row.item).toLocaleLowerCase(locale).includes(search));
94
+ const target=el(kind+'-list');target.replaceChildren();
95
+ const visible=pageRows(kind,filtered,source.length);
96
+ if(!visible.length)return empty(target,t(source.length?'noResults':kind==='memory'?'memoriesEmpty':kind==='project'?'projectsEmpty':'activityEmpty'));
97
+ for(const row of visible) {
98
+ const wrap=node('div','row'),body=node('div','row-body'),title=node('button','row-title row-open',row.title.slice(0,600));
99
+ title.type='button';title.addEventListener('click',()=>showDetails(row.title,row.item,button));
100
+ body.append(title);
101
+ if(kind==='activity') {
102
+ const item=object(row.item);body.append(node('div','row-meta',(item.ok===true?t('success'):t('failed'))+(typeof item.durationMs==='number'?' · '+Math.round(item.durationMs)+' ms':'')));
103
+ wrap.append(node('span','row-marker'+(item.ok===true?'':' failed')));
104
+ } else if(row.detail)body.append(node('div','row-meta',row.detail.slice(0,200)));
105
+ const actions=node('div','row-actions'),button=viewButton(row.title,row.item,kind,row.index);actions.append(button);body.append(actions);wrap.append(body);
106
+ if(kind==='activity')wrap.append(node('time','row-time',date(object(row.item).time)));
107
+ target.append(wrap);
108
+ }
109
+ }
110
+ function engineReason(engine) {
111
+ const known={
112
+ 'Update the official CLI to a version supporting isolated context generation.':'engineUpdateCli',
113
+ 'Sign in with a Claude subscription using claude auth login.':'engineClaudeLogin',
114
+ 'Sign in with ChatGPT using codex login.':'engineCodexLogin',
115
+ 'Official CLI or subscription login unavailable. Install or sign in using the provider CLI.':'engineCliUnavailable',
116
+ 'Vault AI Resume is not available for this account.':'engineVaultUnavailable',
117
+ 'Vault AI Resume is not configured or unavailable for this account.':'engineVaultUnavailable',
118
+ 'Provider readiness is checked when generating.':'engineProviderCheck',
119
+ 'Managed observer is not configured.':'engineObserverMissing',
120
+ 'Configure an OpenRouter API key with vault-go engine key openrouter.':'engineOpenrouterKey',
121
+ 'Configure a Gemini API key with vault-go engine key gemini.':'engineGeminiKey'
122
+ };
123
+ const key=known[scalar(engine.reason)];
124
+ if(key)return (engine.available!==true&&key==='engineProviderCheck'?t('engineUnavailable')+'. ':'')+t(key);
125
+ return t(engine.available===true?'engineAvailable':'engineUnavailable');
126
+ }
127
+ function engineBilling(engine) {
128
+ const key={'vault-ai-resume':'billingVault','claude-subscription':'billingClaude','openai-subscription':'billingOpenai','openrouter':'billingOpenrouter','gemini':'billingGemini'}[engine.id];
129
+ return t(key||'contextBilling');
130
+ }
131
+ function keyStorageLabel(value) {
132
+ const key={'os-keychain':'keyOs','macos-keychain':'keyOs','private-file':'keyFile','ephemeral':'keyEphemeral','unavailable':'keyUnavailable'}[value];
133
+ return t(key||'unknown');
134
+ }
135
+ function renderEngines() {
136
+ const target=el('engine-options');target.replaceChildren();target.setAttribute('aria-label',t('contextEngine'));
137
+ for(const engine of array(engines && engines.engines)) {
138
+ const label=node('label','engine-option'),input=node('input',''),name=node('strong','',scalar(engine.name)||scalar(engine.id));
139
+ input.type='radio';input.name='context-engine';input.value=engine.id;input.checked=engines.selected===engine.id;input.disabled=engineBusy||!!contextJob||contextStarting||engine.available!==true;
140
+ input.addEventListener('change',async()=>{
141
+ if(!input.checked||engineBusy)return;const epoch=sessionEpoch;engineBusy=true;engineMessage='';renderEngines();
142
+ try {const result=await request('/api/context/engine',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({engine:engine.id})});if(result&&!sessionRequired&&epoch===sessionEpoch&&engines){engines.selected=engine.id;engineMessage='engineSaved';}}
143
+ catch {if(epoch===sessionEpoch)engineMessage='engineFailed';}
144
+ finally{engineBusy=false;render();}
145
+ });
146
+ label.append(input,name,node('p','',engineReason(engine)));if(engine.billing)label.append(node('p','',engineBilling(engine)));target.append(label);
147
+ }
148
+ text('engine-message',engineMessage?t(engineMessage):!engines?t('engineLoading'):'');
149
+ const keys=el('engine-keys');keys.replaceChildren();
150
+ if(engines) for(const provider of ['openrouter','gemini']) {
151
+ const wrap=node('div',''),label=node('label','',t(provider==='openrouter'?'openrouterKey':'geminiKey')),input=node('input',''),button=node('button','button',t('saveKey'));
152
+ const engine=array(engines.engines).find(item=>item.id===provider);
153
+ input.type='password';input.autocomplete='off';input.id=provider+'-key';input.placeholder=engine&&engine.available===true?t('keyConfigured'):t('keyMissing');
154
+ button.type='button';button.disabled=engineBusy||!!contextJob||contextStarting;
155
+ button.addEventListener('click',async()=>{
156
+ const key=input.value.trim();if(!key||engineBusy)return;const epoch=sessionEpoch;engineBusy=true;engineMessage='';renderEngines();
157
+ try {await request('/api/context/secret',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider,key})});input.value='';engines=await request('/api/context/engines');if(epoch===sessionEpoch)engineMessage='keySaved';}
158
+ catch{if(epoch===sessionEpoch)engineMessage='keyFailed';}
159
+ finally{engineBusy=false;render();}
160
+ });
161
+ wrap.append(label,input,button);keys.append(wrap);
162
+ }
163
+ const selected=array(engines&&engines.engines).find(engine=>engine.id===engines.selected);
164
+ el('context-preview').disabled=!!contextJob||contextStarting||engineBusy||!selected||selected.available!==true;
165
+ el('context-stop').hidden=!contextJob;el('context-stop').disabled=contextStarting;
166
+ text('context-progress',contextMessage?t(contextMessage):'');
40
167
  }
41
168
  function render() {
42
169
  document.documentElement.lang = locale; el('locale').value = locale;
43
170
  document.querySelectorAll('[data-i18n]').forEach(n => { n.textContent=t(n.dataset.i18n); });
44
171
  document.querySelectorAll('[data-placeholder]').forEach(n => { n.placeholder=t(n.dataset.placeholder); n.setAttribute('aria-label',t(n.dataset.placeholder)); });
45
- el('session-notice').hidden = !sessionRequired;
46
- el('refresh').disabled = busy || sessionRequired;
172
+ const hasSession = !!state && state.authenticated === true && !sessionRequired && !connecting;
173
+ el('onboarding').hidden = hasSession;
174
+ el('dashboard').hidden = !hasSession;
175
+ if(!hasSession && el('detail-dialog').open)el('detail-dialog').close();
176
+ if(!hasSession){engines=null;enginesChecked=0;clearContext();}
177
+ renderEngines();
178
+ const security=object(state&&state.security),cache=object(security.cache),device=object(state&&state.device),facts=el('security-facts');facts.replaceChildren();
179
+ if(hasSession){
180
+ const cacheLine=node('p','',t(cache.encrypted===true?'cacheEncrypted':'cacheUnverified'));facts.append(cacheLine);
181
+ if(cache.keyStorage)facts.append(node('p','',t('keyStorage')+': '+keyStorageLabel(cache.keyStorage)));
182
+ if(security.credentials==='file-permissions')facts.append(node('p','',t('credentialProtection')));
183
+ const transport=object(security.transport);
184
+ if(transport.local==='loopback-http'&&transport.cloud==='https')facts.append(node('p','',t('transportProtection')));
185
+ facts.append(node('p','',t('noEndToEnd')));
186
+ const deviceLabel=({online:'deviceOnline',offline:'deviceOffline',revoked:'deviceRevoked',unregistered:'deviceUnregistered'})[device.status]||'unknown';
187
+ facts.append(node('p','',t('device')+': '+(scalar(device.name)?scalar(device.name)+' · ':'')+t(deviceLabel)+(device.lastSeenAt?' · '+t('lastSeen')+' '+date(device.lastSeenAt):'')));
188
+ if(device.error)facts.append(node('p','error',scalar(device.error)));
189
+ }
190
+ el('refresh').hidden = !hasSession;
191
+ el('reconnect').hidden = !hasSession;
192
+ el('logout').hidden = !hasSession;
193
+ el('logout').disabled=disconnecting;el('reconnect').disabled=disconnecting;el('disconnect-device').disabled=disconnecting;
194
+ document.querySelector('.nav').hidden = !hasSession;
195
+ document.querySelector('.rail-label').hidden = !hasSession;
196
+ el('connect').disabled = connecting;
197
+ el('verify-connection').disabled = busy || connecting;
198
+ text('mcp-command','bunx --bun vault-go@latest setup --lang ' + locale);
199
+ text('connection-message',connectionMessage ? t(connectionMessage) : '');
200
+ el('first-memory').hidden = array(state && state.memories).length > 0;
201
+ el('refresh').disabled = busy || sessionRequired || disconnecting;
47
202
  text('error',failure ? t(failure) : '');
48
203
  text('local-status',sessionRequired ? t('waiting') : state ? t('online') : failure ? t('offline') : t('checking'));
49
204
  el('local-status').className='card-value ' + (state ? 'good' : '');
@@ -52,30 +207,174 @@ export function dashboardScript() {
52
207
  text('cloud-status',state ? t(cloud.connected === true ? 'connected' : 'disconnected') : '—');
53
208
  el('cloud-status').className='card-value ' + (cloud.connected === true ? 'good' : '');
54
209
  text('cloud-note',state ? (cloud.error ? t('syncError') : '') || scalar(state.email) || (state.authenticated ? 'Vault Cloud' : t('signedOut')) : '—');
55
- const activity=array(state && state.activity); text('activity-total',state ? String(activity.length) : '—'); text('activity-count',String(activity.length));
56
- const target=el('activity-list'); target.replaceChildren();
57
- if (!activity.length) empty(target,t('activityEmpty'));
58
- for (const item of activity.slice(0,50)) { const a=object(item),wrap=node('div','row'),body=node('div','row-body');body.append(node('div','row-title',scalar(a.operation) || t('unknown')),node('div','row-meta',(a.ok === true ? t('success') : t('failed')) + (typeof a.durationMs === 'number' && Number.isFinite(a.durationMs) ? ' · ' + Math.round(a.durationMs) + ' ms' : '')));wrap.append(node('span','row-marker'+(a.ok === true ? '' : ' failed')),body,node('time','row-time',date(a.time)));target.append(wrap); }
210
+ const activity=array(state && state.activity); text('activity-total',state ? String(activity.length) : '—');
211
+ list('activity');
59
212
  list('memory'); list('project'); text('updated',state && state.updatedAt ? t('updated') + ' ' + date(state.updatedAt) : t('waiting'));
60
213
  }
61
214
  async function request(path, options) {
62
215
  const response=await fetch(path,{...options,credentials:'same-origin',cache:'no-store'});
63
- if (response.status === 401) { sessionRequired=true; state=null; failure=''; return null; }
216
+ if (response.status === 401) { if(disconnecting)return null; sessionRequired=true; state=null; failure=''; render(); return null; }
64
217
  if (!response.ok) throw new Error('request failed');
65
218
  return response.json();
66
219
  }
67
220
  async function refresh(sync=false) {
68
- if (busy || document.hidden || sessionRequired) return;
221
+ if (busy || document.hidden || sessionRequired || connecting || disconnecting) return;
222
+ const epoch=sessionEpoch;
69
223
  busy=true; failure=''; render();
70
224
  try {
71
225
  if (sync) await request('/api/sync',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
72
- if (!sessionRequired) { const next=await request('/api/status'); if (next && next.service === 'vault-go') state=next; else if (!sessionRequired) throw new Error('invalid status'); }
73
- } catch { failure=sync ? 'syncError' : 'fetchError'; if (!sync) state=null; }
226
+ if (!sessionRequired) { const next=await request('/api/status'); if(epoch!==sessionEpoch||sessionRequired||disconnecting)return; if (next && next.service === 'vault-go') state=next; else throw new Error('invalid status'); }
227
+ if(state && state.authenticated && !sessionRequired && !engineBusy && (sync||!enginesChecked||Date.now()-enginesChecked>60000)) {
228
+ enginesChecked=Date.now();
229
+ try {const result=await request('/api/context/engines');if(result&&epoch===sessionEpoch&&!sessionRequired){engines=result;if(engineMessage==='engineLoadFailed')engineMessage='';}} catch {if(!engines&&epoch===sessionEpoch&&!sessionRequired)engineMessage='engineLoadFailed';}
230
+ }
231
+ } catch { if(epoch===sessionEpoch&&!disconnecting){failure=sync ? 'syncError' : 'fetchError'; if (!sync) state=null;} }
74
232
  finally { busy=false; render(); if(pendingRefresh) {pendingRefresh=false; void refresh(true);} }
75
233
  }
234
+ function stopConnecting(message) {
235
+ connecting=false; connectionMessage=message;
236
+ if(message && state && state.authenticated === true) failure=message;
237
+ if(connectionTimer) clearTimeout(connectionTimer);
238
+ connectionTimer=null; render();
239
+ }
240
+ async function pollConnection() {
241
+ if(!connecting) return;
242
+ if(Date.now()-connectionStarted > 5*60*1000) { stopConnecting('connectExpired'); return; }
243
+ if(document.hidden) { connectionTimer=setTimeout(pollConnection,2000); return; }
244
+ try {
245
+ const response=await fetch('/api/connect/status',{credentials:'same-origin',cache:'no-store'});
246
+ if(!response.ok) throw new Error('connection status failed');
247
+ const result=await response.json();
248
+ if(result.status === 'connected') {
249
+ sessionRequired=false; el('auth-fallback').hidden=true; stopConnecting('');
250
+ await refresh(); return;
251
+ }
252
+ if(result.status === 'failed' || result.status === 'idle') { stopConnecting('connectExpired'); return; }
253
+ if(result.status !== 'pending') throw new Error('invalid connection state');
254
+ connectionTimer=setTimeout(pollConnection,2000);
255
+ } catch { stopConnecting('connectFailed'); }
256
+ }
257
+ el('connect').addEventListener('click',async () => {
258
+ if(connecting) return;
259
+ const popup=window.open('about:blank','_blank','popup,width=560,height=760');
260
+ if(popup) popup.opener=null;
261
+ connecting=true; connectionStarted=Date.now(); connectionMessage='connecting'; failure='';
262
+ el('auth-fallback').hidden=true; render();
263
+ try {
264
+ const response=await fetch('/api/connect',{method:'POST',credentials:'same-origin',cache:'no-store',headers:{'Content-Type':'application/json'},body:JSON.stringify({locale})});
265
+ if(!response.ok) throw new Error('connection failed');
266
+ const result=await response.json();
267
+ const url=new URL(result.url);
268
+ if(url.protocol !== 'https:' || url.hostname !== 'vault.resolveup.com.br' || url.username || url.password || (url.port && url.port !== '443')) throw new Error('invalid authentication URL');
269
+ el('auth-fallback').href=url.href; el('auth-fallback').hidden=false;
270
+ if(popup && !popup.closed) { popup.location.replace(url.href); connectionMessage='awaitingAuth'; }
271
+ else connectionMessage='popupBlocked';
272
+ render(); connectionTimer=setTimeout(pollConnection,1000);
273
+ } catch {if(popup && !popup.closed) popup.close();stopConnecting('connectFailed');}
274
+ });
275
+ el('reconnect').addEventListener('click',() => el('connect').click());
276
+ el('verify-connection').addEventListener('click',() => {sessionRequired=false; void refresh();});
76
277
  el('locale').addEventListener('change',event => {locale=event.target.value;try{localStorage.setItem('vault-local-locale',locale);}catch{}render();});
77
278
  el('refresh').addEventListener('click',() => { if (busy) pendingRefresh=true; else void refresh(true); });
78
- el('memory-search').addEventListener('input',() => list('memory')); el('project-search').addEventListener('input',() => list('project'));
279
+ for(const kind of ['activity','memory','project'])el(kind+'-search').addEventListener('input',()=>{pagination[kind].page=1;list(kind);});
280
+ function clearContext() {
281
+ if(contextTimer)clearTimeout(contextTimer);contextTimer=null;contextJob=null;contextPreview=null;contextResult=null;contextStarting=false;contextMessage='';
282
+ el('context-text').value='';text('context-confirm-text','');text('context-confirm-engine','');text('context-confirm-billing','');
283
+ el('context-consent').checked=false;el('context-confirm-dialog').close();el('detail-copy').hidden=true;
284
+ }
285
+ async function cancelContextJob(message='contextCanceled') {
286
+ const job=contextJob;contextJob=null;
287
+ if(contextTimer)clearTimeout(contextTimer);contextTimer=null;contextMessage='contextCanceling';contextStarting=true;renderEngines();
288
+ try{if(job)await request('/api/context/jobs/'+encodeURIComponent(job),{method:'DELETE',signal:AbortSignal.timeout(2000)});contextMessage=message;}
289
+ catch{contextMessage='contextCancelFailed';}
290
+ finally{contextStarting=false;renderEngines();}
291
+ }
292
+ el('context-stop').addEventListener('click',()=>void cancelContextJob());
293
+ el('context-preview').addEventListener('click',()=>{
294
+ const selected=array(engines&&engines.engines).find(engine=>engine.id===engines.selected),source=el('context-text').value;
295
+ if(!selected||selected.available!==true||!source.trim()||source.length>16000){contextMessage='contextInvalid';renderEngines();return;}
296
+ contextPreview={text:source,engine:selected.id};
297
+ text('context-confirm-text',source);text('context-confirm-engine',selected.name);text('context-confirm-billing',engineBilling(selected));
298
+ el('context-consent').checked=false;el('context-generate').disabled=true;
299
+ el('context-confirm-dialog').showModal();el('context-cancel').focus();
300
+ });
301
+ el('context-cancel').addEventListener('click',()=>el('context-confirm-dialog').close());
302
+ el('context-confirm-dialog').addEventListener('close',()=>{contextPreview=null;text('context-confirm-text','');text('context-confirm-engine','');text('context-confirm-billing','');if(!sessionRequired&&!el('dashboard').hidden)el('context-preview').focus();});
303
+ el('context-consent').addEventListener('change',()=>{el('context-generate').disabled=!el('context-consent').checked||contextStarting;});
304
+ async function pollContextJob(epoch) {
305
+ if(!contextJob||sessionRequired||epoch!==sessionEpoch)return;
306
+ if(Date.now()-contextStarted>100000){await cancelContextJob('contextTimedOut');return;}
307
+ const job=contextJob;
308
+ try {
309
+ const result=await request('/api/context/jobs/'+encodeURIComponent(job));
310
+ if(epoch!==sessionEpoch||sessionRequired||contextJob!==job)return;
311
+ if(!result)throw new Error('missing job');
312
+ if(result.status==='completed'){
313
+ contextJob=null;contextMessage='contextComplete';renderEngines();
314
+ const value=object(result.result);showDetails(first(value,['title'])||t('details'),value,el('context-preview'));
315
+ contextResult=value;el('detail-copy').hidden=false;return;
316
+ }
317
+ if(result.status!=='running')throw new Error('generation failed');
318
+ contextTimer=setTimeout(()=>void pollContextJob(epoch),2000);
319
+ } catch {if(epoch===sessionEpoch&&!sessionRequired&&contextJob===job){contextJob=null;contextMessage='contextFailed';renderEngines();}}
320
+ }
321
+ el('context-generate').addEventListener('click',async()=>{
322
+ if(!contextPreview||!el('context-consent').checked||contextStarting||contextJob)return;
323
+ const payload={...contextPreview,consent:true},epoch=sessionEpoch;
324
+ contextStarting=true;contextMessage='contextRunning';el('context-generate').disabled=true;el('context-confirm-dialog').close();renderEngines();
325
+ try {
326
+ const result=await request('/api/context/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
327
+ if(epoch!==sessionEpoch||sessionRequired)return;
328
+ if(!result||typeof result.jobId!=='string'||!result.jobId)throw new Error('missing job');
329
+ contextJob=result.jobId;contextStarted=Date.now();contextTimer=setTimeout(()=>void pollContextJob(epoch),2000);
330
+ } catch {if(epoch===sessionEpoch&&!sessionRequired)contextMessage='contextFailed';}
331
+ finally{contextStarting=false;contextPreview=null;text('context-confirm-text','');render();}
332
+ });
333
+ el('detail-copy').addEventListener('click',async()=>{
334
+ if(!contextResult)return;
335
+ try{await navigator.clipboard.writeText(JSON.stringify(contextResult,null,2));contextMessage='copied';}
336
+ catch{contextMessage='copyFailed';}
337
+ renderEngines();
338
+ });
339
+ function resetLocalSession() {
340
+ sessionRequired=true;state=null;engines=null;engineMessage='';failure='';pendingRefresh=false;connecting=false;connectionMessage='';
341
+ if(connectionTimer)clearTimeout(connectionTimer);connectionTimer=null;
342
+ el('auth-fallback').hidden=true;el('auth-fallback').removeAttribute('href');
343
+ for(const kind of ['activity','memory','project']){el(kind+'-search').value='';pagination[kind].page=1;pagination[kind].size=10;}
344
+ clearContext();detailFocus='';el('detail-dialog').close();el('detail-fields').replaceChildren();text('detail-title','');
345
+ el('logout-dialog').close();el('disconnect-dialog').close();render();el('connect').focus();
346
+ }
347
+ el('disconnect-device').addEventListener('click',()=>{text('disconnect-error','');el('disconnect-dialog').showModal();el('disconnect-cancel').focus();});
348
+ el('disconnect-cancel').addEventListener('click',()=>el('disconnect-dialog').close());
349
+ el('disconnect-dialog').addEventListener('cancel',event=>{if(disconnecting)event.preventDefault();});
350
+ el('disconnect-dialog').addEventListener('close',()=>{text('disconnect-error','');if(!sessionRequired)el('disconnect-device').focus();});
351
+ el('disconnect-confirm').addEventListener('click',async()=>{
352
+ if(disconnecting)return;
353
+ disconnecting=true;el('disconnect-confirm').disabled=true;el('disconnect-cancel').disabled=true;text('disconnect-error','');render();
354
+ try {
355
+ const response=await fetch('/api/device/disconnect',{method:'POST',credentials:'same-origin',cache:'no-store',headers:{'Content-Type':'application/json'},body:'{}',signal:AbortSignal.timeout(30000)});
356
+ if(!response.ok){text('disconnect-error',t(response.status===409?'disconnectLoginBusy':'disconnectFailed'));return;}
357
+ const result=await response.json();if(result.ok!==true)throw new Error('not confirmed');
358
+ sessionEpoch++;disconnecting=false;resetLocalSession();
359
+ } catch {text('disconnect-error',t('disconnectFailed'));}
360
+ finally{disconnecting=false;el('disconnect-confirm').disabled=false;el('disconnect-cancel').disabled=false;render();}
361
+ });
362
+ el('logout').addEventListener('click',()=>{el('logout-dialog').showModal();el('logout-cancel').focus();});
363
+ el('logout-cancel').addEventListener('click',()=>el('logout-dialog').close());
364
+ el('logout-dialog').addEventListener('close',()=>{if(!sessionRequired)el('logout').focus();});
365
+ el('logout-confirm').addEventListener('click',async()=>{
366
+ el('logout-confirm').disabled=true;
367
+ sessionEpoch++;
368
+ try {
369
+ if(contextJob)await cancelContextJob();
370
+ const result=await request('/api/logout',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
371
+ if(result && result.ok!==true)throw new Error('logout failed');
372
+ resetLocalSession();
373
+ } catch {failure='logoutFailed';el('logout-dialog').close();render();}
374
+ finally{el('logout-confirm').disabled=false;}
375
+ });
376
+ el('detail-close').addEventListener('click',()=>el('detail-dialog').close());
377
+ el('detail-dialog').addEventListener('close',()=>{const trigger=el(detailFocus);if(trigger)trigger.focus();el('detail-fields').replaceChildren();text('detail-title','');contextResult=null;el('detail-copy').hidden=true;});
79
378
  document.querySelectorAll('.nav a').forEach(link => link.addEventListener('click',event => {event.preventDefault(); document.querySelectorAll('.nav a').forEach(n=>n.classList.remove('active'));link.classList.add('active');document.querySelector(link.getAttribute('href')).scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth'});}));
80
379
  document.addEventListener('visibilitychange',() => {if(!document.hidden) void refresh();});
81
380
  async function start() {
@@ -0,0 +1,22 @@
1
+ import { VaultCloudClient } from "./cloud.js";
2
+ import type { privateStore } from "./private-store.js";
3
+ type DeviceCloud = Pick<VaultCloudClient, "registerDevice" | "rotateDevice" | "deviceAction">;
4
+ export type LocalDeviceStatus = {
5
+ status: "online" | "offline" | "revoked" | "unregistered";
6
+ name?: string;
7
+ lastSeenAt?: string;
8
+ error?: string;
9
+ };
10
+ export declare function localDevice(home: string, cloud: DeviceCloud, storage: ReturnType<typeof privateStore> | undefined, version: string): {
11
+ heartbeat: () => Promise<void>;
12
+ close(): void;
13
+ status: () => {
14
+ status: "online" | "offline" | "revoked" | "unregistered";
15
+ name?: string;
16
+ lastSeenAt?: string;
17
+ error?: string;
18
+ };
19
+ disconnect(): Promise<void>;
20
+ reauthorize(): Promise<void>;
21
+ };
22
+ export {};