From 57d6a304db9a71e8c79e35894c932ec20d4ea1a5 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 20:24:41 -0700 Subject: [PATCH] Unify open flow around local vaults --- main.go | 92 +++++++++++++++++++++--- main_test.go | 150 +++++++++++++++++++++++++++++++++++++-- ui_forms.go | 197 ++++----------------------------------------------- 3 files changed, 240 insertions(+), 199 deletions(-) diff --git a/main.go b/main.go index 1aee591..1a35597 100644 --- a/main.go +++ b/main.go @@ -217,6 +217,13 @@ const ( syncDirectionPush syncDirection = "push" ) +type syncDialogPurpose string + +const ( + syncDialogPurposeAdvanced syncDialogPurpose = "advanced" + syncDialogPurposeRemoteSetup syncDialogPurpose = "remote-setup" +) + type ui struct { mode string theme *material.Theme @@ -286,6 +293,7 @@ type ui struct { toggleSyncMenu widget.Clickable toggleMainMenu widget.Clickable openAdvancedSync widget.Clickable + useSavedAdvancedSyncRemote widget.Clickable openSelectedVaultRemote widget.Clickable saveCurrentRemoteBinding widget.Clickable openSecuritySettings widget.Clickable @@ -446,6 +454,7 @@ type ui struct { syncRemoteUsername widget.Editor syncRemotePassword widget.Editor selectedSyncRemoteCredentialEntryID string + syncDialogPurpose syncDialogPurpose syncDialogOpen bool syncMenuOpen bool mainMenuOpen bool @@ -1361,11 +1370,28 @@ func (u *ui) openAdvancedSyncDialog() { u.syncDialogOpen = true u.syncMenuOpen = false u.showSyncPassword = false + u.syncDialogPurpose = syncDialogPurposeAdvanced u.syncSourceMode = u.syncDefaultSourceMode u.syncDirection = u.syncDefaultDirection if strings.TrimSpace(u.syncLocalPath.Text()) == "" { u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text())) } + u.syncSavedRemoteBindingSelection() + u.prefillAdvancedSyncRemoteFromSavedBinding() +} + +func (u *ui) openRemoteSyncSetupDialog() { + u.syncDialogOpen = true + u.syncMenuOpen = false + u.showSyncPassword = false + u.syncDialogPurpose = syncDialogPurposeRemoteSetup + u.syncSourceMode = syncSourceRemote + u.syncDirection = syncDirectionPush + if strings.TrimSpace(u.syncLocalPath.Text()) == "" { + u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text())) + } + u.syncSavedRemoteBindingSelection() + u.prefillAdvancedSyncRemoteFromSavedBinding() } func (u *ui) clearSyncLocalImport() { @@ -2012,12 +2038,15 @@ func (u *ui) restoreStartupLifecycleTarget() { remoteRecord, hasRemote, remoteUsedAt := u.latestRecentRemote() switch { - case hasRemote && (localPath == "" || remoteUsedAt.After(localUsedAt)): - u.lifecycleMode = "remote" - u.applyRecentRemoteRecord(remoteRecord) + case hasRemote && strings.TrimSpace(remoteRecord.LocalVaultPath) != "" && (localPath == "" || remoteUsedAt.After(localUsedAt)): + u.lifecycleMode = "local" + u.vaultPath.SetText(strings.TrimSpace(remoteRecord.LocalVaultPath)) case localPath != "": u.lifecycleMode = "local" u.vaultPath.SetText(localPath) + case hasRemote: + u.lifecycleMode = "remote" + u.applyRecentRemoteRecord(remoteRecord) } } @@ -2251,6 +2280,27 @@ func (u *ui) applyAdvancedSyncRemoteCredentialEntry(entry vault.Entry) { u.syncRemotePassword.SetText(entry.Password) } +func (u *ui) savedAdvancedSyncRemoteBinding() (appstate.ResolvedRemoteBinding, bool) { + if !u.hasOpenVault() { + return appstate.ResolvedRemoteBinding{}, false + } + _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding() + if err != nil || !ok { + return appstate.ResolvedRemoteBinding{}, false + } + return resolved, true +} + +func (u *ui) prefillAdvancedSyncRemoteFromSavedBinding() { + resolved, ok := u.savedAdvancedSyncRemoteBinding() + if !ok { + return + } + u.syncRemoteBaseURL.SetText(resolved.Profile.BaseURL) + u.syncRemotePath.SetText(resolved.Profile.Path) + u.applyAdvancedSyncRemoteCredentialEntry(resolved.Credentials) +} + func (u *ui) selectVaultRemoteProfile(id string) { id = strings.TrimSpace(id) u.selectedVaultRemoteProfileID = id @@ -2484,6 +2534,18 @@ func (u *ui) directRemoteSyncShortcutLabel() string { return "Use Remote Sync" } +func (u *ui) shouldShowRemoteSyncSetupShortcut() bool { + if !u.hasOpenVault() || u.isVaultLocked() || u.state.Section != appstate.SectionEntries { + return false + } + _, ok := u.selectedVaultRemoteBinding() + return !ok +} + +func (u *ui) remoteSyncSetupShortcutLabel() string { + return "Set Up Remote Sync" +} + func remoteBindingSuffix(baseURL, path, username string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username))) return hex.EncodeToString(sum[:8]) @@ -4105,6 +4167,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions { } } } + for u.useSavedAdvancedSyncRemote.Clicked(gtx) { + u.openRemoteSyncSetupDialog() + } for u.openSelectedVaultRemote.Clicked(gtx) { if u.lifecycleBusy() { continue @@ -6646,16 +6711,27 @@ func (u *ui) pathBar(gtx layout.Context) layout.Dimensions { return children }()...) } - if !u.shouldShowDirectRemoteSyncShortcut() { + if !u.shouldShowDirectRemoteSyncShortcut() && !u.shouldShowRemoteSyncSetupShortcut() { return crumbBar(gtx) } - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + children := []layout.FlexChild{ layout.Rigid(crumbBar), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { + } + if u.shouldShowDirectRemoteSyncShortcut() { + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openSelectedVaultRemote, u.directRemoteSyncShortcutLabel()) - }), - ) + })) + } + if u.shouldShowRemoteSyncSetupShortcut() { + if u.shouldShowDirectRemoteSyncShortcut() { + children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + } + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.useSavedAdvancedSyncRemote, u.remoteSyncSetupShortcutLabel()) + })) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) } func (u *ui) visibleBreadcrumbs(displayPath []string) ([]string, []int) { diff --git a/main_test.go b/main_test.go index 5cd044f..3ab66ff 100644 --- a/main_test.go +++ b/main_test.go @@ -4934,14 +4934,11 @@ func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) { second := newUIWithSession("desktop", &session.Manager{}, paths) - if got := second.lifecycleMode; got != "remote" { - t.Fatalf("lifecycleMode = %q, want remote", got) + if got := second.lifecycleMode; got != "local" { + t.Fatalf("lifecycleMode = %q, want local", got) } - if got := second.remoteBaseURL.Text(); got != "https://dav.example.com" { - t.Fatalf("remoteBaseURL = %q, want https://dav.example.com", got) - } - if got := second.remotePath.Text(); got != "vaults/home.kdbx" { - t.Fatalf("remotePath = %q, want vaults/home.kdbx", got) + if got := second.vaultPath.Text(); got != "/tmp/local.kdbx" { + t.Fatalf("vaultPath = %q, want /tmp/local.kdbx", got) } if got := second.remoteUsername.Text(); got != "" { t.Fatalf("remoteUsername = %q, want empty for location-only recent remote", got) @@ -5422,6 +5419,31 @@ func TestRestoreStartupLifecycleTargetSelectsMostRecentLocalVault(t *testing.T) } } +func TestRestoreStartupLifecycleTargetUsesLocalCacheFromRecentRemote(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + u.lifecycleMode = "remote" + u.vaultPath.SetText("") + u.recentVaults = []string{"/tmp/older.kdbx"} + u.recentVaultUsedAt["/tmp/older.kdbx"] = time.Date(2026, time.April, 5, 1, 2, 3, 0, time.UTC) + u.recentRemotes = []recentRemoteRecord{{ + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + LocalVaultPath: "/tmp/family-cache.kdbx", + UsedAt: time.Date(2026, time.April, 5, 2, 2, 3, 0, time.UTC).Format(time.RFC3339Nano), + }} + + u.restoreStartupLifecycleTarget() + + if got := u.lifecycleMode; got != "local" { + t.Fatalf("lifecycleMode after restore = %q, want local", got) + } + if got := u.vaultPath.Text(); got != "/tmp/family-cache.kdbx" { + t.Fatalf("vaultPath after restore = %q, want /tmp/family-cache.kdbx", got) + } +} + func TestShowLocalVaultChooser(t *testing.T) { t.Parallel() @@ -5795,6 +5817,120 @@ func TestUIDirectRemoteSyncShortcutLabelUsesSyncLanguage(t *testing.T) { } } +func TestUIShouldShowRemoteSyncSetupShortcutForOpenedLocalVaultWithoutSavedBinding(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + Entries: []vault.Entry{{ + ID: "entry-1", + Title: "Vault Console", + Path: []string{"Crew", "Internet"}, + }}, + }) + u.state.Section = appstate.SectionEntries + + if !u.shouldShowRemoteSyncSetupShortcut() { + t.Fatal("shouldShowRemoteSyncSetupShortcut() = false, want true for opened local vault without saved binding") + } +} + +func TestUIShouldHideRemoteSyncSetupShortcutWhenSavedBindingExists(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + Entries: []vault.Entry{{ + ID: "remote-creds-1", + Title: "WebDAV Sign-In", + Username: "tjulian", + Path: []string{"Crew", "Internet"}, + }}, + RemoteProfiles: []vault.RemoteProfile{{ + ID: "family-webdav", + Name: "Family Vault", + Backend: vault.RemoteBackendWebDAV, + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + }}, + }) + u.state.Section = appstate.SectionEntries + + if u.shouldShowRemoteSyncSetupShortcut() { + t.Fatal("shouldShowRemoteSyncSetupShortcut() = true, want false when saved binding already exists") + } +} + +func TestUIRemoteSyncSetupShortcutLabelUsesClearLanguage(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + + if got := u.remoteSyncSetupShortcutLabel(); got != "Set Up Remote Sync" { + t.Fatalf("remoteSyncSetupShortcutLabel() = %q, want Set Up Remote Sync", got) + } +} + +func TestUIOpenRemoteSyncSetupDialogPrefillsCurrentVaultSetupFlow(t *testing.T) { + t.Parallel() + + u := newUIWithModel("desktop", vault.Model{ + Entries: []vault.Entry{{ + ID: "remote-creds-1", + Title: "WebDAV Sign-In", + Username: "tjulian", + Password: "token-1", + Path: []string{"Crew", "Internet"}, + }}, + RemoteProfiles: []vault.RemoteProfile{{ + ID: "family-webdav", + Name: "Family Vault", + Backend: vault.RemoteBackendWebDAV, + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + }}, + }) + u.vaultPath.SetText("/vaults/family.kdbx") + + u.openRemoteSyncSetupDialog() + + if !u.syncDialogOpen { + t.Fatal("syncDialogOpen = false, want true") + } + if got := u.syncDialogPurpose; got != syncDialogPurposeRemoteSetup { + t.Fatalf("syncDialogPurpose = %q, want remote setup", got) + } + if got := u.syncSourceMode; got != syncSourceRemote { + t.Fatalf("syncSourceMode = %q, want remote", got) + } + if got := u.syncDirection; got != syncDirectionPush { + t.Fatalf("syncDirection = %q, want push", got) + } + if got := u.syncLocalPath.Text(); got != "/vaults/family.kdbx" { + t.Fatalf("syncLocalPath = %q, want current vault path", got) + } + if got := u.syncRemoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" { + t.Fatalf("syncRemoteBaseURL = %q, want saved remote base URL", got) + } + if got := u.syncRemotePath.Text(); got != "files/family/keepass.kdbx" { + t.Fatalf("syncRemotePath = %q, want saved remote path", got) + } + if got := u.syncRemoteUsername.Text(); got != "tjulian" { + t.Fatalf("syncRemoteUsername = %q, want tjulian", got) + } + if got := u.syncRemotePassword.Text(); got != "token-1" { + t.Fatalf("syncRemotePassword = %q, want token-1", got) + } +} + +func TestUISelectedLocalVaultRemoteSyncSummaryMentionsSetup(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + + if got := u.selectedLocalVaultRemoteSyncSummary("/vaults/family.kdbx"); got != "Open this vault to set up a WebDAV sync target for it." { + t.Fatalf("selectedLocalVaultRemoteSyncSummary() = %q, want setup guidance", got) + } +} + func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) { t.Parallel() diff --git a/ui_forms.go b/ui_forms.go index 7be0422..5149dc6 100644 --- a/ui_forms.go +++ b/ui_forms.go @@ -21,7 +21,6 @@ import ( func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { busy := u.lifecycleBusy() showLocalChooser := u.showLocalVaultChooser() - showRemoteChooser := u.showRemoteConnectionChooser() selectedLocalPath := strings.TrimSpace(u.vaultPath.Text()) return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { @@ -31,172 +30,13 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - message := "Choose a recent vault or enter a .kdbx path, then unlock it." - if u.lifecycleMode == "remote" { - message = u.remoteLifecycleMessage() - } + message := "Choose a recent vault or enter a .kdbx path, then unlock it. Remote sync attaches to that local vault after it opens." lbl := material.Label(u.theme, unit.Sp(14), message) lbl.Color = accentColor return lbl.Layout(gtx) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy { - return passiveSectionTab(gtx, u.theme, "Local Vault", u.lifecycleMode == "local") - } - return sectionTabButton(gtx, u.theme, &u.showLocalLifecycle, "Local Vault", u.lifecycleMode == "local") - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy { - return passiveSectionTab(gtx, u.theme, "Remote Vault", u.lifecycleMode == "remote") - } - return sectionTabButton(gtx, u.theme, &u.showRemoteLifecycle, "Remote Vault", u.lifecycleMode == "remote") - }), - ) - }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.lifecycleMode == "remote" { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, unit.Sp(12), "LOCATION") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return labeledEditorHelp(u.theme, "Remote Base URL", "Base WebDAV endpoint, for example https://server/remote.php/webdav.", &u.remoteBaseURL, false)(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return labeledEditorHelp(u.theme, "Remote Path", "Path to the remote .kdbx file under the WebDAV base URL.", &u.remotePath, false)(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if showRemoteChooser || !u.hasSelectedRemoteTarget() { - return layout.Dimensions{} - } - return layout.Dimensions{} - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if showRemoteChooser && !busy { - return u.recentRemoteList(gtx) - } - return layout.Dimensions{} - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - lbl := material.Label(u.theme, unit.Sp(12), "AUTHENTICATION") - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(4)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return labeledEditorHelp(u.theme, "Remote Username", "Username used to authenticate to the WebDAV server.", &u.remoteUsername, false)(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return labeledEditorHelp(u.theme, "Remote Password", "Password or app token used to authenticate to the WebDAV server.", &u.remotePassword, true)(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(6)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Inset{Top: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return tonedButton(gtx, u.theme, &u.openRemotePrefsHelp, "Settings & Help") - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(13), "Local Cache Bootstrap") - lbl.Color = accentColor - return lbl.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(12), u.remoteLifecycleSetupSummary()) - lbl.Color = mutedColor - return lbl.Layout(gtx) - }), - ) - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if !showRemoteChooser { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(10)}.Layout(gtx) - }), - ) - } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { if !showLocalChooser { @@ -326,29 +166,6 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions { }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if u.lifecycleMode == "remote" { - label := u.remoteOpenButtonLabel() - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy { - return passiveTonedButton(gtx, u.theme, label) - } - return tonedButton(gtx, u.theme, &u.openRemote, label) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy || !u.hasSelectedRemoteTarget() { - return layout.Dimensions{} - } - return layout.Spacer{Height: unit.Dp(8)}.Layout(gtx) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if busy || !u.hasSelectedRemoteTarget() { - return layout.Dimensions{} - } - return u.selectedRemoteConnectionCard(gtx) - }), - ) - } return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { label := "Open Vault" @@ -499,6 +316,11 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime lbl.Color = mutedColor return lbl.Layout(gtx) }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + lbl := material.Label(u.theme, unit.Sp(11), u.selectedLocalVaultRemoteSyncSummary(path)) + lbl.Color = mutedColor + return lbl.Layout(gtx) + }), layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.clearVaultSelection, "Open Different Vault") @@ -508,6 +330,13 @@ func (u *ui) selectedLocalVaultCard(gtx layout.Context, path string) layout.Dime }) } +func (u *ui) selectedLocalVaultRemoteSyncSummary(path string) string { + if record, ok := u.boundRecentRemoteForLocalVault(path); ok { + return "Saved remote sync target: " + friendlyRecentRemoteLabel(record) + } + return "Open this vault to set up a WebDAV sync target for it." +} + func (u *ui) lifecycleSecuritySettingsSummary() string { return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." }