Skip to content

Team vault: unlock when the key lands, name the owner in the waiting copy, role-aware landing - #186

Merged
kipavy merged 1 commit into
devfrom
feat/vault-join-first-access
Aug 26, 2026
Merged

Team vault: unlock when the key lands, name the owner in the waiting copy, role-aware landing#186
kipavy merged 1 commit into
devfrom
feat/vault-join-first-access

Conversation

@kipavy

@kipavy kipavy commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes the parts of #70 that survived review. Rebased-in-place: an earlier revision of this PR carried a first-access welcome card; it was cut after review, see the bottom.

What was already built

#41 shipped the mechanism and the first cut of the keyless UX, and none of it was rebuilt here: the awaiting_key status, the clock-icon sidebar badge, the dedicated waiting panel with its retry button, reconcileAwaitingKeys, reconcileTeamVaultKeys and the team_members:<id> SSE handler all already worked.

The bug this fixes

put_vault_keys (server/src/routes/team_sync.rs) notifies every recipient of a freshly wrapped key with membership_changed. The joiner is already a member when that fires, so handleMembershipChangedEvent computes a zero delta, onTeamAdded never runs, and nothing re-read the key that had just landed.

So the benign, self-healing waiting state was in practice a permanent one: the vault stayed locked until the user hit Retry or restarted the app. refreshAwaitingKeyTeams() now re-fetches any team still in awaiting_key on that event.

It is a foreground fetch on purpose. { background: true } suppresses every status write — including the loaded that a team with no blob yet reaches — so the vault unlocked in memory while the panel kept saying "waiting". That was observed live before it was fixed, which is the only reason it is called out here.

The other two changes

The waiting copy names the owner. waitingForAccessBodyNamed: "Waiting for @handle to grant access — they need to come online once." The panel loads the roster itself, because it stands in for the pages that otherwise would, and falls back to the existing generic sentence while the handle is unresolved rather than flashing a blank name.

Role-aware landing. firstViewNav reads permission bits, not role names, so custom roles are covered too: CONNECT lands on connections, VIEW_SECRETS on the keychain, MANAGE_MEMBERS on members. Applied when a joined vault loads, and when a vault unlocks under the waiting panel the user is actually looking at — never from a background event for a vault that is off screen, which would yank the nav out from under them.

selectedTeamId was extracted out of MainPanel's hook so the "which team is on screen" rule has one definition rather than two.

Verification

Two accounts, two app instances, an isolated server + DB (testing-voltius-team-features-e2e), screenshotted from the running app at each step:

  1. Bob accepts while Alice is offline, then opens the vault → "Waiting for @calm-willow-5636 to grant access — they need to come online once." Clock icon, dimmed badge, no alert triangle, no error toast. Server log: GET .../vault-key → 404.
  2. Alice comes online → Vault keys upserted key_count=2 → Bob's window, untouched, flips to the loaded vault. No manual retry. This is the path that did not work before.
  3. Restart on both sides, no regressions.

Full suite green: 511 files / 3934 tests. tsc --noEmit clean.

Copy I cannot personally review

One new string per locale, layout.mainPanel.teamVault.waitingForAccessBodyNamed, in fr / ru / zh. fr and ru deliberately avoid a gendered verb for the owner ("une seule connexion de sa part suffit" / "достаточно один раз выйти в сеть").

What #70 asked for and this deliberately does not build

A welcome card on first access. The waiting panel already promises this exact outcome ("this happens automatically the next time an owner is online"), so the vault opening is the promise being kept, not an unexplained event that needs narrating. And every fact the card would carry is already on screen: vault name, "team" and member count in the header, role in the Members tab, contents in the list the card would have been covering. It is not worth a persisted store, an entry in the account-park list and 28 locale strings. Recorded on #70 as deliberately-not-built.

Still open on #70

@kipavy
kipavy force-pushed the feat/vault-join-first-access branch from c147246 to de18fc1 Compare August 26, 2026 11:53
@kipavy kipavy changed the title Vault join & first-access: welcome moment, named waiting state, role-aware landing Team vault: unlock when the key lands, name the owner in the waiting copy, role-aware landing Aug 26, 2026
…e owner

Closes the parts of #70 that survived review. The keyless mechanism and the
benign waiting panel shipped with #41; what was missing was that the wait
never ended on its own, and that the copy could not say who it was waiting
for.

- `put_vault_keys` notifies every key recipient with `membership_changed`,
  but the joiner is already a member by then, so the membership delta is
  zero, `onTeamAdded` never fires, and nothing re-read the key that had just
  landed. The honest waiting state was therefore also a permanent one until
  the user hit Retry or restarted. Any team still in `awaiting_key` is now
  re-fetched on that event.

  Foreground fetch on purpose: `{ background: true }` suppresses every status
  write, including the "loaded" a team with no blob yet reaches, which unlocks
  the vault in memory while the panel keeps saying "waiting".

- The waiting copy names the owner — "Waiting for @handle to grant access —
  they need to come online once" — and falls back to the existing generic
  sentence while the roster is unresolved rather than flashing a blank name.
  The panel loads the roster itself, since it stands in for the pages that
  otherwise would.

- A member landing in a team vault for the first time gets the surface their
  role can use, chosen by permission bits rather than role name: a
  connect-only invitee lands on connections, not a keychain of redacted rows.

#70 also asked for a welcome card on first access. Deliberately not built:
the waiting panel already promises this exact outcome, so the vault opening
is the promise being kept rather than an unexplained event, and every fact
such a card would carry — vault name, sharer, role, contents — is already in
the vault header, the Members tab and the list behind it.
@kipavy
kipavy force-pushed the feat/vault-join-first-access branch from de18fc1 to b2865db Compare August 26, 2026 12:44
@kipavy

kipavy commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto dev (through #185) and re-verified live on the rebased code, after the welcome card was cut.

Two accounts, two app instances, isolated server + DB.

  1. kind-umber-0094 (owner) converts a private vault to a team vault and invites proud-quartz-7631 as member, then goes offline.
  2. The invitee accepts and opens the vault → "Waiting for @kind-umber-0094 to grant access — they need to come online once." team_vault_keys holds 1 row (the owner's). Clock icon, no alert triangle, no error toast.
  3. While waiting, the invitee navigates to the Keychain tab — so the landing surface has something to move away from.
  4. The owner comes online → key_count=2 → the invitee's window, untouched: the waiting panel is replaced by the vault, and the active tab flips Keychain → Hosts. That is applyFirstViewNav firing from refreshAwaitingKeyTeams for the vault actually on screen, and it is the behaviour that had no coverage before this run — the auto-unlock itself was verified on the pre-cut revision.

Full suite on the rebased branch: 512 files / 3941 passed, 2 skipped. The one failing suite was TitleBar.syncState.test.tsx — a 20s beforeAll dynamic-import timeout while the box was running two vite dev servers and two app instances; it passes on its own (2/2) and the diff does not touch it. CI on the rebased head is green on all four checks.

@kipavy
kipavy merged commit 9a99803 into dev Aug 26, 2026
4 checks passed
@kipavy
kipavy deleted the feat/vault-join-first-access branch August 26, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant