@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.
- package/package.json +1 -2
- package/src/__tests__/account-home-ui.test.ts +344 -110
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/account-setup.test.ts +880 -0
- package/src/__tests__/account-usage.test.ts +137 -0
- package/src/__tests__/account-vault-admin-token.test.ts +301 -0
- package/src/__tests__/account-vault-token.test.ts +53 -1
- package/src/__tests__/admin-vault-admin-token.test.ts +17 -0
- package/src/__tests__/admin-vaults.test.ts +20 -0
- package/src/__tests__/api-account.test.ts +236 -4
- package/src/__tests__/api-invites.test.ts +217 -0
- package/src/__tests__/api-mint-token.test.ts +259 -10
- package/src/__tests__/api-modules-ops.test.ts +195 -3
- package/src/__tests__/api-modules.test.ts +40 -4
- package/src/__tests__/api-settings-hub-origin.test.ts +13 -8
- package/src/__tests__/auto-wire.test.ts +101 -1
- package/src/__tests__/cli.test.ts +188 -2
- package/src/__tests__/cloudflare-state.test.ts +104 -0
- package/src/__tests__/expose-2fa-warning.test.ts +11 -8
- package/src/__tests__/expose-cloudflare.test.ts +135 -9
- package/src/__tests__/expose-interactive.test.ts +234 -7
- package/src/__tests__/expose-supervisor-version.test.ts +104 -0
- package/src/__tests__/expose.test.ts +10 -5
- package/src/__tests__/grants.test.ts +197 -8
- package/src/__tests__/hub-origin-resolution.test.ts +179 -25
- package/src/__tests__/hub-server.test.ts +761 -13
- package/src/__tests__/hub-unit.test.ts +185 -0
- package/src/__tests__/init.test.ts +579 -3
- package/src/__tests__/install.test.ts +448 -2
- package/src/__tests__/invites.test.ts +220 -0
- package/src/__tests__/launchctl-guard.test.ts +185 -0
- package/src/__tests__/migrate-cutover.test.ts +33 -0
- package/src/__tests__/module-ops-client.test.ts +68 -0
- package/src/__tests__/scope-explanations.test.ts +16 -0
- package/src/__tests__/serve-boot.test.ts +74 -1
- package/src/__tests__/serve.test.ts +121 -7
- package/src/__tests__/setup-wizard.test.ts +110 -0
- package/src/__tests__/spawn-path.test.ts +191 -0
- package/src/__tests__/status.test.ts +64 -0
- package/src/__tests__/supervisor.test.ts +374 -0
- package/src/__tests__/users.test.ts +66 -0
- package/src/__tests__/well-known.test.ts +25 -0
- package/src/__tests__/wizard.test.ts +72 -1
- package/src/account-home-ui.ts +481 -235
- package/src/account-mirror.ts +126 -0
- package/src/account-setup.ts +381 -0
- package/src/account-usage.ts +118 -0
- package/src/account-vault-admin-token.ts +242 -0
- package/src/account-vault-token.ts +36 -2
- package/src/admin-login-ui.ts +121 -0
- package/src/admin-vault-admin-token.ts +8 -2
- package/src/admin-vaults.ts +137 -29
- package/src/api-account.ts +118 -1
- package/src/api-invites.ts +345 -0
- package/src/api-mint-token.ts +81 -0
- package/src/api-modules-ops.ts +168 -53
- package/src/api-modules.ts +36 -0
- package/src/auto-wire.ts +87 -0
- package/src/cli.ts +128 -34
- package/src/cloudflare/detect.ts +1 -1
- package/src/cloudflare/state.ts +104 -8
- package/src/commands/expose-2fa-warning.ts +17 -13
- package/src/commands/expose-cloudflare.ts +103 -36
- package/src/commands/expose-interactive.ts +163 -17
- package/src/commands/expose-supervisor.ts +45 -0
- package/src/commands/init.ts +183 -4
- package/src/commands/install.ts +321 -3
- package/src/commands/migrate-cutover.ts +12 -5
- package/src/commands/serve-boot.ts +33 -3
- package/src/commands/serve.ts +158 -37
- package/src/commands/status.ts +9 -1
- package/src/commands/wizard.ts +36 -2
- package/src/grants.ts +113 -0
- package/src/help.ts +18 -5
- package/src/hub-db.ts +70 -2
- package/src/hub-server.ts +438 -41
- package/src/hub-settings.ts +3 -3
- package/src/hub-unit.ts +259 -9
- package/src/invites.ts +291 -0
- package/src/launchctl-guard.ts +131 -0
- package/src/managed-unit.ts +13 -3
- package/src/migrate-offer.ts +15 -6
- package/src/module-ops-client.ts +47 -22
- package/src/scope-attenuation.ts +19 -0
- package/src/scope-explanations.ts +9 -1
- package/src/service-spec.ts +17 -4
- package/src/setup-wizard.ts +34 -2
- package/src/spawn-path.ts +148 -0
- package/src/supervisor.ts +232 -7
- package/src/users.ts +54 -8
- package/src/vault-hub-origin-env.ts +28 -0
- package/src/vault-name.ts +13 -1
- package/src/well-known.ts +13 -0
- package/web/ui/dist/assets/{index-mz8XcVPP.css → index-BYYUeLGA.css} +1 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-D_0TRjeo.js +0 -61
package/src/account-home-ui.ts
CHANGED
|
@@ -117,31 +117,80 @@ export interface RenderAccountHomeOpts {
|
|
|
117
117
|
*/
|
|
118
118
|
twoFactorEnabled: boolean;
|
|
119
119
|
/**
|
|
120
|
-
* Per-vault
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
-
${
|
|
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
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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 } =
|
|
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).
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
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
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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>
|
|
566
|
+
<p>Your vault${
|
|
375
567
|
assignedVaults.length === 1 ? "" : "s"
|
|
376
|
-
} —
|
|
377
|
-
|
|
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
|
|
409
|
-
*
|
|
410
|
-
*
|
|
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
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
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
|
|
421
|
-
* `/account/vault-token/<name>`
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
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
|
|
618
|
+
function renderBackupBlock(
|
|
427
619
|
vaultName: string,
|
|
428
|
-
|
|
429
|
-
|
|
620
|
+
mirrorLine: string | undefined,
|
|
621
|
+
pushing: boolean,
|
|
622
|
+
holdsAdmin: boolean,
|
|
430
623
|
csrfToken: string,
|
|
431
624
|
): string {
|
|
432
|
-
if (
|
|
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
|
-
<
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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, .
|
|
584
|
-
//
|
|
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
|
-
.
|
|
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
|
-
.
|
|
833
|
-
|
|
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
|
-
|
|
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,
|
|
983
|
-
.
|
|
984
|
-
.
|
|
985
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
`;
|