From 88f0fbc7aab4bfaa89cebd65c8c9cee153683d84 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 12 Apr 2026 15:33:10 -0700 Subject: [PATCH] fix: refine backrest SFTP UI (#1193) --- internal/api/backresthandler.go | 40 +--- webui/package-lock.json | 92 ++++++--- .../features/repositories/AddRepoModal.tsx | 183 +++++++++--------- 3 files changed, 169 insertions(+), 146 deletions(-) diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index 22f3c636..496794f5 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -280,53 +280,29 @@ func (s *BackrestHandler) SetupSftp(ctx context.Context, req *connect.Request[v1 if port == "" { port = "22" } - user := req.Msg.Username - password := req.Msg.Password // Optional if runtime.GOOS == "windows" { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("automated SFTP setup is not supported on Windows")) } - // 1. Host Key Verification/Addition - if err := sftputil.AddHostKey(host, port, env.SSHDir()); err != nil { - return connect.NewResponse(&v1.SetupSftpResponse{ - Error: fmt.Sprintf("Failed to add host key: %v", err), - }), nil - } - - // 2. Generate Key + // 1. Generate key pair (local, always succeeds) _, pubBytes, keyPath, err := sftputil.GenerateKey(host, env.SSHDir()) if err != nil { return nil, fmt.Errorf("failed to generate key: %w", err) } - pubKeyStr := string(pubBytes) - - // 3. Install if password provided - if password != nil { - if err := sftputil.InstallKey(host, port, user, *password, pubBytes); err != nil { - return connect.NewResponse(&v1.SetupSftpResponse{ - Error: fmt.Sprintf("Failed to install key: %v", err), - }), nil - } - - // Verify - privPEM, err := os.ReadFile(keyPath) - if err != nil { - return nil, fmt.Errorf("failed to read generated private key for verification: %w", err) - } - - if err := sftputil.VerifyConnection(host, port, user, privPEM); err != nil { - return connect.NewResponse(&v1.SetupSftpResponse{ - Error: fmt.Sprintf("Key installed but verification failed: %v", err), - }), nil - } + // 2. Scan remote host key into known_hosts (network, non-fatal) + var hostKeyWarning string + if err := sftputil.AddHostKey(host, port, env.SSHDir()); err != nil { + zap.S().Warnf("SFTP host key scan failed for %s: %v", host, err) + hostKeyWarning = fmt.Sprintf("Could not scan host key (%v). Add the host key to known_hosts manually or ensure the host is reachable.", err) } return connect.NewResponse(&v1.SetupSftpResponse{ - PublicKey: pubKeyStr, + PublicKey: string(pubBytes), KeyPath: keyPath, KnownHostsPath: filepath.Join(env.SSHDir(), "known_hosts"), + Error: hostKeyWarning, }), nil } diff --git a/webui/package-lock.json b/webui/package-lock.json index 3fb08c34..a25abf93 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -63,6 +63,7 @@ "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz", "integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/fast-color": "^3.0.0" } @@ -72,6 +73,7 @@ "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.1.tgz", "integrity": "sha512-Lw1Z4cUQxdMmTNir67gU0HCpTl5TtkKCJPZ6UBvCqzcOTl/QmMFB6qAEoj8qFl0CuZDX9qQYa3m9+rEKfaBSbA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", @@ -91,6 +93,7 @@ "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz", "integrity": "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/cssinjs": "^2.0.1", "@babel/runtime": "^7.23.2", @@ -105,25 +108,29 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@ant-design/cssinjs/node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@ant-design/fast-color": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz", "integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.x" } @@ -133,6 +140,7 @@ "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/colors": "^8.0.0", "@ant-design/icons-svg": "^4.4.0", @@ -151,13 +159,15 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@ant-design/react-slick": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", @@ -275,7 +285,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -538,8 +547,7 @@ "version": "2.10.2", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz", "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==", - "license": "(Apache-2.0 AND BSD-3-Clause)", - "peer": true + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@chakra-ui/react": { "version": "3.30.0", @@ -566,7 +574,6 @@ "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz", "integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@bufbuild/protobuf": "^2.7.0" } @@ -598,7 +605,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -693,7 +699,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1352,7 +1357,6 @@ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/helpers": "^0.5.0" } @@ -1702,7 +1706,6 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -2404,6 +2407,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.24.4" }, @@ -2416,6 +2420,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.9.0.tgz", "integrity": "sha512-2jbthe1QZrMBgtCvNKkJFjZYC3uKl4N/aYm5SsMvO3T+F+qRT1CGsSM9bXnh1rLj7jDk/GK0natShWF/jinhWQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/select": "~1.3.0", "@rc-component/tree": "~1.1.0", @@ -2432,6 +2437,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz", "integrity": "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -2446,6 +2452,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.1.2.tgz", "integrity": "sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", @@ -2462,6 +2469,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz", "integrity": "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==", "license": "MIT", + "peer": true, "dependencies": { "@ant-design/fast-color": "^3.0.0", "@rc-component/util": "^1.3.0", @@ -2477,6 +2485,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0" }, @@ -2490,6 +2499,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.5.1.tgz", "integrity": "sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.0.0", @@ -2506,6 +2516,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.3.0.tgz", "integrity": "sha512-rE+sdXEmv2W25VBQ9daGbnb4J4hBIEKmdbj0b3xpY+K7TUmLXDIlSnoXraIbFZdGyek9WxxGKK887uRnFgI+pQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.0.0", @@ -2522,6 +2533,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", @@ -2537,6 +2549,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.6.0.tgz", "integrity": "sha512-A7vrN8kExtw4sW06mrsgCb1rowhvBFFvQU6Bk/NL0Fj6Wet/5GF0QnGCxBu/sG3JI9FEhsJWES0D44BW2d0hzg==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/async-validator": "^5.0.3", "@rc-component/util": "^1.5.0", @@ -2555,6 +2568,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.5.3.tgz", "integrity": "sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.0.0", @@ -2571,6 +2585,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" @@ -2585,6 +2600,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", @@ -2600,6 +2616,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", @@ -2618,6 +2635,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", @@ -2635,6 +2653,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.0" }, @@ -2647,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz", "integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" @@ -2661,6 +2681,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.0" }, @@ -2677,6 +2698,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", @@ -2695,6 +2717,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz", "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", @@ -2711,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -2725,6 +2749,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.9.0.tgz", "integrity": "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", @@ -2763,6 +2788,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.1.0.tgz", "integrity": "sha512-P25IXWkzvBbyEtrAHRfqSNRkILXgAjDfuk0s4daPfHHO0XzVk3D3KJY3Lh069xwuBGtsTZpg+mP4WBLYl9GNaA==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" @@ -2780,6 +2806,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" @@ -2794,6 +2821,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.24.7" }, @@ -2810,6 +2838,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -2827,6 +2856,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz", "integrity": "sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.0" }, @@ -2840,6 +2870,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", @@ -2856,6 +2887,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.3.6.tgz", "integrity": "sha512-CzbJ9TwmWcF5asvTMZ9BMiTE9CkkrigeOGRPpzCNmeZP7KBwwmYrmOIiKh9tMG7d6DyGAEAQ75LBxzPx+pGTHA==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", @@ -2876,6 +2908,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -2893,6 +2926,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" @@ -2910,6 +2944,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -2924,6 +2959,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.0.tgz", "integrity": "sha512-cq3P9FkD+F3eglkFYhBuNlHclg+r4jY8+ZIgK7zbEFo6IwpnA77YL/Gq4ensLw9oua3zFCTA6JDu6YgBei0TxA==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", @@ -2944,6 +2980,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", @@ -2965,6 +3002,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", @@ -2981,6 +3019,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", @@ -2996,6 +3035,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.2.1.tgz", "integrity": "sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/portal": "^2.0.0", "@rc-component/trigger": "^3.0.0", @@ -3015,6 +3055,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz", "integrity": "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.2.1", @@ -3034,6 +3075,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.4.0.tgz", "integrity": "sha512-I3UAlO2hNqy9CSKc8EBaESgnmKk2QaRzuZ2XHZGFCgsSMkGl06mdF97sVfROM02YIb64ocgLKefsjE0Ch4ocwQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/select": "~1.3.0", "@rc-component/tree": "~1.1.0", @@ -3050,6 +3092,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.7.2.tgz", "integrity": "sha512-25x+D2k9SAkaK/MNMNmv2Nlv8FH1D9RtmjoMoLEw1Cid+sMV4pAAT5k49ku59UeXaOA1qwLUVrBUMq4A6gUSsQ==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.0.0", @@ -3070,6 +3113,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", "license": "MIT", + "peer": true, "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" @@ -3084,6 +3128,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.6.0.tgz", "integrity": "sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==", "license": "MIT", + "peer": true, "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" @@ -3098,6 +3143,7 @@ "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", @@ -3645,7 +3691,6 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4841,7 +4886,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5117,7 +5161,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/consola": { "version": "3.4.0", @@ -5993,7 +6038,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/is-number": { "version": "7.0.0", @@ -6077,6 +6123,7 @@ "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", "license": "MIT", + "peer": true, "dependencies": { "string-convert": "^0.2.0" } @@ -6172,7 +6219,6 @@ "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=14.0.0" } @@ -6708,7 +6754,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6721,7 +6766,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6979,7 +7023,6 @@ "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7084,7 +7127,6 @@ "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -7114,6 +7156,7 @@ "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", "license": "MIT", + "peer": true, "dependencies": { "compute-scroll-into-view": "^3.0.2" } @@ -7211,7 +7254,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/string-width": { "version": "5.1.2", @@ -7353,6 +7397,7 @@ "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12.22" } @@ -7427,7 +7472,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -7448,7 +7492,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7594,7 +7637,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/webui/src/features/repositories/AddRepoModal.tsx b/webui/src/features/repositories/AddRepoModal.tsx index 682bef6e..39c3da98 100644 --- a/webui/src/features/repositories/AddRepoModal.tsx +++ b/webui/src/features/repositories/AddRepoModal.tsx @@ -17,7 +17,7 @@ import { AccordionItemTrigger, AccordionRoot, } from "../../components/ui/accordion"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useShowModal } from "../../components/common/ModalManager"; import { CommandPrefix_CPUNiceLevel, @@ -108,6 +108,7 @@ interface SftpConfigSectionProps { onChangeIdentityFile: (path: string) => void; port: number | null; onChangePort: (port: number | null) => void; + knownHostsPath: string; onChangeKnownHostsPath: (path: string) => void; isWindows: boolean; } @@ -118,70 +119,48 @@ const SftpConfigSection = ({ onChangeIdentityFile, port, onChangePort, + knownHostsPath, onChangeKnownHostsPath, isWindows, }: SftpConfigSectionProps) => { - // Setup Keys state - const [sftpUsername, setSftpUsername] = useState(""); - const [sftpPassword, setSftpPassword] = useState(""); const [setupLoading, setSetupLoading] = useState(false); - const [generatedPublicKey, setGeneratedPublicKey] = useState( - null, - ); + const [generatedPublicKey, setGeneratedPublicKey] = useState(null); + const [hostKeyWarning, setHostKeyWarning] = useState(null); + const [keyCopied, setKeyCopied] = useState(false); if (isWindows) return null; - const handleSetupKeys = async () => { + const handleGenerateKey = async () => { setSetupLoading(true); setGeneratedPublicKey(null); + setHostKeyWarning(null); try { if (!uri) return; - // Simple parse of URI for host/port if not fully robust - let host = ""; + + // Parse host and port from the SFTP URI + const authority = uri.replace("sftp:", "").split("/")[0]; + const hostPart = authority.includes("@") ? authority.split("@")[1] : authority; + let host = hostPart; let defaultPort = "22"; - const uriParts = uri.replace("sftp:", "").split("/"); - const authority = uriParts[0]; - let hostPart = authority; - if (authority.includes("@")) { - setSftpUsername(authority.split("@")[0]); - hostPart = authority.split("@")[1]; - } - if (hostPart.includes(":")) { - host = hostPart.split(":")[0]; - defaultPort = hostPart.split(":")[1]; - } else { - host = hostPart; + [host, defaultPort] = hostPart.split(":"); } - // Override from manual input if username is set there - const username = sftpUsername || uri.match(/([^@]+)@/)?.[1] || ""; - const res = await backrestService.setupSftp({ - host: host, + host, port: port ? port.toString() : defaultPort, - username: username, - password: sftpPassword || undefined, + username: "", }); - if (res.error) { - throw new Error(res.error); - } - onChangeIdentityFile(res.keyPath); onChangeKnownHostsPath(res.knownHostsPath); if (res.publicKey) { setGeneratedPublicKey(res.publicKey); } - alerts.success( - "Created SSH keypair at " + - res.keyPath + - " and updated known hosts file at " + - res.knownHostsPath, - ); - alerts.success( - "Updated restic flags to use the SSH keypair and known hosts file.", - ); + if (res.error) { + setHostKeyWarning(res.error); + } + alerts.success("Generated SSH keypair at " + res.keyPath); } catch (e: any) { alerts.error(formatErrorAlert(e, "SFTP Setup Failed")); } finally { @@ -195,34 +174,22 @@ const SftpConfigSection = ({ - Bootstrap SSH Key (Optional) + Setup SSH Key (Optional) - Enter your SSH credentials here. When you click "Setup Keys", - backrest will generate an SSH key pair. + Click "Generate Key" to create an SSH key pair for this host. + Backrest will attempt to scan the host key into known_hosts automatically. + You will then need to add the generated public key to{" "} + ~/.ssh/authorized_keys on the remote server. - - setSftpUsername(e.target.value)} - /> - - - setSftpPassword(e.target.value)} - /> - @@ -237,8 +204,7 @@ const SftpConfigSection = ({ Key Generated Successfully! - Please add the following public key to your server's{" "} - ~/.ssh/authorized_keys file: + Add the following public key to ~/.ssh/authorized_keys on the remote server: { navigator.clipboard.writeText(generatedPublicKey || ""); - alerts.success("Key copied to clipboard"); + setKeyCopied(true); + setTimeout(() => setKeyCopied(false), 2000); }} + colorPalette={keyCopied ? "green" : undefined} > - Copy + {keyCopied ? "Copied!" : "Copy"} + {hostKeyWarning && ( + + + Host key scan failed: {hostKeyWarning} + + + )} )} @@ -288,6 +263,17 @@ const SftpConfigSection = ({ defaultValue={"22"} /> + + + onChangeKnownHostsPath(e.target.value)} + /> + ); }; @@ -304,12 +290,14 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { : toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }), ); - // SFTP specific state // SFTP specific state const [sftpIdentityFile, setSftpIdentityFile] = useState(""); const [sftpPort, setSftpPort] = useState(null); const [sftpKnownHostsPath, setSftpKnownHostsPath] = useState(""); + // Ref to read current flags without making them a useEffect dependency + const flagsRef = useRef([]); + const [confirmation, setConfirmation] = useState({ open: false, title: "", @@ -323,11 +311,30 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { ? toJson(RepoSchema, template, { alwaysEmitImplicit: true }) : toJson(RepoSchema, repoDefaults, { alwaysEmitImplicit: true }), ); - // Reset SFTP fields when template changes (or is null) - if (!template) { - setSftpIdentityFile(""); - setSftpPort(null); - setSftpKnownHostsPath(""); + + setSftpIdentityFile(""); + setSftpPort(null); + setSftpKnownHostsPath(""); + + if (template?.uri?.startsWith("sftp:")) { + // Populate SFTP fields by parsing the existing sftp.args flag + const sftpArgsFlag = (template.flags || []).find( + (f) => f.includes("sftp.args") || f.includes("sftp.command"), + ); + if (sftpArgsFlag) { + const argsMatch = sftpArgsFlag.match(/sftp\.args=['"]?(.+?)['"]?\s*$/); + if (argsMatch) { + const argsStr = argsMatch[1].replace(/^'|'$/g, ""); + const identityMatch = argsStr.match(/-i\s+["']?([^\s"']+)["']?/); + if (identityMatch) setSftpIdentityFile(identityMatch[1]); + const portMatch = argsStr.match(/-p\s+(\d+)/); + if (portMatch) setSftpPort(parseInt(portMatch[1], 10)); + const knownHostsMatch = argsStr.match( + /-oUserKnownHostsFile=["']?([^\s"']+)["']?/, + ); + if (knownHostsMatch) setSftpKnownHostsPath(knownHostsMatch[1]); + } + } } }, [template]); @@ -353,49 +360,45 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { return curr; }; - // Logic to update flags based on SFTP inputs - useEffect(() => { - // If we are editing, we don't touch the flags. The user can edit them manually. - if (template) { - return; - } + // Keep flagsRef in sync with latest formData.flags so the SFTP effect can + // read the current value without flags being a reactive dependency. + flagsRef.current = (formData.flags as string[]) || []; + // Keep sftp.args flag in sync with the SFTP config fields. + useEffect(() => { const uri = getField(["uri"]); if (!uri?.startsWith("sftp:")) { return; } - const currentFlags = getField(["flags"]) || []; + // Read flags via ref so this effect does not re-run whenever the user + // edits the flags list (which would immediately erase empty rows). + const currentFlags = flagsRef.current; const newFlags = currentFlags.filter( (f: string) => f && !f.includes("sftp.args") && !f.includes("sftp.command"), ); + // Always include -oBatchMode=yes; quote paths to handle spaces. let sftpArgs = "-oBatchMode=yes"; - let argsChanged = false; if (sftpIdentityFile) { let cleanPath = sftpIdentityFile; if (cleanPath.startsWith("@")) { cleanPath = cleanPath.substring(1); } - sftpArgs += ` -i ${cleanPath}`; - argsChanged = true; + sftpArgs += ` -i "${cleanPath}"`; } if (sftpPort && sftpPort !== 0 && sftpPort !== 22) { sftpArgs += ` -p ${sftpPort}`; - argsChanged = true; } if (sftpKnownHostsPath) { - sftpArgs += ` -oUserKnownHostsFile=${sftpKnownHostsPath}`; - argsChanged = true; + sftpArgs += ` -oUserKnownHostsFile="${sftpKnownHostsPath}"`; } - if (argsChanged) { - newFlags.push(`--option=sftp.args='${sftpArgs}'`); - } + newFlags.push(`--option=sftp.args='${sftpArgs}'`); const sortedCurrent = [...currentFlags].sort(); const sortedNew = [...newFlags].sort(); @@ -407,8 +410,9 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { getField(["uri"]), sftpIdentityFile, sftpPort, - template, - getField(["flags"]), + sftpKnownHostsPath, + // flags intentionally omitted: flagsRef avoids a circular dep where any + // user edit to flags would re-trigger the effect and erase empty rows. ]); if (!config) return null; @@ -784,13 +788,14 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => { {/* SFTP Specific Fields */} - {getField(["uri"])?.startsWith("sftp:") && !template && ( + {getField(["uri"])?.startsWith("sftp:") && (