diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..1a7bd25 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,39 @@ +{ + "permissions": { + "allow": [ + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: initialize project\" --files .planning/PROJECT.md)", + "Bash(node \"C:/Users/SebastienQUEROL/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: complete project research\" --files .planning/research/)", + "WebSearch", + "Bash(npm test:*)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" phase complete \"02\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-02\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/02-generators/02-VERIFICATION.md)", + "Bash(npx vitest:*)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state advance-plan)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state update-progress)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-metric --phase \"03-wizard-ui\" --plan \"05\" --duration \"5min\" --tasks \"3\" --files \"5\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state add-decision --phase \"03-wizard-ui\" --summary \"[03-05] Human verification approved — full 3-step wizard flow confirmed working end-to-end\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-session --stopped-at \"Completed 03-wizard-ui-03-05-PLAN.md\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" roadmap update-plan-progress \"03\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" requirements mark-complete WIZD-02 WIZD-03)", + "Bash(ls C:/Users/SebastienQUEROL/Documents/projets/Ready2Blob/.planning/phases/03-wizard-ui/*-VERIFICATION.md)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-03\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/03-wizard-ui/03-VERIFICATION.md)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-metric --phase \"04-review-download-security\" --plan \"05\" --duration \"1min\" --tasks \"1\" --files \"0\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state add-decision --phase \"04-review-download-security\" --summary \"[04-05] Human verification approved — Phase 4 full feature set confirmed working end-to-end in real browser\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" state record-session --stopped-at \"Completed 04-05-PLAN.md — Phase 4 human verification approved, all 18 checks passed\")", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" roadmap update-plan-progress 4)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" requirements mark-complete CONF-02 CONF-03 DOWN-01 DOWN-02 DOWN-03 DOWN-04 DOWN-05 DOWN-06 SECU-01 SECU-02 SECU-03)", + "Bash(ls /c/Users/SebastienQUEROL/Documents/projets/Ready2Blob/.planning/phases/04-review-download-security/*VERIFICATION*)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs\\(phase-04\\): complete phase execution\" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md .planning/phases/04-review-download-security/04-VERIFICATION.md)", + "Bash(ls -d .planning/milestones/v*-phases)", + "Bash(ls -d .planning/phases/*/)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" milestone complete \"v1.0\" --name \"MVP\")", + "Bash(ls .planning/*.md)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" commit \"docs: start milestone v1.1 Backlog & Tech Debt\" --files .planning/PROJECT.md .planning/STATE.md)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" init new-milestone)", + "Bash(node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" config-set workflow.research true)", + "Bash(mkdir -p .planning/research)" + ] + } +} diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 82abc14..dbaef5e 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -10,8 +10,8 @@ - [x] **TECH-01**: User sees only the script output blocks matching their selected deployment targets (Intune and/or RMM) in ReviewStep - [x] **TECH-02**: User can navigate back from ReviewStep using an explicit Back button - [x] **TECH-03**: Backend list in BackendSelectionStep is automatically derived from BACKEND_REGISTRY keys (no hardcoded list) -- [ ] **TECH-04**: Dead `BackendFormValues` export is removed from `src/schemas/index.ts` -- [ ] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings +- [x] **TECH-04**: Dead `BackendFormValues` export is removed from `src/schemas/index.ts` +- [x] **TECH-05**: BackendSelectionStep test suite runs without `act()` warnings ### Backends @@ -49,8 +49,8 @@ | TECH-01 | Phase 5 | Complete | | TECH-02 | Phase 5 | Complete | | TECH-03 | Phase 5 | Complete | -| TECH-04 | Phase 5 | Pending | -| TECH-05 | Phase 5 | Pending | +| TECH-04 | Phase 5 | Complete | +| TECH-05 | Phase 5 | Complete | | BACK-01 | Phase 6 | Pending | | BACK-02 | Phase 6 | Pending | | BACK-03 | Phase 6 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 34e1d29..d6671e7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -23,7 +23,7 @@ Full phase details: [.planning/milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADM **Milestone Goal:** Ship all deferred tech debt and backlog features from v1.0 — scriptTargets filtering, UX consistency fixes, registry-driven backend list, 4 new backends, field validation, and contextual tooltips. -- [ ] **Phase 5: Tech Debt** - Fix all deferred v1.0 issues (scriptTargets filtering, Back button, registry-driven UI, dead export, test warnings) +- [x] **Phase 5: Tech Debt** - Fix all deferred v1.0 issues (scriptTargets filtering, Back button, registry-driven UI, dead export, test warnings) (completed 2026-03-30) - [ ] **Phase 6: New Backends** - Add OneDrive, SFTP, Google Cloud Storage, and Backblaze B2 to the backend registry and wizard - [ ] **Phase 7: Validation & UX Polish** - Add field-level format validation and contextual tooltips across all backends @@ -78,6 +78,6 @@ Plans: | 2. Generators | v1.0 | 4/4 | Complete | 2026-03-26 | | 3. Wizard UI | v1.0 | 5/5 | Complete | 2026-03-27 | | 4. Review, Download & Security | v1.0 | 5/5 | Complete | 2026-03-27 | -| 5. Tech Debt | 3/4 | In Progress| | - | +| 5. Tech Debt | 4/4 | Complete | 2026-03-30 | - | | 6. New Backends | v1.1 | 0/TBD | Not started | - | | 7. Validation & UX Polish | v1.1 | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 9faff93..6513ffd 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Backlog & Tech Debt status: planning -stopped_at: Completed 05-tech-debt 05-01-PLAN.md -last_updated: "2026-03-30T07:39:32.011Z" +stopped_at: Completed 05-tech-debt 05-03-PLAN.md +last_updated: "2026-03-30T07:44:48.651Z" last_activity: 2026-03-27 — v1.1 roadmap created, 11 requirements mapped across 3 phases progress: total_phases: 3 - completed_phases: 0 + completed_phases: 1 total_plans: 4 - completed_plans: 3 + completed_plans: 4 percent: 0 --- @@ -51,6 +51,7 @@ Progress: [░░░░░░░░░░] 0% (v1.1) | Phase 05-tech-debt P00 | 3 | 2 tasks | 2 files | | Phase 05-tech-debt P02 | 4min | 1 tasks | 1 files | | Phase 05-tech-debt P01 | 5 | 2 tasks | 4 files | +| Phase 05-tech-debt P03 | 3 | 2 tasks | 2 files | ## Accumulated Context @@ -68,6 +69,8 @@ Recent decisions affecting v1.1 work: - [Phase 05-tech-debt]: Unmount OutputBlocks entirely (conditional render with &&) not CSS hidden — avoids stale component state when toggling scriptTargets - [Phase 05-tech-debt]: Keep useMemo calls for all scripts even when OutputBlocks hidden — ZIP handler always needs the content for dynamic bundle building - [Phase 05-tech-debt]: BACKEND_REGISTRY shape enriched to { displayName, description, fields } — registry is single source of truth for display AND field data; BackendSelectionStep wired via Object.entries(BACKEND_REGISTRY) +- [Phase 05-tech-debt]: userEvent.setup() per test body pattern established — replaces fireEvent for async React state interactions +- [Phase 05-tech-debt]: vi.useFakeTimers() in beforeEach + vi.useRealTimers() in afterEach pattern established for suppressing setTimeout act() warnings ### Pending Todos @@ -82,6 +85,6 @@ None yet. ## Session Continuity -Last session: 2026-03-30T07:39:32.005Z -Stopped at: Completed 05-tech-debt 05-01-PLAN.md +Last session: 2026-03-30T07:44:48.644Z +Stopped at: Completed 05-tech-debt 05-03-PLAN.md Resume file: None diff --git a/.planning/phases/03-wizard-ui/03-01-PLAN.md b/.planning/phases/03-wizard-ui/03-01-PLAN.md new file mode 100644 index 0000000..4560593 --- /dev/null +++ b/.planning/phases/03-wizard-ui/03-01-PLAN.md @@ -0,0 +1,333 @@ +--- +phase: 03-wizard-ui +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - vite.config.ts + - src/App.test.tsx + - src/components/wizard/BackendSelectionStep.test.tsx + - src/components/wizard/RemoteConfigStep.test.tsx + - src/components/wizard/StepIndicator.test.tsx +autonomous: true +requirements: + - WIZD-01 + - WIZD-02 + - WIZD-03 + - WIZD-04 + - BACK-01 + - BACK-02 + - BACK-03 + +must_haves: + truths: + - "Vitest runs React component tests without 'document is not defined' errors" + - "All four test stub files exist and fail RED with descriptive 'not yet implemented' messages" + - "npx vitest run exits non-zero (tests fail) confirming stubs are RED" + artifacts: + - path: "vite.config.ts" + provides: "jsdom test environment config" + contains: "test: { environment: 'jsdom'" + - path: "src/App.test.tsx" + provides: "Wave 0 stub for WIZD-02 step routing" + - path: "src/components/wizard/BackendSelectionStep.test.tsx" + provides: "Wave 0 stubs for WIZD-01, WIZD-04" + - path: "src/components/wizard/RemoteConfigStep.test.tsx" + provides: "Wave 0 stubs for BACK-01, BACK-02, BACK-03" + - path: "src/components/wizard/StepIndicator.test.tsx" + provides: "Wave 0 stubs for WIZD-03" + key_links: + - from: "vite.config.ts" + to: "src/**/*.test.tsx" + via: "test.environment: 'jsdom'" + pattern: "environment.*jsdom" +--- + + +Set up the Wave 0 test infrastructure: configure Vitest for jsdom component testing and create failing test stubs for all Phase 3 wizard components. + +Purpose: Establish the RED baseline before implementation — tests must fail in a descriptive way so Plans 03–05 can drive to GREEN. +Output: Updated vite.config.ts and four test stub files. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-wizard-ui/03-CONTEXT.md +@.planning/phases/03-wizard-ui/03-RESEARCH.md +@.planning/phases/03-wizard-ui/03-VALIDATION.md + + + + +From src/store/types.ts: +```typescript +export interface WizardState { + currentStep: number; + remote: { + name: string; + backendType: BackendType | null; + params: Record; + }; + deployment: { + includeInstall: boolean; + configPath: 'machine-wide' | 'user-profile'; + scriptTargets: ('intune' | 'rmm')[]; + }; +} + +export type WizardAction = + | { type: 'SET_STEP'; payload: number } + | { type: 'SET_BACKEND_TYPE'; payload: BackendType } + | { type: 'SET_REMOTE_NAME'; payload: string } + | { type: 'SET_REMOTE_PARAMS'; payload: Record } + | { type: 'SET_DEPLOYMENT'; payload: Partial } + | { type: 'RESET' }; +``` + +From src/schemas/registry.ts: +```typescript +export type BackendType = 'azureblob' | 's3' | 's3-compatible'; +``` + +From src/store/context.tsx: +```typescript +export function useWizard(): WizardContextValue; +export function WizardProvider({ children }: { children: React.ReactNode }): JSX.Element; +``` + + + + + + + Task 1: Configure Vitest jsdom environment in vite.config.ts + vite.config.ts + + Update vite.config.ts to add a `test` block inside `defineConfig`. The existing config has only `plugins: [react(), tailwindcss()]`. Add: + + ```typescript + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import tailwindcss from '@tailwindcss/vite'; + + export default defineConfig({ + plugins: [react(), tailwindcss()], + test: { + environment: 'jsdom', + passWithNoTests: true, + }, + }); + ``` + + Do NOT add `/// ` — this is not needed with vitest ^4.x when using `test` config inside `defineConfig`. The `passWithNoTests: true` is already established project convention (see STATE.md decision from Phase 01-01). + + + npx vitest run --reporter=verbose 2>&1 | head -20 + + Vitest starts without "document is not defined" errors. If no test files exist yet the suite exits 0 due to passWithNoTests: true. + + + + Task 2: Create Wave 0 test stubs (RED baseline for all 7 requirements) + + src/App.test.tsx, + src/components/wizard/BackendSelectionStep.test.tsx, + src/components/wizard/RemoteConfigStep.test.tsx, + src/components/wizard/StepIndicator.test.tsx + + + Create the `src/components/wizard/` directory (it does not exist yet). Create four test stub files. Each stub imports the component it will test and throws a todo error — this gives RED tests with clear names rather than import errors. + + NOTE: The component files do NOT exist yet. Import them anyway. Vitest will fail at import with "Cannot find module" — this is the correct RED state. + + **src/App.test.tsx** + ```typescript + // @vitest-environment jsdom + // Covers WIZD-02: App renders the correct step component for currentStep 0, 1, 2 + import { describe, it, expect } from 'vitest'; + + describe('App — step routing', () => { + it('renders BackendSelectionStep when currentStep is 0', () => { + expect.fail('not yet implemented'); + }); + + it('renders RemoteConfigStep when currentStep is 1', () => { + expect.fail('not yet implemented'); + }); + + it('renders DeploymentStep when currentStep is 2', () => { + expect.fail('not yet implemented'); + }); + }); + ``` + + **src/components/wizard/BackendSelectionStep.test.tsx** + ```typescript + // @vitest-environment jsdom + // Covers WIZD-01: card grid renders Azure Blob, Amazon S3, S3-Compatible (Azure first) + // Covers WIZD-04: remote name field validates alphanumeric/dash/underscore + import { describe, it, expect } from 'vitest'; + + describe('BackendSelectionStep', () => { + describe('WIZD-01: backend card grid', () => { + it('renders Azure Blob Storage card', () => { + expect.fail('not yet implemented'); + }); + + it('renders Amazon S3 card', () => { + expect.fail('not yet implemented'); + }); + + it('renders S3-Compatible card', () => { + expect.fail('not yet implemented'); + }); + + it('Azure Blob is listed before S3 in DOM order', () => { + expect.fail('not yet implemented'); + }); + + it('clicking a backend card dispatches SET_BACKEND_TYPE and SET_STEP', () => { + expect.fail('not yet implemented'); + }); + }); + + describe('WIZD-04: remote name field', () => { + it('renders remote name input at the top of the step', () => { + expect.fail('not yet implemented'); + }); + + it('shows no error before first Next attempt', () => { + expect.fail('not yet implemented'); + }); + + it('shows inline error after first Next attempt with invalid name', () => { + expect.fail('not yet implemented'); + }); + + it('accepts alphanumeric, dashes, and underscores', () => { + expect.fail('not yet implemented'); + }); + + it('rejects names with spaces or special characters', () => { + expect.fail('not yet implemented'); + }); + }); + }); + ``` + + **src/components/wizard/RemoteConfigStep.test.tsx** + ```typescript + // @vitest-environment jsdom + // Covers BACK-01: Azure Blob config form — account + SAS/Key toggle, both values preserved + // Covers BACK-02: Amazon S3 config form — access_key_id, secret_access_key, region + // Covers BACK-03: S3-Compatible config form — same as S3 plus endpoint field + import { describe, it, expect } from 'vitest'; + + describe('RemoteConfigStep', () => { + describe('BACK-01: Azure Blob form', () => { + it('renders Storage Account Name field', () => { + expect.fail('not yet implemented'); + }); + + it('shows SAS URL field by default (default auth method)', () => { + expect.fail('not yet implemented'); + }); + + it('switching auth toggle to Access Key shows key field and hides SAS URL', () => { + expect.fail('not yet implemented'); + }); + + it('switching auth toggle does not clear the hidden field value', () => { + expect.fail('not yet implemented'); + }); + }); + + describe('BACK-02: Amazon S3 form', () => { + it('renders access_key_id field', () => { + expect.fail('not yet implemented'); + }); + + it('renders secret_access_key field', () => { + expect.fail('not yet implemented'); + }); + + it('renders region field', () => { + expect.fail('not yet implemented'); + }); + }); + + describe('BACK-03: S3-Compatible form', () => { + it('renders endpoint field in addition to S3 fields', () => { + expect.fail('not yet implemented'); + }); + }); + }); + ``` + + **src/components/wizard/StepIndicator.test.tsx** + ```typescript + // @vitest-environment jsdom + // Covers WIZD-03: going back preserves remote.params; deployment options are untouched + import { describe, it, expect } from 'vitest'; + + describe('StepIndicator', () => { + describe('WIZD-03: back navigation preserves state', () => { + it('clicking a completed step dispatches SET_STEP', () => { + expect.fail('not yet implemented'); + }); + + it('clicking back to step 0 dispatches SET_REMOTE_PARAMS({}) to clear params', () => { + expect.fail('not yet implemented'); + }); + + it('clicking back to step 0 does NOT dispatch RESET (deployment preserved)', () => { + expect.fail('not yet implemented'); + }); + + it('step 0 shows as active when currentStep is 0', () => { + expect.fail('not yet implemented'); + }); + + it('completed steps are clickable', () => { + expect.fail('not yet implemented'); + }); + }); + }); + ``` + + + npx vitest run --reporter=verbose 2>&1 | tail -30 + + + All four test files exist. Vitest runs and reports failures (RED). The failures are either "Cannot find module" (acceptable — component files not yet created) or "not yet implemented" (from expect.fail). No passing tests exist yet. vite.config.ts has jsdom environment. + + + + + + +Run after both tasks complete: +```bash +npx vitest run --reporter=verbose 2>&1 +``` +Expected: Vitest runs (no "document is not defined"), tests fail RED with module-not-found or "not yet implemented" errors, suite reports failures. The key outcome is that jsdom works and tests are scaffolded. + + + +- vite.config.ts has `test: { environment: 'jsdom', passWithNoTests: true }` +- Four test files exist under src/ with descriptive failing test names +- `npx vitest run` runs without crashing the vitest process itself (failures are expected) +- Test names directly trace to requirements (WIZD-01, WIZD-02, WIZD-03, WIZD-04, BACK-01, BACK-02, BACK-03) + + + +After completion, create `.planning/phases/03-wizard-ui/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-wizard-ui/03-02-PLAN.md b/.planning/phases/03-wizard-ui/03-02-PLAN.md new file mode 100644 index 0000000..bab65ea --- /dev/null +++ b/.planning/phases/03-wizard-ui/03-02-PLAN.md @@ -0,0 +1,426 @@ +--- +phase: 03-wizard-ui +plan: "02" +type: execute +wave: 2 +depends_on: + - "03-01" +files_modified: + - src/components/ui/BackendCard.tsx + - src/components/ui/PasswordField.tsx + - src/components/ui/FieldRenderer.tsx + - src/components/wizard/AzureAuthToggle.tsx +autonomous: true +requirements: + - BACK-01 + - BACK-02 + - BACK-03 + +must_haves: + truths: + - "BackendCard renders a clickable card with backend name and calls onClick when clicked" + - "PasswordField renders a password input with a show/hide eye toggle button" + - "FieldRenderer renders the correct input type for text, password, select, and toggle FieldDef entries" + - "AzureAuthToggle renders a segmented SAS/Key toggle, shows the active field, and defaults to SAS" + artifacts: + - path: "src/components/ui/BackendCard.tsx" + provides: "Clickable backend selection card" + exports: ["BackendCard"] + - path: "src/components/ui/PasswordField.tsx" + provides: "Password input with show/hide toggle" + exports: ["PasswordField"] + - path: "src/components/ui/FieldRenderer.tsx" + provides: "Single FieldDef renderer for registry-driven forms" + exports: ["FieldRenderer"] + - path: "src/components/wizard/AzureAuthToggle.tsx" + provides: "Azure SAS vs Access Key segmented toggle" + exports: ["AzureAuthToggle"] + key_links: + - from: "src/components/wizard/AzureAuthToggle.tsx" + to: "src/schemas/registry.ts" + via: "renders key and sas_url FieldDef entries via PasswordField" + pattern: "PasswordField" + - from: "src/components/ui/FieldRenderer.tsx" + to: "src/schemas/registry.ts" + via: "accepts FieldDef and renders the correct input element" + pattern: "FieldDef" +--- + + +Create the four reusable UI atom components that the wizard step components depend on: BackendCard, PasswordField, FieldRenderer, and AzureAuthToggle. + +Purpose: Establish shared building blocks so Plans 03 and 04 can implement their step components without duplicating input logic. +Output: Four new component files in src/components/ui/ and src/components/wizard/. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-wizard-ui/03-CONTEXT.md +@.planning/phases/03-wizard-ui/03-RESEARCH.md + + + + +From src/schemas/registry.ts: +```typescript +export type BackendType = 'azureblob' | 's3' | 's3-compatible'; + +export interface FieldDef { + key: string; // MUST match rclone config key exactly (snake_case) + label: string; + inputType: 'text' | 'password' | 'select' | 'toggle'; + required: boolean; + placeholder?: string; + helpText?: string; + options?: { value: string; label: string }[]; // for inputType: 'select' +} + +export const BACKEND_REGISTRY: Record = { + azureblob: [ + { key: 'account', label: 'Storage Account Name', inputType: 'text', required: true }, + { key: 'key', label: 'Access Key', inputType: 'password', required: false }, + { key: 'sas_url', label: 'SAS URL', inputType: 'password', required: false }, + ], + s3: [ + { key: 'provider', label: 'Provider', inputType: 'select', required: true }, + { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true }, + { key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true }, + { key: 'region', label: 'Region', inputType: 'text', required: true }, + ], + 's3-compatible': [ + { key: 'provider', label: 'Provider', inputType: 'select', required: true }, + { key: 'access_key_id', label: 'Access Key ID', inputType: 'text', required: true }, + { key: 'secret_access_key', label: 'Secret Access Key',inputType: 'password', required: true }, + { key: 'endpoint', label: 'Endpoint URL', inputType: 'text', required: true }, + { key: 'region', label: 'Region', inputType: 'text', required: false }, + ], +}; +``` + +From react-hook-form (installed as react-hook-form ^7.72.0): +```typescript +// UseFormRegister and FieldError types used in FieldRenderer and PasswordField +import type { UseFormRegister, FieldError } from 'react-hook-form'; +``` + + + + + + + Task 1: Create BackendCard and PasswordField UI atoms + src/components/ui/BackendCard.tsx, src/components/ui/PasswordField.tsx + + Create `src/components/ui/` directory and the two simplest atom components. + + **src/components/ui/BackendCard.tsx** + A clickable card for the backend selection grid. Props: `name` (display string), `description` (short subtitle), `onClick` (void callback), `selected` (boolean for visual highlight). + + ```tsx + import type { ButtonHTMLAttributes } from 'react'; + + interface BackendCardProps extends Omit, 'onClick'> { + name: string; + description: string; + selected?: boolean; + onClick: () => void; + } + + export function BackendCard({ name, description, selected = false, onClick, ...rest }: BackendCardProps) { + return ( + + ); + } + ``` + + **src/components/ui/PasswordField.tsx** + Password input with a show/hide eye toggle. Per locked decision: all password-type fields (access key, SAS URL, S3 secret) have a show/hide toggle. Per project instructions: each field has its own independent visibility state. + + Props: `id` (string), `label` (string), `error` (FieldError | undefined), `registration` (return value of react-hook-form `register()`), `placeholder` (optional). + + ```tsx + import { useState } from 'react'; + import type { FieldError, UseFormRegisterReturn } from 'react-hook-form'; + + interface PasswordFieldProps { + id: string; + label: string; + error?: FieldError; + registration: UseFormRegisterReturn; + placeholder?: string; + helpText?: string; + } + + export function PasswordField({ id, label, error, registration, placeholder, helpText }: PasswordFieldProps) { + const [show, setShow] = useState(false); + return ( +
+ +
+ + +
+ {helpText && !error &&

{helpText}

} + {error &&

{error.message}

} +
+ ); + } + ``` +
+ + npx vitest run --reporter=verbose 2>&1 | tail -20 + + BackendCard.tsx and PasswordField.tsx exist with correct exports. Vitest still reports RED (test stubs still fail — components exist but tests have expect.fail). +
+ + + Task 2: Create FieldRenderer and AzureAuthToggle + src/components/ui/FieldRenderer.tsx, src/components/wizard/AzureAuthToggle.tsx + + **src/components/ui/FieldRenderer.tsx** + Renders a single `FieldDef` from the BACKEND_REGISTRY as the appropriate input element. This is the registry-driven form engine — no hardcoded per-backend JSX is allowed anywhere else. + + The `provider` select field (s3 and s3-compatible backends) has only one option — hide it from the user and auto-register it with its default value. This follows the recommendation from 03-RESEARCH.md open question #2. + + Props: `field` (FieldDef), `register` (UseFormRegister), `error` (FieldError | undefined), `defaultValue` (string | undefined — used to auto-register hidden fields). + + ```tsx + import type { UseFormRegister, FieldError } from 'react-hook-form'; + import type { FieldDef } from '../../schemas/registry'; + import { PasswordField } from './PasswordField'; + + interface FieldRendererProps { + field: FieldDef; + register: UseFormRegister; + error?: FieldError; + } + + export function FieldRenderer({ field, register, error }: FieldRendererProps) { + // provider field: single-option select — hide from UI, auto-register with default value + if (field.key === 'provider' && field.options?.length === 1) { + return ( + + ); + } + + if (field.inputType === 'password') { + return ( + + ); + } + + if (field.inputType === 'select' && field.options) { + return ( +
+ + + {field.helpText && !error &&

{field.helpText}

} + {error &&

{error.message}

} +
+ ); + } + + // text (default) + return ( +
+ + + {field.helpText && !error &&

{field.helpText}

} + {error &&

{error.message}

} +
+ ); + } + ``` + + **src/components/wizard/AzureAuthToggle.tsx** + Azure-specific segmented control that toggles between "SAS URL" and "Access Key". Per locked decision: + - Default method: SAS URL + - Switching PRESERVES both field values (both are always registered in react-hook-form) + - Only the active field is displayed + - Both values flow into `remote.params` on form submit (generator strips inactive one) + + Props: `register` (UseFormRegister), `errors` (object with key and sas_url FieldError entries). + + ```tsx + import { useState } from 'react'; + import type { UseFormRegister, FieldError } from 'react-hook-form'; + import { PasswordField } from '../ui/PasswordField'; + + type AuthMethod = 'sas' | 'key'; + + interface AzureAuthToggleProps { + register: UseFormRegister; + errors: { + key?: FieldError; + sas_url?: FieldError; + }; + } + + export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) { + const [authMethod, setAuthMethod] = useState('sas'); + + return ( +
+ {/* Segmented control */} +
+ + +
+ + {/* Both fields always registered — only active one visible */} +
+ +
+
+ +
+
+ ); + } + ``` + + CRITICAL: Using CSS `hidden` class (not conditional rendering) ensures both fields are registered with react-hook-form and their values are preserved when toggling. Do not use `{authMethod === 'sas' && }` — that unmounts the hidden field and react-hook-form loses its value. +
+ + npx vitest run --reporter=verbose 2>&1 | tail -20 + + + FieldRenderer.tsx and AzureAuthToggle.tsx exist with correct exports. All four atoms exist. Vitest still RED (stubs still have expect.fail — implementation tasks come in Plans 03 and 04). + +
+ +
+ + +Run after both tasks complete: +```bash +npx vitest run --reporter=verbose 2>&1 +``` +Vitest runs cleanly (no crashes). Tests remain RED from stubs. All four atom files exist with named exports. + +Check exports: +```bash +grep -n "^export" src/components/ui/BackendCard.tsx src/components/ui/PasswordField.tsx src/components/ui/FieldRenderer.tsx src/components/wizard/AzureAuthToggle.tsx +``` + + + +- BackendCard, PasswordField, FieldRenderer, AzureAuthToggle all export their named components +- FieldRenderer hides the `provider` field for single-option selects and auto-registers the default value +- AzureAuthToggle registers BOTH `key` and `sas_url` with react-hook-form regardless of active method (uses CSS show/hide, not conditional rendering) +- PasswordField has per-instance show/hide state (not a global toggle) +- No new Tailwind config file created (Tailwind v4 CSS-first — all styles use utility classes directly) + + + +After completion, create `.planning/phases/03-wizard-ui/03-02-SUMMARY.md` + diff --git a/.planning/phases/03-wizard-ui/03-03-PLAN.md b/.planning/phases/03-wizard-ui/03-03-PLAN.md new file mode 100644 index 0000000..9941b0b --- /dev/null +++ b/.planning/phases/03-wizard-ui/03-03-PLAN.md @@ -0,0 +1,210 @@ +--- +phase: 03-wizard-ui +plan: "03" +type: execute +wave: 3 +depends_on: + - "03-01" + - "03-02" +files_modified: + - src/components/wizard/BackendSelectionStep.tsx +autonomous: true +requirements: + - WIZD-01 + - WIZD-04 + +must_haves: + truths: + - "BackendSelectionStep renders Azure Blob, Amazon S3, and S3-Compatible backend cards in that order" + - "Clicking a backend card dispatches SET_BACKEND_TYPE + SET_REMOTE_PARAMS({}) + SET_STEP(1) — no Next button needed" + - "Remote name field is rendered at the top of the step before the backend cards" + - "Remote name field validates alphanumeric/dash/underscore only — inline error shown only after first Next attempt" + - "Entering an invalid remote name and clicking a card does not advance (validation fires first)" + artifacts: + - path: "src/components/wizard/BackendSelectionStep.tsx" + provides: "Step 0 — remote name + backend card grid" + exports: ["BackendSelectionStep"] + key_links: + - from: "src/components/wizard/BackendSelectionStep.tsx" + to: "src/store/context.tsx" + via: "useWizard() for state and dispatch" + pattern: "useWizard" + - from: "src/components/wizard/BackendSelectionStep.tsx" + to: "src/components/ui/BackendCard.tsx" + via: "renders three BackendCard instances" + pattern: "BackendCard" + - from: "src/components/wizard/BackendSelectionStep.tsx" + to: "src/store/types.ts" + via: "dispatches SET_BACKEND_TYPE, SET_REMOTE_PARAMS, SET_STEP" + pattern: "dispatch.*SET_BACKEND_TYPE|SET_REMOTE_PARAMS|SET_STEP" +--- + + +Implement BackendSelectionStep — the first wizard step where the user sets a remote name and selects a storage backend. + +Purpose: Satisfies WIZD-01 (popularity-sorted card grid) and WIZD-04 (remote name validation). +Output: src/components/wizard/BackendSelectionStep.tsx, BackendSelectionStep tests GREEN. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-wizard-ui/03-CONTEXT.md +@.planning/phases/03-wizard-ui/03-RESEARCH.md +@.planning/phases/03-wizard-ui/03-01-SUMMARY.md +@.planning/phases/03-wizard-ui/03-02-SUMMARY.md + + + + +From src/store/types.ts: +```typescript +export type BackendType = 'azureblob' | 's3' | 's3-compatible'; +export type WizardAction = + | { type: 'SET_STEP'; payload: number } + | { type: 'SET_BACKEND_TYPE'; payload: BackendType } + | { type: 'SET_REMOTE_NAME'; payload: string } + | { type: 'SET_REMOTE_PARAMS'; payload: Record } + | { type: 'SET_DEPLOYMENT'; payload: Partial } + | { type: 'RESET' }; + +// INITIAL_STATE.remote.name = '' +// INITIAL_STATE.remote.backendType = null +``` + +From src/store/context.tsx: +```typescript +export function useWizard(): { state: WizardState; dispatch: React.Dispatch } +``` + +From src/components/ui/BackendCard.tsx (created in Plan 02): +```typescript +export function BackendCard(props: { + name: string; + description: string; + selected?: boolean; + onClick: () => void; +}): JSX.Element +``` + +Remote name validation regex (from RESEARCH.md): +```typescript +const remoteNameSchema = z.object({ + name: z.string() + .min(1, 'Remote name is required') + .regex(/^[a-zA-Z0-9_-]+$/, 'Only letters, numbers, dashes, and underscores allowed'), +}); +``` + +react-hook-form mode pattern (LOCKED — from RESEARCH.md): +```typescript +const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(remoteNameSchema), + mode: 'onSubmit', // no errors on initial fill + reValidateMode: 'onChange', // live errors once submitted once + defaultValues: { name: state.remote.name }, +}); +``` + + + + + + + Task 1: Implement BackendSelectionStep and make its tests GREEN + src/components/wizard/BackendSelectionStep.tsx + + - WIZD-01: Renders Azure Blob, Amazon S3, S3-Compatible cards in that exact DOM order + - WIZD-01: Azure Blob card appears before S3 card in the DOM (popularity-sorted) + - WIZD-01: Clicking a card dispatches SET_BACKEND_TYPE with the correct BackendType value + - WIZD-01: Clicking a card also dispatches SET_REMOTE_PARAMS({}) to clear stale params (CRITICAL — reducer does not auto-clear) + - WIZD-01: Clicking a card dispatches SET_STEP(1) to advance after dispatching backend type + - WIZD-04: Remote name input is rendered before the backend cards in DOM order + - WIZD-04: No error message shown before the user has attempted to submit (mode: 'onSubmit') + - WIZD-04: After submit attempt with empty name, "Remote name is required" error shows below the input + - WIZD-04: After submit attempt with "my remote!", "Only letters, numbers, dashes, and underscores allowed" error shows + - WIZD-04: "my-remote_01" passes validation (alphanumeric, dashes, underscores allowed) + - WIZD-04: Clicking a card validates the name first — if invalid, error shows but navigation does NOT proceed + + + Create `src/components/wizard/BackendSelectionStep.tsx`. + + The component uses react-hook-form for the remote name field with `mode: 'onSubmit'` and `reValidateMode: 'onChange'`. The backend cards trigger `handleSubmit` internally — clicking a card submits the form, and if validation passes, it dispatches the three actions (SET_REMOTE_NAME → SET_BACKEND_TYPE → SET_REMOTE_PARAMS → SET_STEP). + + Backend list (hardcoded display order — WIZD-01 popularity sort): + 1. azureblob → "Azure Blob Storage" / "Microsoft Azure cloud storage" + 2. s3 → "Amazon S3" / "AWS Simple Storage Service" + 3. s3-compatible → "S3-Compatible" / "Wasabi, MinIO, Cloudflare R2, and others" + + Implementation approach: + - `useForm` with zodResolver for name validation; `mode: 'onSubmit'`, `reValidateMode: 'onChange'` + - `defaultValues: { name: state.remote.name }` to restore previously entered name on back-nav + - Each BackendCard's `onClick` calls a handler that sets a `pendingBackend` ref, then calls `handleSubmit(onValidSubmit)()` + - `onValidSubmit` receives validated values, dispatches: `SET_REMOTE_NAME` (with validated name), `SET_BACKEND_TYPE` (with pendingBackend), `SET_REMOTE_PARAMS({})` (clear stale params), `SET_STEP(1)` + - CRITICAL dispatch order: SET_BACKEND_TYPE before SET_REMOTE_PARAMS({}) — SET_REMOTE_PARAMS clears old backend's params, SET_BACKEND_TYPE sets the new type. The reducer handles each action independently so order matters for clarity, not correctness. + - ANTI-PATTERN WARNING: Do NOT dispatch RESET — that wipes deployment options. Use SET_REMOTE_PARAMS({}) only. + + Render structure: + ``` +
(outermost wrapper) +

Step 1: Select Backend

+
+ Remote name section (at top — WIZD-04): +
+ ``` + + Now update the test stub at `src/components/wizard/BackendSelectionStep.test.tsx` to make all tests GREEN. Tests must wrap the component in `WizardProvider` and use `@testing-library/react` render + screen + fireEvent. Import `WizardProvider` from `../../store/context`. + + Test patterns: + - Render: `render()` + - WIZD-01 card order: `const cards = screen.getAllByRole('button', { name: /Azure|Amazon|S3-Compatible/ })` — check order by textContent + - WIZD-01 click dispatches: Mock or spy on dispatch is complex with context. Instead verify navigation by checking that after clicking a card with a valid name pre-set (set state.remote.name via initial state), the appropriate DOM change happens. Alternatively, use a custom WizardProvider wrapper with observable dispatch for testing. + - WIZD-04 errors: `fireEvent.click(getByRole('button', { name: /Azure/i }))` with empty name → `await screen.findByRole('alert')` or `screen.getByText(/required/i)` +
+ + npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose 2>&1 + + + All BackendSelectionStep tests pass GREEN. WIZD-01 and WIZD-04 test cases are all green. BackendSelectionStep.tsx exports the component. The component correctly dispatches the three required actions on card click with valid name. + +
+ +
+ + +```bash +npx vitest run src/components/wizard/BackendSelectionStep.test.tsx --reporter=verbose 2>&1 +``` +All WIZD-01 and WIZD-04 tests GREEN. Then run full suite: +```bash +npx vitest run 2>&1 | tail -10 +``` +Other test stubs remain RED (expected). BackendSelectionStep tests all GREEN. + + + +- BackendSelectionStep.tsx exists with named export +- All WIZD-01 tests green: three cards render, Azure first, click dispatches correct actions +- All WIZD-04 tests green: name field at top, no errors before submit, inline errors after failed submit, valid names pass +- Clicking a card with invalid name shows validation error and does NOT navigate to step 1 +- Component uses `mode: 'onSubmit', reValidateMode: 'onChange'` (locked UX decision) +- Dispatches SET_REMOTE_PARAMS({}) on backend selection (clears stale params — critical for correctness) +- Does NOT dispatch RESET (deployment options must be preserved) + + + +After completion, create `.planning/phases/03-wizard-ui/03-03-SUMMARY.md` + diff --git a/.planning/phases/03-wizard-ui/03-04-PLAN.md b/.planning/phases/03-wizard-ui/03-04-PLAN.md new file mode 100644 index 0000000..f8cbe24 --- /dev/null +++ b/.planning/phases/03-wizard-ui/03-04-PLAN.md @@ -0,0 +1,299 @@ +--- +phase: 03-wizard-ui +plan: "04" +type: execute +wave: 3 +depends_on: + - "03-01" + - "03-02" +files_modified: + - src/components/wizard/RemoteConfigStep.tsx +autonomous: true +requirements: + - BACK-01 + - BACK-02 + - BACK-03 + +must_haves: + truths: + - "RemoteConfigStep renders the correct fields for azureblob backend using BACKEND_REGISTRY iteration" + - "RemoteConfigStep renders the correct fields for s3 backend using BACKEND_REGISTRY iteration" + - "RemoteConfigStep renders the correct fields for s3-compatible backend including an endpoint field" + - "AzureAuthToggle appears for azureblob backend — SAS URL shown by default, toggle switches to Access Key" + - "Toggling Azure auth method does not clear the hidden field value (both registered)" + - "Errors only show after first Next attempt, then update live (mode: onSubmit, reValidateMode: onChange)" + - "RemoteConfigStep component is keyed on backendType to force remount on backend change" + artifacts: + - path: "src/components/wizard/RemoteConfigStep.tsx" + provides: "Step 1 — registry-driven backend configuration form" + exports: ["RemoteConfigStep"] + key_links: + - from: "src/components/wizard/RemoteConfigStep.tsx" + to: "src/schemas/registry.ts" + via: "BACKEND_REGISTRY[backendType] drives field rendering loop" + pattern: "BACKEND_REGISTRY" + - from: "src/components/wizard/RemoteConfigStep.tsx" + to: "src/schemas/index.ts" + via: "BACKEND_SCHEMAS[backendType] provides Zod resolver" + pattern: "BACKEND_SCHEMAS" + - from: "src/components/wizard/RemoteConfigStep.tsx" + to: "src/store/context.tsx" + via: "dispatches SET_REMOTE_PARAMS on Next, SET_STEP(2)" + pattern: "dispatch.*SET_REMOTE_PARAMS|SET_STEP" +--- + + +Implement RemoteConfigStep — the second wizard step with a registry-driven form for configuring the selected backend. + +Purpose: Satisfies BACK-01 (Azure Blob config with auth toggle), BACK-02 (S3 config), BACK-03 (S3-compatible with endpoint). +Output: src/components/wizard/RemoteConfigStep.tsx, RemoteConfigStep tests GREEN. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-wizard-ui/03-CONTEXT.md +@.planning/phases/03-wizard-ui/03-RESEARCH.md +@.planning/phases/03-wizard-ui/03-01-SUMMARY.md +@.planning/phases/03-wizard-ui/03-02-SUMMARY.md + + + + +From src/schemas/registry.ts: +```typescript +export type BackendType = 'azureblob' | 's3' | 's3-compatible'; +export const BACKEND_REGISTRY: Record +// azureblob fields: account (text, required), key (password, optional), sas_url (password, optional) +// s3 fields: provider (select/hidden), access_key_id (text), secret_access_key (password), region (text) +// s3-compatible fields: provider (select/hidden), access_key_id, secret_access_key, endpoint (text, required), region (text, optional) +``` + +From src/schemas/index.ts: +```typescript +export const BACKEND_SCHEMAS = { + azureblob: ZodObject, + s3: ZodObject, + 's3-compatible': ZodObject, +} as const; +// All schemas built from BACKEND_REGISTRY — key/sas_url are optional (z.string().optional()) +``` + +From src/store/types.ts: +```typescript +// On Next: dispatch SET_REMOTE_PARAMS with all form values +// Then: dispatch SET_STEP(2) +dispatch({ type: 'SET_REMOTE_PARAMS', payload: values }); +dispatch({ type: 'SET_STEP', payload: 2 }); +``` + +From src/components/ui/FieldRenderer.tsx (Plan 02): +```typescript +export function FieldRenderer(props: { + field: FieldDef; + register: UseFormRegister; + error?: FieldError; +}): JSX.Element +// Handles text, password, select, hidden (provider single-option) +``` + +From src/components/wizard/AzureAuthToggle.tsx (Plan 02): +```typescript +export function AzureAuthToggle(props: { + register: UseFormRegister; + errors: { key?: FieldError; sas_url?: FieldError }; +}): JSX.Element +// Renders segmented SAS/Key toggle with both fields always registered +``` + +react-hook-form registry-driven pattern (from RESEARCH.md): +```tsx +const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit', + reValidateMode: 'onChange', + defaultValues: state.remote.params, +}); +const onNext = (values: Record) => { + dispatch({ type: 'SET_REMOTE_PARAMS', payload: values }); + dispatch({ type: 'SET_STEP', payload: 2 }); +}; +``` + +Remount on backend change (from RESEARCH.md Pitfall 1): +```tsx +// In App.tsx (or wherever RemoteConfigStep is rendered): + +// This forces full remount when backend changes — prevents stale defaultValues +``` + + + + + + + Task 1: Implement RemoteConfigStep and make its tests GREEN + src/components/wizard/RemoteConfigStep.tsx + + - BACK-01: When backendType is 'azureblob', renders the 'account' text input + - BACK-01: When backendType is 'azureblob', renders AzureAuthToggle (not separate key/sas_url FieldRenderer calls) + - BACK-01: Azure form submits with both sas_url and key values in params (even if one is empty string) + - BACK-01: Toggling auth method in AzureAuthToggle does not remove the hidden field from form state + - BACK-02: When backendType is 's3', renders access_key_id, secret_access_key, and region inputs + - BACK-02: S3 provider field is hidden (auto-registered with value 'AWS') — no visible dropdown + - BACK-03: When backendType is 's3-compatible', renders endpoint field in addition to access_key_id, secret_access_key + - BACK-03: S3-compatible provider field is hidden (auto-registered with value 'Other') + - All backends: Errors do not show before first Next attempt (mode: 'onSubmit') + - All backends: Errors show after first failed Next attempt and update live (reValidateMode: 'onChange') + - All backends: Clicking Next with valid data dispatches SET_REMOTE_PARAMS then SET_STEP(2) + + + Create `src/components/wizard/RemoteConfigStep.tsx`. + + The component reads `state.remote.backendType` and `state.remote.params` from `useWizard()`. It must NOT render if `backendType` is null (user somehow reached step 1 without selecting — guard with early return or redirect to step 0). + + For the Azure backend, the field rendering is SPECIAL — do not loop `BACKEND_REGISTRY['azureblob']` naively: + - Render `account` field via `FieldRenderer` + - Render `AzureAuthToggle` for `key` / `sas_url` (handles both fields internally) + - Do NOT pass key/sas_url through the generic FieldRenderer loop for azureblob + + For S3 and S3-compatible, loop all `BACKEND_REGISTRY[backendType]` fields through `FieldRenderer`. The `provider` field with a single option is auto-hidden by `FieldRenderer` already. + + Implementation structure: + ```tsx + export function RemoteConfigStep() { + const { state, dispatch } = useWizard(); + const backendType = state.remote.backendType; + + // Guard — should never happen but prevents runtime errors + if (!backendType) { + dispatch({ type: 'SET_STEP', payload: 0 }); + return null; + } + + const schema = BACKEND_SCHEMAS[backendType]; + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit', + reValidateMode: 'onChange', + defaultValues: state.remote.params, + }); + + const onNext = (values: Record) => { + dispatch({ type: 'SET_REMOTE_PARAMS', payload: values }); + dispatch({ type: 'SET_STEP', payload: 2 }); + }; + + const backendLabel = { + azureblob: 'Azure Blob Storage', + s3: 'Amazon S3', + 's3-compatible': 'S3-Compatible Storage', + }[backendType]; + + return ( +
+

Step 2: Configure {backendLabel}

+
+ {backendType === 'azureblob' ? ( + <> + {/* Account field via FieldRenderer */} + f.key === 'account')!} + register={register} + error={errors.account} + /> + {/* Auth toggle handles key + sas_url — both always registered */} + + + ) : ( + /* S3 and S3-compatible: full registry loop — FieldRenderer handles provider hiding */ + BACKEND_REGISTRY[backendType].map(field => ( + + )) + )} +
+ + +
+ +
+ ); + } + ``` + + Now update `src/components/wizard/RemoteConfigStep.test.tsx` to make all tests GREEN. Tests wrap component in `WizardProvider`. To set the backendType before rendering, create a helper that renders with a specific initial step/backend — dispatch actions in a test wrapper component, or initialize a custom context. + + Practical test approach: Create a `TestWrapper` helper inside the test file that wraps with `WizardProvider` and dispatches the desired initial state before rendering `RemoteConfigStep`: + ```tsx + function renderWithBackend(backendType: BackendType) { + function Setup() { + const { dispatch } = useWizard(); + useEffect(() => { + dispatch({ type: 'SET_BACKEND_TYPE', payload: backendType }); + dispatch({ type: 'SET_STEP', payload: 1 }); + }, []); + return ; + } + return render(); + } + ``` +
+ + npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose 2>&1 + + + All RemoteConfigStep tests pass GREEN. BACK-01 (Azure form with toggle), BACK-02 (S3 fields), BACK-03 (S3-compatible with endpoint) all green. RemoteConfigStep.tsx exports the component correctly. + +
+ +
+ + +```bash +npx vitest run src/components/wizard/RemoteConfigStep.test.tsx --reporter=verbose 2>&1 +``` +All BACK-01, BACK-02, BACK-03 tests GREEN. + +Full suite check: +```bash +npx vitest run 2>&1 | tail -10 +``` +RemoteConfigStep tests GREEN. BackendSelectionStep tests GREEN (from Plan 03). App and StepIndicator stubs still RED (expected — Plan 05). + + + +- RemoteConfigStep.tsx exists with named export +- Azure Blob form: account field + AzureAuthToggle renders (both key and sas_url registered, only one visible) +- S3 form: access_key_id, secret_access_key, region fields render; provider field is hidden +- S3-compatible form: same as S3 plus endpoint field renders; provider field is hidden +- Touch-then-live validation: no errors on initial render, errors after first failed submit +- On valid submit: dispatches SET_REMOTE_PARAMS with all field values (including hidden auth field), then SET_STEP(2) +- All BACK-01, BACK-02, BACK-03 tests GREEN + + + +After completion, create `.planning/phases/03-wizard-ui/03-04-SUMMARY.md` + diff --git a/.planning/phases/03-wizard-ui/03-05-PLAN.md b/.planning/phases/03-wizard-ui/03-05-PLAN.md new file mode 100644 index 0000000..7507c01 --- /dev/null +++ b/.planning/phases/03-wizard-ui/03-05-PLAN.md @@ -0,0 +1,321 @@ +--- +phase: 03-wizard-ui +plan: "05" +type: execute +wave: 4 +depends_on: + - "03-03" + - "03-04" +files_modified: + - src/components/wizard/DeploymentStep.tsx + - src/components/wizard/StepIndicator.tsx + - src/App.tsx +autonomous: false +requirements: + - WIZD-02 + - WIZD-03 + +must_haves: + truths: + - "App.tsx renders BackendSelectionStep for currentStep 0, RemoteConfigStep for step 1, DeploymentStep for step 2" + - "StepIndicator shows 3 labeled steps with checkmarks on completed steps" + - "Clicking a completed step in StepIndicator dispatches SET_STEP — clicking step 0 also dispatches SET_REMOTE_PARAMS({})" + - "DeploymentStep renders includeInstall toggle, configPath radio group, and scriptTargets checkboxes" + - "DeploymentStep dispatches SET_DEPLOYMENT on each change, keeping deployment state in sync" + - "Full forward + backward wizard navigation works without losing entered data in any step" + artifacts: + - path: "src/components/wizard/DeploymentStep.tsx" + provides: "Step 2 — deployment options (includeInstall, configPath, scriptTargets)" + exports: ["DeploymentStep"] + - path: "src/components/wizard/StepIndicator.tsx" + provides: "Breadcrumb navigation — 1.Backend > 2.Remote Config > 3.Deployment" + exports: ["StepIndicator"] + - path: "src/App.tsx" + provides: "Step router — renders correct step component by currentStep" + key_links: + - from: "src/App.tsx" + to: "src/components/wizard/BackendSelectionStep.tsx" + via: "STEPS[0] — renders when currentStep === 0" + pattern: "BackendSelectionStep" + - from: "src/App.tsx" + to: "src/components/wizard/RemoteConfigStep.tsx" + via: "STEPS[1] — renders when currentStep === 1, keyed on backendType" + pattern: "RemoteConfigStep" + - from: "src/components/wizard/StepIndicator.tsx" + to: "src/store/context.tsx" + via: "dispatches SET_STEP and conditionally SET_REMOTE_PARAMS on back-nav to step 0" + pattern: "dispatch.*SET_STEP|SET_REMOTE_PARAMS" + - from: "src/components/wizard/DeploymentStep.tsx" + to: "src/store/context.tsx" + via: "dispatches SET_DEPLOYMENT on every control change" + pattern: "dispatch.*SET_DEPLOYMENT" +--- + + +Wire the complete wizard: implement DeploymentStep and StepIndicator, then update App.tsx to route between all three steps and render the breadcrumb navigation. + +Purpose: Satisfies WIZD-02 (multi-step navigation shell) and WIZD-03 (back navigation without data loss). Closes the full wizard loop. +Output: DeploymentStep, StepIndicator, App.tsx wired — all tests GREEN. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/phases/03-wizard-ui/03-CONTEXT.md +@.planning/phases/03-wizard-ui/03-RESEARCH.md +@.planning/phases/03-wizard-ui/03-03-SUMMARY.md +@.planning/phases/03-wizard-ui/03-04-SUMMARY.md + + + + +From src/store/types.ts: +```typescript +export interface WizardState { + currentStep: number; // 0=Backend, 1=RemoteConfig, 2=Deployment + remote: { + name: string; + backendType: BackendType | null; + params: Record; + }; + deployment: { + includeInstall: boolean; // toggle — default: false + configPath: 'machine-wide' | 'user-profile'; // radio — default: 'machine-wide' + scriptTargets: ('intune' | 'rmm')[]; // checkboxes — default: both selected + }; +} + +// Deploy step actions: +dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: true } }); +dispatch({ type: 'SET_DEPLOYMENT', payload: { configPath: 'user-profile' } }); +dispatch({ type: 'SET_DEPLOYMENT', payload: { scriptTargets: ['intune'] } }); + +// Back-nav to step 0 (CRITICAL: clears params but NOT deployment): +dispatch({ type: 'SET_REMOTE_PARAMS', payload: {} }); +dispatch({ type: 'SET_STEP', payload: 0 }); +``` + +From src/store/context.tsx: +```typescript +export function useWizard(): { state: WizardState; dispatch: React.Dispatch } +``` + +Step routing pattern (from RESEARCH.md): +```tsx +// App.tsx step routing +import { BackendSelectionStep } from './components/wizard/BackendSelectionStep'; +import { RemoteConfigStep } from './components/wizard/RemoteConfigStep'; +import { DeploymentStep } from './components/wizard/DeploymentStep'; +import { StepIndicator } from './components/wizard/StepIndicator'; + +// Key RemoteConfigStep on backendType to force remount on backend change (Pitfall 1 fix) +const STEPS = [ + () => , + () => , + () => , +]; +``` + +StepIndicator back-nav pattern (from RESEARCH.md): +```tsx +const handleStepClick = (targetStep: number) => { + if (targetStep === 0 && state.currentStep > 0) { + dispatch({ type: 'SET_REMOTE_PARAMS', payload: {} }); // clear params (backend may change) + // Do NOT dispatch RESET — deployment options must be preserved + } + dispatch({ type: 'SET_STEP', payload: targetStep }); +}; +``` + + + + + + + Task 1: Implement DeploymentStep and StepIndicator + src/components/wizard/DeploymentStep.tsx, src/components/wizard/StepIndicator.tsx + + DeploymentStep: + - Renders an "Include rclone installation" toggle (checkbox or switch); state.deployment.includeInstall default false + - Renders a "Config deployment path" radio group: 'machine-wide' (C:\ProgramData\rclone\) and 'user-profile' (%APPDATA%\rclone\); default machine-wide + - Renders a "Script targets" checkbox group: 'intune' and 'rmm' checkboxes; both checked by default + - Each control dispatches SET_DEPLOYMENT immediately on change (live sync, no Next button needed for deployment options) + - Renders a "Back" button that dispatches SET_STEP(1) and a "Next / Review" button that dispatches SET_STEP(3) to advance to Phase 4's review step + + StepIndicator: + - Renders three labeled steps: "1. Backend", "2. Remote Config", "3. Deployment" + - Current step is bold/active + - Completed steps (step index < currentStep) show a checkmark prefix and are clickable buttons + - Future steps are not clickable (non-interactive) + - Clicking a completed step that is NOT step 0 dispatches only SET_STEP(targetStep) + - Clicking step 0 dispatches SET_REMOTE_PARAMS({}) THEN SET_STEP(0) — clears params for potential backend change + - Does NOT dispatch RESET on any click + + + Create `src/components/wizard/DeploymentStep.tsx`: + - Uses `useWizard()` to read `state.deployment` and `dispatch` + - Does NOT use react-hook-form — deployment fields dispatch directly via onChange handlers + - `includeInstall` toggle: ` dispatch({ type: 'SET_DEPLOYMENT', payload: { includeInstall: e.target.checked } })} />` + - `configPath` radios: two `` elements for 'machine-wide' and 'user-profile' + - `scriptTargets` checkboxes: two `` for 'intune' and 'rmm' — on change, compute new array and dispatch SET_DEPLOYMENT + - Back button: `dispatch({ type: 'SET_STEP', payload: 1 })` + - Next button: `dispatch({ type: 'SET_STEP', payload: 3 })` — step 3 is Phase 4's review/download (placeholder for now) + + Create `src/components/wizard/StepIndicator.tsx`: + - Uses `useWizard()` to read `state.currentStep` + - Renders a horizontal breadcrumb with separators (›) + - Step labels: `['Backend', 'Remote Config', 'Deployment']` + - For each step index i: + - If i < currentStep: completed — render as `