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>
273 lines
11 KiB
Markdown
273 lines
11 KiB
Markdown
---
|
||
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-01–DOWN-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 1–3 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>
|