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:
@@ -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>
|
||||
Reference in New Issue
Block a user