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 + + + +After completion, create `.planning/phases/06-new-backends/06-00-SUMMARY.md` + 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 + + + +After completion, create `.planning/phases/06-new-backends/06-01-SUMMARY.md` + 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 + + + +After completion, create `.planning/phases/06-new-backends/06-02-SUMMARY.md` + 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]}

+
+ {backendType === 'azureblob' ? ( + <> + f.key === 'account')!} register={register} error={errors.account as FieldError | undefined} /> + + + ) : ( + BACKEND_REGISTRY[backendType].fields.map(field => ( + + )) + )} +
+ + +
+ +
+ ); +} +``` + +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 + + + +After completion, create `.planning/phases/06-new-backends/06-03-SUMMARY.md` +