docs(04-review-download-security): create phase 4 plan

5 plans across 5 waves: Wave 0 TDD stubs, utilities + OutputBlock,
ReviewStep implementation, App.tsx wiring, human verify checkpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 10:23:14 +01:00
co-authored by Claude Sonnet 4.6
parent 8d5f7090e2
commit 405bf477b4
6 changed files with 976 additions and 2 deletions
+9 -2
View File
@@ -76,7 +76,14 @@ Plans:
3. The app prominently displays that no data is sent to any server, and wizard state is never written to localStorage or sessionStorage 3. The app prominently displays that no data is sent to any server, and wizard state is never written to localStorage or sessionStorage
4. User can download each artifact individually (rclone.conf, Intune install script, Intune detection script, RMM script) or all together as a ZIP bundle 4. User can download each artifact individually (rclone.conf, Intune install script, Intune detection script, RMM script) or all together as a ZIP bundle
5. User can copy any output block to clipboard without downloading 5. User can copy any output block to clipboard without downloading
**Plans**: TBD **Plans**: 5 plans
Plans:
- [ ] 04-01-PLAN.md — Wave 0 TDD stubs: ReviewStep.test.tsx (all requirement stubs RED) + SECU-03 assertion in reducer.test.ts
- [ ] 04-02-PLAN.md — Utilities and OutputBlock: downloadFile.ts, downloadZip.ts (install jszip), OutputBlock.tsx component
- [ ] 04-03-PLAN.md — Implement ReviewStep.tsx: live preview, security gate, all downloads and clipboard — all stubs turn GREEN
- [ ] 04-04-PLAN.md — Wire ReviewStep into App.tsx (step 3) and add 'Review' to StepIndicator STEP_LABELS
- [ ] 04-05-PLAN.md — Human verification: full 4-step wizard end-to-end, all downloads, ZIP, clipboard, SECU-03 DevTools check
## Progress ## Progress
@@ -88,4 +95,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| 1. Foundation | 4/4 | Complete | 2026-03-26 | | 1. Foundation | 4/4 | Complete | 2026-03-26 |
| 2. Generators | 4/4 | Complete | 2026-03-26 | | 2. Generators | 4/4 | Complete | 2026-03-26 |
| 3. Wizard UI | 5/5 | Complete | 2026-03-27 | | 3. Wizard UI | 5/5 | Complete | 2026-03-27 |
| 4. Review, Download & Security | 0/? | Not started | - | | 4. Review, Download & Security | 0/5 | Not started | - |
@@ -0,0 +1,207 @@
---
phase: 04-review-download-security
plan: 01
type: tdd
wave: 1
depends_on: []
files_modified:
- src/components/wizard/ReviewStep.test.tsx
- src/store/reducer.test.ts
autonomous: true
requirements: [CONF-02, CONF-03, DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, SECU-01, SECU-02, SECU-03]
must_haves:
truths:
- "All ReviewStep requirement behaviors have named, failing test stubs (RED)"
- "SECU-03 is verified by a spy on Storage.prototype.setItem in reducer.test.ts"
- "Mock setup for URL.createObjectURL, URL.revokeObjectURL, navigator.clipboard.writeText exists in test file"
- "npm test runs without crashes (passWithNoTests: true covers missing implementation)"
artifacts:
- path: "src/components/wizard/ReviewStep.test.tsx"
provides: "RED test stubs for CONF-02, CONF-03, DOWN-01DOWN-06, SECU-01, SECU-02"
exports: []
- path: "src/store/reducer.test.ts"
provides: "SECU-03 assertion added to existing reducer test suite"
contains: "spyOn(Storage.prototype, 'setItem')"
key_links:
- from: "src/components/wizard/ReviewStep.test.tsx"
to: "src/components/wizard/ReviewStep"
via: "import ReviewStep"
pattern: "import.*ReviewStep"
- from: "src/components/wizard/ReviewStep.test.tsx"
to: "src/utils/downloadFile"
via: "import downloadFile (mocked)"
pattern: "vi.mock.*downloadFile"
---
<objective>
Write Wave 0 TDD stubs: all failing test cases for Phase 4 requirements, plus SECU-03 assertion in existing reducer tests.
Purpose: Establish the RED baseline before any implementation. Tests define the contract that implementation plans must satisfy. Following the established project pattern from Phases 13.
Output: ReviewStep.test.tsx (stub file, RED tests for all 11 req IDs) + SECU-03 assertion in reducer.test.ts.
</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/04-review-download-security/04-RESEARCH.md
@.planning/phases/04-review-download-security/04-VALIDATION.md
@src/store/types.ts
@src/store/context.tsx
@src/store/reducer.test.ts
@src/generators/index.ts
</context>
<interfaces>
<!-- Key contracts the executor needs — extracted from codebase. No exploration needed. -->
From src/store/types.ts:
```typescript
export interface WizardState {
currentStep: number;
remote: {
name: string;
backendType: BackendType | null;
params: Record<string, string>;
};
deployment: {
includeInstall: boolean;
configPath: 'machine-wide' | 'user-profile';
scriptTargets: ('intune' | 'rmm')[];
};
}
```
From src/store/context.tsx:
```typescript
export function useWizard(): { state: WizardState; dispatch: React.Dispatch<WizardAction> }
export function WizardProvider({ children }: { children: React.ReactNode }): JSX.Element
```
From src/generators/index.ts:
```typescript
export { buildRcloneConf } from './rclone-conf';
export { buildIntuneInstall } from './intune-install';
export { buildIntuneDetection } from './intune-detection';
export { buildRmmScript } from './rmm-script';
// All accept (state: WizardState): string
// buildRcloneConf throws when backendType is null or name is empty
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Write ReviewStep.test.tsx with RED stubs for all Phase 4 requirements</name>
<files>src/components/wizard/ReviewStep.test.tsx</files>
<behavior>
- CONF-02: rclone.conf preview text is present in rendered output when state has a valid backendType
- CONF-03: clicking "Copy" on the rclone.conf block calls navigator.clipboard.writeText with the conf content
- DOWN-01: clicking "Download rclone.conf" calls downloadFile with content and filename 'rclone.conf'
- DOWN-02: clicking "Download Intune Install" calls downloadFile with content and filename 'intune-install.ps1'
- DOWN-03: clicking "Download Intune Detection" calls downloadFile with content and filename 'intune-detection.ps1'
- DOWN-04: clicking "Download RMM script" calls downloadFile with content and filename 'rmm-script.ps1'
- DOWN-05: clicking "Download ZIP" calls downloadZip with all 4 file entries
- DOWN-06: each output block has a copy button that calls clipboard.writeText with that block's content
- SECU-01: download buttons are disabled when security checkbox is unchecked; enabled after checking
- SECU-02: rendered output contains text about no data being sent to a server
</behavior>
<action>
Create src/components/wizard/ReviewStep.test.tsx. Follow the established project pattern:
- Use `expect.fail('not yet implemented')` for each stub (named RED failure, not import-error RED)
- Import `{ describe, it, expect, vi, beforeEach }` from 'vitest'
- Import `{ render, screen, fireEvent }` from '@testing-library/react'
- Import `ReviewStep` from './ReviewStep' (will not exist yet — that is fine; tests will fail at import or at stub)
- Import `* as downloadFileModule` from '../../utils/downloadFile' for mocking
- Import `* as downloadZipModule` from '../../utils/downloadZip' for mocking
File-level mock setup (before describe block):
```typescript
vi.mock('../../utils/downloadFile', () => ({ downloadFile: vi.fn() }));
vi.mock('../../utils/downloadZip', () => ({ downloadZip: vi.fn() }));
```
In a beforeEach, stub global APIs:
```typescript
beforeEach(() => {
vi.stubGlobal('navigator', {
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
});
vi.stubGlobal('URL', {
createObjectURL: vi.fn(() => 'blob:mock'),
revokeObjectURL: vi.fn(),
});
vi.clearAllMocks();
});
```
Helper: create a WizardProvider wrapper with a fully populated state (azureblob backend, name='my-remote', params={account:'acct',key:'k'}, deployment defaults) for rendering ReviewStep in tests.
Write one `describe` block per requirement ID (embed ID in describe name for traceability, e.g. `describe('CONF-02: live rclone.conf preview', ...)`). Each describe contains an `it` that calls `expect.fail('not yet implemented')`.
Do NOT write real assertions yet — the whole file should be stubs that fail with 'not yet implemented' (except the import/mock wiring which must work).
Run: `npm test -- src/components/wizard/ReviewStep.test.tsx` — expect FAIL (RED) because ReviewStep does not exist yet. That is the correct Wave 0 state.
</action>
<verify>
<automated>npm test -- src/components/wizard/ReviewStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>ReviewStep.test.tsx exists with named stubs for all 10 requirement behaviors (CONF-02, CONF-03, DOWN-01 through DOWN-06, SECU-01, SECU-02). npm test fails with import or stub errors for ReviewStep only — NOT with TypeScript compilation errors in the test file itself.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Add SECU-03 assertion to reducer.test.ts</name>
<files>src/store/reducer.test.ts</files>
<behavior>
- SECU-03: localStorage.setItem and sessionStorage.setItem are never called during any wizard reducer dispatch
</behavior>
<action>
Edit src/store/reducer.test.ts. Add a new `it` block at the end of the existing `describe('wizardReducer', ...)` block:
```typescript
it('SECU-03: never writes to localStorage or sessionStorage', () => {
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
// Dispatch every action type to cover all reducer branches
wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 1 });
wizardReducer(INITIAL_STATE, { type: 'SET_BACKEND_TYPE', payload: 'azureblob' });
wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_NAME', payload: 'test' });
wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_PARAMS', payload: { key: 'val' } });
wizardReducer(INITIAL_STATE, { type: 'SET_DEPLOYMENT', payload: { includeInstall: true } });
wizardReducer(INITIAL_STATE, { type: 'RESET' });
expect(setItemSpy).not.toHaveBeenCalled();
setItemSpy.mockRestore();
});
```
Also add `vi` to the import line: change `import { describe, it, expect }` to `import { describe, it, expect, vi }`.
Run: `npm test -- src/store/reducer.test.ts` — expect GREEN (reducer already does not touch storage).
</action>
<verify>
<automated>npm test -- src/store/reducer.test.ts 2>&1 | tail -10</automated>
</verify>
<done>reducer.test.ts has the SECU-03 test and it passes GREEN. All pre-existing reducer tests still pass.</done>
</task>
</tasks>
<verification>
After both tasks: `npm test` runs without TypeScript errors in reviewed files. reducer.test.ts is fully GREEN. ReviewStep.test.tsx fails with stub or module-not-found errors (RED — expected).
</verification>
<success_criteria>
- src/components/wizard/ReviewStep.test.tsx exists with 10 named describe/it blocks (one per requirement behavior)
- src/store/reducer.test.ts has SECU-03 assertion and passes fully GREEN
- No new TypeScript compilation errors anywhere in the test files
- Wave 0 baseline established: RED for ReviewStep, GREEN for SECU-03
</success_criteria>
<output>
After completion, create `.planning/phases/04-review-download-security/04-01-SUMMARY.md`
</output>
@@ -0,0 +1,243 @@
---
phase: 04-review-download-security
plan: 02
type: execute
wave: 2
depends_on: [04-01]
files_modified:
- src/utils/downloadFile.ts
- src/utils/downloadZip.ts
- src/components/wizard/OutputBlock.tsx
autonomous: true
requirements: [DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, CONF-03]
must_haves:
truths:
- "downloadFile(content, filename) creates a Blob and triggers an anchor click download"
- "downloadZip(files, zipName) builds a JSZip archive and triggers a download"
- "OutputBlock renders pre-formatted content, a copy button, and an optional download button"
- "OutputBlock copy button calls clipboard.writeText with block content"
artifacts:
- path: "src/utils/downloadFile.ts"
provides: "Pure download utility — Blob + createObjectURL + anchor click"
exports: ["downloadFile"]
- path: "src/utils/downloadZip.ts"
provides: "ZIP bundle download utility using JSZip"
exports: ["downloadZip"]
- path: "src/components/wizard/OutputBlock.tsx"
provides: "Reusable code block with copy and optional download buttons"
exports: ["OutputBlock", "OutputBlockProps"]
key_links:
- from: "src/components/wizard/OutputBlock.tsx"
to: "src/utils/downloadFile"
via: "import { downloadFile }"
pattern: "downloadFile"
- from: "src/utils/downloadZip.ts"
to: "jszip"
via: "import JSZip from 'jszip'"
pattern: "JSZip"
---
<objective>
Create the three utility/component building blocks that ReviewStep will compose: the file download helper, the ZIP bundle helper (installs jszip), and the reusable OutputBlock component.
Purpose: These are pure, independently testable pieces. Isolating them here keeps ReviewStep.tsx focused on layout and state wiring, not download mechanics.
Output: src/utils/downloadFile.ts, src/utils/downloadZip.ts, src/components/wizard/OutputBlock.tsx
</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/phases/04-review-download-security/04-RESEARCH.md
@src/store/types.ts
@src/components/wizard/ReviewStep.test.tsx
</context>
<interfaces>
<!-- Contracts the executor must implement — the test file already imports these paths. -->
Expected by ReviewStep.test.tsx:
```typescript
// src/utils/downloadFile.ts
export function downloadFile(content: string, filename: string): void
// src/utils/downloadZip.ts
export async function downloadZip(
files: { name: string; content: string }[],
zipName: string
): Promise<void>
// src/components/wizard/OutputBlock.tsx
export interface OutputBlockProps {
label: string;
content: string;
filename?: string; // if provided, show download button
disabled?: boolean; // gates both copy and download (security gate)
}
export function OutputBlock(props: OutputBlockProps): JSX.Element
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create downloadFile.ts and downloadZip.ts utilities</name>
<files>src/utils/downloadFile.ts, src/utils/downloadZip.ts</files>
<behavior>
- downloadFile creates a Blob with text/plain;charset=utf-8 MIME type, creates an object URL, clicks a hidden anchor, then revokes the URL immediately after click
- downloadZip accepts an array of { name, content } entries, adds each to a JSZip instance, generates a blob asynchronously, then triggers download using the same Blob/anchor pattern
- Both utilities have no React dependency (pure browser functions)
</behavior>
<action>
First, install jszip (the only new runtime dependency this phase):
```bash
npm install jszip
```
JSZip ships its own TypeScript declarations. Do NOT install @types/jszip (stale, 6 years old).
Create src/utils/downloadFile.ts:
```typescript
// src/utils/downloadFile.ts
// Triggers a browser file download from a string.
// Pattern: MDN URL.createObjectURL — https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static
export function downloadFile(content: string, filename: string): void {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
```
Create src/utils/downloadZip.ts:
```typescript
// src/utils/downloadZip.ts
// Builds a ZIP archive from file entries and triggers download.
// Pattern: JSZip official docs — https://stuk.github.io/jszip/documentation/examples/download-zip-file.html
import JSZip from 'jszip';
export async function downloadZip(
files: { name: string; content: string }[],
zipName: string
): Promise<void> {
const zip = new JSZip();
for (const { name, content } of files) {
zip.file(name, content);
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = zipName;
a.click();
URL.revokeObjectURL(url);
}
```
Do NOT create a src/utils/index.ts barrel — ReviewStep will import directly from the specific utility files to keep the mock paths in tests predictable (the test file already uses '../../utils/downloadFile' and '../../utils/downloadZip').
</action>
<verify>
<automated>npm run build 2>&1 | tail -10</automated>
</verify>
<done>Both utility files exist and TypeScript compilation succeeds with no errors. jszip appears in package.json dependencies.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Create OutputBlock.tsx reusable component</name>
<files>src/components/wizard/OutputBlock.tsx</files>
<behavior>
- OutputBlock renders a labelled section with a pre/code block showing the content
- A "Copy" button calls navigator.clipboard.writeText(content); button label changes to "Copied!" for 2 seconds then reverts
- When filename prop is provided, a "Download" button calls downloadFile(content, filename)
- When disabled prop is true, both Copy and Download buttons are disabled (HTML disabled attribute) — this is the SECU-01 gate applied per-block
- Copy and Download buttons are always rendered (never conditionally hidden) — disabled state is visual only
</behavior>
<action>
Create src/components/wizard/OutputBlock.tsx:
```typescript
// src/components/wizard/OutputBlock.tsx
// Reusable output block: pre-formatted content + copy button + optional download button.
// disabled prop gates both actions (SECU-01 security acknowledgement gate).
import { useState } from 'react';
import { downloadFile } from '../../utils/downloadFile';
export interface OutputBlockProps {
label: string;
content: string;
filename?: string;
disabled?: boolean;
}
export function OutputBlock({ label, content, filename, disabled = false }: OutputBlockProps) {
const [copied, setCopied] = useState(false);
async function handleCopy() {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="mb-6">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-gray-700">{label}</span>
<div className="flex gap-2">
<button
type="button"
disabled={disabled}
onClick={handleCopy}
className="text-xs px-2 py-1 rounded border border-gray-300 disabled:opacity-40"
>
{copied ? 'Copied!' : 'Copy'}
</button>
{filename && (
<button
type="button"
disabled={disabled}
onClick={() => downloadFile(content, filename)}
className="text-xs px-2 py-1 rounded border border-gray-300 disabled:opacity-40"
>
Download
</button>
)}
</div>
</div>
<pre className="bg-gray-900 text-gray-100 rounded p-4 text-xs overflow-x-auto whitespace-pre-wrap">
<code>{content}</code>
</pre>
</div>
);
}
```
Styling uses Tailwind classes consistent with the existing wizard components. The `disabled:opacity-40` class communicates the locked state visually without hiding buttons.
</action>
<verify>
<automated>npm test 2>&1 | tail -15</automated>
</verify>
<done>OutputBlock.tsx exists and compiles cleanly. npm test still shows ReviewStep.test.tsx as RED (component does not exist yet) and all other tests GREEN. No new TypeScript errors.</done>
</task>
</tasks>
<verification>
`npm run build` passes. `npm test` shows: reducer.test.ts GREEN, ReviewStep.test.tsx still RED (ReviewStep not yet created — expected), all prior phase tests GREEN.
</verification>
<success_criteria>
- src/utils/downloadFile.ts exports downloadFile function
- src/utils/downloadZip.ts exports downloadZip async function
- src/components/wizard/OutputBlock.tsx exports OutputBlock and OutputBlockProps
- jszip listed in package.json dependencies
- TypeScript compilation clean
</success_criteria>
<output>
After completion, create `.planning/phases/04-review-download-security/04-02-SUMMARY.md`
</output>
@@ -0,0 +1,272 @@
---
phase: 04-review-download-security
plan: 03
type: tdd
wave: 3
depends_on: [04-02]
files_modified:
- src/components/wizard/ReviewStep.tsx
autonomous: true
requirements: [CONF-02, CONF-03, DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, SECU-01, SECU-02]
must_haves:
truths:
- "ReviewStep renders a live preview of the generated rclone.conf content"
- "A security warning checkbox must be checked before any download or copy button is enabled"
- "A 'no data sent to server' notice is visible in the ReviewStep render output"
- "OutputBlocks for all four generated files are shown (rclone.conf, intune-install.ps1, intune-detection.ps1, rmm-script.ps1)"
- "A 'Download ZIP' button calls downloadZip with all 4 file entries"
- "All test stubs from Plan 01 pass GREEN after this plan"
artifacts:
- path: "src/components/wizard/ReviewStep.tsx"
provides: "Step 3 component — live preview + security gate + all download/copy actions"
exports: ["ReviewStep"]
min_lines: 60
key_links:
- from: "src/components/wizard/ReviewStep.tsx"
to: "src/generators/index.ts"
via: "import { buildRcloneConf, buildIntuneInstall, buildIntuneDetection, buildRmmScript }"
pattern: "buildRcloneConf"
- from: "src/components/wizard/ReviewStep.tsx"
to: "src/store/context.tsx"
via: "import { useWizard }"
pattern: "useWizard"
- from: "src/components/wizard/ReviewStep.tsx"
to: "src/components/wizard/OutputBlock.tsx"
via: "import { OutputBlock }"
pattern: "OutputBlock"
- from: "src/components/wizard/ReviewStep.tsx"
to: "src/utils/downloadZip.ts"
via: "import { downloadZip }"
pattern: "downloadZip"
---
<objective>
Implement ReviewStep.tsx: the full Phase 4 wizard step that composes all download, copy, preview, and security behaviors. This plan turns every RED stub from Plan 01 GREEN.
Purpose: This is the primary deliverable of Phase 4. All eleven requirement behaviors (CONF-02, CONF-03, DOWN-01DOWN-06, SECU-01, SECU-02) are implemented in this single component, composed from the utilities and OutputBlock built in Plan 02.
Output: src/components/wizard/ReviewStep.tsx — fully implemented, all tests GREEN.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/04-review-download-security/04-RESEARCH.md
@src/store/types.ts
@src/store/context.tsx
@src/generators/index.ts
@src/components/wizard/OutputBlock.tsx
@src/utils/downloadFile.ts
@src/utils/downloadZip.ts
@src/components/wizard/ReviewStep.test.tsx
</context>
<interfaces>
<!-- Contracts the executor must satisfy — read the test file for exact assertion details -->
From src/store/context.tsx:
```typescript
const { state } = useWizard();
// state.remote.backendType: BackendType | null
// state.remote.name: string
// state.deployment: { includeInstall, configPath, scriptTargets }
```
From src/generators/index.ts (all accept WizardState, return string, buildRcloneConf throws on incomplete state):
```typescript
export function buildRcloneConf(state: WizardState): string
export function buildIntuneInstall(state: WizardState): string
export function buildIntuneDetection(state: WizardState): string
export function buildRmmScript(state: WizardState): string
```
From src/components/wizard/OutputBlock.tsx:
```typescript
export interface OutputBlockProps {
label: string;
content: string;
filename?: string; // when provided, shows Download button
disabled?: boolean; // SECU-01 gate
}
export function OutputBlock(props: OutputBlockProps): JSX.Element
```
From src/utils/downloadZip.ts:
```typescript
export async function downloadZip(
files: { name: string; content: string }[],
zipName: string
): Promise<void>
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement ReviewStep.tsx — full component turning all stubs GREEN</name>
<files>src/components/wizard/ReviewStep.tsx</files>
<behavior>
- CONF-02: rcloneConfContent computed via useMemo calling buildRcloneConf(state); wrapped in try/catch returning placeholder on error
- CONF-03: rclone.conf OutputBlock has no filename (copy-only for live preview area); separate rclone.conf OutputBlock with filename='rclone.conf' for download
- DOWN-01: OutputBlock with filename='rclone.conf' calls downloadFile(content, 'rclone.conf') on Download click
- DOWN-02: OutputBlock with filename='intune-install.ps1' calls downloadFile on click
- DOWN-03: OutputBlock with filename='intune-detection.ps1' calls downloadFile on click
- DOWN-04: OutputBlock with filename='rmm-script.ps1' calls downloadFile on click
- DOWN-05: "Download All (ZIP)" button calls downloadZip([...4 files...], 'rclone-deployment.zip')
- DOWN-06: every OutputBlock's Copy button calls clipboard.writeText with that block's content
- SECU-01: acknowledged useState boolean controls disabled prop on all OutputBlocks and Download ZIP button
- SECU-02: a visible notice "No data is sent to any server" (or similar) is present in the render
</behavior>
<action>
Create src/components/wizard/ReviewStep.tsx. Follow the TDD cycle:
RED: Run `npm test -- src/components/wizard/ReviewStep.test.tsx` first to confirm all stubs fail.
GREEN: Implement the component, run tests after each significant block.
Implementation structure:
```typescript
// src/components/wizard/ReviewStep.tsx
import { useMemo, useState } from 'react';
import { useWizard } from '../../store/context';
import {
buildRcloneConf,
buildIntuneInstall,
buildIntuneDetection,
buildRmmScript,
} from '../../generators/index';
import { OutputBlock } from './OutputBlock';
import { downloadZip } from '../../utils/downloadZip';
const PLACEHOLDER = '# Fill in the wizard steps to generate your rclone.conf';
export function ReviewStep() {
const { state } = useWizard();
const [acknowledged, setAcknowledged] = useState(false);
// CONF-02: live preview — updates on every state change
const rcloneConf = useMemo(() => {
try { return buildRcloneConf(state); } catch { return PLACEHOLDER; }
}, [state]);
const intuneInstall = useMemo(() => {
try { return buildIntuneInstall(state); } catch { return ''; }
}, [state]);
const intuneDetection = useMemo(() => {
try { return buildIntuneDetection(state); } catch { return ''; }
}, [state]);
const rmmScript = useMemo(() => {
try { return buildRmmScript(state); } catch { return ''; }
}, [state]);
// DOWN-05: ZIP bundle — always include all 4 files
async function handleDownloadZip() {
await downloadZip(
[
{ name: 'rclone.conf', content: rcloneConf },
{ name: 'intune-install.ps1', content: intuneInstall },
{ name: 'intune-detection.ps1', content: intuneDetection },
{ name: 'rmm-script.ps1', content: rmmScript },
],
'rclone-deployment.zip'
);
}
return (
<div>
{/* SECU-02: client-side only notice */}
<p className="text-sm text-green-700 bg-green-50 rounded p-3 mb-6">
No data is sent to any server. All file generation happens in your browser.
</p>
{/* SECU-01: security acknowledgement gate */}
<div className="bg-yellow-50 border border-yellow-300 rounded p-4 mb-6">
<p className="text-sm text-yellow-800 font-medium mb-2">
Security warning: generated files contain credentials in plain text.
</p>
<label className="flex items-center gap-2 text-sm text-yellow-900 cursor-pointer">
<input
type="checkbox"
checked={acknowledged}
onChange={(e) => setAcknowledged(e.target.checked)}
/>
I understand that the generated files contain credentials in plain text.
</label>
</div>
{/* Output blocks — DOWN-01 through DOWN-04, CONF-03, DOWN-06 */}
<OutputBlock
label="rclone.conf"
content={rcloneConf}
filename="rclone.conf"
disabled={!acknowledged}
/>
<OutputBlock
label="Intune Install Script"
content={intuneInstall}
filename="intune-install.ps1"
disabled={!acknowledged}
/>
<OutputBlock
label="Intune Detection Script"
content={intuneDetection}
filename="intune-detection.ps1"
disabled={!acknowledged}
/>
<OutputBlock
label="RMM Script"
content={rmmScript}
filename="rmm-script.ps1"
disabled={!acknowledged}
/>
{/* DOWN-05: ZIP bundle */}
<button
type="button"
disabled={!acknowledged}
onClick={handleDownloadZip}
className="w-full py-2 px-4 bg-blue-600 text-white rounded font-medium disabled:opacity-40 disabled:cursor-not-allowed hover:bg-blue-700"
>
Download All (ZIP)
</button>
</div>
);
}
```
After writing the file, run tests and fix any assertion mismatches:
- If a test expects a specific button text, match it exactly
- If a test queries by role or label, ensure the rendered output has matching accessible text
- Update test stubs to real assertions as you implement (replace `expect.fail('not yet implemented')` with actual assertions matching the rendered output)
PITFALL — buildRcloneConf throws on incomplete state: Always use try/catch in useMemo. Show PLACEHOLDER string when thrown. This prevents the component from crashing during navigation with partial state.
PITFALL — useMemo dependency: Pass `[state]` (the full state object) as the dependency array. WizardContext re-renders on every dispatch, so this will update on every form change (satisfies CONF-02).
</action>
<verify>
<automated>npm test -- src/components/wizard/ReviewStep.test.tsx 2>&1 | tail -20</automated>
</verify>
<done>All test stubs in ReviewStep.test.tsx are replaced with real assertions and pass GREEN. npm test shows 0 failing tests for this file. All pre-existing tests in other files remain GREEN.</done>
</task>
</tasks>
<verification>
`npm test` full suite passes GREEN. ReviewStep.test.tsx: all CONF-02, CONF-03, DOWN-01 through DOWN-06, SECU-01, SECU-02 assertions pass. reducer.test.ts SECU-03 passes. All Phase 13 tests still GREEN.
</verification>
<success_criteria>
- src/components/wizard/ReviewStep.tsx exists and exports ReviewStep
- All 10 requirement behaviors tested in ReviewStep.test.tsx pass GREEN
- No TypeScript errors in the component
- Full test suite green
</success_criteria>
<output>
After completion, create `.planning/phases/04-review-download-security/04-03-SUMMARY.md`
</output>
@@ -0,0 +1,129 @@
---
phase: 04-review-download-security
plan: 04
type: execute
wave: 4
depends_on: [04-03]
files_modified:
- src/App.tsx
- src/components/wizard/StepIndicator.tsx
autonomous: true
requirements: [CONF-02, CONF-03, DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, SECU-01, SECU-02, SECU-03]
must_haves:
truths:
- "App.tsx renders ReviewStep when currentStep is 3"
- "StepIndicator shows 4 labels: Backend, Remote Config, Deployment, Review"
- "Navigating to step 3 via StepIndicator dispatch renders the ReviewStep component"
- "Full npm test suite remains GREEN after wiring"
artifacts:
- path: "src/App.tsx"
provides: "Wired ReviewStep as step index 3 in the steps array"
contains: "ReviewStep"
- path: "src/components/wizard/StepIndicator.tsx"
provides: "STEP_LABELS updated with fourth entry 'Review'"
contains: "Review"
key_links:
- from: "src/App.tsx"
to: "src/components/wizard/ReviewStep.tsx"
via: "import { ReviewStep }"
pattern: "ReviewStep"
- from: "src/components/wizard/StepIndicator.tsx"
to: "STEP_LABELS array"
via: "array entry at index 3"
pattern: "'Review'"
---
<objective>
Wire ReviewStep into the running application: add it as step index 3 in App.tsx and add 'Review' as the fourth entry in StepIndicator's STEP_LABELS array.
Purpose: ReviewStep is fully implemented and tested in isolation (Plan 03). This plan completes the integration so the full 4-step wizard flow is navigable end-to-end.
Output: Updated App.tsx and StepIndicator.tsx. No new files.
</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/phases/04-review-download-security/04-RESEARCH.md
@src/App.tsx
@src/components/wizard/StepIndicator.tsx
@src/App.test.tsx
</context>
<interfaces>
<!-- Existing extension points — both files were pre-annotated for this change -->
From src/App.tsx (current — step 3 add point annotated in comments):
```typescript
// Guard: clamp to valid range (phase 4 will add step 3 for review/download)
const stepIndex = Math.min(state.currentStep, steps.length - 1);
// steps array currently has indices 0, 1, 2
```
From src/components/wizard/StepIndicator.tsx (current):
```typescript
const STEP_LABELS = ['Backend', 'Remote Config', 'Deployment'];
// Add 'Review' as index 3
```
The App.tsx clamp logic (`Math.min(state.currentStep, steps.length - 1)`) already handles step 3 correctly once ReviewStep is in the array — no logic changes needed, only array population.
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Wire ReviewStep into App.tsx and add 'Review' label to StepIndicator</name>
<files>src/App.tsx, src/components/wizard/StepIndicator.tsx</files>
<action>
Edit src/App.tsx:
1. Add import: `import { ReviewStep } from './components/wizard/ReviewStep';`
2. Add ReviewStep as the fourth element in the steps array:
```typescript
const steps = [
<BackendSelectionStep key="backend" />,
<RemoteConfigStep key={state.remote.backendType ?? 'none'} />,
<DeploymentStep key="deployment" />,
<ReviewStep key="review" />, // step index 3 — Phase 4
];
```
3. Remove or update the comment "phase 4 will add step 3 for review/download" — it is now implemented.
4. The `Math.min(state.currentStep, steps.length - 1)` clamp requires NO change — it automatically works with 4 steps.
Edit src/components/wizard/StepIndicator.tsx:
1. Update STEP_LABELS to include 'Review' as the fourth entry:
```typescript
const STEP_LABELS = ['Backend', 'Remote Config', 'Deployment', 'Review'];
```
2. Update the header comment to reflect the new step: `// 1.Backend > 2.Remote Config > 3.Deployment > 4.Review`
3. No other logic changes needed — the map over STEP_LABELS automatically renders the fourth step.
After edits, run `npm test` to verify:
- App.test.tsx (existing step routing tests) must remain GREEN
- StepIndicator.test.tsx must remain GREEN
- All other tests must remain GREEN
</action>
<verify>
<automated>npm test 2>&1 | tail -20</automated>
</verify>
<done>App.tsx imports and renders ReviewStep at step index 3. StepIndicator shows 4 step labels including 'Review'. Full npm test suite GREEN. TypeScript compilation clean.</done>
</task>
</tasks>
<verification>
`npm run build` succeeds. `npm test` full suite GREEN. The app wires ReviewStep without breaking any existing step routing, StepIndicator navigation, or prior test assertions.
</verification>
<success_criteria>
- src/App.tsx imports ReviewStep and includes it as steps[3]
- src/components/wizard/StepIndicator.tsx STEP_LABELS has 4 entries ending with 'Review'
- All existing tests (App.test.tsx, StepIndicator.test.tsx, all Phase 13 tests) remain GREEN
- `npm run build` produces no errors
</success_criteria>
<output>
After completion, create `.planning/phases/04-review-download-security/04-04-SUMMARY.md`
</output>
@@ -0,0 +1,116 @@
---
phase: 04-review-download-security
plan: 05
type: checkpoint
wave: 5
depends_on: [04-04]
files_modified: []
autonomous: false
requirements: [CONF-02, CONF-03, DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, SECU-01, SECU-02, SECU-03]
must_haves:
truths:
- "IT admin can navigate the full 4-step wizard end-to-end in a real browser"
- "Security checkbox gate prevents downloads until acknowledged"
- "All four individual file downloads work in the browser"
- "ZIP bundle downloads and contains all 4 files"
- "Copy-to-clipboard works for each output block"
- "No data is written to localStorage or sessionStorage at any point"
artifacts:
- path: "src/components/wizard/ReviewStep.tsx"
provides: "Visually verified step 3 component"
- path: "src/utils/downloadFile.ts"
provides: "Verified individual download helper"
- path: "src/utils/downloadZip.ts"
provides: "Verified ZIP bundle helper"
key_links:
- from: "Browser navigation (steps 0→3)"
to: "ReviewStep rendered at step 3"
via: "App.tsx step router"
pattern: "n/a"
---
<objective>
Human verification of the complete Phase 4 feature set in a running browser. All automated tests pass, but ZIP content, actual file downloads, and clipboard paste cannot be verified by jsdom.
Purpose: Confirm the full IT admin workflow — complete wizard, review generated files, acknowledge security warning, download individually and as ZIP, copy to clipboard — works correctly end-to-end.
Output: Human approval that Phase 4 is shippable.
</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/ROADMAP.md
</context>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Complete Phase 4 feature set:
- ReviewStep (step 3) with live rclone.conf preview
- Security acknowledgement checkbox gate (all downloads disabled until checked)
- "No data sent to server" client-side notice
- Individual download buttons for rclone.conf, intune-install.ps1, intune-detection.ps1, rmm-script.ps1
- Copy-to-clipboard button per output block
- "Download All (ZIP)" button producing rclone-deployment.zip with all 4 files
- 4-label StepIndicator (Backend > Remote Config > Deployment > Review)
- Full automated test suite GREEN (including SECU-03 localStorage spy)
</what-built>
<how-to-verify>
Run the dev server: `npm run dev` then open http://localhost:5173
Step 1 — Navigate the full wizard:
1. Select a backend (e.g., Azure Blob)
2. Enter a remote name and fill in all required fields (storage account, SAS token or access key)
3. Proceed to Deployment Options, leave defaults or adjust, click Next
4. Confirm you reach step 4 "Review" with StepIndicator showing "1. Backend 2. Remote Config 3. Deployment 4. Review"
Step 2 — Verify the live preview (CONF-02):
5. The rclone.conf output block shows a populated INI-format config reflecting your entered values
6. Go back to step 2, change a field value, return to step 4 — verify the preview updated
Step 3 — Verify security gate (SECU-01, SECU-02):
7. Before checking the checkbox: confirm all Download and Copy buttons appear disabled/greyed out
8. Confirm the "No data is sent to any server" notice is visible
9. Check the acknowledgement checkbox — confirm all buttons become active
Step 4 — Individual downloads (DOWN-01 through DOWN-04):
10. Click "Download" on the rclone.conf block — verify rclone.conf file saved to Downloads
11. Click "Download" on Intune Install Script — verify intune-install.ps1 saved
12. Click "Download" on Intune Detection Script — verify intune-detection.ps1 saved
13. Click "Download" on RMM Script — verify rmm-script.ps1 saved
Step 5 — Clipboard copy (CONF-03, DOWN-06):
14. Click "Copy" on the rclone.conf block — paste into a text editor, verify content matches displayed preview
15. Click "Copy" on at least one script block — verify clipboard content matches displayed script
Step 6 — ZIP bundle (DOWN-05):
16. Click "Download All (ZIP)" — verify rclone-deployment.zip saved to Downloads
17. Open the ZIP file with your OS ZIP tool — verify it contains exactly 4 files: rclone.conf, intune-install.ps1, intune-detection.ps1, rmm-script.ps1
Step 7 — SECU-03 (no storage writes):
18. Open DevTools → Application → Local Storage and Session Storage
19. Navigate the full wizard end-to-end — verify both storage areas remain empty at all times
</how-to-verify>
<resume-signal>Type "approved" if all checks pass, or describe which checks failed with details</resume-signal>
</task>
</tasks>
<verification>
Human approval received. All 7 verification steps confirmed passing. Phase 4 requirements CONF-02, CONF-03, DOWN-01DOWN-06, SECU-01, SECU-02, SECU-03 verified in a real browser.
</verification>
<success_criteria>
- Human has confirmed "approved" after completing all 18 verification steps
- No failures reported for any requirement behavior
- Phase 4 declared complete
</success_criteria>
<output>
After approval, create `.planning/phases/04-review-download-security/04-05-SUMMARY.md`
</output>