diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index d6671e7..01e1e00 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -57,7 +57,13 @@ Plans:
3. User can select Google Cloud Storage as a backend, fill in the required fields, and generate a valid rclone.conf containing `[type = google cloud storage]`
4. User can select Backblaze B2 as a backend, enter their application key credentials, and generate a valid rclone.conf containing `[type = b2]`
5. All four new backends appear in the BackendSelectionStep list and each produces a downloadable config+scripts bundle
-**Plans**: TBD
+**Plans**: 4 plans
+
+Plans:
+- [ ] 06-00-PLAN.md — Wave 0 TDD stubs: failing tests for all four new backends across registry, rclone-conf, and RemoteConfigStep
+- [ ] 06-01-PLAN.md — Registry + schema + RCLONE_TYPE_MAP for OneDrive, GCS, Backblaze B2
+- [ ] 06-02-PLAN.md — Registry + schema + RCLONE_TYPE_MAP for SFTP + SftpAuthToggle component
+- [ ] 06-03-PLAN.md — Wire all four backends into RemoteConfigStep + human verification
### Phase 7: Validation & UX Polish
**Goal**: Users receive immediate inline feedback when they enter incorrectly formatted values, and can access plain-language explanations on confusing credential fields without leaving the wizard
@@ -79,5 +85,5 @@ Plans:
| 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | v1.0 | 5/5 | Complete | 2026-03-27 |
| 5. Tech Debt | 4/4 | Complete | 2026-03-30 | - |
-| 6. New Backends | v1.1 | 0/TBD | Not started | - |
+| 6. New Backends | v1.1 | 0/4 | Not started | - |
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
diff --git a/.planning/phases/06-new-backends/06-00-PLAN.md b/.planning/phases/06-new-backends/06-00-PLAN.md
new file mode 100644
index 0000000..98db238
--- /dev/null
+++ b/.planning/phases/06-new-backends/06-00-PLAN.md
@@ -0,0 +1,506 @@
+---
+phase: 06-new-backends
+plan: "00"
+type: tdd
+wave: 0
+depends_on: []
+files_modified:
+ - src/schemas/registry.test.ts
+ - src/generators/rclone-conf.test.ts
+ - src/components/wizard/RemoteConfigStep.test.tsx
+autonomous: true
+requirements:
+ - BACK-01
+ - BACK-02
+ - BACK-03
+ - BACK-04
+
+must_haves:
+ truths:
+ - "registry.test.ts fails (red) with 4 new backend assertions before implementation"
+ - "rclone-conf.test.ts fails (red) with fixtures for onedrive, sftp-password, sftp-key, gcs, b2"
+ - "RemoteConfigStep.test.tsx fails (red) with describe blocks for OneDrive, SFTP, GCS, B2"
+ - "All test stubs reference real field names per rclone docs (token, drive_id, drive_type, host, user, pass, key_pem, project_number, service_account_credentials, account, key)"
+ artifacts:
+ - path: "src/schemas/registry.test.ts"
+ provides: "Failing assertions for all 7 backends including 4 new ones"
+ contains: "EXPECTED_BACKENDS"
+ - path: "src/generators/rclone-conf.test.ts"
+ provides: "Failing fixtures for onedrive, sftp (password), sftp (key), gcs, b2"
+ contains: "type = google cloud storage"
+ - path: "src/components/wizard/RemoteConfigStep.test.tsx"
+ provides: "Failing describe blocks for BACK-01 through BACK-04"
+ contains: "BACK-04: Backblaze B2"
+ key_links:
+ - from: "src/schemas/registry.test.ts"
+ to: "src/schemas/registry.ts"
+ via: "EXPECTED_BACKENDS array import"
+ pattern: "EXPECTED_BACKENDS.*BackendType"
+ - from: "src/generators/rclone-conf.test.ts"
+ to: "src/generators/rclone-conf.ts"
+ via: "buildRcloneConf fixtures with new WizardState shapes"
+ pattern: "backendType.*gcs|b2|onedrive|sftp"
+---
+
+
+Write Wave 0 failing test stubs for all four new backends across three test files.
+
+Purpose: Establish RED state before any implementation — ensures tests drive and verify the work in Plans 01-03.
+Output: Three updated test files with failing assertions for OneDrive, SFTP, GCS, and Backblaze B2.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-new-backends/06-RESEARCH.md
+
+
+
+
+From src/schemas/registry.test.ts:
+```typescript
+const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible'];
+// Pattern: loops over EXPECTED_BACKENDS for generic assertions,
+// then individual describe blocks for backend-specific field assertions
+```
+
+From src/generators/rclone-conf.test.ts:
+```typescript
+// Fixture pattern:
+const azureState: WizardState = {
+ ...INITIAL_STATE,
+ remote: { name: 'my-azure', backendType: 'azureblob', params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' } },
+};
+// describe('buildRcloneConf — azureblob', () => { ... })
+```
+
+From src/components/wizard/RemoteConfigStep.test.tsx:
+```typescript
+// renderWithBackend helper:
+function renderWithBackend(backendType: BackendType) {
+ function Setup() {
+ const { dispatch } = useWizard();
+ useEffect(() => {
+ dispatch({ type: 'SET_BACKEND_TYPE', payload: backendType });
+ dispatch({ type: 'SET_STEP', payload: 1 });
+ }, []);
+ return ;
+ }
+ return render();
+}
+```
+
+
+
+
+
+
+ Task 1: Update registry.test.ts with 7-backend assertions (RED)
+ src/schemas/registry.test.ts
+
+ - EXPECTED_BACKENDS now includes all 7 types: ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2']
+ - Generic loop tests (has fields, has displayName/description, snake_case keys) run over all 7
+ - New describe 'OneDrive': token field (required), drive_id field (required), drive_type field (required, select)
+ - New describe 'SFTP': host field (required), user field (required), pass field (optional), key_pem field (optional)
+ - New describe 'GCS': project_number field (required), service_account_credentials field (required)
+ - New describe 'Backblaze B2': account field (required), key field (required)
+ - Running `npx vitest run src/schemas/registry.test.ts` MUST fail (RED) because registry.ts still has only 3 BackendType values
+
+
+ Replace the content of src/schemas/registry.test.ts with the updated version.
+
+ 1. Change EXPECTED_BACKENDS to:
+ `const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];`
+ Note: This line will fail TypeScript because BackendType in registry.ts does not yet include the 4 new values. That is the intended RED state.
+
+ 2. Keep all existing generic loop tests and existing backend-specific describe blocks unchanged.
+
+ 3. Append four new describe blocks at the bottom:
+
+ ```typescript
+ describe('OneDrive backend', () => {
+ it('has token field (required)', () => {
+ const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'token');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ expect(f!.inputType).toBe('password');
+ });
+ it('has drive_id field (required)', () => {
+ const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'drive_id');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ });
+ it('has drive_type field as select', () => {
+ const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'drive_type');
+ expect(f).toBeDefined();
+ expect(f!.inputType).toBe('select');
+ expect(f!.options).toBeDefined();
+ });
+ });
+
+ describe('SFTP backend', () => {
+ it('has host field (required)', () => {
+ const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'host');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ });
+ it('has user field (required)', () => {
+ const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'user');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ });
+ it('has pass field (optional — SftpAuthToggle handles display)', () => {
+ const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'pass');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(false);
+ });
+ it('has key_pem field (optional — SftpAuthToggle handles display)', () => {
+ const f = BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'key_pem');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(false);
+ });
+ });
+
+ describe('Google Cloud Storage backend', () => {
+ it('has project_number field (required)', () => {
+ const f = BACKEND_REGISTRY.gcs.fields.find(f => f.key === 'project_number');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ });
+ it('has service_account_credentials field (required, password)', () => {
+ const f = BACKEND_REGISTRY.gcs.fields.find(f => f.key === 'service_account_credentials');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ expect(f!.inputType).toBe('password');
+ });
+ });
+
+ describe('Backblaze B2 backend', () => {
+ it('has account field (required) — applicationKeyId', () => {
+ const f = BACKEND_REGISTRY.b2.fields.find(f => f.key === 'account');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ });
+ it('has key field (required) — application key secret', () => {
+ const f = BACKEND_REGISTRY.b2.fields.find(f => f.key === 'key');
+ expect(f).toBeDefined();
+ expect(f!.required).toBe(true);
+ expect(f!.inputType).toBe('password');
+ });
+ });
+ ```
+
+ Do NOT suppress TypeScript errors. The RED state is the goal.
+
+
+ MISSING — run after Wave 0 to confirm RED: npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20
+
+ registry.test.ts references all 7 backends and asserts new field requirements; file committed; test suite fails (RED) because registry.ts has not been updated yet
+
+
+
+ Task 2: Update rclone-conf.test.ts with new backend fixtures (RED)
+ src/generators/rclone-conf.test.ts
+
+ - Five new WizardState fixtures: onedriveState, sftpPasswordState, sftpKeyState, gcsState, b2State
+ - describe 'buildRcloneConf — onedrive': type = onedrive, contains token, drive_id, drive_type
+ - describe 'buildRcloneConf — sftp (password)': type = sftp, contains host, user, pass; does NOT contain key_pem
+ - describe 'buildRcloneConf — sftp (key)': type = sftp, contains host, user, key_pem; does NOT contain pass
+ - describe 'buildRcloneConf — gcs': type = google cloud storage (WITH spaces), contains project_number, service_account_credentials
+ - describe 'buildRcloneConf — b2': type = b2, contains account (applicationKeyId), key (application key)
+ - Running `npx vitest run src/generators/rclone-conf.test.ts` MUST fail (RED) because RCLONE_TYPE_MAP has no entries for onedrive/sftp/gcs/b2
+
+
+ Append the following fixtures and describe blocks to the END of src/generators/rclone-conf.test.ts (after the existing error cases block). Do not modify existing content.
+
+ ```typescript
+ // --- New backend fixtures (Phase 6) ---
+
+ const onedriveState: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-onedrive',
+ backendType: 'onedrive',
+ params: {
+ token: '{"access_token":"TOKEN","token_type":"Bearer","refresh_token":"REFRESH","expiry":"2026-01-01T00:00:00Z"}',
+ drive_id: 'b!TESTDRIVEID',
+ drive_type: 'business',
+ },
+ },
+ };
+
+ const sftpPasswordState: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-sftp-pass',
+ backendType: 'sftp',
+ params: { host: 'sftp.example.com', user: 'admin', pass: 'secretpass', key_pem: '' },
+ },
+ };
+
+ const sftpKeyState: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-sftp-key',
+ backendType: 'sftp',
+ params: { host: 'sftp.example.com', user: 'admin', pass: '', key_pem: '-----BEGIN RSA PRIVATE KEY-----\nMIIEo...\n-----END RSA PRIVATE KEY-----' },
+ },
+ };
+
+ const gcsState: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-gcs',
+ backendType: 'gcs',
+ params: {
+ project_number: '123456789',
+ service_account_credentials: '{"type":"service_account","project_id":"my-project"}',
+ },
+ },
+ };
+
+ const b2State: WizardState = {
+ ...INITIAL_STATE,
+ remote: {
+ name: 'my-b2',
+ backendType: 'b2',
+ params: { account: 'APP_KEY_ID', key: 'APP_KEY_SECRET' },
+ },
+ };
+
+ describe('buildRcloneConf — onedrive', () => {
+ it('contains type = onedrive', () => {
+ expect(buildRcloneConf(onedriveState)).toContain('type = onedrive');
+ });
+ it('contains token value', () => {
+ expect(buildRcloneConf(onedriveState)).toContain('token = ');
+ });
+ it('contains drive_id', () => {
+ expect(buildRcloneConf(onedriveState)).toContain('drive_id = b!TESTDRIVEID');
+ });
+ it('contains drive_type = business', () => {
+ expect(buildRcloneConf(onedriveState)).toContain('drive_type = business');
+ });
+ });
+
+ describe('buildRcloneConf — sftp (password auth)', () => {
+ it('contains type = sftp', () => {
+ expect(buildRcloneConf(sftpPasswordState)).toContain('type = sftp');
+ });
+ it('contains host', () => {
+ expect(buildRcloneConf(sftpPasswordState)).toContain('host = sftp.example.com');
+ });
+ it('contains user', () => {
+ expect(buildRcloneConf(sftpPasswordState)).toContain('user = admin');
+ });
+ it('contains pass', () => {
+ expect(buildRcloneConf(sftpPasswordState)).toContain('pass = secretpass');
+ });
+ it('omits key_pem when empty', () => {
+ expect(buildRcloneConf(sftpPasswordState)).not.toContain('key_pem');
+ });
+ });
+
+ describe('buildRcloneConf — sftp (key auth)', () => {
+ it('contains type = sftp', () => {
+ expect(buildRcloneConf(sftpKeyState)).toContain('type = sftp');
+ });
+ it('contains key_pem', () => {
+ expect(buildRcloneConf(sftpKeyState)).toContain('key_pem = -----BEGIN RSA PRIVATE KEY-----');
+ });
+ it('omits pass when empty', () => {
+ expect(buildRcloneConf(sftpKeyState)).not.toContain('pass =');
+ });
+ });
+
+ describe('buildRcloneConf — gcs', () => {
+ it('contains type = google cloud storage (with spaces)', () => {
+ expect(buildRcloneConf(gcsState)).toContain('type = google cloud storage');
+ });
+ it('does NOT contain type = gcs', () => {
+ expect(buildRcloneConf(gcsState)).not.toContain('type = gcs');
+ });
+ it('contains project_number', () => {
+ expect(buildRcloneConf(gcsState)).toContain('project_number = 123456789');
+ });
+ it('contains service_account_credentials', () => {
+ expect(buildRcloneConf(gcsState)).toContain('service_account_credentials = ');
+ });
+ });
+
+ describe('buildRcloneConf — b2', () => {
+ it('contains type = b2', () => {
+ expect(buildRcloneConf(b2State)).toContain('type = b2');
+ });
+ it('contains account (applicationKeyId)', () => {
+ expect(buildRcloneConf(b2State)).toContain('account = APP_KEY_ID');
+ });
+ it('contains key (application key secret)', () => {
+ expect(buildRcloneConf(b2State)).toContain('key = APP_KEY_SECRET');
+ });
+ });
+ ```
+
+ Note: WizardState type accepts `backendType: 'onedrive' | 'sftp' | 'gcs' | 'b2'` only after Plan 01/02 extend BackendType. In RED state TypeScript will error on these — that is expected. If needed, cast as `backendType: 'onedrive' as BackendType` to allow the file to parse while staying RED on the runtime assertions.
+
+
+ MISSING — run after Wave 0 to confirm RED: npx vitest run src/generators/rclone-conf.test.ts 2>&1 | tail -20
+
+ rclone-conf.test.ts has fixtures and assertions for all five new backend scenarios; tests fail (RED) because RCLONE_TYPE_MAP does not yet contain the new types
+
+
+
+ Task 3: Update RemoteConfigStep.test.tsx with new backend describe blocks (RED)
+ src/components/wizard/RemoteConfigStep.test.tsx
+
+ - New describe 'BACK-01 (Phase 6): OneDrive form': renders OAuth Token field (password), renders Drive ID field, renders Drive Type select
+ - New describe 'BACK-02 (Phase 6): SFTP form': renders Host field, renders Username field, renders Password tab button, renders Private Key tab button, Password tab shows pass field, Private Key tab shows key_pem field, switching tabs preserves hidden field value (CSS-hidden pattern, same as Azure toggle test)
+ - New describe 'BACK-03 (Phase 6): GCS form': renders Project Number field, renders Service Account JSON field
+ - New describe 'BACK-04 (Phase 6): Backblaze B2 form': renders Application Key ID field, renders Application Key field
+ - Running `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` MUST fail (RED) because RemoteConfigStep.tsx does not yet handle these backends and BackendType does not include them
+
+
+ Append the following describe blocks to the BOTTOM of the existing describe('RemoteConfigStep') block in src/components/wizard/RemoteConfigStep.test.tsx (inside the outermost describe, after the BACK-03 S3-Compatible block):
+
+ ```typescript
+ describe('BACK-01 (Phase 6): OneDrive form', () => {
+ it('renders OAuth Token field', async () => {
+ renderWithBackend('onedrive' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/oauth token/i)).toBeDefined();
+ });
+ });
+ it('renders Drive ID field', async () => {
+ renderWithBackend('onedrive' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/drive id/i)).toBeDefined();
+ });
+ });
+ it('renders Drive Type select', async () => {
+ renderWithBackend('onedrive' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/drive type/i)).toBeDefined();
+ });
+ });
+ });
+
+ describe('BACK-02 (Phase 6): SFTP form', () => {
+ it('renders Host field', async () => {
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/host/i)).toBeDefined();
+ });
+ });
+ it('renders Username field', async () => {
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/username/i)).toBeDefined();
+ });
+ });
+ it('shows Password tab button by default', async () => {
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByText(/^password$/i, { selector: 'button' })).toBeDefined();
+ });
+ });
+ it('shows Private Key tab button', async () => {
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByText(/private key/i, { selector: 'button' })).toBeDefined();
+ });
+ });
+ it('switching to Private Key tab CSS-hides password field and shows key_pem field', async () => {
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByText(/private key/i, { selector: 'button' })).toBeDefined();
+ });
+ fireEvent.click(screen.getByText(/private key/i, { selector: 'button' }));
+ await waitFor(() => {
+ const passInput = screen.getByLabelText(/^password$/i);
+ const passWrapper = passInput.closest('div.hidden');
+ expect(passWrapper).toBeDefined();
+ const keyInput = screen.getByLabelText(/private key.*pem/i);
+ const keyWrapper = keyInput.closest('div.block');
+ expect(keyWrapper).toBeDefined();
+ });
+ });
+ it('switching SFTP auth tabs preserves hidden field value', async () => {
+ const user = userEvent.setup();
+ renderWithBackend('sftp' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/^password$/i)).toBeDefined();
+ });
+ await user.type(screen.getByLabelText(/^password$/i), 'mysftppass');
+ fireEvent.click(screen.getByText(/private key/i, { selector: 'button' }));
+ fireEvent.click(screen.getByText(/^password$/i, { selector: 'button' }));
+ await waitFor(() => {
+ const input = screen.getByLabelText(/^password$/i) as HTMLInputElement;
+ expect(input.value).toBe('mysftppass');
+ });
+ });
+ });
+
+ describe('BACK-03 (Phase 6): Google Cloud Storage form', () => {
+ it('renders Project Number field', async () => {
+ renderWithBackend('gcs' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/project number/i)).toBeDefined();
+ });
+ });
+ it('renders Service Account JSON field', async () => {
+ renderWithBackend('gcs' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/service account json/i)).toBeDefined();
+ });
+ });
+ });
+
+ describe('BACK-04 (Phase 6): Backblaze B2 form', () => {
+ it('renders Application Key ID field', async () => {
+ renderWithBackend('b2' as BackendType);
+ await waitFor(() => {
+ expect(screen.getByLabelText(/application key id/i)).toBeDefined();
+ });
+ });
+ it('renders Application Key field', async () => {
+ renderWithBackend('b2' as BackendType);
+ await waitFor(() => {
+ // Matches "Application Key" but not "Application Key ID"
+ expect(screen.getByLabelText(/application key$/i)).toBeDefined();
+ });
+ });
+ });
+ ```
+
+ The `as BackendType` casts allow the file to parse. Tests will fail at runtime (RED) because RemoteConfigStep does not yet know about these backends and TypeScript errors on the union.
+
+
+ MISSING — run after Wave 0 to confirm RED: npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | tail -20
+
+ RemoteConfigStep.test.tsx has describe blocks for OneDrive, SFTP (with toggle), GCS, and B2; tests fail (RED) because implementation does not exist yet
+
+
+
+
+
+After all three tasks: run the full suite to confirm RED state.
+`npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts src/components/wizard/RemoteConfigStep.test.tsx`
+Expected: failures for all new backend assertions. Existing tests (azureblob, s3, s3-compatible) must still pass.
+
+
+
+- All three test files updated with new backend assertions
+- Existing passing tests remain green
+- New backend assertions are red (implementation not yet done)
+- No test stubs use vague matchers — all assertions reference exact field labels and rclone keys
+
+
+
diff --git a/.planning/phases/06-new-backends/06-01-PLAN.md b/.planning/phases/06-new-backends/06-01-PLAN.md
new file mode 100644
index 0000000..5e5383f
--- /dev/null
+++ b/.planning/phases/06-new-backends/06-01-PLAN.md
@@ -0,0 +1,288 @@
+---
+phase: 06-new-backends
+plan: "01"
+type: execute
+wave: 1
+depends_on:
+ - "06-00"
+files_modified:
+ - src/schemas/registry.ts
+ - src/schemas/index.ts
+ - src/generators/rclone-conf.ts
+autonomous: true
+requirements:
+ - BACK-01
+ - BACK-03
+ - BACK-04
+
+must_haves:
+ truths:
+ - "BackendType union includes 'onedrive', 'gcs', 'b2' (and 'sftp' added by Plan 02)"
+ - "BACKEND_REGISTRY has onedrive, gcs, b2 entries with correct field keys matching rclone docs"
+ - "BACKEND_SCHEMAS has onedrive, gcs, b2 entries built via buildZodSchema"
+ - "RCLONE_TYPE_MAP maps gcs to 'google cloud storage' (with spaces)"
+ - "buildRcloneConf(gcsState) outputs 'type = google cloud storage'"
+ - "buildRcloneConf(b2State) outputs 'type = b2'"
+ - "buildRcloneConf(onedriveState) outputs 'type = onedrive'"
+ artifacts:
+ - path: "src/schemas/registry.ts"
+ provides: "BackendType union with 6 types (onedrive, gcs, b2 added); registry entries for all three"
+ contains: "onedrive.*gcs.*b2"
+ - path: "src/schemas/index.ts"
+ provides: "BACKEND_SCHEMAS with onedrive, gcs, b2"
+ contains: "buildZodSchema('onedrive')"
+ - path: "src/generators/rclone-conf.ts"
+ provides: "RCLONE_TYPE_MAP with gcs mapped to 'google cloud storage'"
+ contains: "google cloud storage"
+ key_links:
+ - from: "src/schemas/index.ts"
+ to: "src/schemas/registry.ts"
+ via: "buildZodSchema calls — BackendType must include new types before BACKEND_SCHEMAS can reference them"
+ pattern: "buildZodSchema\\('(onedrive|gcs|b2)'\\)"
+ - from: "src/generators/rclone-conf.ts"
+ to: "RCLONE_TYPE_MAP"
+ via: "gcs key maps to string with spaces — critical for correct rclone.conf output"
+ pattern: "gcs.*google cloud storage"
+---
+
+
+Add OneDrive, GCS, and Backblaze B2 to the data layer — BackendType union, registry entries, Zod schemas, and RCLONE_TYPE_MAP.
+
+Purpose: Three simple backends (no custom toggle UI) that follow the pure registry + FieldRenderer pattern. Implementing them first keeps SFTP (Plan 02) isolated.
+Output: registry.ts, index.ts, rclone-conf.ts updated; registry and rclone-conf tests go green for these three backends.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-new-backends/06-RESEARCH.md
+@.planning/phases/06-new-backends/06-00-SUMMARY.md
+
+
+
+
+From src/schemas/registry.ts (current):
+```typescript
+export type BackendType = 'azureblob' | 's3' | 's3-compatible';
+
+export interface FieldDef {
+ key: string;
+ label: string;
+ inputType: 'text' | 'password' | 'select' | 'toggle';
+ required: boolean;
+ placeholder?: string;
+ helpText?: string;
+ options?: { value: string; label: string }[];
+}
+
+export const BACKEND_REGISTRY: Record = { azureblob: {...}, s3: {...}, 's3-compatible': {...} };
+```
+
+From src/schemas/index.ts (current):
+```typescript
+function buildZodSchema(backendType: BackendType): z.ZodObject>
+export const BACKEND_SCHEMAS = {
+ azureblob: buildZodSchema('azureblob'),
+ s3: buildZodSchema('s3'),
+ 's3-compatible': buildZodSchema('s3-compatible'),
+} as const;
+```
+
+From src/generators/rclone-conf.ts (current):
+```typescript
+const RCLONE_TYPE_MAP: Record = {
+ azureblob: 'azureblob',
+ s3: 's3',
+ 's3-compatible': 's3',
+};
+```
+
+
+
+
+
+
+ Task 1: Extend registry.ts — BackendType union + onedrive/gcs/b2 entries
+ src/schemas/registry.ts
+
+ - BackendType union becomes: 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2'
+ (include 'sftp' in union now — Plan 02 adds its registry entry; including it in the union prevents TypeScript errors in Plan 02's parallel work)
+ - BACKEND_REGISTRY gains onedrive entry: token (password, required), drive_id (text, required), drive_type (select, required, options: personal/business/documentLibrary)
+ - BACKEND_REGISTRY gains gcs entry: project_number (text, required), service_account_credentials (password, required)
+ - BACKEND_REGISTRY gains b2 entry: account (text, required), key (password, required)
+ - sftp entry NOT added here — Plan 02 adds it. TypeScript will error on BACKEND_REGISTRY type until Plan 02 adds sftp. This is acceptable during parallel execution.
+ - `npx vitest run src/schemas/registry.test.ts` passes for onedrive, gcs, b2 assertions; sftp assertions still fail (Plan 02 fixes those)
+
+
+ Edit src/schemas/registry.ts:
+
+ 1. Replace BackendType line with:
+ `export type BackendType = 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2';`
+
+ 2. Add three new entries to BACKEND_REGISTRY after the 's3-compatible' entry:
+
+ ```typescript
+ onedrive: {
+ displayName: 'OneDrive',
+ description: 'Microsoft OneDrive (paste pre-obtained rclone token)',
+ fields: [
+ {
+ key: 'token',
+ label: 'OAuth Token (JSON)',
+ inputType: 'password',
+ required: true,
+ placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
+ helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
+ },
+ {
+ key: 'drive_id',
+ label: 'Drive ID',
+ inputType: 'text',
+ required: true,
+ placeholder: 'b!...',
+ helpText: 'The drive ID from your OneDrive. Found in the rclone authorize output.',
+ },
+ {
+ key: 'drive_type',
+ label: 'Drive Type',
+ inputType: 'select',
+ required: true,
+ options: [
+ { value: 'personal', label: 'Personal' },
+ { value: 'business', label: 'Business' },
+ { value: 'documentLibrary', label: 'SharePoint Document Library' },
+ ],
+ helpText: 'Personal for consumer OneDrive, Business for Microsoft 365',
+ },
+ ],
+ },
+ gcs: {
+ displayName: 'Google Cloud Storage',
+ description: 'Google Cloud Storage',
+ fields: [
+ {
+ key: 'project_number',
+ label: 'Project Number',
+ inputType: 'text',
+ required: true,
+ placeholder: '123456789',
+ helpText: 'Your GCP project number (not project ID)',
+ },
+ {
+ key: 'service_account_credentials',
+ label: 'Service Account JSON',
+ inputType: 'password',
+ required: true,
+ placeholder: '{"type":"service_account","project_id":"..."}',
+ helpText: 'Paste the full content of your service account JSON key file',
+ },
+ ],
+ },
+ b2: {
+ displayName: 'Backblaze B2',
+ description: 'Backblaze B2 Cloud Storage',
+ fields: [
+ {
+ key: 'account',
+ label: 'Application Key ID',
+ inputType: 'text',
+ required: true,
+ placeholder: 'your-application-key-id',
+ helpText: 'The applicationKeyId — not the master account ID',
+ },
+ {
+ key: 'key',
+ label: 'Application Key',
+ inputType: 'password',
+ required: true,
+ helpText: 'The application key (secret value from the B2 dashboard)',
+ },
+ ],
+ },
+ ```
+
+ Note: BACKEND_REGISTRY type is `Record` which requires ALL BackendType values. After this task, TypeScript will error because 'sftp' is in the union but not yet in the BACKEND_REGISTRY object. Plan 02 adds the sftp entry. If running `npx tsc --noEmit` between plans, expect this error temporarily. The Vitest tests will still run.
+
+
+ npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20
+
+ BackendType includes all 7 types; onedrive, gcs, b2 registry entries present with correct field keys; registry tests pass for those three backends
+
+
+
+ Task 2: Extend index.ts and rclone-conf.ts — schemas + type map for onedrive/gcs/b2
+ src/schemas/index.ts, src/generators/rclone-conf.ts
+
+ - BACKEND_SCHEMAS gains onedrive, gcs, b2 entries using buildZodSchema
+ - RCLONE_TYPE_MAP gains: onedrive → 'onedrive', gcs → 'google cloud storage', b2 → 'b2'
+ - buildRcloneConf(gcsState) produces a line 'type = google cloud storage' (with spaces — NOT 'type = gcs')
+ - buildRcloneConf(b2State) produces 'type = b2' with account and key lines
+ - buildRcloneConf(onedriveState) produces 'type = onedrive' with token, drive_id, drive_type lines
+ - `npx vitest run src/generators/rclone-conf.test.ts` passes for onedrive, gcs, b2 describe blocks
+
+
+ **src/schemas/index.ts:** Add three entries to BACKEND_SCHEMAS:
+ ```typescript
+ export const BACKEND_SCHEMAS = {
+ azureblob: buildZodSchema('azureblob'),
+ s3: buildZodSchema('s3'),
+ 's3-compatible': buildZodSchema('s3-compatible'),
+ onedrive: buildZodSchema('onedrive'),
+ gcs: buildZodSchema('gcs'),
+ b2: buildZodSchema('b2'),
+ } as const;
+ ```
+ Do NOT add sftp yet — Plan 02 adds it after adding the sftp registry entry.
+
+ **src/generators/rclone-conf.ts:** Extend RCLONE_TYPE_MAP:
+ ```typescript
+ const RCLONE_TYPE_MAP: Record = {
+ azureblob: 'azureblob',
+ s3: 's3',
+ 's3-compatible': 's3',
+ onedrive: 'onedrive',
+ gcs: 'google cloud storage', // NOTE: spaces in rclone type value — do NOT shorten
+ b2: 'b2',
+ };
+ ```
+ Do NOT add sftp yet — Plan 02 adds it.
+
+ After editing, run full suite to confirm existing tests still pass and new backend tests pass.
+
+
+ npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20
+
+ BACKEND_SCHEMAS covers onedrive/gcs/b2; RCLONE_TYPE_MAP maps gcs to 'google cloud storage'; rclone-conf tests for onedrive/gcs/b2 pass; no regressions on existing backends
+
+
+
+
+
+`npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts`
+Expected: onedrive/gcs/b2 tests GREEN; sftp tests still RED (Plan 02 fixes those); no regressions on azureblob/s3/s3-compatible.
+
+`npx tsc --noEmit` — expect one error: BACKEND_REGISTRY missing 'sftp' key (fixed by Plan 02). All other TypeScript must be clean.
+
+
+
+- BackendType union has all 7 types
+- onedrive, gcs, b2 registry entries with correct rclone key names
+- BACKEND_SCHEMAS covers onedrive, gcs, b2
+- gcs maps to 'google cloud storage' in RCLONE_TYPE_MAP
+- rclone-conf tests pass for onedrive, gcs, b2
+- Zero TypeScript errors outside the known sftp-missing-key error
+
+
+
diff --git a/.planning/phases/06-new-backends/06-02-PLAN.md b/.planning/phases/06-new-backends/06-02-PLAN.md
new file mode 100644
index 0000000..467e278
--- /dev/null
+++ b/.planning/phases/06-new-backends/06-02-PLAN.md
@@ -0,0 +1,266 @@
+---
+phase: 06-new-backends
+plan: "02"
+type: execute
+wave: 1
+depends_on:
+ - "06-00"
+files_modified:
+ - src/schemas/registry.ts
+ - src/schemas/index.ts
+ - src/generators/rclone-conf.ts
+ - src/components/wizard/SftpAuthToggle.tsx
+autonomous: true
+requirements:
+ - BACK-02
+
+must_haves:
+ truths:
+ - "BACKEND_REGISTRY has sftp entry with host (required), user (required), pass (optional), key_pem (optional)"
+ - "SftpAuthToggle renders a Password tab and Private Key tab; inactive tab's field is CSS-hidden (not unmounted)"
+ - "Switching SFTP auth tabs does not clear the hidden field value (CSS-hidden, not conditional render)"
+ - "buildRcloneConf(sftpPasswordState) outputs 'type = sftp' with pass, omits key_pem"
+ - "buildRcloneConf(sftpKeyState) outputs 'type = sftp' with key_pem, omits pass"
+ artifacts:
+ - path: "src/schemas/registry.ts"
+ provides: "sftp entry in BACKEND_REGISTRY"
+ contains: "key_pem"
+ - path: "src/components/wizard/SftpAuthToggle.tsx"
+ provides: "SFTP password vs private-key toggle following AzureAuthToggle CSS-hidden pattern"
+ exports: ["SftpAuthToggle"]
+ - path: "src/generators/rclone-conf.ts"
+ provides: "RCLONE_TYPE_MAP sftp entry"
+ contains: "sftp.*sftp"
+ key_links:
+ - from: "src/components/wizard/SftpAuthToggle.tsx"
+ to: "src/components/ui/PasswordField.tsx"
+ via: "PasswordField component for both pass and key_pem fields"
+ pattern: "PasswordField.*pass|PasswordField.*key_pem"
+ - from: "src/schemas/registry.ts"
+ to: "BACKEND_REGISTRY sftp entry"
+ via: "pass and key_pem fields required: false — SftpAuthToggle registers them directly"
+ pattern: "key_pem.*required.*false"
+---
+
+
+Add SFTP to the data layer and create the SftpAuthToggle component.
+
+Purpose: SFTP is the only new backend requiring a custom auth-method toggle (password vs private key). Isolating SFTP work in its own plan keeps Plan 01 clean and allows parallel execution.
+Output: sftp entry in registry.ts/index.ts/rclone-conf.ts; SftpAuthToggle.tsx component following the AzureAuthToggle CSS-hidden pattern.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-new-backends/06-RESEARCH.md
+@.planning/phases/06-new-backends/06-00-SUMMARY.md
+
+
+
+
+From src/components/wizard/AzureAuthToggle.tsx:
+```typescript
+import { useState } from 'react';
+import type { UseFormRegister, FieldError } from 'react-hook-form';
+import { PasswordField } from '../ui/PasswordField';
+
+type AuthMethod = 'sas' | 'key';
+
+interface AzureAuthToggleProps {
+ register: UseFormRegister;
+ errors: { key?: FieldError; sas_url?: FieldError; };
+}
+
+export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
+ const [authMethod, setAuthMethod] = useState('sas');
+ return (
+
+
+
+
+
+ {/* Both fields always registered — CSS toggling only */}
+
+
+
+
+
+
+
+ );
+}
+```
+
+From src/schemas/registry.ts (current sftp fields — to add):
+```typescript
+// sftp registry entry (add to BACKEND_REGISTRY):
+sftp: {
+ displayName: 'SFTP',
+ description: 'SSH File Transfer Protocol',
+ fields: [
+ { key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'sftp.example.com', helpText: 'The SSH server hostname or IP address' },
+ { key: 'user', label: 'Username', inputType: 'text', required: true, placeholder: 'admin' },
+ { key: 'pass', label: 'Password', inputType: 'password', required: false, helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value.' },
+ { key: 'key_pem', label: 'Private Key (PEM)', inputType: 'password', required: false, helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)' },
+ ],
+},
+```
+
+
+
+
+
+
+ Task 1: Add sftp entry to registry.ts, index.ts, and rclone-conf.ts
+ src/schemas/registry.ts, src/schemas/index.ts, src/generators/rclone-conf.ts
+
+ - BACKEND_REGISTRY gains sftp entry with host (text, required), user (text, required), pass (password, optional), key_pem (password, optional)
+ - pass helpText mentions rclone obscure requirement (security note, does not block generation)
+ - BACKEND_SCHEMAS gains sftp entry via buildZodSchema('sftp')
+ - RCLONE_TYPE_MAP gains sftp → 'sftp'
+ - `npx vitest run src/schemas/registry.test.ts` passes all sftp assertions (host, user, pass, key_pem)
+ - `npx vitest run src/generators/rclone-conf.test.ts` passes sftp password and sftp key describe blocks
+ - `npx tsc --noEmit` is clean (no more "sftp missing from BACKEND_REGISTRY" error)
+
+
+ **src/schemas/registry.ts:** Add sftp entry to BACKEND_REGISTRY (after 's3-compatible', before or after the entries added by Plan 01 — order does not matter):
+
+ ```typescript
+ sftp: {
+ displayName: 'SFTP',
+ description: 'SSH File Transfer Protocol',
+ fields: [
+ {
+ key: 'host',
+ label: 'Host',
+ inputType: 'text',
+ required: true,
+ placeholder: 'sftp.example.com',
+ helpText: 'The SSH server hostname or IP address',
+ },
+ {
+ key: 'user',
+ label: 'Username',
+ inputType: 'text',
+ required: true,
+ placeholder: 'admin',
+ },
+ {
+ key: 'pass',
+ label: 'Password',
+ inputType: 'password',
+ required: false,
+ helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value instead of the plain password.',
+ },
+ {
+ key: 'key_pem',
+ label: 'Private Key (PEM)',
+ inputType: 'password',
+ required: false,
+ helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)',
+ },
+ ],
+ },
+ ```
+
+ **src/schemas/index.ts:** Add sftp to BACKEND_SCHEMAS:
+ ```typescript
+ sftp: buildZodSchema('sftp'),
+ ```
+
+ **src/generators/rclone-conf.ts:** Add sftp to RCLONE_TYPE_MAP:
+ ```typescript
+ sftp: 'sftp',
+ ```
+
+ Note: BackendType union already includes 'sftp' (added by Plan 01 Task 1). This plan only adds the registry/schema/map entries. If Plan 01 and Plan 02 run in parallel on separate branches, merge Plan 01 first then add sftp entries on top.
+
+
+ npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20
+
+ sftp entry in registry with all four field keys; sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP; registry and rclone-conf tests fully green for all 7 backends; zero TypeScript errors
+
+
+
+ Task 2: Create SftpAuthToggle.tsx
+ src/components/wizard/SftpAuthToggle.tsx
+
+ - Component renders a segmented control with two buttons: "Password" (left) and "Private Key" (right)
+ - Default auth method is 'password' — Password tab is active on first render
+ - Password tab active: pass field wrapper has class 'block', key_pem field wrapper has class 'hidden'
+ - Private Key tab active: pass field wrapper has class 'hidden', key_pem field wrapper has class 'block'
+ - Both fields are ALWAYS registered with react-hook-form (CSS-hidden, not conditional render)
+ - Pass field: id="pass", label="Password", uses PasswordField
+ - Key field: id="key_pem", label="Private Key (PEM)", uses PasswordField with multiline hint in helpText
+ - Active button has class 'bg-blue-600 text-white', inactive has 'bg-white text-gray-700 hover:bg-gray-50'
+ - Component accepts register and errors props matching SftpAuthToggleProps interface
+
+
+ Create src/components/wizard/SftpAuthToggle.tsx following AzureAuthToggle.tsx exactly, with these substitutions:
+ - AuthMethod type: 'password' | 'key' (was 'sas' | 'key')
+ - Initial state: 'password' (was 'sas')
+ - Tab 1 button text: "Password" (was "SAS URL"), onClick: setAuthMethod('password')
+ - Tab 2 button text: "Private Key" (was "Access Key"), onClick: setAuthMethod('key')
+ - Active condition for tab 1: authMethod === 'password'
+ - Active condition for tab 2: authMethod === 'key'
+ - Field 1 wrapper: className={authMethod === 'password' ? 'block' : 'hidden'}
+ PasswordField: id="pass", label="Password", error={errors.pass}, registration={register('pass')},
+ helpText="SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value."
+ - Field 2 wrapper: className={authMethod === 'key' ? 'block' : 'hidden'}
+ PasswordField: id="key_pem", label="Private Key (PEM)", error={errors.key_pem}, registration={register('key_pem')},
+ helpText="Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)"
+
+ Props interface:
+ ```typescript
+ interface SftpAuthToggleProps {
+ register: UseFormRegister;
+ errors: {
+ pass?: FieldError;
+ key_pem?: FieldError;
+ };
+ }
+ ```
+
+ The SFTP toggle test in RemoteConfigStep.test.tsx asserts:
+ - getByText(/^password$/i, { selector: 'button' }) — button text must be exactly "Password"
+ - getByText(/private key/i, { selector: 'button' }) — button text must contain "Private Key"
+ - getByLabelText(/^password$/i) — PasswordField label must be exactly "Password"
+ - getByLabelText(/private key.*pem/i) — PasswordField label must match "Private Key (PEM)"
+ Ensure button text and label text match these patterns exactly.
+
+
+ npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | grep -E "(BACK-02|SFTP|PASS|FAIL)" | head -20
+
+ SftpAuthToggle.tsx created; component matches AzureAuthToggle pattern; SFTP toggle tests in RemoteConfigStep.test.tsx will pass after Plan 03 wires it into RemoteConfigStep (component exists and is importable)
+
+
+
+
+
+`npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts`
+Expected: fully green — all 7 backends pass in both files.
+
+`npx tsc --noEmit`
+Expected: zero errors (sftp now present in BACKEND_REGISTRY satisfying the exhaustive Record type).
+
+
+
+- sftp registry entry with host, user, pass, key_pem (correct rclone key names)
+- SftpAuthToggle.tsx exists, exports SftpAuthToggle, follows AzureAuthToggle CSS-hidden pattern
+- sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP
+- registry and rclone-conf tests fully green for all 7 backends
+- Zero TypeScript errors
+
+
+
diff --git a/.planning/phases/06-new-backends/06-03-PLAN.md b/.planning/phases/06-new-backends/06-03-PLAN.md
new file mode 100644
index 0000000..d84ffa0
--- /dev/null
+++ b/.planning/phases/06-new-backends/06-03-PLAN.md
@@ -0,0 +1,291 @@
+---
+phase: 06-new-backends
+plan: "03"
+type: execute
+wave: 2
+depends_on:
+ - "06-01"
+ - "06-02"
+files_modified:
+ - src/components/wizard/RemoteConfigStep.tsx
+autonomous: false
+requirements:
+ - BACK-01
+ - BACK-02
+ - BACK-03
+ - BACK-04
+
+must_haves:
+ truths:
+ - "User can select OneDrive in BackendSelectionStep and see token, drive_id, drive_type fields in RemoteConfigStep"
+ - "User can select SFTP and see host, user, and the Password/Private Key toggle in RemoteConfigStep"
+ - "User can select GCS and see project_number and service_account_credentials fields"
+ - "User can select Backblaze B2 and see Application Key ID and Application Key fields"
+ - "All four new backends appear in BackendSelectionStep (automatic — driven by BACKEND_REGISTRY)"
+ - "Full Vitest suite passes with zero failures and zero TypeScript errors"
+ artifacts:
+ - path: "src/components/wizard/RemoteConfigStep.tsx"
+ provides: "Extended component handling all 7 backends; sftp branch with SftpAuthToggle"
+ contains: "SftpAuthToggle"
+ key_links:
+ - from: "src/components/wizard/RemoteConfigStep.tsx"
+ to: "src/components/wizard/SftpAuthToggle.tsx"
+ via: "sftp branch import and render"
+ pattern: "backendType === 'sftp'"
+ - from: "src/components/wizard/RemoteConfigStep.tsx"
+ to: "src/schemas/index.ts BACKEND_SCHEMAS"
+ via: "schema lookup — all 7 BackendType values must be keys in BACKEND_SCHEMAS"
+ pattern: "BACKEND_SCHEMAS\\[backendType\\]"
+ - from: "src/components/wizard/RemoteConfigStep.tsx"
+ to: "src/schemas/registry.ts BACKEND_REGISTRY"
+ via: "backendLabel record must be exhaustive over all 7 BackendType values"
+ pattern: "backendLabel.*onedrive|sftp|gcs|b2"
+---
+
+
+Wire all four new backends into RemoteConfigStep — extend backendLabel, add the sftp branch with SftpAuthToggle, and ensure gcs/b2/onedrive render via the existing registry loop.
+
+Purpose: The only UI wiring step. After Plans 01 and 02, the data layer is complete. This plan makes it visible and interactive in the wizard.
+Output: RemoteConfigStep.tsx updated; all RemoteConfigStep tests green; full suite green.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-new-backends/06-RESEARCH.md
+@.planning/phases/06-new-backends/06-01-SUMMARY.md
+@.planning/phases/06-new-backends/06-02-SUMMARY.md
+
+
+
+
+From src/components/wizard/RemoteConfigStep.tsx (current — pre-modification):
+```typescript
+import { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useWizard } from '../../store/context';
+import { BACKEND_REGISTRY } from '../../schemas/registry';
+import { BACKEND_SCHEMAS } from '../../schemas';
+import { FieldRenderer } from '../ui/FieldRenderer';
+import { AzureAuthToggle } from './AzureAuthToggle';
+import type { FieldError } from 'react-hook-form';
+
+export function RemoteConfigStep() {
+ const { state, dispatch } = useWizard();
+ const backendType = state.remote.backendType;
+
+ useEffect(() => {
+ if (!backendType) dispatch({ type: 'SET_STEP', payload: 0 });
+ }, [backendType, dispatch]);
+
+ const schema = backendType ? BACKEND_SCHEMAS[backendType] : BACKEND_SCHEMAS['azureblob'];
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ resolver: zodResolver(schema),
+ mode: 'onSubmit',
+ reValidateMode: 'onChange',
+ defaultValues: state.remote.params,
+ });
+
+ if (!backendType) return null;
+
+ const onNext = (values: Record) => {
+ dispatch({ type: 'SET_REMOTE_PARAMS', payload: values });
+ dispatch({ type: 'SET_STEP', payload: 2 });
+ };
+
+ const backendLabel: Record, string> = {
+ azureblob: 'Azure Blob Storage',
+ s3: 'Amazon S3',
+ 's3-compatible': 'S3-Compatible Storage',
+ };
+
+ return (
+
+
Step 2: Configure {backendLabel[backendType]}
+
+
+ );
+}
+```
+
+From src/components/wizard/SftpAuthToggle.tsx (created by Plan 02):
+```typescript
+export function SftpAuthToggle({ register, errors }: SftpAuthToggleProps)
+// Props: { register: UseFormRegister; errors: { pass?: FieldError; key_pem?: FieldError } }
+```
+
+
+
+
+
+
+ Task 1: Extend RemoteConfigStep.tsx — backendLabel + sftp branch + all four new backends
+ src/components/wizard/RemoteConfigStep.tsx
+
+ - Import SftpAuthToggle from './SftpAuthToggle'
+ - backendLabel record covers all 7 BackendType values: adds onedrive/'OneDrive', sftp/'SFTP', gcs/'Google Cloud Storage', b2/'Backblaze B2'
+ - The ternary in JSX becomes a three-branch chain:
+ 1. backendType === 'azureblob' → existing AzureAuthToggle branch (unchanged)
+ 2. backendType === 'sftp' → host field + user field via FieldRenderer, then SftpAuthToggle for pass/key_pem
+ 3. all others (onedrive, gcs, b2, s3, s3-compatible) → registry loop (unchanged)
+ - SFTP branch renders host and user fields from BACKEND_REGISTRY.sftp.fields using FieldRenderer (same as azureblob renders account), then SftpAuthToggle
+ - onedrive, gcs, b2 render entirely via the registry loop — no custom branch needed
+ - `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` fully green for all 7 backends
+ - `npx vitest run` (full suite) fully green
+ - `npx tsc --noEmit` zero errors
+
+
+ Edit src/components/wizard/RemoteConfigStep.tsx:
+
+ 1. Add import at top (after AzureAuthToggle import):
+ `import { SftpAuthToggle } from './SftpAuthToggle';`
+
+ 2. Extend backendLabel record:
+ ```typescript
+ const backendLabel: Record, string> = {
+ azureblob: 'Azure Blob Storage',
+ s3: 'Amazon S3',
+ 's3-compatible': 'S3-Compatible Storage',
+ onedrive: 'OneDrive',
+ sftp: 'SFTP',
+ gcs: 'Google Cloud Storage',
+ b2: 'Backblaze B2',
+ };
+ ```
+
+ 3. Replace the ternary block in JSX (inside the form, before the button div):
+ ```tsx
+ {backendType === 'azureblob' ? (
+ <>
+ {/* Account field via FieldRenderer */}
+ f.key === 'account')!}
+ register={register}
+ error={errors.account as FieldError | undefined}
+ />
+ {/* Auth toggle handles key + sas_url — both always registered */}
+
+ >
+ ) : backendType === 'sftp' ? (
+ <>
+ {/* host and user via FieldRenderer */}
+ f.key === 'host')!}
+ register={register}
+ error={errors.host as FieldError | undefined}
+ />
+ f.key === 'user')!}
+ register={register}
+ error={errors.user as FieldError | undefined}
+ />
+ {/* Auth toggle handles pass + key_pem — both always registered */}
+
+ >
+ ) : (
+ /* All others (onedrive, gcs, b2, s3, s3-compatible): full registry loop */
+ BACKEND_REGISTRY[backendType].fields.map(field => (
+
+ ))
+ )}
+ ```
+
+ After editing, run the full suite immediately. Confirm all 7 backend describe blocks in RemoteConfigStep.test.tsx are green.
+
+ Note on OneDrive drive_type field: BACKEND_REGISTRY.onedrive has drive_type with inputType: 'select'. FieldRenderer already handles 'select' type — no changes to FieldRenderer needed. The test asserts getByLabelText(/drive type/i) — ensure FieldRenderer renders a label matching that text.
+
+
+ npx vitest run 2>&1 | tail -20
+
+ RemoteConfigStep handles all 7 backends; SFTP branch uses SftpAuthToggle; onedrive/gcs/b2 render via registry loop; full Vitest suite green; zero TypeScript errors from `npx tsc --noEmit`
+
+
+
+ Task 2: Human verify all four new backends in the wizard UI
+ src/components/wizard/RemoteConfigStep.tsx
+ Run `npm run dev` and manually test each new backend through the wizard UI flow.
+
+ npx vitest run 2>&1 | tail -5
+
+ All four backends visually verified in the running app by the user
+
+ All four new backends fully wired into the wizard:
+ - OneDrive: token (password field), Drive ID, Drive Type (select: Personal/Business/SharePoint)
+ - SFTP: Host, Username, Password/Private Key toggle (CSS-hidden, preserves values on switch)
+ - Google Cloud Storage: Project Number, Service Account JSON (password field)
+ - Backblaze B2: Application Key ID, Application Key (password field)
+ All four backends appear as cards in BackendSelectionStep. Automated tests are fully green.
+
+
+ 1. `npm run dev` — open http://localhost:5173
+ 2. Verify BackendSelectionStep shows 7 backend cards including OneDrive, SFTP, Google Cloud Storage, Backblaze B2
+ 3. Select OneDrive → confirm OAuth Token (JSON), Drive ID, Drive Type fields appear
+ 4. Go back, select SFTP → confirm Host, Username fields + Password/Private Key tab buttons
+ - Type a value in Password field, switch to Private Key, switch back — value must still be there
+ 5. Go back, select GCS → confirm Project Number and Service Account JSON fields appear
+ 6. Go back, select B2 → confirm Application Key ID and Application Key fields appear
+ 7. For any backend: fill all required fields, click Next — wizard must advance without errors
+
+ Type "approved" to complete Phase 6, or describe any visual issues found
+
+
+
+
+
+`npx vitest run` — all tests green including all 4 new backend describe blocks in RemoteConfigStep.test.tsx
+`npx tsc --noEmit` — zero TypeScript errors
+Manual: all 4 new backends visible and functional in the wizard UI
+
+
+
+- RemoteConfigStep handles all 7 BackendType values without TypeScript errors
+- SFTP branch uses SftpAuthToggle with CSS-hidden pattern
+- onedrive, gcs, b2 render entirely via the registry loop
+- All 4 new backends visible in BackendSelectionStep UI (automatic via registry)
+- Full Vitest suite green
+- Zero TypeScript errors
+- Human verification: all backends visually correct and functional
+
+
+