docs(07): create phase plan — 3 plans for VALID-01 and UX-01

This commit is contained in:
2026-03-31 09:20:11 +02:00
parent 78f54e3c9e
commit 9d02764fa3
4 changed files with 672 additions and 2 deletions
+7 -2
View File
@@ -74,7 +74,12 @@ Plans:
2. User who enters an S3 region in an invalid format (e.g., spaces or uppercase) sees an inline error message 2. User who enters an S3 region in an invalid format (e.g., spaces or uppercase) sees an inline error message
3. User can hover or click a tooltip icon on sensitive fields (SAS token, access key, SFTP auth method, OneDrive token) and read a plain-language explanation without navigating away 3. User can hover or click a tooltip icon on sensitive fields (SAS token, access key, SFTP auth method, OneDrive token) and read a plain-language explanation without navigating away
4. Tooltip content accurately distinguishes SAS token from storage account key in the Azure backend form 4. Tooltip content accurately distinguishes SAS token from storage account key in the Azure backend form
**Plans**: TBD **Plans**: 3 plans
Plans:
- [ ] 07-00-PLAN.md — Wave 0 TDD stubs: failing tests for VALID-01 regex rejection/acceptance and UX-01 tooltip toggle
- [ ] 07-01-PLAN.md — VALID-01: FieldDef validate extension + buildZodSchema regex chaining for 3 fields
- [ ] 07-02-PLAN.md — UX-01: tooltipText in registry + FieldRenderer/PasswordField ⓘ toggle + AzureAuthToggle/SftpAuthToggle wiring + human verification
## Progress ## Progress
@@ -86,4 +91,4 @@ Plans:
| 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 | 4/4 | Complete | 2026-03-31 | - | | 6. New Backends | 4/4 | Complete | 2026-03-31 | - |
| 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - | | 7. Validation & UX Polish | v1.1 | 0/3 | Not started | - |
@@ -0,0 +1,140 @@
---
phase: 07-validation-ux-polish
plan: "00"
type: tdd
wave: 1
depends_on: []
files_modified:
- src/components/wizard/RemoteConfigStep.test.tsx
autonomous: true
requirements:
- VALID-01
- UX-01
must_haves:
truths:
- "VALID-01 failing tests exist for Azure account name regex rejection and acceptance"
- "VALID-01 failing tests exist for S3 region regex rejection and acceptance"
- "VALID-01 failing tests exist for GCS project_number regex rejection and acceptance"
- "UX-01 failing tests exist for ⓘ button presence on sas_url, azureblob key, onedrive token, and SFTP auth method"
- "UX-01 failing tests exist for tooltip toggle behavior (click shows, click again hides)"
- "All existing 147 tests still pass after stub additions"
artifacts:
- path: "src/components/wizard/RemoteConfigStep.test.tsx"
provides: "Failing test stubs for VALID-01 and UX-01"
contains: "describe.*VALID-01|describe.*UX-01"
key_links:
- from: "RemoteConfigStep.test.tsx VALID-01 block"
to: "azureblob account field validation"
via: "userEvent.type + form submit + screen.getByText(error message)"
- from: "RemoteConfigStep.test.tsx UX-01 block"
to: "ⓘ button toggle"
via: "getByRole('button', { name: /more info/i }) + userEvent.click"
---
<objective>
Write failing test stubs for VALID-01 (format validation) and UX-01 (tooltip toggle) in the existing RemoteConfigStep test file.
Purpose: Establish the RED state before implementation — tests fail because the validate property and tooltipText property don't exist yet.
Output: Augmented RemoteConfigStep.test.tsx with 11 new failing its; all 147 existing tests still pass.
</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/07-validation-ux-polish/07-CONTEXT.md
@.planning/phases/07-validation-ux-polish/07-RESEARCH.md
<interfaces>
<!-- Key types and contracts extracted from codebase. Executor uses these directly. -->
From src/schemas/registry.ts (current — no validate or tooltipText yet):
```typescript
export type BackendType = 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2';
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
// NOTE: validate? and tooltipText? do NOT exist yet — that's why tests will fail
}
```
From src/schemas/index.ts (current — no regex chaining yet):
```typescript
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>>
// Current loop: z.string().min(1, ...) OR z.string().optional()
// No .regex() chaining yet — that's why VALID-01 tests fail
```
Test file patterns already established (from Phase 5/6):
- userEvent.setup() per test body
- vi.useFakeTimers() in beforeEach + vi.useRealTimers() in afterEach
- WizardConsumerSetup pattern: in-test React component dispatches SET_DEPLOYMENT via useEffect
- Label text used for assertions, not filename text
</interfaces>
</context>
<feature>
<name>Wave 0 TDD stubs — VALID-01 and UX-01</name>
<files>src/components/wizard/RemoteConfigStep.test.tsx</files>
<behavior>
VALID-01 — Azure account name:
- it('rejects account name with uppercase letters') → submit form with account='MyStorage' → expect error 'Must be 324 lowercase alphanumeric characters'
- it('rejects account name shorter than 3 characters') → submit form with account='ab' → expect same error
- it('accepts valid account name') → submit form with account='mystorageaccount' → expect NO format error
VALID-01 — S3 region:
- it('rejects S3 region with invalid format (spaces)') → submit with region='us east 1' → expect error text (e.g. 'valid AWS region format')
- it('accepts valid S3 region') → submit with region='us-east-1' → expect no error
VALID-01 — GCS project_number:
- it('rejects GCS project_number with non-digits') → submit with project_number='abc' → expect error (e.g. 'digits only')
- it('accepts valid GCS project_number') → submit with project_number='123456789' → expect no error
UX-01 — Tooltip toggle:
- it('renders ⓘ button on azureblob sas_url field') → render azureblob step → expect button with aria-label matching /more info about SAS URL/i
- it('shows tooltip panel when ⓘ is clicked on sas_url') → click ⓘ → expect tooltip text visible
- it('hides tooltip panel when ⓘ is clicked again') → click twice → expect tooltip text not visible
- it('renders ⓘ button on SFTP auth method section') → render sftp step → expect button with aria-label matching /more info about authentication/i
- it('renders ⓘ button on OneDrive token field') → render onedrive step → expect button with aria-label matching /more info about/i on token field
All new tests: write them to FAIL now (the ⓘ button doesn't exist, regex validation doesn't exist).
Existing 147 tests: MUST still pass after adding these stubs.
</behavior>
<implementation>
Add two new describe blocks to the existing RemoteConfigStep.test.tsx:
1. `describe('VALID-01 — format validation')` with 7 its (3 backends × ~2-3 tests each)
2. `describe('UX-01 — contextual tooltips')` with 4 its (button presence + toggle behavior)
Use the existing test setup patterns already in the file (WizardConsumerSetup, userEvent.setup(), vi.useFakeTimers in beforeEach).
Confirm all tests run by running the suite — expect 11 new failures + 147 existing passes.
</implementation>
</feature>
<verification>
Run after writing stubs:
1. `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` — expect 11 new FAILs (RED state confirmed), 0 regressions on existing tests
2. `npx vitest run` — expect all 147 pre-existing tests still pass (no collateral breakage)
</verification>
<success_criteria>
- 11 new failing tests added (RED state for VALID-01 and UX-01)
- 0 pre-existing test regressions
- Test file committed with message: `test(07-00): add failing stubs for VALID-01 and UX-01`
</success_criteria>
<output>
After completion, create `.planning/phases/07-validation-ux-polish/07-00-SUMMARY.md`
</output>
@@ -0,0 +1,209 @@
---
phase: 07-validation-ux-polish
plan: "01"
type: execute
wave: 2
depends_on:
- "07-00"
files_modified:
- src/schemas/registry.ts
- src/schemas/index.ts
autonomous: true
requirements:
- VALID-01
must_haves:
truths:
- "User who enters 'MyStorage' in the Azure account name field sees an inline error before advancing"
- "User who enters 'us east 1' in the S3 region field sees an inline error"
- "User who enters 'abc' in the GCS project_number field sees an inline error"
- "Valid values ('mystorageaccount', 'us-east-1', '123456789') produce no format error"
- "Empty required fields still show 'required' error (not the regex error) — empty string hits min(1) before regex"
- "All 147 pre-existing tests still pass after adding validation"
artifacts:
- path: "src/schemas/registry.ts"
provides: "FieldDef interface with validate property; 3 registry entries with validate rules"
contains: "validate\\?: \\{ regex: RegExp; message: string \\}"
- path: "src/schemas/index.ts"
provides: "buildZodSchema() chains .regex() when field.validate is present"
contains: "field\\.validate"
key_links:
- from: "BACKEND_REGISTRY azureblob.account"
to: "buildZodSchema('azureblob')"
via: "field.validate.regex applied as .regex() on ZodString"
pattern: "field\\.validate"
- from: "buildZodSchema()"
to: "BACKEND_SCHEMAS"
via: "Zod schema exported, consumed by zodResolver in RemoteConfigStep"
---
<objective>
Implement VALID-01: extend FieldDef with a validate property, add regex rules to 3 registry entries (azureblob account, s3 region, gcs project_number), and extend buildZodSchema() to chain .regex() when the property is present.
Purpose: Users see inline format errors on malformed credential fields before they can advance in the wizard.
Output: Modified registry.ts (FieldDef + 3 validate rules) and index.ts (buildZodSchema regex chaining).
</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/phases/07-validation-ux-polish/07-CONTEXT.md
@.planning/phases/07-validation-ux-polish/07-RESEARCH.md
@.planning/phases/07-validation-ux-polish/07-00-SUMMARY.md
<interfaces>
<!-- Extracted from codebase. Executor uses these directly. -->
Current FieldDef (src/schemas/registry.ts):
```typescript
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
// validate and tooltipText DO NOT EXIST YET
}
```
Current buildZodSchema (src/schemas/index.ts):
```typescript
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>> {
const fields = BACKEND_REGISTRY[backendType].fields;
const shape: Record<string, z.ZodTypeAny> = {};
for (const field of fields) {
shape[field.key] = field.required
? z.string().min(1, `${field.label} is required`)
: z.string().optional();
}
return z.object(shape);
}
```
Zod v4 important: cast to (schema as z.ZodString).regex(...) because schema variable is typed ZodTypeAny.
Zod v4 chaining order: .min(1).regex() is correct for required fields. .regex().optional() for optional (not needed here — all 3 validated fields are required).
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Extend FieldDef and add validate rules to 3 registry entries</name>
<files>src/schemas/registry.ts</files>
<behavior>
- Adding validate to azureblob.account and submitting 'ABC' produces Zod error 'Must be 324 lowercase alphanumeric characters'
- Adding validate to s3.region and submitting 'us east 1' produces Zod error matching region format
- Adding validate to gcs.project_number and submitting 'abc' produces Zod error 'digits only' (or similar)
- Submitting '' on account still produces the required error (not the regex error) — min(1) fires first
</behavior>
<action>
1. Add two optional properties to FieldDef interface:
```typescript
validate?: { regex: RegExp; message: string };
tooltipText?: string; // also add here for Plan 02 — it's just an interface addition, no behavior yet
```
Note: Adding tooltipText here avoids a second interface-only edit in Plan 02.
2. In BACKEND_REGISTRY, locate the azureblob `account` field entry and add:
```typescript
validate: {
regex: /^[a-z0-9]{3,24}$/,
message: 'Must be 324 lowercase alphanumeric characters (no hyphens or uppercase)',
},
```
3. Locate the s3 `region` field entry and add:
```typescript
validate: {
regex: /^[a-z][a-z0-9-]+[a-z0-9]$/,
message: 'Must be a valid AWS region format (e.g. us-east-1)',
},
```
4. Locate the gcs `project_number` field entry and add:
```typescript
validate: {
regex: /^\d+$/,
message: 'Must contain digits only',
},
```
No other fields get validate rules — user decision is explicit on scope.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>FieldDef has validate and tooltipText properties. 3 registry entries have validate rules. TypeScript compiles without errors (npx tsc --noEmit).</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Extend buildZodSchema() to chain .regex() from field.validate</name>
<files>src/schemas/index.ts</files>
<behavior>
- buildZodSchema('azureblob') produces a schema where the account field rejects 'ABC' with the registry message
- buildZodSchema('s3') produces a schema where the region field rejects 'us east 1'
- buildZodSchema('gcs') produces a schema where project_number rejects 'abc'
- buildZodSchema('onedrive') and others without validate are unaffected
- VALID-01 failing tests from Plan 00 now PASS
</behavior>
<action>
Replace the buildZodSchema loop body with the regex-aware version:
```typescript
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>> {
const fields = BACKEND_REGISTRY[backendType].fields;
const shape: Record<string, z.ZodTypeAny> = {};
for (const field of fields) {
let schema: z.ZodTypeAny = field.required
? z.string().min(1, `${field.label} is required`)
: z.string();
if (field.validate) {
schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
}
if (!field.required) {
schema = (schema as z.ZodString).optional();
}
shape[field.key] = schema;
}
return z.object(shape);
}
```
Critical: cast to (schema as z.ZodString) before .regex() — ZodTypeAny does not expose .regex() in TypeScript but it is present at runtime. This pattern is verified.
Do NOT touch BACKEND_SCHEMAS export or anything else in the file.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>VALID-01 tests from Plan 00 all pass (GREEN). Full suite still at 147+ pass with 0 failures: `npx vitest run`.</done>
</task>
</tasks>
<verification>
1. `npx tsc --noEmit` — no TypeScript errors
2. `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` — VALID-01 tests pass (GREEN), UX-01 stubs still fail (expected — UX-01 is Plan 02)
3. `npx vitest run` — all pre-existing 147 tests pass, no regressions
</verification>
<success_criteria>
- FieldDef has validate and tooltipText properties
- 3 registry entries (azureblob.account, s3.region, gcs.project_number) have validate rules with the exact regexes from CONTEXT.md
- buildZodSchema() chains .regex() when field.validate is present
- VALID-01 test stubs from Plan 00 all pass
- Full test suite passes
</success_criteria>
<output>
After completion, create `.planning/phases/07-validation-ux-polish/07-01-SUMMARY.md`
</output>
@@ -0,0 +1,316 @@
---
phase: 07-validation-ux-polish
plan: "02"
type: execute
wave: 3
depends_on:
- "07-01"
files_modified:
- src/schemas/registry.ts
- src/components/ui/FieldRenderer.tsx
- src/components/ui/PasswordField.tsx
- src/components/wizard/SftpAuthToggle.tsx
- src/components/wizard/AzureAuthToggle.tsx
autonomous: false
requirements:
- UX-01
must_haves:
truths:
- "User can click ⓘ next to the SAS URL label and read an explanation of SAS URL vs access key"
- "User can click ⓘ next to the Access Key label and read an explanation of full account key access"
- "User can click ⓘ on the SFTP authentication method section and read the difference between password and key-based auth"
- "User can click ⓘ next to the OneDrive token label and read how to obtain the JSON token"
- "Clicking ⓘ again hides the tooltip panel (toggle behavior)"
- "All 147+ tests (including VALID-01 from Plan 01) pass after tooltip implementation"
artifacts:
- path: "src/schemas/registry.ts"
provides: "tooltipText populated for azureblob.sas_url, azureblob.key, onedrive.token"
contains: "tooltipText:"
- path: "src/components/ui/FieldRenderer.tsx"
provides: "ⓘ button + inline tooltip panel when field.tooltipText is present"
contains: "showTooltip"
- path: "src/components/ui/PasswordField.tsx"
provides: "ⓘ button + inline tooltip panel when tooltipText prop is present"
contains: "tooltipText"
- path: "src/components/wizard/SftpAuthToggle.tsx"
provides: "ⓘ button above auth method toggle with inline explanation"
contains: "showAuthTip"
- path: "src/components/wizard/AzureAuthToggle.tsx"
provides: "Passes tooltipText from registry to PasswordField for sas_url and key"
contains: "tooltipText"
key_links:
- from: "BACKEND_REGISTRY azureblob.sas_url.tooltipText"
to: "AzureAuthToggle → PasswordField tooltipText prop"
via: "BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText"
pattern: "BACKEND_REGISTRY\\.azureblob\\.fields\\.find"
- from: "BACKEND_REGISTRY onedrive.token.tooltipText"
to: "FieldRenderer ⓘ button rendered"
via: "field.tooltipText present → useState showTooltip"
- from: "SftpAuthToggle showAuthTip state"
to: "inline explanation panel"
via: "useState + conditional render"
---
<objective>
Implement UX-01: add tooltipText data to 3 registry fields, render ⓘ toggle buttons in FieldRenderer and PasswordField, wire AzureAuthToggle to pass tooltipText from the registry, and add a standalone auth-method tooltip to SftpAuthToggle.
Purpose: Users can access plain-language explanations of sensitive or confusing credential fields without leaving the wizard.
Output: 5 modified files; ⓘ buttons visible on sas_url, key, onedrive token, and SFTP auth method.
</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/phases/07-validation-ux-polish/07-CONTEXT.md
@.planning/phases/07-validation-ux-polish/07-RESEARCH.md
@.planning/phases/07-validation-ux-polish/07-01-SUMMARY.md
<interfaces>
<!-- Extracted from codebase + Plan 01 additions. Executor uses these directly. -->
After Plan 01, FieldDef in src/schemas/registry.ts:
```typescript
export interface FieldDef {
key: string;
label: string;
inputType: 'text' | 'password' | 'select' | 'toggle';
required: boolean;
placeholder?: string;
helpText?: string;
options?: { value: string; label: string }[];
validate?: { regex: RegExp; message: string };
tooltipText?: string; // Added in Plan 01 — now populate it for tooltip fields
}
```
Current PasswordField props (src/components/ui/PasswordField.tsx) — to add tooltipText?:
```typescript
interface PasswordFieldProps {
id: string;
label: string;
error?: FieldError;
registration: UseFormRegisterReturn;
placeholder?: string;
helpText?: string;
// tooltipText?: string ← add this
}
```
CRITICAL pitfall: azureblob.sas_url and azureblob.key are NOT rendered through the FieldRenderer registry loop.
They are rendered inside AzureAuthToggle.tsx which calls PasswordField with hardcoded props.
To wire the tooltip, AzureAuthToggle must read from BACKEND_REGISTRY:
BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText
BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'key')?.tooltipText
Then pass those strings as tooltipText prop to the respective PasswordField calls.
SFTP auth method tooltip is NOT field-level — it's a standalone explanation above the segmented control.
SftpAuthToggle manages its own useState for showAuthTip.
The PasswordField calls inside SftpAuthToggle for 'pass' and 'key_pem' do NOT get tooltipText.
CSS-hidden toggle pattern is established project convention (AzureAuthToggle, SftpAuthToggle use div.block/div.hidden).
CONTEXT.md decision: use CSS-hidden pattern (div.hidden / div.block) for tooltip panel visibility — not conditional render with &&.
Actually re-read: CONTEXT.md says "toggle state is local to the field component (no global state needed)" and research shows
both CSS-hidden and conditional render work; CSS-hidden is the project convention. Use div.hidden / div.block OR useState + conditional render ({showTooltip && <p>...}).
Research Pattern 3 shows conditional render. Either is fine — use Claude's discretion for tooltip panel; CSS-hidden for SftpAuthToggle to stay consistent with its existing pattern.
Accessibility: the ⓘ button MUST be outside (sibling to, not inside) the <label> element to avoid label text contamination.
Wrap label + ⓘ button in a flex container (e.g. <div className="flex items-center gap-1">).
aria-label on the ⓘ button: `More info about ${field.label}` (for FieldRenderer/PasswordField) or 'More info about authentication methods' (for SftpAuthToggle).
⓪ icon source: use the Unicode character ⓘ (text character — no Heroicons needed, no new dependency).
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Populate tooltipText in registry + add ⓘ to FieldRenderer and PasswordField</name>
<files>src/schemas/registry.ts, src/components/ui/FieldRenderer.tsx, src/components/ui/PasswordField.tsx</files>
<behavior>
- FieldRenderer renders a ⓘ button next to the label when field.tooltipText is present
- Clicking ⓘ shows the tooltip panel below the input
- Clicking ⓘ again hides the tooltip panel
- PasswordField renders the same ⓘ + panel when tooltipText prop is provided
- OneDrive token field (inputType: password, rendered via FieldRenderer → PasswordField) shows ⓘ
- Fields without tooltipText show no ⓘ (no empty button rendered)
</behavior>
<action>
Step 1 — Populate tooltipText in src/schemas/registry.ts for 3 fields:
azureblob sas_url field — add:
```typescript
tooltipText: 'A SAS URL (Shared Access Signature) bundles the storage endpoint with a time-limited, scope-limited token. It grants access only to containers you specify and expires automatically. Use this if you want limited-access credentials. If you have the full account key, switch to Access Key.',
```
azureblob key field — add:
```typescript
tooltipText: 'The full storage account key grants unrestricted read/write access to all containers in the account. Keep this secret. If you only need limited access, use a SAS URL instead.',
```
onedrive token field — add:
```typescript
tooltipText: 'This is the JSON token obtained by running `rclone authorize "onedrive"` on a machine with a browser. The command opens a browser window, you authenticate, and rclone prints a JSON token — paste that entire JSON blob here.',
```
Step 2 — Update PasswordField (src/components/ui/PasswordField.tsx):
- Add `tooltipText?: string` to PasswordFieldProps interface
- Add `const [showTooltip, setShowTooltip] = useState(false)` inside the component
- Add import for useState if not already imported
- Wrap the existing `<label>` and any required star in a flex container `<div className="flex items-center gap-1">`, place the ⓘ button OUTSIDE and AFTER the label element:
```tsx
<div className="flex items-center gap-1">
<label htmlFor={id} className="text-sm font-medium text-gray-700">{label}{required star if present}</label>
{tooltipText && (
<button
type="button"
onClick={() => setShowTooltip(v => !v)}
aria-label={`More info about ${label}`}
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
>
</button>
)}
</div>
```
- Add tooltip panel below the input (above helpText/error):
```tsx
{tooltipText && showTooltip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
{tooltipText}
</p>
)}
```
Step 3 — Update FieldRenderer (src/components/ui/FieldRenderer.tsx):
- Add `const [showTooltip, setShowTooltip] = useState(false)` (one useState per FieldRenderer instance)
- Add import for useState if not already present
- For text and select input cases: apply the same pattern as PasswordField — flex container for label + ⓘ button sibling (outside label), tooltip panel below input
- For password input case: FieldRenderer calls PasswordField — pass `tooltipText={field.tooltipText}` as a prop
Note: FieldRenderer likely has a switch/if on field.inputType. Apply the ⓘ + panel to each branch (text, select), and pass tooltipText down to PasswordField for the password branch.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>UX-01 tests for OneDrive token ⓘ presence and toggle behavior pass. All VALID-01 tests still pass.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Wire AzureAuthToggle to pass tooltipText + add SFTP auth-method tooltip</name>
<files>src/components/wizard/AzureAuthToggle.tsx, src/components/wizard/SftpAuthToggle.tsx</files>
<behavior>
- azureblob sas_url PasswordField call in AzureAuthToggle shows ⓘ with the registry tooltipText
- azureblob key PasswordField call in AzureAuthToggle shows ⓘ with the registry tooltipText
- SftpAuthToggle renders a ⓘ button labeled 'More info about authentication methods' above the segmented control
- Clicking that ⓘ shows the auth-method explanation panel
- Clicking again hides it
- SFTP pass and key_pem PasswordField calls do NOT get tooltipText (auth method ⓘ is above the whole toggle)
</behavior>
<action>
AzureAuthToggle.tsx:
- Import BACKEND_REGISTRY from '../schemas/registry' (or '../../schemas/registry' — verify the import path)
- Add two lookups at the top of the component function:
```typescript
const sasUrlTooltip = BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText;
const keyTooltip = BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'key')?.tooltipText;
```
- Pass `tooltipText={sasUrlTooltip}` to the PasswordField call for sas_url
- Pass `tooltipText={keyTooltip}` to the PasswordField call for key
SftpAuthToggle.tsx:
- Add `const [showAuthTip, setShowAuthTip] = useState(false)` to the component
- Import useState if not already present (it likely already is — component uses CSS-hidden toggle state)
- Before the segmented control div (the Password / Private Key tabs), add:
```tsx
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-gray-700">Authentication Method</span>
<button
type="button"
onClick={() => setShowAuthTip(v => !v)}
aria-label="More info about authentication methods"
className="text-blue-500 hover:text-blue-700 text-xs leading-none"
>
</button>
</div>
{showAuthTip && (
<p className="text-xs text-blue-700 bg-blue-50 border border-blue-200 rounded px-2 py-1.5 mt-1">
Password auth sends your password to the SFTP server on each connection.
Key-based auth uses a private key (more secure — the server only stores your public key).
Paste the full private key PEM content (including the -----BEGIN/END----- lines) if using key-based auth.
</p>
)}
```
- Do NOT add tooltipText to the pass or key_pem PasswordField calls inside SftpAuthToggle.
</action>
<verify>
<automated>npx vitest run src/components/wizard/RemoteConfigStep.test.tsx</automated>
</verify>
<done>All UX-01 tests from Plan 00 pass (ⓘ on sas_url, key, SFTP auth method, onedrive token; toggle behavior). Full suite green: `npx vitest run`.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verify — tooltip toggle and inline validation in the running wizard</name>
<what-built>
Phase 7 complete: inline format validation on Azure account, S3 region, and GCS project number; contextual ⓘ tooltips on SAS URL, access key, SFTP auth method, and OneDrive token.
</what-built>
<how-to-verify>
Start the dev server: `npm run dev` (or `npx vite`)
1. Go to the wizard, select Azure Blob Storage
- Type 'ABC' in Storage Account Name → expect inline error 'Must be 324 lowercase alphanumeric characters' after clicking Next
- Type 'mystorageaccount' → error clears
- Confirm ⓘ icon appears next to 'SAS URL' and 'Access Key' labels
- Click ⓘ on SAS URL → explanation panel appears below the field
- Click ⓘ again → explanation panel disappears
2. Select Amazon S3
- Type 'us east 1' in Region → expect inline error about valid format
- Type 'us-east-1' → error clears
3. Select Google Cloud Storage
- Type 'abc' in Project Number → expect inline error about digits only
- Type '123456789' → error clears
4. Select SFTP
- Confirm ⓘ icon appears above the Password/Private Key tabs with label 'Authentication Method ⓘ'
- Click ⓘ → explanation panel appears
- Click ⓘ again → panel disappears
5. Select OneDrive
- Confirm ⓘ icon appears next to the Token field label
- Click ⓘ → explanation of rclone authorize JSON token appears
</how-to-verify>
<action>Run `npm run dev` and follow the verification steps below to confirm all tooltip and validation behaviors work in the browser.</action>
<verify>
<automated>npx vitest run</automated>
</verify>
<done>Human confirms all 4 tooltip fields show ⓘ with correct explanations, toggle works, and 3 validation fields show inline errors on bad input.</done>
<resume-signal>Type "approved" if all tooltip and validation behaviors work correctly, or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
1. `npx tsc --noEmit` — no TypeScript errors across all 5 modified files
2. `npx vitest run` — full suite passes (147+ tests, 0 failures)
3. Human verification: all 4 tooltip fields show ⓘ, toggle works, 3 validation fields show errors on bad input
</verification>
<success_criteria>
- UX-01 tests from Plan 00 all pass (GREEN)
- All VALID-01 tests from Plan 01 still pass
- Full test suite passes with 0 regressions
- Human verifies tooltip toggle and inline validation in the running wizard UI
- Phase 7 complete — VALID-01 and UX-01 requirements satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/07-validation-ux-polish/07-02-SUMMARY.md`
</output>