@openparachute/hub 0.6.4-rc.4 → 0.6.4-rc.5

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.
@@ -117,15 +117,16 @@ export interface RenderAccountHomeOpts {
117
117
  */
118
118
  twoFactorEnabled: boolean;
119
119
  /**
120
- * Per-vault mintable verbs for the "mint an access token" affordance on
121
- * each vault tile (friend headless-client path). Maps `vaultName` → the
122
- * verbs the user's assignment role permits (today always
123
- * `["read", "write"]` since every `user_vaults.role` is `'write'`). A
124
- * vault absent from this map (or mapped to an empty list) renders no mint
125
- * affordance — the UI never offers a verb the server would reject. The
126
- * `/account/` GET handler builds this from `vaultVerbsForUserVault` for
127
- * each assigned vault. Omitted (or empty) for the admin / no-vault
128
- * branches, where no token-mint tile is shown.
120
+ * Per-vault verbs the user's assignment role permits. Maps `vaultName` → the
121
+ * verbs (today `["read", "write", "admin"]` since every `user_vaults.role` is
122
+ * `'write'`, which grants admin). Now used solely to GATE the per-tile
123
+ * "Advanced vault settings ↗" deep-link (and the "Back up to GitHub ↗" action)
124
+ * on the `admin` verb the deep-link mints a `vault:<name>:admin` token, so
125
+ * the button never offers authority the POST handler would 403. (The old
126
+ * token-mint affordance this map also drove was dropped from `/account/`
127
+ * 2026-06-04 OAuth-first; minting header-auth tokens is an advanced concern
128
+ * that lives in the vault config SPA.) Omitted (or empty) for the admin /
129
+ * no-vault branches, where no vault tile is shown.
129
130
  */
130
131
  mintableVerbs?: Record<string, VaultVerb[]>;
131
132
  /**
@@ -137,18 +138,46 @@ export interface RenderAccountHomeOpts {
137
138
  * that don't resolve). Omitted entirely on the admin / no-vault branches.
138
139
  */
139
140
  usageStats?: Record<string, string>;
141
+ /**
142
+ * Per-vault backup (mirror) line for each assigned vault tile. Maps
143
+ * `vaultName` → the warm, pre-formatted line ("Backed up — full version
144
+ * history", or "… + GitHub" when a push remote is configured). A vault absent
145
+ * from this map renders no backup line — the page tolerates a vault whose
146
+ * mirror endpoint failed / is unreachable / is backup-off (the `/account/` GET
147
+ * handler builds this map by fetching `/.parachute/mirror` per admin-held
148
+ * vault and omitting any that don't resolve or read backup-off). Omitted
149
+ * entirely on the admin / no-vault branches.
150
+ */
151
+ mirrorLines?: Record<string, string>;
152
+ /**
153
+ * Per-vault "is backup already pushing to a remote?" flag (the vault's
154
+ * `config.auto_push`, threaded from `VaultMirrorStat.backedUpToRemote`). Maps
155
+ * `vaultName` → `true` when an auto-push remote is configured. Drives whether
156
+ * the tile suppresses the "Back up to GitHub ↗" action — gated on this proper
157
+ * boolean, NOT re-derived from the `mirrorLines` display string. A vault absent
158
+ * defaults to `false` (offer the action). Built alongside `mirrorLines` by the
159
+ * GET handler; omitted on the admin / no-vault branches.
160
+ */
161
+ mirrorPushing?: Record<string, boolean>;
140
162
  /**
141
163
  * Set after a successful `POST /account/vault-token/<name>` to show the
142
164
  * freshly-minted token ONCE (the only time it's ever shown — the hub keeps
143
165
  * no plaintext copy). Drives the show-once banner at the top of the page.
144
166
  * Absent on the normal GET render.
167
+ *
168
+ * NOT vestigial after the 2026-06-04 token-mint-UI removal: the page no
169
+ * longer renders the mint *form*, but the `POST /account/vault-token/<name>`
170
+ * route still exists (a script/advanced path) and on success re-renders THIS
171
+ * page with `mintedToken` set, so the show-once banner still fires for that
172
+ * flow. The renderer keeps the prop + banner for it.
145
173
  */
146
174
  mintedToken?: MintedTokenView;
147
175
  /**
148
176
  * Set after a `POST /account/vault-token/<name>` that failed authorization
149
177
  * or validation, to surface an inline error banner on the re-rendered page
150
178
  * (e.g. unassigned vault, capped verb, rate-limited). Absent on success and
151
- * on the normal GET render.
179
+ * on the normal GET render. Same non-vestigial note as `mintedToken`: the
180
+ * mint route still re-renders this page, so the error banner stays live.
152
181
  */
153
182
  mintError?: string;
154
183
  /**
@@ -210,6 +239,11 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
210
239
  // condenses to a quiet "you're connected" line so it stops nagging. Shown
211
240
  // only on the assigned-vault branch — the admin + no-vault branches have no
212
241
  // single "your vault" to connect, so the checklist would be misleading there.
242
+ // TODO(multi-vault): `connectedVault` is true if ANY of the user's vaults has
243
+ // a grant (handler uses `.some(...)`), but the checklist shows the connect step
244
+ // for the FIRST vault only. With multiple vaults, connecting vault B condenses
245
+ // the checklist even though the displayed primary vault A isn't connected. Fine
246
+ // today — single-vault is the live case; revisit if multi-vault ships.
213
247
  const checklist =
214
248
  assignedVaults.length > 0
215
249
  ? renderOnboardingChecklist({
@@ -226,6 +260,8 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
226
260
  csrfToken,
227
261
  mintableVerbs: opts.mintableVerbs ?? {},
228
262
  usageStats: opts.usageStats ?? {},
263
+ mirrorLines: opts.mirrorLines ?? {},
264
+ mirrorPushing: opts.mirrorPushing ?? {},
229
265
  });
230
266
 
231
267
  const accountCard = renderAccountCard({
@@ -352,7 +388,7 @@ function renderOnboardingChecklist(opts: OnboardingChecklistOpts): string {
352
388
  </li>
353
389
  </ol>
354
390
  <p class="onboarding-foot" data-testid="onboarding-foot">Your vault is
355
- <code>${safeVault}</code>. Full connect details, Notes, backup, and access tokens are
391
+ <code>${safeVault}</code>. Its size, backup state, Notes, and advanced settings are
356
392
  just below.</p>
357
393
  </section>`;
358
394
  }
@@ -403,6 +439,10 @@ interface VaultCardOpts {
403
439
  mintableVerbs: Record<string, VaultVerb[]>;
404
440
  /** `vaultName` → pre-formatted usage stat ("X notes · Y MB"). */
405
441
  usageStats: Record<string, string>;
442
+ /** `vaultName` → pre-formatted backup line ("Backed up — full version history"). */
443
+ mirrorLines: Record<string, string>;
444
+ /** `vaultName` → is backup already pushing to a remote (gates the GitHub action). */
445
+ mirrorPushing: Record<string, boolean>;
406
446
  }
407
447
 
408
448
  /**
@@ -434,84 +474,56 @@ export function accountClaudeMcpAddCommand(trimmedOrigin: string, vaultName: str
434
474
  function renderVaultCard(opts: VaultCardOpts): string {
435
475
  const { assignedVaults, trimmedOrigin, isFirstAdmin, csrfToken, mintableVerbs, usageStats } =
436
476
  opts;
477
+ const { mirrorLines, mirrorPushing } = opts;
437
478
 
438
479
  if (assignedVaults.length > 0) {
439
- // One vault tile per assignment (multi-user Phase 2 PR 2). Each tile
440
- // leads with a friendly "connect your AI assistant to this vault" block
441
- // that covers BOTH connect paths a non-technical friend is likely to
442
- // use Claude Code (the `claude mcp add` CLI command) and Claude.ai on
443
- // the web (Settings Connectors Add custom connector, pointed at the
444
- // endpoint). Both are the OAuth path no token to paste, the first
445
- // connection opens a browser to sign in + approve. The Notes "Open" CTA
446
- // sits alongside as the browser-UI option. Phrasing mirrors
447
- // parachute.computer/install.njk's #connect-mcp-clients section so the
448
- // operator docs and the friend's account page stay consistent.
449
- //
450
- // This closes the multi-user gap where the friend tile read as MCP
451
- // jargon ("Connect an MCP client") rather than "here's how to connect
452
- // this to your AI" — and where the web (Claude.ai) path was entirely
453
- // missing, only the Claude Code CLI command was offered.
480
+ // One vault tile per assignment (multi-user Phase 2 PR 2). The tile is the
481
+ // everyday "here's your vault" detail card name, size, backup state, a
482
+ // browser-UI on-ramp (Notes + "build your own"), and a single deep-link
483
+ // into the advanced vault settings SPA. It deliberately does NOT repeat the
484
+ // "Connect your AI" instructions: the onboarding checklist above owns that
485
+ // step (collapsing to "✓ You're connected" once a grant lands), so the
486
+ // page never shows the connect endpoint + both methods twice. Token minting
487
+ // + raw mirror config are advanced concerns that live in the vault config
488
+ // SPA, reached via "Advanced vault settings ↗" — not duplicated here.
454
489
  const heading = assignedVaults.length === 1 ? "<h2>Your vault</h2>" : "<h2>Your vaults</h2>";
455
490
  const tiles = assignedVaults
456
491
  .map((vaultName) => {
457
492
  const safeVault = escapeHtml(vaultName);
458
493
  const vaultUrlForAdd = encodeURIComponent(`${trimmedOrigin}/vault/${vaultName}`);
459
- const endpoint = accountMcpEndpoint(trimmedOrigin, vaultName);
460
- const addCmd = accountClaudeMcpAddCommand(trimmedOrigin, vaultName);
461
- const safeEndpoint = escapeHtml(endpoint);
462
- const safeAddCmd = escapeHtml(addCmd);
463
494
  const verbsForVault = mintableVerbs[vaultName] ?? [];
464
- const tokenMintBlock = renderTokenMintBlock(vaultName, safeVault, verbsForVault, csrfToken);
465
- // "Configure / back up this vault ↗" — only for users whose assignment
466
- // grants `admin` (the verb the deep-link mints). Today every assigned
467
- // user holds admin, but gate on the verb so the button never offers
468
- // authority the POST handler would 403.
469
- const manageBlock = verbsForVault.includes("admin")
470
- ? renderVaultAdminLink(vaultName, csrfToken)
471
- : "";
495
+ const holdsAdmin = verbsForVault.includes("admin");
496
+ // "Advanced vault settings ↗" — only for users whose assignment grants
497
+ // `admin` (the verb the deep-link mints). Today every assigned user
498
+ // holds admin, but gate on the verb so the button never offers
499
+ // authority the POST handler would 403. The single advanced entry point:
500
+ // schema, tokens, retention, raw mirror config all live in the SPA.
501
+ const manageBlock = holdsAdmin ? renderVaultAdminLink(vaultName, csrfToken) : "";
472
502
  // Compact usage stat ("X notes · Y MB"), when the vault's usage endpoint
473
503
  // resolved. Omitted gracefully otherwise.
474
504
  const usageStat = usageStats[vaultName];
475
505
  const usageLine = usageStat
476
506
  ? `<p class="vault-usage" data-testid="vault-usage">${escapeHtml(usageStat)}</p>`
477
507
  : "";
508
+ // Backup state line + a "Back up to GitHub ↗" deep-link when not already
509
+ // pushing. Both gated on admin: the backup line is only fetched for
510
+ // admin-held vaults (the mirror endpoint is admin-scoped), and the
511
+ // GitHub action reuses the same `/account/vault-admin-token/<name>`
512
+ // deep-link that opens the vault config SPA. Omitted silently when the
513
+ // mirror fetch failed / backup is off (the renderer just gets no entry).
514
+ const mirrorLine = mirrorLines[vaultName];
515
+ const backupBlock = renderBackupBlock(
516
+ vaultName,
517
+ mirrorLine,
518
+ mirrorPushing[vaultName] ?? false,
519
+ holdsAdmin,
520
+ csrfToken,
521
+ );
478
522
  return `
479
523
  <div class="vault-tile" data-testid="vault-tile" data-vault-name="${safeVault}">
480
524
  <p class="vault-name"><strong>${safeVault}</strong></p>
481
525
  ${usageLine}
482
- <div class="mcp-connect" data-testid="mcp-connect">
483
- <p class="mcp-connect-label" data-testid="connect-ai-heading">Connect your AI
484
- assistant to this vault</p>
485
- <p class="mcp-connect-intro">Two common ways. Both sign you in to this hub over
486
- HTTPS and ask you to approve access the first time — no token to copy.</p>
487
-
488
- <div class="mcp-method" data-testid="connect-method-claude-code">
489
- <p class="mcp-method-title">Claude Code (terminal)</p>
490
- <p class="mcp-method-sub">Run this in your terminal:</p>
491
- <div class="copy-row">
492
- <code data-testid="mcp-add-command">${safeAddCmd}</code>
493
- <button type="button" class="btn btn-copy" data-copy="${safeAddCmd}"
494
- data-testid="copy-mcp-add-command">Copy</button>
495
- </div>
496
- </div>
497
-
498
- <div class="mcp-method" data-testid="connect-method-claude-ai">
499
- <p class="mcp-method-title">Claude.ai (web)</p>
500
- <p class="mcp-method-sub">In Claude.ai, open <strong>Settings → Connectors</strong>,
501
- choose <strong>Add custom connector</strong>, and paste this endpoint:</p>
502
- <div class="copy-row">
503
- <code data-testid="mcp-endpoint">${safeEndpoint}</code>
504
- <button type="button" class="btn btn-copy" data-copy="${safeEndpoint}"
505
- data-testid="copy-mcp-endpoint">Copy</button>
506
- </div>
507
- <p class="mcp-method-note">Claude.ai then redirects you here to sign in and
508
- approve. (Your hub must be reachable from the web for this.)</p>
509
- </div>
510
-
511
- <p class="mcp-connect-hint" data-testid="connect-any-client-hint">Using something
512
- else? Point any MCP client at the same endpoint above. (ChatGPT and some other
513
- web UIs call these "connectors.")</p>
514
- </div>
526
+ ${backupBlock}
515
527
  <p class="vault-notes-cta">
516
528
  <a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
517
529
  target="_blank" rel="noopener" data-testid="open-notes-cta">Open Notes ↗</a>
@@ -521,19 +533,21 @@ function renderVaultCard(opts: VaultCardOpts): string {
521
533
  capture in this vault — or jump straight to bulk-importing Markdown/Obsidian
522
534
  notes into it.</span>
523
535
  </p>
536
+ <p class="vault-build-ui" data-testid="build-your-own-ui">Notes is just one way to see
537
+ your vault — when you're ready, your AI can build you a custom UI for it in a few
538
+ minutes. <a href="https://parachute.computer/onboarding/surface-build/"
539
+ target="_blank" rel="noopener" data-testid="build-your-own-ui-link">Build your own ↗</a></p>
524
540
  ${manageBlock}
525
- ${tokenMintBlock}
526
541
  </div>`;
527
542
  })
528
543
  .join("");
529
544
  return `
530
545
  <section class="section" data-testid="vault-card">
531
546
  ${heading}
532
- <p>Connect Claude (or any AI assistant) to your vault${
547
+ <p>Your vault${
533
548
  assignedVaults.length === 1 ? "" : "s"
534
- } — pick Claude Code or
535
- Claude.ai below or open Notes for a browser UI. The first connection signs you in
536
- to your hub over HTTPS and asks you to approve access.</p>
549
+ } at a glance size, backup, and a browser UI. You connect your AI from the
550
+ steps above; the deeper settings live one click away.</p>
537
551
  <div class="vault-tiles">${tiles}
538
552
  </div>
539
553
  </section>`;
@@ -563,83 +577,62 @@ function renderVaultCard(opts: VaultCardOpts): string {
563
577
  }
564
578
 
565
579
  /**
566
- * The "mint an access token (for scripts / headless clients)" affordance on a
567
- * vault tile. Sits BELOW the OAuth connect block + Notes CTA — the no-token
568
- * OAuth path stays the recommended default; this is the secondary, opt-in
569
- * path for clients that can't do an interactive browser sign-in (cron jobs,
570
- * headless agents, a `curl` script).
580
+ * The backup-state block on a vault tile: a warm, plain-language line telling
581
+ * the owner their vault is backed up (local version history, optionally pushed
582
+ * to GitHub), plus a "Back up to GitHub ↗" action when no push remote is set.
571
583
  *
572
- * Renders one radio per verb the user's assignment role permits (`verbs` —
573
- * today always `["read", "write"]`). The UI NEVER offers a verb the server
574
- * would reject: a read-only assignment shows only "Read". An empty `verbs`
575
- * list (unknown / unmappable role) renders nothingfail-closed, matching
576
- * the server's `vaultVerbsForUserVault` returning `[]`.
584
+ * `mirrorLine` is the pre-formatted backup line ("Backed up full version
585
+ * history" / " + GitHub") the GET handler built from the vault's mirror status,
586
+ * or `undefined` when the mirror fetch failed / backup is off / the user doesn't
587
+ * hold admin. When absent, the whole block is omitted silently the everyday
588
+ * home never nags with a "not backed up" warning.
577
589
  *
578
- * The form POSTs `application/x-www-form-urlencoded` to
579
- * `/account/vault-token/<name>` with the CSRF hidden field + a `verb` radio —
580
- * same no-JS-required posture as the change-password and sign-out forms. The
581
- * `<details>` keeps it collapsed by default so the tile leads with the
582
- * recommended OAuth path.
590
+ * The "Back up to GitHub ↗" action reuses the EXISTING
591
+ * `/account/vault-admin-token/<name>` deep-link (the same POST `renderVaultAdminLink`
592
+ * uses) it mints a `vault:<name>:admin` token and opens the vault config SPA,
593
+ * where the GitHub push is configured. We do NOT invent a new auth path. It's
594
+ * shown only when the user holds admin AND `pushing` is false (not already
595
+ * pushing to a remote) — `pushing` is a proper boolean threaded from the mirror
596
+ * status (`VaultMirrorStat.backedUpToRemote`), never re-derived from the
597
+ * display string.
583
598
  */
584
- function renderTokenMintBlock(
599
+ function renderBackupBlock(
585
600
  vaultName: string,
586
- safeVault: string,
587
- verbs: VaultVerb[],
601
+ mirrorLine: string | undefined,
602
+ pushing: boolean,
603
+ holdsAdmin: boolean,
588
604
  csrfToken: string,
589
605
  ): string {
590
- if (verbs.length === 0) return "";
591
- // Path segment is URL-encoded; the action attribute is HTML-escaped on top.
592
- const action = escapeHtml(`/account/vault-token/${encodeURIComponent(vaultName)}`);
593
- const radios = verbs
594
- .map((verb, i) => {
595
- const checked = i === 0 ? " checked" : "";
596
- const label =
597
- verb === "read"
598
- ? "Read-only"
599
- : verb === "admin"
600
- ? "Full (read, write, rotate tokens + config)"
601
- : "Read + write";
602
- return `
603
- <label class="mint-verb-option">
604
- <input type="radio" name="verb" value="${verb}"${checked}
605
- data-testid="mint-verb-${verb}" />
606
- <span><strong>${verb}</strong> — ${label} access to <code>${safeVault}</code></span>
607
- </label>`;
608
- })
609
- .join("");
606
+ if (!mirrorLine) return "";
607
+ // "Back up to GitHub ↗" only when admin (the deep-link mints admin) and not
608
+ // already pushing. Reuses the vault-admin-token deep-link to open the SPA's
609
+ // backup page; no new auth path.
610
+ const action = escapeHtml(`/account/vault-admin-token/${encodeURIComponent(vaultName)}`);
611
+ const githubAction =
612
+ holdsAdmin && !pushing
613
+ ? `
614
+ <form method="POST" action="${action}" class="vault-backup-github"
615
+ data-testid="backup-github-form">
616
+ ${renderCsrfHiddenInput(csrfToken)}
617
+ <button type="submit" class="btn btn-secondary" data-testid="backup-github-button">
618
+ Back up to GitHub ↗
619
+ </button>
620
+ </form>`
621
+ : "";
610
622
  return `
611
- <details class="token-mint" data-testid="token-mint">
612
- <summary data-testid="token-mint-summary">Mint an access token
613
- <span class="token-mint-sub">for scripts / headless clients</span></summary>
614
- <div class="token-mint-body">
615
- <p class="token-mint-intro">Most clients should use the no-token
616
- connect options above — they sign you in over HTTPS and never
617
- ask you to paste a secret. Mint a token only for a script or
618
- headless client that can't open a browser to sign in. It's a
619
- bearer for <code>vault:${safeVault}:&lt;verb&gt;</code>, scoped to
620
- <strong>this vault only</strong>, and you'll see it once.</p>
621
- <form method="POST" action="${action}" class="mint-form"
622
- data-testid="mint-form">
623
- ${renderCsrfHiddenInput(csrfToken)}
624
- <fieldset class="mint-verbs">
625
- <legend>Access level</legend>${radios}
626
- </fieldset>
627
- <button type="submit" class="btn btn-secondary"
628
- data-testid="mint-token-button">Mint token</button>
629
- </form>
630
- </div>
631
- </details>`;
623
+ <div class="vault-backup" data-testid="vault-backup">
624
+ <p class="vault-backup-line" data-testid="backup-state-line">
625
+ <span class="vault-backup-check" aria-hidden="true">✓</span>${escapeHtml(mirrorLine)}</p>${githubAction}
626
+ </div>`;
632
627
  }
633
628
 
634
629
  /**
635
- * The "Configure / back up this vault ↗" affordance on a vault tile. A small
636
- * POST form to `/account/vault-admin-token/<name>` that mints a
637
- * `vault:<name>:admin` deep-link token and redirects into the vault's own admin
638
- * SPA — where the assigned user can rotate vault tokens AND set up Git backup /
639
- * mirror config. Shown only when the user's assignment grants `admin` (gated by
640
- * the caller). A plain form button (no `<details>`) this is a primary admin
641
- * action for an individual user, more prominent than the headless-token mint
642
- * below it.
630
+ * The "Advanced vault settings ↗" affordance on a vault tile the single
631
+ * advanced entry point. A small POST form to `/account/vault-admin-token/<name>`
632
+ * that mints a `vault:<name>:admin` deep-link token and redirects into the
633
+ * vault's own config SPA — where the assigned user can manage schema, rotate
634
+ * access tokens, set retention, and edit raw mirror/backup config. Shown only
635
+ * when the user's assignment grants `admin` (gated by the caller).
643
636
  *
644
637
  * No-JS posture: a same-origin form POST that 303-redirects on success, same
645
638
  * shape as the other `/account/*` forms. CSRF-gated via the hidden field.
@@ -652,10 +645,10 @@ function renderVaultAdminLink(vaultName: string, csrfToken: string): string {
652
645
  data-testid="vault-admin-form">
653
646
  ${renderCsrfHiddenInput(csrfToken)}
654
647
  <button type="submit" class="btn btn-secondary" data-testid="vault-admin-button">
655
- Configure / back up this vault
648
+ Advanced vault settings
656
649
  </button>
657
- <span class="vault-admin-sub">Open this vault's admin tools rotate access tokens,
658
- and set up Git backup (mirror to a repository).</span>
650
+ <span class="vault-admin-sub">Open this vault's configschema, access tokens,
651
+ retention, and raw backup settings.</span>
659
652
  </form>`;
660
653
  }
661
654
 
@@ -766,8 +759,8 @@ const COPY_SCRIPT = `
766
759
  //
767
760
  // Same brand palette + font stack as account-change-password-ui.ts so the
768
761
  // `/account/*` family is visually cohesive. Extra rules (.section, .kv,
769
- // .vault-name, .mcp-connect, .copy-row) describe the new card + MCP
770
- // connect-block shapes this page introduces.
762
+ // .vault-name, .vault-backup, .vault-build-ui, .copy-row) describe the card +
763
+ // backup-state + onboarding-checklist shapes this page introduces.
771
764
 
772
765
  const STYLES = `
773
766
  *, *::before, *::after { box-sizing: border-box; }
@@ -989,6 +982,36 @@ const STYLES = `
989
982
  color: ${PALETTE.fgMuted};
990
983
  margin: 0 0 0.5rem;
991
984
  }
985
+ .vault-backup { margin: 0 0 0.5rem; }
986
+ .vault-backup-line {
987
+ display: flex;
988
+ align-items: center;
989
+ gap: 0.4rem;
990
+ font-size: 0.85rem;
991
+ color: ${PALETTE.success};
992
+ margin: 0;
993
+ }
994
+ .vault-backup-check {
995
+ flex: 0 0 auto;
996
+ width: 1.1rem;
997
+ height: 1.1rem;
998
+ border-radius: 999px;
999
+ background: ${PALETTE.successSoft};
1000
+ color: ${PALETTE.success};
1001
+ border: 1px solid ${PALETTE.success};
1002
+ font-size: 0.7rem;
1003
+ display: inline-flex;
1004
+ align-items: center;
1005
+ justify-content: center;
1006
+ }
1007
+ .vault-backup-github { margin: 0.4rem 0 0; }
1008
+ .vault-build-ui {
1009
+ font-size: 0.82rem;
1010
+ color: ${PALETTE.fgMuted};
1011
+ margin: 0.6rem 0 0;
1012
+ padding-top: 0.6rem;
1013
+ border-top: 1px solid ${PALETTE.borderLight};
1014
+ }
992
1015
  .vault-tiles {
993
1016
  display: flex;
994
1017
  flex-direction: column;
@@ -1004,52 +1027,6 @@ const STYLES = `
1004
1027
  .vault-tile p { margin: 0.2rem 0; }
1005
1028
  .vault-tile p:last-child { margin-top: 0.5rem; }
1006
1029
 
1007
- .mcp-connect {
1008
- margin-bottom: 0.75rem;
1009
- }
1010
- .mcp-connect-label {
1011
- font-family: ${FONT_SERIF};
1012
- font-size: 1.05rem;
1013
- font-weight: 400;
1014
- color: ${PALETTE.fg};
1015
- margin: 0 0 0.3rem;
1016
- }
1017
- .mcp-connect-intro {
1018
- font-size: 0.85rem;
1019
- color: ${PALETTE.fgMuted};
1020
- margin: 0 0 0.75rem;
1021
- }
1022
- .mcp-method {
1023
- margin: 0.75rem 0;
1024
- padding-top: 0.6rem;
1025
- border-top: 1px solid ${PALETTE.borderLight};
1026
- }
1027
- .mcp-method-title {
1028
- font-size: 0.9rem;
1029
- font-weight: 600;
1030
- color: ${PALETTE.fg};
1031
- margin: 0 0 0.15rem;
1032
- }
1033
- .mcp-method-sub {
1034
- font-size: 0.82rem;
1035
- color: ${PALETTE.fgMuted};
1036
- margin: 0 0 0.4rem;
1037
- }
1038
- .mcp-method-note {
1039
- font-size: 0.78rem;
1040
- color: ${PALETTE.fgMuted};
1041
- margin: 0.35rem 0 0;
1042
- }
1043
- .mcp-field { margin: 0.5rem 0; }
1044
- .mcp-field-label {
1045
- display: block;
1046
- font-size: 0.7rem;
1047
- text-transform: uppercase;
1048
- letter-spacing: 0.06em;
1049
- color: ${PALETTE.fgMuted};
1050
- font-family: ${FONT_MONO};
1051
- margin-bottom: 0.2rem;
1052
- }
1053
1030
  .vault-notes-cta {
1054
1031
  margin: 0.9rem 0 0;
1055
1032
  padding-top: 0.6rem;
@@ -1090,11 +1067,6 @@ const STYLES = `
1090
1067
  border-color: ${PALETTE.border};
1091
1068
  }
1092
1069
  .btn-copy:hover { background: ${PALETTE.bgSoft}; border-color: ${PALETTE.accent}; }
1093
- .mcp-connect-hint {
1094
- font-size: 0.82rem;
1095
- color: ${PALETTE.fgMuted};
1096
- margin: 0.4rem 0 0;
1097
- }
1098
1070
 
1099
1071
  .vault-admin-link {
1100
1072
  margin: 0.9rem 0 0;
@@ -1111,53 +1083,6 @@ const STYLES = `
1111
1083
  flex: 1 1 12rem;
1112
1084
  }
1113
1085
 
1114
- .token-mint {
1115
- margin: 0.9rem 0 0;
1116
- padding-top: 0.6rem;
1117
- border-top: 1px solid ${PALETTE.borderLight};
1118
- }
1119
- .token-mint > summary {
1120
- cursor: pointer;
1121
- font-size: 0.88rem;
1122
- font-weight: 600;
1123
- color: ${PALETTE.fg};
1124
- list-style: revert;
1125
- }
1126
- .token-mint-sub {
1127
- font-weight: 400;
1128
- font-size: 0.8rem;
1129
- color: ${PALETTE.fgMuted};
1130
- }
1131
- .token-mint-body { margin-top: 0.6rem; }
1132
- .token-mint-intro {
1133
- font-size: 0.8rem;
1134
- color: ${PALETTE.fgMuted};
1135
- margin: 0 0 0.6rem;
1136
- }
1137
- .mint-verbs {
1138
- border: 1px solid ${PALETTE.borderLight};
1139
- border-radius: 6px;
1140
- padding: 0.5rem 0.7rem;
1141
- margin: 0 0 0.6rem;
1142
- }
1143
- .mint-verbs legend {
1144
- font-size: 0.7rem;
1145
- text-transform: uppercase;
1146
- letter-spacing: 0.06em;
1147
- color: ${PALETTE.fgMuted};
1148
- font-family: ${FONT_MONO};
1149
- padding: 0 0.3rem;
1150
- }
1151
- .mint-verb-option {
1152
- display: flex;
1153
- align-items: baseline;
1154
- gap: 0.5rem;
1155
- font-size: 0.85rem;
1156
- margin: 0.3rem 0;
1157
- }
1158
- .mint-verb-option input { margin: 0; }
1159
- .mint-form .btn { margin-top: 0.2rem; }
1160
-
1161
1086
  .minted-banner {
1162
1087
  border: 1px solid ${PALETTE.accent};
1163
1088
  background: ${PALETTE.accentSoft};
@@ -1266,19 +1191,18 @@ const STYLES = `
1266
1191
  body { background: #1a1815; color: #e8e4dc; }
1267
1192
  .card { background: #25221d; border-color: #3a362f; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); }
1268
1193
  h1, h2 { color: #f0ece4; }
1269
- .subtitle, .kv dt, .mcp-field-label, .mcp-connect-hint,
1270
- .mcp-connect-intro, .mcp-method-sub, .mcp-method-note,
1271
- .vault-notes-cta-sub, .vault-usage,
1194
+ .subtitle, .kv dt,
1195
+ .vault-notes-cta-sub, .vault-usage, .vault-build-ui,
1272
1196
  .onboarding-intro, .onboarding-step-sub, .onboarding-method,
1273
1197
  .onboarding-foot { color: #a8a29a; }
1274
- .vault-name strong, .mcp-connect-label, .mcp-method-title,
1198
+ .vault-name strong,
1275
1199
  .onboarding-step-title, .onboarding-method strong,
1276
1200
  .onboarding-done-line { color: #f0ece4; }
1277
1201
  .onboarding-step-done .onboarding-step-title { color: #a8a29a; }
1278
1202
  code { background: #1f1c18; color: #e8e4dc; }
1279
1203
  .copy-row code { background: transparent; }
1280
1204
  .section { border-top-color: #3a362f; }
1281
- .mcp-method, .vault-notes-cta, .token-mint,
1205
+ .vault-notes-cta, .vault-build-ui,
1282
1206
  .vault-admin-link, .account-security { border-top-color: #3a362f; }
1283
1207
  .vault-admin-sub { color: #a8a29a; }
1284
1208
  .get-started h3 { color: #f0ece4; }
@@ -1291,9 +1215,7 @@ const STYLES = `
1291
1215
  .copy-row { background: #1f1c18; border-color: #3a362f; }
1292
1216
  .btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
1293
1217
  .btn-secondary:hover, .btn-copy:hover { background: #1f1c18; border-color: ${PALETTE.accent}; }
1294
- .token-mint > summary { color: #f0ece4; }
1295
- .token-mint-sub, .token-mint-intro, .mint-verbs legend, .minted-hint { color: #a8a29a; }
1296
- .mint-verbs { border-color: #3a362f; }
1218
+ .minted-hint { color: #a8a29a; }
1297
1219
  .minted-title, .minted-warn { color: #f0ece4; }
1298
1220
  }
1299
1221
  `;