Skip to content

feat: add reusable SSH tunnel profiles#385

Merged
datlechin merged 11 commits intomainfrom
feat/ssh-profiles
Mar 20, 2026
Merged

feat: add reusable SSH tunnel profiles#385
datlechin merged 11 commits intomainfrom
feat/ssh-profiles

Conversation

@datlechin
Copy link
Collaborator

@datlechin datlechin commented Mar 20, 2026

Closes #381

Summary

  • Add SSHProfile model with all SSH configuration fields (host, port, auth method, jump hosts, TOTP)
  • Add SSHProfileStorage for profile persistence (UserDefaults) and secrets (Keychain)
  • Add sshProfileId: UUID? to DatabaseConnection for profile reference
  • Update DatabaseManager.buildEffectiveConnection to resolve SSH config from profile or inline
  • Add profile picker to connection form SSH tab with read-only profile summary
  • SSH secrets stored per-profile in Keychain (separate key namespace from per-connection)
  • Backward compatible: existing connections with inline SSH config work unchanged

Architecture

Follows the existing ConnectionGroup/ConnectionTag pattern:

  • Model: SSHProfile (Identifiable, Codable, Sendable) with toSSHConfiguration() conversion
  • Storage: SSHProfileStorage singleton with CRUD + 3 Keychain secret categories
  • Resolution: Profile resolved at connect time in buildEffectiveConnection — plugins remain unaware
  • UI: Picker in SSH tab, profile summary when selected, inline fields when not

Test plan

  • Create SSH profile with password auth → verify saved in UserDefaults + Keychain
  • Create connection using SSH profile → verify tunnel connects via profile config
  • Create second connection using same profile → verify both work
  • Edit profile → reconnect → verify changes take effect on both connections
  • Delete profile → verify connection shows warning, falls back to disabled SSH
  • Existing connections without profiles → verify no regression
  • Test connection button with profile selected → verify tunnel uses profile
  • Duplicate connection with profile → verify profile reference preserved

Summary by CodeRabbit

  • New Features
    • Reusable SSH tunnel profiles: create, edit, delete, and pick profiles from connection forms; view profile summary and save inline settings as a new profile; per-profile secret storage for reuse across connections.
  • Bug Fixes
    • Gracefully handles missing or orphaned profiles with a warning and an option to revert to inline SSH configuration.

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reusable SSH tunnel profiles: new SSHProfile model and SSHProfileStorage, persists sshProfileId on DatabaseConnection, updates Connection form to select/use profiles, and makes DatabaseManager prefer profile-backed SSH settings and load profile-scoped secrets when building tunnels.

Changes

Cohort / File(s) Summary
SSH Profile model & storage
TablePro/Models/Connection/SSHProfile.swift, TablePro/Core/Storage/SSHProfileStorage.swift
Adds SSHProfile model and SSHProfileStorage singleton (UserDefaults-backed list) with CRUD and per-profile Keychain secret save/load/delete APIs.
Connection model & persistence
TablePro/Models/Connection/DatabaseConnection.swift, TablePro/Core/Storage/ConnectionStorage.swift
Adds optional sshProfileId: UUID? to DatabaseConnection and updates storage encoding/decoding and duplication to persist the profile ID.
Tunnel resolution & credential loading
TablePro/Core/Database/DatabaseManager.swift
buildEffectiveConnection now prefers an SSH profile when sshProfileId is present, resolves SSH config from the profile or inline, and loads SSH/TOTP secrets using the appropriate secret-owner (profile id vs connection id) before tunnel creation.
Connection form UI & secret persistence
TablePro/Views/Connection/ConnectionFormView.swift
Adds SSH Profile picker and editor flows; shows profile summary or inline fields; saves per-connection secrets only when no profile is selected; includes UI/flow to save current inline settings as a profile.
SSH Profile editor view
TablePro/Views/Connection/SSHProfileEditorView.swift
New SwiftUI view to create/edit/delete SSH profiles, manage jump hosts, auth, and persist per-profile secrets via SSHProfileStorage.
Changelog
CHANGELOG.md
Documents "Reusable SSH tunnel profiles" under Unreleased.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (ConnectionFormView)
    participant CStor as ConnectionStorage
    participant SStor as SSHProfileStorage
    participant DBMgr as DatabaseManager
    participant Keychain as KeychainHelper
    participant Tunnel as SSH Tunnel

    User->>CStor: Save connection (includes sshProfileId?)
    CStor-->>User: Persisted

    User->>DBMgr: Test/Build connection
    DBMgr->>CStor: Load connection
    CStor-->>DBMgr: connection (+ sshProfileId)

    alt sshProfileId present
        DBMgr->>SStor: profile(for: sshProfileId)
        SStor-->>DBMgr: SSHProfile?
        DBMgr->>Keychain: load secrets (owner = profileId)
    else inline or no profile
        DBMgr->>Keychain: load secrets (owner = connectionId)
    end

    Keychain-->>DBMgr: secrets
    DBMgr->>DBMgr: resolve effective SSH config
    DBMgr->>Tunnel: create tunnel with resolved config
    Tunnel-->>DBMgr: tunnel established
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I dug a profile neat and small,

Saved my keys so I needn't stall.
Hop once, hop twice, connections cheer,
One tunnel shared, the path is clear.
🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add reusable SSH tunnel profiles' directly and concisely describes the main feature being added, which aligns with the primary objective of the PR.
Linked Issues check ✅ Passed The PR implements all key requirements from issue #381: adds reusable SSH profiles, integrates profile selection in connection forms, persists profiles separately, maintains backward compatibility with inline SSH configs, and enables profile reuse across multiple connections.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objectives of implementing reusable SSH profiles. No unrelated modifications to unrelated features or components are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ssh-profiles
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
TablePro/Views/Connection/ConnectionFormView.swift (1)

1278-1289: ⚠️ Potential issue | 🟡 Minor

Test connection saves SSH secrets even when using a profile.

When sshProfileId is set, the test connection flow still saves SSH credentials to the connection-level keychain keys (lines 1278-1289). This is inconsistent with the save logic (lines 1111-1126) which skips saving secrets when a profile is used.

For test connections with a profile, the credentials should come from the profile storage, not be temporarily saved to connection storage.

🐛 Suggested fix
                 if !password.isEmpty {
                     ConnectionStorage.shared.savePassword(password, for: testConn.id)
                 }
-                if sshEnabled
-                    && (sshAuthMethod == .password || sshAuthMethod == .keyboardInteractive)
-                    && !sshPassword.isEmpty
-                {
-                    ConnectionStorage.shared.saveSSHPassword(sshPassword, for: testConn.id)
-                }
-                if sshEnabled && sshAuthMethod == .privateKey && !keyPassphrase.isEmpty {
-                    ConnectionStorage.shared.saveKeyPassphrase(keyPassphrase, for: testConn.id)
-                }
-                if sshEnabled && totpMode == .autoGenerate && !totpSecret.isEmpty {
-                    ConnectionStorage.shared.saveTOTPSecret(totpSecret, for: testConn.id)
+                // Only save SSH secrets for test when using inline config (not a profile)
+                if sshEnabled && sshProfileId == nil {
+                    if (sshAuthMethod == .password || sshAuthMethod == .keyboardInteractive)
+                        && !sshPassword.isEmpty
+                    {
+                        ConnectionStorage.shared.saveSSHPassword(sshPassword, for: testConn.id)
+                    }
+                    if sshAuthMethod == .privateKey && !keyPassphrase.isEmpty {
+                        ConnectionStorage.shared.saveKeyPassphrase(keyPassphrase, for: testConn.id)
+                    }
+                    if totpMode == .autoGenerate && !totpSecret.isEmpty {
+                        ConnectionStorage.shared.saveTOTPSecret(totpSecret, for: testConn.id)
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 1278 - 1289,
The test connection flow is incorrectly saving SSH secrets to connection storage
even when an SSH profile is used (sshProfileId is set); update the block in the
test connection path that calls ConnectionStorage.shared.saveSSHPassword,
saveKeyPassphrase, and saveTOTPSecret to first check that sshProfileId is
nil/empty (i.e., only save when no profile is selected), mirroring the save
logic used in the main save flow (so use sshProfileId guard around the save
calls for testConn.id).
🧹 Nitpick comments (3)
TablePro/Views/Connection/ConnectionFormView.swift (1)

476-485: Consider caching the profiles list.

SSHProfileStorage.shared.loadProfiles() is called directly in the ForEach within the view body. This could cause repeated decoding if the view re-renders frequently. Consider loading profiles into a @State property on appear or using a computed property with caching.

♻️ Suggested approach
+    `@State` private var availableSSHProfiles: [SSHProfile] = []
+
     // In onAppear or .task:
+    availableSSHProfiles = SSHProfileStorage.shared.loadProfiles()

     // In sshProfileSection:
     Picker(String(localized: "Profile"), selection: $sshProfileId) {
         Text("Inline Configuration").tag(UUID?.none)
-        ForEach(SSHProfileStorage.shared.loadProfiles()) { profile in
+        ForEach(availableSSHProfiles) { profile in
             Text("\(profile.name) (\(profile.username)@\(profile.host))").tag(UUID?.some(profile.id))
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/ConnectionFormView.swift` around lines 476 - 485,
The view currently calls SSHProfileStorage.shared.loadProfiles() directly inside
the ForEach in sshProfileSection which can re-decode profiles on every render;
change this by adding a `@State` (e.g., var sshProfiles: [SSHProfile]) to hold the
loaded profiles, populate it once in onAppear (or update it when profiles
change) and replace the ForEach(SSHProfileStorage.shared.loadProfiles()) with
ForEach(sshProfiles) so sshProfileSection, Picker and ForEach use the cached
sshProfiles list instead of calling loadProfiles() repeatedly.
TablePro/Core/Storage/SSHProfileStorage.swift (1)

9-18: Declare SSHProfileStorage’s visibility explicitly.

Please make the type’s access level explicit here instead of relying on the default, e.g. internal final class SSHProfileStorage.

♻️ Minimal fix
-final class SSHProfileStorage {
+internal final class SSHProfileStorage {
As per coding guidelines, "Always specify access control explicitly (private, internal, public) on extensions and types. Specify on the extension itself, not on individual members."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Core/Storage/SSHProfileStorage.swift` around lines 9 - 18, The
SSHProfileStorage type currently relies on the default access level; explicitly
declare its visibility (e.g., change the declaration of SSHProfileStorage to
include an access modifier like internal or public) so the class has a clear
access control, updating the top-level declaration for final class
SSHProfileStorage accordingly.
TablePro/Models/Connection/SSHProfile.swift (1)

8-8: Declare SSHProfile’s visibility explicitly.

Please spell out the intended access level here instead of relying on the default, e.g. internal struct SSHProfile.

♻️ Minimal fix
-struct SSHProfile: Identifiable, Hashable, Codable, Sendable {
+internal struct SSHProfile: Identifiable, Hashable, Codable, Sendable {
As per coding guidelines, "Always specify access control explicitly (private, internal, public) on extensions and types. Specify on the extension itself, not on individual members."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Models/Connection/SSHProfile.swift` at line 8, The type SSHProfile
currently relies on default access control; explicitly declare its intended
visibility (for example change "struct SSHProfile" to "internal struct
SSHProfile" or "public struct SSHProfile" as appropriate) so the SSHProfile
declaration and its conformance (Identifiable, Hashable, Codable, Sendable) have
an explicit access level per project guidelines; update the SSHProfile
definition to include the correct access modifier and run a quick build to
ensure no downstream visibility errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro.xcodeproj/project.pbxproj`:
- Around line 1204-1221: The TablePro app target is missing a dependency on the
new DynamoDBDriverPlugin target and a Copy Plug-Ins build phase to embed
DynamoDBDriverPlugin.tableplugin into the app; update the TablePro
PBXNativeTarget to add DynamoDBDriverPlugin (target name DynamoDBDriverPlugin /
productReference 5AE460EC2F6CEDB70097AC5B) to its dependencies and add or update
a "Copy Plug-Ins" build phase that lists DynamoDBDriverPlugin.tableplugin
(productName DynamoDBDriverPlugin) so the .tableplugin bundle is copied into the
app bundle at build time.
- Around line 658-663: The project links TableProPluginKit.framework in the
PBXFrameworksBuildPhase but is missing the runtime search path used by other
plugin targets; update the build settings for the native target that links
TableProPluginKit.framework (the PBXNativeTarget for
DynamoDBDriverPlugin.tableplugin or the corresponding build configuration block)
to include LD_RUNPATH_SEARCH_PATHS = "$(inherited)
`@executable_path/`../Frameworks" (or add `@executable_path/`../Frameworks to the
existing LD_RUNPATH_SEARCH_PATHS array) so dyld can resolve
TableProPluginKit.framework at runtime.

In `@TablePro/Core/Storage/SSHProfileStorage.swift`:
- Around line 22-32: The decode failure should not be treated as an empty store:
make loadProfiles surface the error instead of returning [] (e.g., change func
loadProfiles() to throw or return Result<[SSHProfile], Error>) and remove the
silent catch that returns an empty array; then update all mutating methods
(addProfile / updateProfile / removeProfile or whichever methods at 44-66 call
loadProfiles) to check for a load error and block any writes (propagate the
error or return a failure) until the stored payload is recovered/migrated; keep
the existing error logging (Self.logger.error) but also ensure callers receive
the error so the UI/consumer can show recovery options.

---

Outside diff comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 1278-1289: The test connection flow is incorrectly saving SSH
secrets to connection storage even when an SSH profile is used (sshProfileId is
set); update the block in the test connection path that calls
ConnectionStorage.shared.saveSSHPassword, saveKeyPassphrase, and saveTOTPSecret
to first check that sshProfileId is nil/empty (i.e., only save when no profile
is selected), mirroring the save logic used in the main save flow (so use
sshProfileId guard around the save calls for testConn.id).

---

Nitpick comments:
In `@TablePro/Core/Storage/SSHProfileStorage.swift`:
- Around line 9-18: The SSHProfileStorage type currently relies on the default
access level; explicitly declare its visibility (e.g., change the declaration of
SSHProfileStorage to include an access modifier like internal or public) so the
class has a clear access control, updating the top-level declaration for final
class SSHProfileStorage accordingly.

In `@TablePro/Models/Connection/SSHProfile.swift`:
- Line 8: The type SSHProfile currently relies on default access control;
explicitly declare its intended visibility (for example change "struct
SSHProfile" to "internal struct SSHProfile" or "public struct SSHProfile" as
appropriate) so the SSHProfile declaration and its conformance (Identifiable,
Hashable, Codable, Sendable) have an explicit access level per project
guidelines; update the SSHProfile definition to include the correct access
modifier and run a quick build to ensure no downstream visibility errors.

In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 476-485: The view currently calls
SSHProfileStorage.shared.loadProfiles() directly inside the ForEach in
sshProfileSection which can re-decode profiles on every render; change this by
adding a `@State` (e.g., var sshProfiles: [SSHProfile]) to hold the loaded
profiles, populate it once in onAppear (or update it when profiles change) and
replace the ForEach(SSHProfileStorage.shared.loadProfiles()) with
ForEach(sshProfiles) so sshProfileSection, Picker and ForEach use the cached
sshProfiles list instead of calling loadProfiles() repeatedly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c76171d8-3915-49ea-bae3-1455a62d6a1f

📥 Commits

Reviewing files that changed from the base of the PR and between ae17f84 and 8743051.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • TablePro.xcodeproj/project.pbxproj
  • TablePro/Core/Database/DatabaseManager.swift
  • TablePro/Core/Storage/ConnectionStorage.swift
  • TablePro/Core/Storage/SSHProfileStorage.swift
  • TablePro/Models/Connection/DatabaseConnection.swift
  • TablePro/Models/Connection/SSHProfile.swift
  • TablePro/Views/Connection/ConnectionFormView.swift

- Fix testConnection leaking Keychain secrets and overriding profile passwords
- Clean up all temporary Keychain entries (password, passphrase, TOTP) after test
- Skip inline SSH validation when a profile is selected (isValid)
- Move profile list loading from section onAppear to loadConnectionData
- Separate onSave/onDelete callbacks in SSHProfileEditorView
- Pass inline secrets to Save as Profile flow so TOTP secret is preserved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
TablePro/Views/Connection/SSHProfileEditorView.swift (1)

8-8: Declare SSHProfileEditorView with an explicit access level.

The new top-level type currently relies on Swift’s default internal visibility.

As per coding guidelines, "Always specify access control explicitly (private, internal, public) on extensions and types. Specify on the extension itself, not on individual members."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TablePro/Views/Connection/SSHProfileEditorView.swift` at line 8, Declare the
top-level struct with an explicit access level by updating the
SSHProfileEditorView declaration to include an access modifier (e.g., `internal
struct SSHProfileEditorView` or `public struct SSHProfileEditorView` as
appropriate for its usage); set the modifier on the type itself (not its
members) so the view’s visibility conforms to the coding guideline requiring
explicit access control for top-level types like SSHProfileEditorView.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 480-570: Extract the entire SSH profile UI and helpers into a
dedicated subview/extension: move sshProfileSection, reloadProfiles(),
buildProfileFromInlineConfig(), and sshProfileSummarySection(_:) into a new
SwiftUI View (e.g., SSHProfilesSection) in its own file and replace the inline
code in ConnectionFormView with that subview. Expose and pass required `@Binding`
and values (sshProfileId, sshProfiles, showingCreateProfile, editingProfile,
showingSaveAsProfile, sshEnabled, sshHost, sshPort, sshUsername, sshAuthMethod,
sshPrivateKeyPath, selectedSSHConfigHost, resolvedSSHAgentSocketPath, jumpHosts,
totpMode, totpAlgorithm, totpDigits, totpPeriod) and a reloadProfiles callback
so the new view can call SSHProfileStorage.shared.loadProfiles() and update
selection logic exactly as in reloadProfiles(); keep Sheet presentations and
buildProfileFromInlineConfig behavior inside the new view so ConnectionFormView
only instantiates SSHProfilesSection.
- Around line 1047-1051: Don't re-enable SSH when a profile exists: stop
unconditionally setting sshEnabled = true based on existing.sshProfileId and
SSHProfileStorage.shared.profile(for:). Instead respect the persisted flag by
initializing/keeping sshEnabled from existing.sshConfig.enabled (e.g. only set
sshEnabled = true if existing.sshConfig.enabled is true and the profile exists),
or alternatively ensure that when the SSH toggle is turned off elsewhere you
clear existing.sshProfileId; update the code around existing.sshProfileId,
SSHProfileStorage.shared.profile(for:), and sshEnabled to implement one of these
behaviors so reopening the form does not flip the user's persisted off-state
back on.
- Around line 455-472: The form becomes invalid when an SSH profile is selected
because isValid still checks the inline draft fields (sshHost, sshUsername,
auth, jumpHosts) even when sshProfileId is non-nil; update the validation logic
(isValid) to short-circuit or validate against the selected profile when
SSHProfileStorage.shared.profile(for: sshProfileId) returns a profile (used by
sshProfileSection and sshProfileSummarySection), i.e., if sshProfileId != nil
treat SSH as valid based on the profile (or validate against that profile's
values) instead of the hidden sshInlineFields, so Save/Test enablement reflects
the selected profile state.
- Around line 1181-1195: The current code only calls
storage.deleteTOTPSecret(for:) inside the inline-SSH branch, so when a user
switches an existing connection from inline SSH to a profile (sshProfileId !=
nil) or disables SSH the old per-connection TOTP secret is left in Keychain;
update the logic in ConnectionFormView (the block that handles SSH inline saving
for connectionToSave.id, using sshEnabled, sshProfileId, totpMode, totpSecret,
and storage.saveTOTPSecret/saveKeyPassphrase/saveSSHPassword) so that
storage.deleteTOTPSecret(for: connectionToSave.id) is also invoked whenever the
connection is no longer using inline SSH (i.e. when !(sshEnabled && sshProfileId
== nil)) or when totpMode != .autoGenerate or totpSecret.isEmpty, rather than
only in the inline-config branch.

In `@TablePro/Views/Connection/SSHProfileEditorView.swift`:
- Around line 48-49: existingProfile is being used both to prefill the editor
and to indicate "editing an existing stored profile", causing saveProfile() to
call updateProfile(_:) for synthesized drafts and showing edit/delete UI for new
drafts; change the logic to distinguish the two by introducing a computed flag
like editingStoredProfile (e.g., let editingStoredProfile: Bool =
existingProfile != nil && SSHProfileStorage.shared.contains(id:
existingProfile!.id)) and use that: (1) replace the current isEditing computed
property with editingStoredProfile, (2) in saveProfile() call updateProfile(_:)
only when editingStoredProfile is true otherwise call createProfile, and (3)
gate edit/delete affordances in the view on editingStoredProfile; update
SSHProfileStorage usage (contains or fetchById) to reliably detect stored
profiles before treating existingProfile as editable.
- Around line 50-53: The current isValid computed property only checks
profileName and host; update isValid to mirror ConnectionFormView validation by
also verifying port is numeric and within 1–65535, ensuring when authMethod ==
.privateKey the privateKeyPath is non-empty (and optionally exists), and
validating any jumpHosts entries (host non-empty and their ports
numeric/in-range) using the same rules as ConnectionFormView (reuse its
validation logic or helper functions) so the Save action cannot persist profiles
with invalid port, missing key path, or malformed jump hosts; reference the
isValid property, profileName, host, port, authMethod, privateKeyPath and
jumpHosts/JumpHost validation helpers when making the change.

---

Nitpick comments:
In `@TablePro/Views/Connection/SSHProfileEditorView.swift`:
- Line 8: Declare the top-level struct with an explicit access level by updating
the SSHProfileEditorView declaration to include an access modifier (e.g.,
`internal struct SSHProfileEditorView` or `public struct SSHProfileEditorView`
as appropriate for its usage); set the modifier on the type itself (not its
members) so the view’s visibility conforms to the coding guideline requiring
explicit access control for top-level types like SSHProfileEditorView.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48a2a1f2-bc05-4bc4-991f-f046e2fbb462

📥 Commits

Reviewing files that changed from the base of the PR and between 61b1ab3 and 021cc7e.

📒 Files selected for processing (2)
  • TablePro/Views/Connection/ConnectionFormView.swift
  • TablePro/Views/Connection/SSHProfileEditorView.swift

- Add SyncRecordType.sshProfile with CKRecord mapping
- Add syncSSHProfiles toggle to SyncSettings
- Add sync tracking (markDirty/markDeleted) in SSHProfileStorage
- Add saveProfilesWithoutSync for applying remote changes
- Update SyncCoordinator: push, pull, delete, conflict handling
- Handle .sshProfile in ConflictResolutionView
- Add SSH Profiles feature docs (EN/VI/ZH)
- Update SSH Tunneling docs with link to profiles
- Update docs.json navigation
@datlechin datlechin merged commit a7d9a69 into main Mar 20, 2026
3 checks passed
@datlechin datlechin deleted the feat/ssh-profiles branch March 20, 2026 06:19
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.

feat: Separate SSH tunnel configuration from database connections

1 participant