@openparachute/hub 0.6.3 → 0.6.4-rc.10

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.
Files changed (97) hide show
  1. package/package.json +1 -2
  2. package/src/__tests__/account-home-ui.test.ts +344 -110
  3. package/src/__tests__/account-mirror.test.ts +156 -0
  4. package/src/__tests__/account-setup.test.ts +880 -0
  5. package/src/__tests__/account-usage.test.ts +137 -0
  6. package/src/__tests__/account-vault-admin-token.test.ts +301 -0
  7. package/src/__tests__/account-vault-token.test.ts +53 -1
  8. package/src/__tests__/admin-vault-admin-token.test.ts +17 -0
  9. package/src/__tests__/admin-vaults.test.ts +20 -0
  10. package/src/__tests__/api-account.test.ts +236 -4
  11. package/src/__tests__/api-invites.test.ts +217 -0
  12. package/src/__tests__/api-mint-token.test.ts +259 -10
  13. package/src/__tests__/api-modules-ops.test.ts +195 -3
  14. package/src/__tests__/api-modules.test.ts +40 -4
  15. package/src/__tests__/api-settings-hub-origin.test.ts +13 -8
  16. package/src/__tests__/auto-wire.test.ts +101 -1
  17. package/src/__tests__/cli.test.ts +188 -2
  18. package/src/__tests__/cloudflare-state.test.ts +104 -0
  19. package/src/__tests__/expose-2fa-warning.test.ts +11 -8
  20. package/src/__tests__/expose-cloudflare.test.ts +135 -9
  21. package/src/__tests__/expose-interactive.test.ts +234 -7
  22. package/src/__tests__/expose-supervisor-version.test.ts +104 -0
  23. package/src/__tests__/expose.test.ts +10 -5
  24. package/src/__tests__/grants.test.ts +197 -8
  25. package/src/__tests__/hub-origin-resolution.test.ts +179 -25
  26. package/src/__tests__/hub-server.test.ts +761 -13
  27. package/src/__tests__/hub-unit.test.ts +185 -0
  28. package/src/__tests__/init.test.ts +579 -3
  29. package/src/__tests__/install.test.ts +448 -2
  30. package/src/__tests__/invites.test.ts +220 -0
  31. package/src/__tests__/launchctl-guard.test.ts +185 -0
  32. package/src/__tests__/migrate-cutover.test.ts +33 -0
  33. package/src/__tests__/module-ops-client.test.ts +68 -0
  34. package/src/__tests__/scope-explanations.test.ts +16 -0
  35. package/src/__tests__/serve-boot.test.ts +74 -1
  36. package/src/__tests__/serve.test.ts +121 -7
  37. package/src/__tests__/setup-wizard.test.ts +110 -0
  38. package/src/__tests__/spawn-path.test.ts +191 -0
  39. package/src/__tests__/status.test.ts +64 -0
  40. package/src/__tests__/supervisor.test.ts +374 -0
  41. package/src/__tests__/users.test.ts +66 -0
  42. package/src/__tests__/well-known.test.ts +25 -0
  43. package/src/__tests__/wizard.test.ts +72 -1
  44. package/src/account-home-ui.ts +481 -235
  45. package/src/account-mirror.ts +126 -0
  46. package/src/account-setup.ts +381 -0
  47. package/src/account-usage.ts +118 -0
  48. package/src/account-vault-admin-token.ts +242 -0
  49. package/src/account-vault-token.ts +36 -2
  50. package/src/admin-login-ui.ts +121 -0
  51. package/src/admin-vault-admin-token.ts +8 -2
  52. package/src/admin-vaults.ts +137 -29
  53. package/src/api-account.ts +118 -1
  54. package/src/api-invites.ts +345 -0
  55. package/src/api-mint-token.ts +81 -0
  56. package/src/api-modules-ops.ts +168 -53
  57. package/src/api-modules.ts +36 -0
  58. package/src/auto-wire.ts +87 -0
  59. package/src/cli.ts +128 -34
  60. package/src/cloudflare/detect.ts +1 -1
  61. package/src/cloudflare/state.ts +104 -8
  62. package/src/commands/expose-2fa-warning.ts +17 -13
  63. package/src/commands/expose-cloudflare.ts +103 -36
  64. package/src/commands/expose-interactive.ts +163 -17
  65. package/src/commands/expose-supervisor.ts +45 -0
  66. package/src/commands/init.ts +183 -4
  67. package/src/commands/install.ts +321 -3
  68. package/src/commands/migrate-cutover.ts +12 -5
  69. package/src/commands/serve-boot.ts +33 -3
  70. package/src/commands/serve.ts +158 -37
  71. package/src/commands/status.ts +9 -1
  72. package/src/commands/wizard.ts +36 -2
  73. package/src/grants.ts +113 -0
  74. package/src/help.ts +18 -5
  75. package/src/hub-db.ts +70 -2
  76. package/src/hub-server.ts +438 -41
  77. package/src/hub-settings.ts +3 -3
  78. package/src/hub-unit.ts +259 -9
  79. package/src/invites.ts +291 -0
  80. package/src/launchctl-guard.ts +131 -0
  81. package/src/managed-unit.ts +13 -3
  82. package/src/migrate-offer.ts +15 -6
  83. package/src/module-ops-client.ts +47 -22
  84. package/src/scope-attenuation.ts +19 -0
  85. package/src/scope-explanations.ts +9 -1
  86. package/src/service-spec.ts +17 -4
  87. package/src/setup-wizard.ts +34 -2
  88. package/src/spawn-path.ts +148 -0
  89. package/src/supervisor.ts +232 -7
  90. package/src/users.ts +54 -8
  91. package/src/vault-hub-origin-env.ts +28 -0
  92. package/src/vault-name.ts +13 -1
  93. package/src/well-known.ts +13 -0
  94. package/web/ui/dist/assets/{index-mz8XcVPP.css → index-BYYUeLGA.css} +1 -1
  95. package/web/ui/dist/assets/index-D3cDUOOj.js +61 -0
  96. package/web/ui/dist/index.html +2 -2
  97. package/web/ui/dist/assets/index-D_0TRjeo.js +0 -61
@@ -117,31 +117,80 @@ 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[]>;
132
+ /**
133
+ * Per-vault usage stat (`"X notes · Y MB"`) for each assigned vault tile.
134
+ * Maps `vaultName` → the pre-formatted stat string. A vault absent from this
135
+ * map renders no stat — the page tolerates a vault whose usage endpoint
136
+ * failed / is unreachable / predates the feature (the `/account/` GET handler
137
+ * builds this map by fetching `/.parachute/usage` per vault and omitting any
138
+ * that don't resolve). Omitted entirely on the admin / no-vault branches.
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>;
131
162
  /**
132
163
  * Set after a successful `POST /account/vault-token/<name>` to show the
133
164
  * freshly-minted token ONCE (the only time it's ever shown — the hub keeps
134
165
  * no plaintext copy). Drives the show-once banner at the top of the page.
135
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.
136
173
  */
137
174
  mintedToken?: MintedTokenView;
138
175
  /**
139
176
  * Set after a `POST /account/vault-token/<name>` that failed authorization
140
177
  * or validation, to surface an inline error banner on the re-rendered page
141
178
  * (e.g. unassigned vault, capped verb, rate-limited). Absent on success and
142
- * 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.
143
181
  */
144
182
  mintError?: string;
183
+ /**
184
+ * Whether this user has already connected an AI to (any of) their assigned
185
+ * vault(s) — true when a `grants` row touches one of their vaults (see
186
+ * `userHasVaultGrant`). Drives the first-run onboarding checklist: when
187
+ * `false`, the checklist leads with the hero "Connect your AI" step (inline
188
+ * endpoint + both methods); when `true`, the checklist condenses to a quiet
189
+ * "you're connected" line so it stops nagging returning users. The full vault
190
+ * card below remains the working surface either way. Omitted (defaults to
191
+ * `false`) on the admin / no-vault branches, where no checklist is shown.
192
+ */
193
+ connectedVault?: boolean;
145
194
  }
146
195
 
147
196
  /**
@@ -183,12 +232,36 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
183
232
  const hasNoVault = !isFirstAdmin && assignedVaults.length === 0;
184
233
  const startedCard = hasNoVault ? "" : renderGetStartedCard();
185
234
 
235
+ // First-run onboarding checklist — the lead surface for a friend with at
236
+ // least one assigned vault. Walks them through the obvious path: account
237
+ // ready → connect your AI (the hero step, inline endpoint + both methods) →
238
+ // set up your vault. Once connected (a grant touches one of their vaults) it
239
+ // condenses to a quiet "you're connected" line so it stops nagging. Shown
240
+ // only on the assigned-vault branch — the admin + no-vault branches have no
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.
247
+ const checklist =
248
+ assignedVaults.length > 0
249
+ ? renderOnboardingChecklist({
250
+ primaryVault: assignedVaults[0] as string,
251
+ trimmedOrigin,
252
+ connected: opts.connectedVault ?? false,
253
+ })
254
+ : "";
255
+
186
256
  const vaultCard = renderVaultCard({
187
257
  assignedVaults,
188
258
  trimmedOrigin,
189
259
  isFirstAdmin,
190
260
  csrfToken,
191
261
  mintableVerbs: opts.mintableVerbs ?? {},
262
+ usageStats: opts.usageStats ?? {},
263
+ mirrorLines: opts.mirrorLines ?? {},
264
+ mirrorPushing: opts.mirrorPushing ?? {},
192
265
  });
193
266
 
194
267
  const accountCard = renderAccountCard({
@@ -206,13 +279,139 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
206
279
  </div>
207
280
  ${mintedBanner}
208
281
  ${mintErrorBanner}
209
- ${startedCard}
282
+ ${checklist}
210
283
  ${vaultCard}
284
+ ${startedCard}
211
285
  ${accountCard}
212
286
  </div>${COPY_SCRIPT}`;
213
287
  return baseDocument(`${username} — Parachute`, body);
214
288
  }
215
289
 
290
+ interface OnboardingChecklistOpts {
291
+ /**
292
+ * The vault the checklist's "Connect your AI" step shows the endpoint for.
293
+ * For the common single-vault case this is their only vault; for the rare
294
+ * multi-vault case it's the first/primary one (the per-vault tiles below
295
+ * still list every vault). Already validated/escaped at render time.
296
+ */
297
+ primaryVault: string;
298
+ trimmedOrigin: string;
299
+ /** Whether they've already connected an AI (a grant touches a vault). */
300
+ connected: boolean;
301
+ }
302
+
303
+ /**
304
+ * The first-run "Get set up" checklist — the lead surface on `/account/` for a
305
+ * friend with an assigned vault. Three numbered steps give a non-technical
306
+ * person an obvious path:
307
+ *
308
+ * ① Your account is ready — always done (they're signed in, password set).
309
+ * ② Connect your AI — the hero. Inline endpoint (`/vault/<name>/mcp`)
310
+ * with a copy button + the two short connect
311
+ * methods (Claude.ai connector, Claude Code
312
+ * `claude mcp add`). Marked done when `connected`.
313
+ * ③ Set up your vault — links the vault-setup starter prompt.
314
+ *
315
+ * When `connected` is true the whole thing condenses to a quiet "✓ You're
316
+ * connected" line so it doesn't nag returning users — the full vault card below
317
+ * stays the working surface either way.
318
+ *
319
+ * Server-rendered, no-JS-required: the copy button is progressive enhancement
320
+ * (the endpoint stays selectable text without it), matching the rest of the page.
321
+ */
322
+ function renderOnboardingChecklist(opts: OnboardingChecklistOpts): string {
323
+ const { primaryVault, trimmedOrigin, connected } = opts;
324
+ const safeVault = escapeHtml(primaryVault);
325
+ const endpoint = accountMcpEndpoint(trimmedOrigin, primaryVault);
326
+ const addCmd = accountClaudeMcpAddCommand(trimmedOrigin, primaryVault);
327
+ const safeEndpoint = escapeHtml(endpoint);
328
+ const safeAddCmd = escapeHtml(addCmd);
329
+
330
+ // The endpoint + both connect methods. Shared between the full checklist
331
+ // (step 2) and the condensed "Connect another AI" expander (hub#583) so a
332
+ // genuinely-connected user can still wire up a SECOND client without losing
333
+ // the instructions.
334
+ const connectMethods = `
335
+ <div class="copy-row">
336
+ <code data-testid="onboarding-mcp-endpoint">${safeEndpoint}</code>
337
+ <button type="button" class="btn btn-copy" data-copy="${safeEndpoint}"
338
+ data-testid="copy-onboarding-endpoint">Copy</button>
339
+ </div>
340
+ <p class="onboarding-method"><strong>Claude.ai (web):</strong> open
341
+ Settings → Connectors → Add custom connector, and paste the address above.</p>
342
+ <p class="onboarding-method"><strong>Claude Code (terminal):</strong> run this command:</p>
343
+ <div class="copy-row">
344
+ <code data-testid="onboarding-mcp-add-command">${safeAddCmd}</code>
345
+ <button type="button" class="btn btn-copy" data-copy="${safeAddCmd}"
346
+ data-testid="copy-onboarding-add-command">Copy</button>
347
+ </div>`;
348
+
349
+ // Condensed state — they've connected, so the checklist shrinks to a quiet
350
+ // reassuring line. But keep a "Connect another AI" expander (hub#583): the
351
+ // condensed line used to DELETE the endpoint + methods outright, leaving a
352
+ // connected user no way to wire up a second client. A <details> expander
353
+ // (server-rendered, no-JS-required — the copy buttons stay progressive
354
+ // enhancement) re-reveals the full inline instructions on demand.
355
+ if (connected) {
356
+ return `
357
+ <section class="section onboarding onboarding-done" data-testid="onboarding-checklist"
358
+ data-connected="true">
359
+ <p class="onboarding-done-line" data-testid="onboarding-done-line">
360
+ <span class="onboarding-check" aria-hidden="true">✓</span>
361
+ You're connected — here's your vault.</p>
362
+ <details class="onboarding-connect-another" data-testid="onboarding-connect-another">
363
+ <summary data-testid="onboarding-connect-another-summary">Connect another AI →</summary>
364
+ <div class="onboarding-step-body">
365
+ <p class="onboarding-step-sub">Point another AI client at your vault using this
366
+ address — you'll sign in and approve the first time:</p>
367
+ ${connectMethods}
368
+ </div>
369
+ </details>
370
+ </section>`;
371
+ }
372
+
373
+ return `
374
+ <section class="section onboarding" data-testid="onboarding-checklist"
375
+ data-connected="false">
376
+ <h2>Get set up</h2>
377
+ <p class="onboarding-intro">Three quick steps to start using your vault with your AI.</p>
378
+ <ol class="onboarding-steps">
379
+ <li class="onboarding-step onboarding-step-done" data-testid="onboarding-step-1">
380
+ <span class="onboarding-num onboarding-num-done" aria-hidden="true">✓</span>
381
+ <div class="onboarding-step-body">
382
+ <p class="onboarding-step-title">Your account is ready</p>
383
+ <p class="onboarding-step-sub">You're signed in and your password is set. Nothing to
384
+ do here.</p>
385
+ </div>
386
+ </li>
387
+
388
+ <li class="onboarding-step onboarding-step-hero" data-testid="onboarding-step-2">
389
+ <span class="onboarding-num" aria-hidden="true">2</span>
390
+ <div class="onboarding-step-body">
391
+ <p class="onboarding-step-title">Connect your AI</p>
392
+ <p class="onboarding-step-sub">Point Claude (or another AI) at your vault using this
393
+ address — no token to copy, you'll sign in and approve the first time:</p>
394
+ ${connectMethods}
395
+ </div>
396
+ </li>
397
+
398
+ <li class="onboarding-step" data-testid="onboarding-step-3">
399
+ <span class="onboarding-num" aria-hidden="true">3</span>
400
+ <div class="onboarding-step-body">
401
+ <p class="onboarding-step-title">Set up your vault</p>
402
+ <p class="onboarding-step-sub">Open a new Claude chat and paste the
403
+ <a href="https://parachute.computer/onboarding/vault-setup/" target="_blank"
404
+ rel="noopener" data-testid="onboarding-vault-setup-link">vault-setup prompt</a> —
405
+ your AI interviews you and structures your vault around how you think.</p>
406
+ </div>
407
+ </li>
408
+ </ol>
409
+ <p class="onboarding-foot" data-testid="onboarding-foot">Your vault is
410
+ <code>${safeVault}</code>. Its size, backup state, Notes, and advanced settings are
411
+ just below.</p>
412
+ </section>`;
413
+ }
414
+
216
415
  /**
217
416
  * The "Get started with your AI" card — the real first stop for a friend
218
417
  * landing on `/account/`. Mirrors the operator setup-wizard's
@@ -221,9 +420,9 @@ export function renderAccountHome(opts: RenderAccountHomeOpts): string {
221
420
  * live on parachute.computer rather than embedded here so they iterate
222
421
  * without a hub release; this card just links.
223
422
  *
224
- * Placed near the top of the page (after any banners, before the vault card)
225
- * because "what do I actually do with this?" is the friend's first question
226
- * the connect details below answer "how", this answers "what next".
423
+ * Placed AFTER the connect/vault card (connect-before-prompts): the prompts are
424
+ * only useful once the vault is connected, so the page leads with the connect
425
+ * checklist + vault details, and these "what next" prompts sit below them.
227
426
  */
228
427
  function renderGetStartedCard(): string {
229
428
  return `
@@ -257,6 +456,12 @@ interface VaultCardOpts {
257
456
  isFirstAdmin: boolean;
258
457
  csrfToken: string;
259
458
  mintableVerbs: Record<string, VaultVerb[]>;
459
+ /** `vaultName` → pre-formatted usage stat ("X notes · Y MB"). */
460
+ usageStats: Record<string, string>;
461
+ /** `vaultName` → pre-formatted backup line ("Backed up — full version history"). */
462
+ mirrorLines: Record<string, string>;
463
+ /** `vaultName` → is backup already pushing to a remote (gates the GitHub action). */
464
+ mirrorPushing: Record<string, boolean>;
260
465
  }
261
466
 
262
467
  /**
@@ -286,75 +491,58 @@ export function accountClaudeMcpAddCommand(trimmedOrigin: string, vaultName: str
286
491
  }
287
492
 
288
493
  function renderVaultCard(opts: VaultCardOpts): string {
289
- const { assignedVaults, trimmedOrigin, isFirstAdmin, csrfToken, mintableVerbs } = opts;
494
+ const { assignedVaults, trimmedOrigin, isFirstAdmin, csrfToken, mintableVerbs, usageStats } =
495
+ opts;
496
+ const { mirrorLines, mirrorPushing } = opts;
290
497
 
291
498
  if (assignedVaults.length > 0) {
292
- // One vault tile per assignment (multi-user Phase 2 PR 2). Each tile
293
- // leads with a friendly "connect your AI assistant to this vault" block
294
- // that covers BOTH connect paths a non-technical friend is likely to
295
- // use Claude Code (the `claude mcp add` CLI command) and Claude.ai on
296
- // the web (Settings Connectors Add custom connector, pointed at the
297
- // endpoint). Both are the OAuth path no token to paste, the first
298
- // connection opens a browser to sign in + approve. The Notes "Open" CTA
299
- // sits alongside as the browser-UI option. Phrasing mirrors
300
- // parachute.computer/install.njk's #connect-mcp-clients section so the
301
- // operator docs and the friend's account page stay consistent.
302
- //
303
- // This closes the multi-user gap where the friend tile read as MCP
304
- // jargon ("Connect an MCP client") rather than "here's how to connect
305
- // this to your AI" — and where the web (Claude.ai) path was entirely
306
- // missing, only the Claude Code CLI command was offered.
499
+ // One vault tile per assignment (multi-user Phase 2 PR 2). The tile is the
500
+ // everyday "here's your vault" detail card name, size, backup state, a
501
+ // browser-UI on-ramp (Notes + "build your own"), and a single deep-link
502
+ // into the advanced vault settings SPA. It deliberately does NOT repeat the
503
+ // "Connect your AI" instructions: the onboarding checklist above owns that
504
+ // step (collapsing to "✓ You're connected" once a grant lands), so the
505
+ // page never shows the connect endpoint + both methods twice. Token minting
506
+ // + raw mirror config are advanced concerns that live in the vault config
507
+ // SPA, reached via "Advanced vault settings ↗" — not duplicated here.
307
508
  const heading = assignedVaults.length === 1 ? "<h2>Your vault</h2>" : "<h2>Your vaults</h2>";
308
509
  const tiles = assignedVaults
309
510
  .map((vaultName) => {
310
511
  const safeVault = escapeHtml(vaultName);
311
512
  const vaultUrlForAdd = encodeURIComponent(`${trimmedOrigin}/vault/${vaultName}`);
312
- const endpoint = accountMcpEndpoint(trimmedOrigin, vaultName);
313
- const addCmd = accountClaudeMcpAddCommand(trimmedOrigin, vaultName);
314
- const safeEndpoint = escapeHtml(endpoint);
315
- const safeAddCmd = escapeHtml(addCmd);
316
- const tokenMintBlock = renderTokenMintBlock(
513
+ const verbsForVault = mintableVerbs[vaultName] ?? [];
514
+ const holdsAdmin = verbsForVault.includes("admin");
515
+ // "Advanced vault settings ↗" — only for users whose assignment grants
516
+ // `admin` (the verb the deep-link mints). Today every assigned user
517
+ // holds admin, but gate on the verb so the button never offers
518
+ // authority the POST handler would 403. The single advanced entry point:
519
+ // schema, tokens, retention, raw mirror config all live in the SPA.
520
+ const manageBlock = holdsAdmin ? renderVaultAdminLink(vaultName, csrfToken) : "";
521
+ // Compact usage stat ("X notes · Y MB"), when the vault's usage endpoint
522
+ // resolved. Omitted gracefully otherwise.
523
+ const usageStat = usageStats[vaultName];
524
+ const usageLine = usageStat
525
+ ? `<p class="vault-usage" data-testid="vault-usage">${escapeHtml(usageStat)}</p>`
526
+ : "";
527
+ // Backup state line + a "Back up to GitHub ↗" deep-link when not already
528
+ // pushing. Both gated on admin: the backup line is only fetched for
529
+ // admin-held vaults (the mirror endpoint is admin-scoped), and the
530
+ // GitHub action reuses the same `/account/vault-admin-token/<name>`
531
+ // deep-link that opens the vault config SPA. Omitted silently when the
532
+ // mirror fetch failed / backup is off (the renderer just gets no entry).
533
+ const mirrorLine = mirrorLines[vaultName];
534
+ const backupBlock = renderBackupBlock(
317
535
  vaultName,
318
- safeVault,
319
- mintableVerbs[vaultName] ?? [],
536
+ mirrorLine,
537
+ mirrorPushing[vaultName] ?? false,
538
+ holdsAdmin,
320
539
  csrfToken,
321
540
  );
322
541
  return `
323
542
  <div class="vault-tile" data-testid="vault-tile" data-vault-name="${safeVault}">
324
543
  <p class="vault-name"><strong>${safeVault}</strong></p>
325
- <div class="mcp-connect" data-testid="mcp-connect">
326
- <p class="mcp-connect-label" data-testid="connect-ai-heading">Connect your AI
327
- assistant to this vault</p>
328
- <p class="mcp-connect-intro">Two common ways. Both sign you in to this hub over
329
- HTTPS and ask you to approve access the first time — no token to copy.</p>
330
-
331
- <div class="mcp-method" data-testid="connect-method-claude-code">
332
- <p class="mcp-method-title">Claude Code (terminal)</p>
333
- <p class="mcp-method-sub">Run this in your terminal:</p>
334
- <div class="copy-row">
335
- <code data-testid="mcp-add-command">${safeAddCmd}</code>
336
- <button type="button" class="btn btn-copy" data-copy="${safeAddCmd}"
337
- data-testid="copy-mcp-add-command">Copy</button>
338
- </div>
339
- </div>
340
-
341
- <div class="mcp-method" data-testid="connect-method-claude-ai">
342
- <p class="mcp-method-title">Claude.ai (web)</p>
343
- <p class="mcp-method-sub">In Claude.ai, open <strong>Settings → Connectors</strong>,
344
- choose <strong>Add custom connector</strong>, and paste this endpoint:</p>
345
- <div class="copy-row">
346
- <code data-testid="mcp-endpoint">${safeEndpoint}</code>
347
- <button type="button" class="btn btn-copy" data-copy="${safeEndpoint}"
348
- data-testid="copy-mcp-endpoint">Copy</button>
349
- </div>
350
- <p class="mcp-method-note">Claude.ai then redirects you here to sign in and
351
- approve. (Your hub must be reachable from the web for this.)</p>
352
- </div>
353
-
354
- <p class="mcp-connect-hint" data-testid="connect-any-client-hint">Using something
355
- else? Point any MCP client at the same endpoint above. (ChatGPT and some other
356
- web UIs call these "connectors.")</p>
357
- </div>
544
+ ${usageLine}
545
+ ${backupBlock}
358
546
  <p class="vault-notes-cta">
359
547
  <a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}"
360
548
  target="_blank" rel="noopener" data-testid="open-notes-cta">Open Notes ↗</a>
@@ -364,18 +552,21 @@ function renderVaultCard(opts: VaultCardOpts): string {
364
552
  capture in this vault — or jump straight to bulk-importing Markdown/Obsidian
365
553
  notes into it.</span>
366
554
  </p>
367
- ${tokenMintBlock}
555
+ <p class="vault-build-ui" data-testid="build-your-own-ui">Notes is just one way to see
556
+ your vault — when you're ready, your AI can build you a custom UI for it in a few
557
+ minutes. <a href="https://parachute.computer/onboarding/surface-build/"
558
+ target="_blank" rel="noopener" data-testid="build-your-own-ui-link">Build your own ↗</a></p>
559
+ ${manageBlock}
368
560
  </div>`;
369
561
  })
370
562
  .join("");
371
563
  return `
372
564
  <section class="section" data-testid="vault-card">
373
565
  ${heading}
374
- <p>Connect Claude (or any AI assistant) to your vault${
566
+ <p>Your vault${
375
567
  assignedVaults.length === 1 ? "" : "s"
376
- } — pick Claude Code or
377
- Claude.ai below or open Notes for a browser UI. The first connection signs you in
378
- to your hub over HTTPS and asks you to approve access.</p>
568
+ } at a glance size, backup, and a browser UI. You connect your AI from the
569
+ steps above; the deeper settings live one click away.</p>
379
570
  <div class="vault-tiles">${tiles}
380
571
  </div>
381
572
  </section>`;
@@ -405,72 +596,79 @@ function renderVaultCard(opts: VaultCardOpts): string {
405
596
  }
406
597
 
407
598
  /**
408
- * The "mint an access token (for scripts / headless clients)" affordance on a
409
- * vault tile. Sits BELOW the OAuth connect block + Notes CTA — the no-token
410
- * OAuth path stays the recommended default; this is the secondary, opt-in
411
- * path for clients that can't do an interactive browser sign-in (cron jobs,
412
- * headless agents, a `curl` script).
599
+ * The backup-state block on a vault tile: a warm, plain-language line telling
600
+ * the owner their vault is backed up (local version history, optionally pushed
601
+ * to GitHub), plus a "Back up to GitHub ↗" action when no push remote is set.
413
602
  *
414
- * Renders one radio per verb the user's assignment role permits (`verbs` —
415
- * today always `["read", "write"]`). The UI NEVER offers a verb the server
416
- * would reject: a read-only assignment shows only "Read". An empty `verbs`
417
- * list (unknown / unmappable role) renders nothingfail-closed, matching
418
- * the server's `vaultVerbsForUserVault` returning `[]`.
603
+ * `mirrorLine` is the pre-formatted backup line ("Backed up full version
604
+ * history" / " + GitHub") the GET handler built from the vault's mirror status,
605
+ * or `undefined` when the mirror fetch failed / backup is off / the user doesn't
606
+ * hold admin. When absent, the whole block is omitted silently the everyday
607
+ * home never nags with a "not backed up" warning.
419
608
  *
420
- * The form POSTs `application/x-www-form-urlencoded` to
421
- * `/account/vault-token/<name>` with the CSRF hidden field + a `verb` radio —
422
- * same no-JS-required posture as the change-password and sign-out forms. The
423
- * `<details>` keeps it collapsed by default so the tile leads with the
424
- * recommended OAuth path.
609
+ * The "Back up to GitHub ↗" action reuses the EXISTING
610
+ * `/account/vault-admin-token/<name>` deep-link (the same POST `renderVaultAdminLink`
611
+ * uses) it mints a `vault:<name>:admin` token and opens the vault config SPA,
612
+ * where the GitHub push is configured. We do NOT invent a new auth path. It's
613
+ * shown only when the user holds admin AND `pushing` is false (not already
614
+ * pushing to a remote) — `pushing` is a proper boolean threaded from the mirror
615
+ * status (`VaultMirrorStat.backedUpToRemote`), never re-derived from the
616
+ * display string.
425
617
  */
426
- function renderTokenMintBlock(
618
+ function renderBackupBlock(
427
619
  vaultName: string,
428
- safeVault: string,
429
- verbs: VaultVerb[],
620
+ mirrorLine: string | undefined,
621
+ pushing: boolean,
622
+ holdsAdmin: boolean,
430
623
  csrfToken: string,
431
624
  ): string {
432
- if (verbs.length === 0) return "";
625
+ if (!mirrorLine) return "";
626
+ // "Back up to GitHub ↗" — only when admin (the deep-link mints admin) and not
627
+ // already pushing. Reuses the vault-admin-token deep-link to open the SPA's
628
+ // backup page; no new auth path.
629
+ const action = escapeHtml(`/account/vault-admin-token/${encodeURIComponent(vaultName)}`);
630
+ const githubAction =
631
+ holdsAdmin && !pushing
632
+ ? `
633
+ <form method="POST" action="${action}" class="vault-backup-github"
634
+ data-testid="backup-github-form">
635
+ ${renderCsrfHiddenInput(csrfToken)}
636
+ <button type="submit" class="btn btn-secondary" data-testid="backup-github-button">
637
+ Back up to GitHub ↗
638
+ </button>
639
+ </form>`
640
+ : "";
641
+ return `
642
+ <div class="vault-backup" data-testid="vault-backup">
643
+ <p class="vault-backup-line" data-testid="backup-state-line">
644
+ <span class="vault-backup-check" aria-hidden="true">✓</span>${escapeHtml(mirrorLine)}</p>${githubAction}
645
+ </div>`;
646
+ }
647
+
648
+ /**
649
+ * The "Advanced vault settings ↗" affordance on a vault tile — the single
650
+ * advanced entry point. A small POST form to `/account/vault-admin-token/<name>`
651
+ * that mints a `vault:<name>:admin` deep-link token and redirects into the
652
+ * vault's own config SPA — where the assigned user can manage schema, rotate
653
+ * access tokens, set retention, and edit raw mirror/backup config. Shown only
654
+ * when the user's assignment grants `admin` (gated by the caller).
655
+ *
656
+ * No-JS posture: a same-origin form POST that 303-redirects on success, same
657
+ * shape as the other `/account/*` forms. CSRF-gated via the hidden field.
658
+ */
659
+ function renderVaultAdminLink(vaultName: string, csrfToken: string): string {
433
660
  // Path segment is URL-encoded; the action attribute is HTML-escaped on top.
434
- const action = escapeHtml(`/account/vault-token/${encodeURIComponent(vaultName)}`);
435
- const radios = verbs
436
- .map((verb, i) => {
437
- const checked = i === 0 ? " checked" : "";
438
- const label =
439
- verb === "read"
440
- ? "Read-only"
441
- : verb === "admin"
442
- ? "Full (read, write, rotate tokens + config)"
443
- : "Read + write";
444
- return `
445
- <label class="mint-verb-option">
446
- <input type="radio" name="verb" value="${verb}"${checked}
447
- data-testid="mint-verb-${verb}" />
448
- <span><strong>${verb}</strong> — ${label} access to <code>${safeVault}</code></span>
449
- </label>`;
450
- })
451
- .join("");
661
+ const action = escapeHtml(`/account/vault-admin-token/${encodeURIComponent(vaultName)}`);
452
662
  return `
453
- <details class="token-mint" data-testid="token-mint">
454
- <summary data-testid="token-mint-summary">Mint an access token
455
- <span class="token-mint-sub">for scripts / headless clients</span></summary>
456
- <div class="token-mint-body">
457
- <p class="token-mint-intro">Most clients should use the no-token
458
- connect options above — they sign you in over HTTPS and never
459
- ask you to paste a secret. Mint a token only for a script or
460
- headless client that can't open a browser to sign in. It's a
461
- bearer for <code>vault:${safeVault}:&lt;verb&gt;</code>, scoped to
462
- <strong>this vault only</strong>, and you'll see it once.</p>
463
- <form method="POST" action="${action}" class="mint-form"
464
- data-testid="mint-form">
465
- ${renderCsrfHiddenInput(csrfToken)}
466
- <fieldset class="mint-verbs">
467
- <legend>Access level</legend>${radios}
468
- </fieldset>
469
- <button type="submit" class="btn btn-secondary"
470
- data-testid="mint-token-button">Mint token</button>
471
- </form>
472
- </div>
473
- </details>`;
663
+ <form method="POST" action="${action}" class="vault-admin-link"
664
+ data-testid="vault-admin-form">
665
+ ${renderCsrfHiddenInput(csrfToken)}
666
+ <button type="submit" class="btn btn-secondary" data-testid="vault-admin-button">
667
+ Advanced vault settings
668
+ </button>
669
+ <span class="vault-admin-sub">Open this vault's config schema, access tokens,
670
+ retention, and raw backup settings.</span>
671
+ </form>`;
474
672
  }
475
673
 
476
674
  /**
@@ -580,8 +778,8 @@ const COPY_SCRIPT = `
580
778
  //
581
779
  // Same brand palette + font stack as account-change-password-ui.ts so the
582
780
  // `/account/*` family is visually cohesive. Extra rules (.section, .kv,
583
- // .vault-name, .mcp-connect, .copy-row) describe the new card + MCP
584
- // connect-block shapes this page introduces.
781
+ // .vault-name, .vault-backup, .vault-build-ui, .copy-row) describe the card +
782
+ // backup-state + onboarding-checklist shapes this page introduces.
585
783
 
586
784
  const STYLES = `
587
785
  *, *::before, *::after { box-sizing: border-box; }
@@ -696,6 +894,99 @@ const STYLES = `
696
894
  .starter-grid { grid-template-columns: 1fr; }
697
895
  }
698
896
 
897
+ .onboarding-intro { color: ${PALETTE.fgMuted}; font-size: 0.95rem; margin: 0 0 0.4rem; }
898
+ .onboarding-steps {
899
+ list-style: none;
900
+ margin: 0.75rem 0 0.4rem;
901
+ padding: 0;
902
+ display: flex;
903
+ flex-direction: column;
904
+ gap: 0.85rem;
905
+ }
906
+ .onboarding-step {
907
+ display: flex;
908
+ align-items: flex-start;
909
+ gap: 0.7rem;
910
+ }
911
+ .onboarding-num {
912
+ flex: 0 0 auto;
913
+ width: 1.5rem;
914
+ height: 1.5rem;
915
+ border-radius: 999px;
916
+ background: ${PALETTE.accent};
917
+ color: ${PALETTE.cardBg};
918
+ font-size: 0.85rem;
919
+ font-weight: 600;
920
+ display: inline-flex;
921
+ align-items: center;
922
+ justify-content: center;
923
+ margin-top: 0.1rem;
924
+ }
925
+ .onboarding-num-done {
926
+ background: ${PALETTE.successSoft};
927
+ color: ${PALETTE.success};
928
+ border: 1px solid ${PALETTE.success};
929
+ }
930
+ .onboarding-step-body { flex: 1 1 auto; min-width: 0; }
931
+ .onboarding-step-title {
932
+ font-weight: 600;
933
+ font-size: 0.95rem;
934
+ color: ${PALETTE.fg};
935
+ margin: 0 0 0.15rem;
936
+ }
937
+ .onboarding-step-sub {
938
+ font-size: 0.85rem;
939
+ color: ${PALETTE.fgMuted};
940
+ margin: 0 0 0.4rem;
941
+ }
942
+ .onboarding-step-done .onboarding-step-title { color: ${PALETTE.fgMuted}; font-weight: 500; }
943
+ .onboarding-method {
944
+ font-size: 0.85rem;
945
+ color: ${PALETTE.fgMuted};
946
+ margin: 0.5rem 0 0.3rem;
947
+ }
948
+ .onboarding-method strong { color: ${PALETTE.fg}; }
949
+ .onboarding-step .copy-row { margin: 0.35rem 0; }
950
+ .onboarding-foot {
951
+ font-size: 0.82rem;
952
+ color: ${PALETTE.fgMuted};
953
+ margin: 0.6rem 0 0;
954
+ }
955
+ .onboarding-done-line {
956
+ display: flex;
957
+ align-items: center;
958
+ gap: 0.5rem;
959
+ font-size: 1rem;
960
+ font-weight: 500;
961
+ color: ${PALETTE.fg};
962
+ margin: 0;
963
+ }
964
+ .onboarding-check {
965
+ flex: 0 0 auto;
966
+ width: 1.4rem;
967
+ height: 1.4rem;
968
+ border-radius: 999px;
969
+ background: ${PALETTE.successSoft};
970
+ color: ${PALETTE.success};
971
+ border: 1px solid ${PALETTE.success};
972
+ font-size: 0.85rem;
973
+ display: inline-flex;
974
+ align-items: center;
975
+ justify-content: center;
976
+ }
977
+ .onboarding-connect-another { margin: 0.7rem 0 0; }
978
+ .onboarding-connect-another > summary {
979
+ cursor: pointer;
980
+ font-size: 0.85rem;
981
+ font-weight: 500;
982
+ color: ${PALETTE.accent};
983
+ list-style: none;
984
+ user-select: none;
985
+ }
986
+ .onboarding-connect-another > summary::-webkit-details-marker { display: none; }
987
+ .onboarding-connect-another[open] > summary { margin-bottom: 0.4rem; }
988
+ .onboarding-connect-another .copy-row { margin: 0.35rem 0; }
989
+
699
990
  .account-security {
700
991
  margin: 0.9rem 0 0;
701
992
  padding-top: 0.6rem;
@@ -717,6 +1008,41 @@ const STYLES = `
717
1008
  margin: 0 0 0.6rem;
718
1009
  }
719
1010
  .vault-name strong { color: ${PALETTE.fg}; font-weight: 600; }
1011
+ .vault-usage {
1012
+ font-size: 0.8rem;
1013
+ color: ${PALETTE.fgMuted};
1014
+ margin: 0 0 0.5rem;
1015
+ }
1016
+ .vault-backup { margin: 0 0 0.5rem; }
1017
+ .vault-backup-line {
1018
+ display: flex;
1019
+ align-items: center;
1020
+ gap: 0.4rem;
1021
+ font-size: 0.85rem;
1022
+ color: ${PALETTE.success};
1023
+ margin: 0;
1024
+ }
1025
+ .vault-backup-check {
1026
+ flex: 0 0 auto;
1027
+ width: 1.1rem;
1028
+ height: 1.1rem;
1029
+ border-radius: 999px;
1030
+ background: ${PALETTE.successSoft};
1031
+ color: ${PALETTE.success};
1032
+ border: 1px solid ${PALETTE.success};
1033
+ font-size: 0.7rem;
1034
+ display: inline-flex;
1035
+ align-items: center;
1036
+ justify-content: center;
1037
+ }
1038
+ .vault-backup-github { margin: 0.4rem 0 0; }
1039
+ .vault-build-ui {
1040
+ font-size: 0.82rem;
1041
+ color: ${PALETTE.fgMuted};
1042
+ margin: 0.6rem 0 0;
1043
+ padding-top: 0.6rem;
1044
+ border-top: 1px solid ${PALETTE.borderLight};
1045
+ }
720
1046
  .vault-tiles {
721
1047
  display: flex;
722
1048
  flex-direction: column;
@@ -732,52 +1058,6 @@ const STYLES = `
732
1058
  .vault-tile p { margin: 0.2rem 0; }
733
1059
  .vault-tile p:last-child { margin-top: 0.5rem; }
734
1060
 
735
- .mcp-connect {
736
- margin-bottom: 0.75rem;
737
- }
738
- .mcp-connect-label {
739
- font-family: ${FONT_SERIF};
740
- font-size: 1.05rem;
741
- font-weight: 400;
742
- color: ${PALETTE.fg};
743
- margin: 0 0 0.3rem;
744
- }
745
- .mcp-connect-intro {
746
- font-size: 0.85rem;
747
- color: ${PALETTE.fgMuted};
748
- margin: 0 0 0.75rem;
749
- }
750
- .mcp-method {
751
- margin: 0.75rem 0;
752
- padding-top: 0.6rem;
753
- border-top: 1px solid ${PALETTE.borderLight};
754
- }
755
- .mcp-method-title {
756
- font-size: 0.9rem;
757
- font-weight: 600;
758
- color: ${PALETTE.fg};
759
- margin: 0 0 0.15rem;
760
- }
761
- .mcp-method-sub {
762
- font-size: 0.82rem;
763
- color: ${PALETTE.fgMuted};
764
- margin: 0 0 0.4rem;
765
- }
766
- .mcp-method-note {
767
- font-size: 0.78rem;
768
- color: ${PALETTE.fgMuted};
769
- margin: 0.35rem 0 0;
770
- }
771
- .mcp-field { margin: 0.5rem 0; }
772
- .mcp-field-label {
773
- display: block;
774
- font-size: 0.7rem;
775
- text-transform: uppercase;
776
- letter-spacing: 0.06em;
777
- color: ${PALETTE.fgMuted};
778
- font-family: ${FONT_MONO};
779
- margin-bottom: 0.2rem;
780
- }
781
1061
  .vault-notes-cta {
782
1062
  margin: 0.9rem 0 0;
783
1063
  padding-top: 0.6rem;
@@ -818,58 +1098,21 @@ const STYLES = `
818
1098
  border-color: ${PALETTE.border};
819
1099
  }
820
1100
  .btn-copy:hover { background: ${PALETTE.bgSoft}; border-color: ${PALETTE.accent}; }
821
- .mcp-connect-hint {
822
- font-size: 0.82rem;
823
- color: ${PALETTE.fgMuted};
824
- margin: 0.4rem 0 0;
825
- }
826
1101
 
827
- .token-mint {
1102
+ .vault-admin-link {
828
1103
  margin: 0.9rem 0 0;
829
1104
  padding-top: 0.6rem;
830
1105
  border-top: 1px solid ${PALETTE.borderLight};
1106
+ display: flex;
1107
+ align-items: center;
1108
+ flex-wrap: wrap;
1109
+ gap: 0.4rem 0.75rem;
831
1110
  }
832
- .token-mint > summary {
833
- cursor: pointer;
834
- font-size: 0.88rem;
835
- font-weight: 600;
836
- color: ${PALETTE.fg};
837
- list-style: revert;
838
- }
839
- .token-mint-sub {
840
- font-weight: 400;
841
- font-size: 0.8rem;
842
- color: ${PALETTE.fgMuted};
843
- }
844
- .token-mint-body { margin-top: 0.6rem; }
845
- .token-mint-intro {
846
- font-size: 0.8rem;
847
- color: ${PALETTE.fgMuted};
848
- margin: 0 0 0.6rem;
849
- }
850
- .mint-verbs {
851
- border: 1px solid ${PALETTE.borderLight};
852
- border-radius: 6px;
853
- padding: 0.5rem 0.7rem;
854
- margin: 0 0 0.6rem;
855
- }
856
- .mint-verbs legend {
857
- font-size: 0.7rem;
858
- text-transform: uppercase;
859
- letter-spacing: 0.06em;
1111
+ .vault-admin-sub {
1112
+ font-size: 0.82rem;
860
1113
  color: ${PALETTE.fgMuted};
861
- font-family: ${FONT_MONO};
862
- padding: 0 0.3rem;
863
- }
864
- .mint-verb-option {
865
- display: flex;
866
- align-items: baseline;
867
- gap: 0.5rem;
868
- font-size: 0.85rem;
869
- margin: 0.3rem 0;
1114
+ flex: 1 1 12rem;
870
1115
  }
871
- .mint-verb-option input { margin: 0; }
872
- .mint-form .btn { margin-top: 0.2rem; }
873
1116
 
874
1117
  .minted-banner {
875
1118
  border: 1px solid ${PALETTE.accent};
@@ -979,15 +1222,20 @@ const STYLES = `
979
1222
  body { background: #1a1815; color: #e8e4dc; }
980
1223
  .card { background: #25221d; border-color: #3a362f; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); }
981
1224
  h1, h2 { color: #f0ece4; }
982
- .subtitle, .kv dt, .mcp-field-label, .mcp-connect-hint,
983
- .mcp-connect-intro, .mcp-method-sub, .mcp-method-note,
984
- .vault-notes-cta-sub { color: #a8a29a; }
985
- .vault-name strong, .mcp-connect-label, .mcp-method-title { color: #f0ece4; }
1225
+ .subtitle, .kv dt,
1226
+ .vault-notes-cta-sub, .vault-usage, .vault-build-ui,
1227
+ .onboarding-intro, .onboarding-step-sub, .onboarding-method,
1228
+ .onboarding-foot { color: #a8a29a; }
1229
+ .vault-name strong,
1230
+ .onboarding-step-title, .onboarding-method strong,
1231
+ .onboarding-done-line { color: #f0ece4; }
1232
+ .onboarding-step-done .onboarding-step-title { color: #a8a29a; }
986
1233
  code { background: #1f1c18; color: #e8e4dc; }
987
1234
  .copy-row code { background: transparent; }
988
1235
  .section { border-top-color: #3a362f; }
989
- .mcp-method, .vault-notes-cta, .token-mint,
990
- .account-security { border-top-color: #3a362f; }
1236
+ .vault-notes-cta, .vault-build-ui,
1237
+ .vault-admin-link, .account-security { border-top-color: #3a362f; }
1238
+ .vault-admin-sub { color: #a8a29a; }
991
1239
  .get-started h3 { color: #f0ece4; }
992
1240
  .starter-tile { border-color: #3a362f; background: #1f1c18; }
993
1241
  .starter-tile:hover { border-color: ${PALETTE.accent}; }
@@ -998,9 +1246,7 @@ const STYLES = `
998
1246
  .copy-row { background: #1f1c18; border-color: #3a362f; }
999
1247
  .btn-secondary, .btn-copy { color: #e8e4dc; border-color: #3a362f; }
1000
1248
  .btn-secondary:hover, .btn-copy:hover { background: #1f1c18; border-color: ${PALETTE.accent}; }
1001
- .token-mint > summary { color: #f0ece4; }
1002
- .token-mint-sub, .token-mint-intro, .mint-verbs legend, .minted-hint { color: #a8a29a; }
1003
- .mint-verbs { border-color: #3a362f; }
1249
+ .minted-hint { color: #a8a29a; }
1004
1250
  .minted-title, .minted-warn { color: #f0ece4; }
1005
1251
  }
1006
1252
  `;