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