docs(06-new-backends): create phase plan

4 plans across 3 waves: Wave 0 TDD stubs, Wave 1 data layer (parallel: OneDrive/GCS/B2 + SFTP/SftpAuthToggle), Wave 2 RemoteConfigStep wiring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 13:38:46 +02:00
co-authored by Claude Sonnet 4.6
parent e9bf449183
commit b314e65536
5 changed files with 1359 additions and 2 deletions
+8 -2
View File
@@ -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]` 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]` 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 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 ### 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 **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 | | 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | 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 | - | | 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 | - | | 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - |
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-new-backends/06-RESEARCH.md
<interfaces>
<!-- Existing test file shapes the executor must follow. -->
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 <RemoteConfigStep key={backendType} />;
}
return render(<WizardProvider><Setup /></WizardProvider>);
}
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Update registry.test.ts with 7-backend assertions (RED)</name>
<files>src/schemas/registry.test.ts</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Update rclone-conf.test.ts with new backend fixtures (RED)</name>
<files>src/generators/rclone-conf.test.ts</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/generators/rclone-conf.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Update RemoteConfigStep.test.tsx with new backend describe blocks (RED)</name>
<files>src/components/wizard/RemoteConfigStep.test.tsx</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>MISSING — run after Wave 0 to confirm RED: npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>RemoteConfigStep.test.tsx has describe blocks for OneDrive, SFTP (with toggle), GCS, and B2; tests fail (RED) because implementation does not exist yet</done>
</task>
</tasks>
<verification>
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.
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/06-new-backends/06-00-SUMMARY.md`
</output>
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Current state of files before modification -->
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<BackendType, {
displayName: string;
description: string;
fields: FieldDef[];
}> = { azureblob: {...}, s3: {...}, 's3-compatible': {...} };
```
From src/schemas/index.ts (current):
```typescript
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>>
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<string, string> = {
azureblob: 'azureblob',
s3: 's3',
's3-compatible': 's3',
};
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Extend registry.ts — BackendType union + onedrive/gcs/b2 entries</name>
<files>src/schemas/registry.ts</files>
<behavior>
- 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)
</behavior>
<action>
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<BackendType, ...>` 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.
</action>
<verify>
<automated>npx vitest run src/schemas/registry.test.ts 2>&1 | tail -20</automated>
</verify>
<done>BackendType includes all 7 types; onedrive, gcs, b2 registry entries present with correct field keys; registry tests pass for those three backends</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Extend index.ts and rclone-conf.ts — schemas + type map for onedrive/gcs/b2</name>
<files>src/schemas/index.ts, src/generators/rclone-conf.ts</files>
<behavior>
- 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
</behavior>
<action>
**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<string, string> = {
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.
</action>
<verify>
<automated>npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
</tasks>
<verification>
`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.
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/06-new-backends/06-01-SUMMARY.md`
</output>
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- AzureAuthToggle.tsx — the exact pattern SftpAuthToggle must follow -->
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<any>;
errors: { key?: FieldError; sas_url?: FieldError; };
}
export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
const [authMethod, setAuthMethod] = useState<AuthMethod>('sas');
return (
<div className="flex flex-col gap-3">
<div className="flex rounded-md border border-gray-300 overflow-hidden">
<button type="button" onClick={() => setAuthMethod('sas')} className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'sas' ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50',
].join(' ')}>SAS URL</button>
<button type="button" onClick={() => setAuthMethod('key')} className={...}>Access Key</button>
</div>
{/* Both fields always registered — CSS toggling only */}
<div className={authMethod === 'sas' ? 'block' : 'hidden'}>
<PasswordField id="sas_url" label="SAS URL" error={errors.sas_url} registration={register('sas_url')} ... />
</div>
<div className={authMethod === 'key' ? 'block' : 'hidden'}>
<PasswordField id="key" label="Access Key" error={errors.key} registration={register('key')} ... />
</div>
</div>
);
}
```
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 <password>`. 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-----)' },
],
},
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Add sftp entry to registry.ts, index.ts, and rclone-conf.ts</name>
<files>src/schemas/registry.ts, src/schemas/index.ts, src/generators/rclone-conf.ts</files>
<behavior>
- 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)
</behavior>
<action>
**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 <password>`. 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.
</action>
<verify>
<automated>npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20</automated>
</verify>
<done>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</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Create SftpAuthToggle.tsx</name>
<files>src/components/wizard/SftpAuthToggle.tsx</files>
<behavior>
- 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
</behavior>
<action>
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 <password>`. 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<any>;
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.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | grep -E "(BACK-02|SFTP|PASS|FAIL)" | head -20</automated>
</verify>
<done>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)</done>
</task>
</tasks>
<verification>
`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).
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/06-new-backends/06-02-SUMMARY.md`
</output>
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Current RemoteConfigStep.tsx — full file for reference -->
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<string, string>) => {
dispatch({ type: 'SET_REMOTE_PARAMS', payload: values });
dispatch({ type: 'SET_STEP', payload: 2 });
};
const backendLabel: Record<NonNullable<typeof backendType>, string> = {
azureblob: 'Azure Blob Storage',
s3: 'Amazon S3',
's3-compatible': 'S3-Compatible Storage',
};
return (
<div>
<h2>Step 2: Configure {backendLabel[backendType]}</h2>
<form onSubmit={handleSubmit(onNext)}>
{backendType === 'azureblob' ? (
<>
<FieldRenderer field={BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account')!} register={register} error={errors.account as FieldError | undefined} />
<AzureAuthToggle register={register} errors={{ key: errors.key as FieldError | undefined, sas_url: errors.sas_url as FieldError | undefined }} />
</>
) : (
BACKEND_REGISTRY[backendType].fields.map(field => (
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
))
)}
<div className="flex gap-3 mt-6">
<button type="button" onClick={() => dispatch({ type: 'SET_STEP', payload: 0 })} className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">Back</button>
<button type="submit" className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700">Next</button>
</div>
</form>
</div>
);
}
```
From src/components/wizard/SftpAuthToggle.tsx (created by Plan 02):
```typescript
export function SftpAuthToggle({ register, errors }: SftpAuthToggleProps)
// Props: { register: UseFormRegister<any>; errors: { pass?: FieldError; key_pem?: FieldError } }
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Extend RemoteConfigStep.tsx — backendLabel + sftp branch + all four new backends</name>
<files>src/components/wizard/RemoteConfigStep.tsx</files>
<behavior>
- 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
</behavior>
<action>
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<NonNullable<typeof backendType>, 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 */}
<FieldRenderer
field={BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account')!}
register={register}
error={errors.account as FieldError | undefined}
/>
{/* Auth toggle handles key + sas_url — both always registered */}
<AzureAuthToggle
register={register}
errors={{
key: errors.key as FieldError | undefined,
sas_url: errors.sas_url as FieldError | undefined,
}}
/>
</>
) : backendType === 'sftp' ? (
<>
{/* host and user via FieldRenderer */}
<FieldRenderer
field={BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'host')!}
register={register}
error={errors.host as FieldError | undefined}
/>
<FieldRenderer
field={BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'user')!}
register={register}
error={errors.user as FieldError | undefined}
/>
{/* Auth toggle handles pass + key_pem — both always registered */}
<SftpAuthToggle
register={register}
errors={{
pass: errors.pass as FieldError | undefined,
key_pem: errors.key_pem as FieldError | undefined,
}}
/>
</>
) : (
/* All others (onedrive, gcs, b2, s3, s3-compatible): full registry loop */
BACKEND_REGISTRY[backendType].fields.map(field => (
<FieldRenderer
key={field.key}
field={field}
register={register}
error={errors[field.key] as FieldError | undefined}
/>
))
)}
```
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.
</action>
<verify>
<automated>npx vitest run 2>&1 | tail -20</automated>
</verify>
<done>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`</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Human verify all four new backends in the wizard UI</name>
<files>src/components/wizard/RemoteConfigStep.tsx</files>
<action>Run `npm run dev` and manually test each new backend through the wizard UI flow.</action>
<verify>
<automated>npx vitest run 2>&1 | tail -5</automated>
</verify>
<done>All four backends visually verified in the running app by the user</done>
<what-built>
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.
</what-built>
<how-to-verify>
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
</how-to-verify>
<resume-signal>Type "approved" to complete Phase 6, or describe any visual issues found</resume-signal>
</task>
</tasks>
<verification>
`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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/06-new-backends/06-03-SUMMARY.md`
</output>