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