# Architecture Patterns **Domain:** Pure frontend multi-step configuration wizard (static site, no backend) **Project:** Ready2Blob **Researched:** 2026-03-26 **Confidence:** MEDIUM — rclone.conf format and Intune deployment patterns verified from training data (stable, well-documented domains); client-side download patterns are stable browser APIs; web research unavailable for cross-validation --- ## Recommended Architecture ### High-Level System Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ Browser (Static App) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ │ │ Wizard UI │───▶│ Wizard State│───▶│ Config Builders │ │ │ │ (Steps/Nav) │ │ (Form Data) │ │ (rclone.conf + │ │ │ └──────────────┘ └──────────────┘ │ PS Scripts) │ │ │ └────────┬───────────┘ │ │ │ │ │ ┌────────▼───────────┐ │ │ │ Download Manager │ │ │ │ (individual files │ │ │ │ or ZIP bundle) │ │ │ └────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ (file download only) │ User's disk ``` No network requests leave the browser. All state is ephemeral (in-memory for the session lifetime). --- ## Component Boundaries | Component | Responsibility | Inputs | Outputs | Communicates With | |-----------|---------------|--------|---------|-------------------| | **Wizard UI** | Render step forms, handle navigation (next/back/jump), validate per-step | User interactions | Step completion events | Wizard State | | **Wizard State** | Single source of truth for all collected form data; tracks current step and completion status | Step form submissions | Reactive state object | Wizard UI (reads), Config Builders (reads) | | **Backend Schema Registry** | Defines the fields required per rclone backend type (Azure Blob, S3, OneDrive, etc.) | Backend type selection | Field definitions for each step | Wizard UI (drives dynamic form rendering) | | **rclone.conf Builder** | Transforms wizard state into a valid rclone.conf string | Wizard State snapshot | `rclone.conf` string | Download Manager | | **PowerShell Script Builder** | Generates PS scripts (Intune Win32 or RMM) from wizard state + optional rclone install flag | Wizard State snapshot, script type selection | `.ps1` string(s) | Download Manager | | **Download Manager** | Packages one or more text files and triggers browser download (individual or ZIP) | File content strings + filenames | Browser file download | rclone.conf Builder, PS Script Builder | --- ## Data Flow ``` User fills wizard step N │ ▼ Wizard UI validates step N inputs │ ▼ Wizard State updated (merge step N data into central store) │ ▼ User reaches Review/Download step │ ├──▶ rclone.conf Builder │ reads: [remote_name, backend_type, ...backend-specific fields] │ produces: rclone.conf string │ ├──▶ PowerShell Script Builder (Intune) │ reads: [remote_name, mount_path, include_install_flag, install_source_url] │ produces: deploy-intune.ps1 string │ └──▶ PowerShell Script Builder (RMM) reads: [remote_name, mount_path, include_install_flag] produces: deploy-rmm.ps1 string All strings → Download Manager User selects files → individual download (Blob URL) or ZIP (JSZip) ``` Key invariant: builders are pure functions — same wizard state always produces the same file content. There is no side-effectful build step. --- ## rclone.conf Format (INI-like) **Confidence: HIGH** — rclone.conf format is stable and well-documented. The rclone config file uses a simple INI-like format. Each remote is a named section. ```ini [remote-name] type = azureblob account = mystorageaccount key = base64encodedaccesskey== ``` ### Structure Rules - Section header: `[remote-name]` — any identifier the user chooses; appears in rclone commands as `remote-name:` - Each key-value pair on its own line: `key = value` (spaces around `=` are conventional but optional) - No quoting of values needed (rclone parses raw strings) - Comments: lines starting with `#` or `;` - Multiple remotes = multiple sections in the same file ### Required Fields Per Backend | Backend | `type` value | Minimum required fields | Common optional fields | |---------|-------------|------------------------|------------------------| | Azure Blob | `azureblob` | `account`, then one of: `key`, `sas_url`, or `client_id`+`client_secret`+`tenant` | `endpoint`, `chunk_size`, `upload_cutoff` | | AWS S3 | `s3` | `provider = AWS`, `access_key_id`, `secret_access_key`, `region` | `storage_class`, `server_side_encryption` | | S3-compatible | `s3` | `provider = Other`, `access_key_id`, `secret_access_key`, `endpoint` | varies by provider | | OneDrive | `onedrive` | `client_id`, `client_secret`, `token` (OAuth flow) | `drive_id`, `drive_type` | | SFTP | `sftp` | `host`, `user`, then one of `pass` or `key_file` | `port`, `use_insecure_cipher` | | Google Drive | `drive` | `client_id`, `client_secret`, `token` (OAuth flow) | `team_drive`, `shared_with_me` | ### Example Full Config (Azure Blob + S3) ```ini [my-azure] type = azureblob account = contosostorage key = dGhpcyBpcyBhIHBsYWNlaG9sZGVyIGtleQ== [my-s3] type = s3 provider = AWS access_key_id = AKIAIOSFODNN7EXAMPLE secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY region = us-east-1 ``` ### Generation Pattern The rclone.conf builder is pure string templating: ```typescript function buildRcloneConf(remotes: RemoteConfig[]): string { return remotes.map(remote => { const lines = [`[${remote.name}]`, `type = ${remote.type}`]; for (const [key, value] of Object.entries(remote.params)) { if (value !== undefined && value !== '') { lines.push(`${key} = ${value}`); } } return lines.join('\n'); }).join('\n\n'); } ``` No library needed — plain string concatenation is the correct approach. --- ## Intune / RMM Deployment Patterns **Confidence: MEDIUM** — Intune Win32 app deployment is well-established; exact script conventions vary by org. ### Intune Win32 App Approach Intune Win32 apps require a `.intunewin` package, but PowerShell-only deployments (Intune PowerShell scripts feature) are simpler and sufficient for this use case. The generated script must: 1. (Optionally) download and install rclone — copy `rclone.exe` to a stable path (e.g., `C:\ProgramData\rclone\`) 2. Write `rclone.conf` to the user-appropriate path (`$env:APPDATA\rclone\rclone.conf` for per-user, or `C:\ProgramData\rclone\rclone.conf` for system-wide) 3. (Optionally) create a scheduled task or a startup script to mount the remote on login 4. Return exit code 0 on success; non-zero on failure (Intune reads exit codes) ### Intune Script Deployment Constraints - Scripts run as SYSTEM by default, or as logged-on user (configurable) - Script must handle the case where rclone is already installed (idempotent) - 64-bit PowerShell is required for `rclone.exe` (32-bit PS cannot run 64-bit binaries reliably) - Scripts have a 30-minute execution timeout in Intune - Output directory for config must account for execution context (SYSTEM vs user) ### RMM Script Approach (NinjaRMM, Datto, etc.) Simpler than Intune Win32: paste PS script, run as SYSTEM or user. Same functional requirements as above but no `.intunewin` packaging. Script should be self-contained. ### PowerShell Script Structure ```powershell #Requires -RunAsAdministrator # or omit if running as user context $rclonePath = "C:\ProgramData\rclone" $rcloneExe = "$rclonePath\rclone.exe" $rcloneConf = "$rclonePath\rclone.conf" # --- Optional: Install rclone --- # if ($InstallRclone) { ... download from $RcloneDownloadUrl ... } # --- Write config --- $confContent = @" [remote-name] type = azureblob account = REPLACE_ME key = REPLACE_ME "@ New-Item -ItemType Directory -Force -Path $rclonePath | Out-Null Set-Content -Path $rcloneConf -Value $confContent -Encoding UTF8 # --- Optional: Register mount as scheduled task --- # ... exit 0 ``` The PS script builder generates this structure by substituting values from wizard state into a template string. --- ## Patterns to Follow ### Pattern 1: Centralized Wizard State (Single Store) **What:** All form data lives in one top-level state object, passed down or accessed via context/store. Steps read from and write to slices of this store. **When:** Any multi-step form where later steps depend on earlier choices (e.g., backend type selection in step 1 drives which fields appear in step 2). **Why:** Avoids prop-drilling, makes "go back and edit" trivial, makes config builders pure functions with a single well-typed input. **Shape:** ```typescript interface WizardState { currentStep: number; remote: { name: string; backendType: BackendType; params: Record; // backend-specific key/value pairs }; deployment: { includeInstall: boolean; installSource: 'github' | 'custom'; customInstallUrl?: string; mountPath?: string; scriptTargets: ('intune' | 'rmm')[]; }; outputOptions: { includeConf: boolean; includeIntune: boolean; includeRmm: boolean; bundleAsZip: boolean; }; } ``` ### Pattern 2: Backend Schema Registry **What:** A static data structure (not code) that defines, per backend type, which fields are required, their labels, input types, placeholder text, and validation rules. **When:** The wizard needs to render dynamic forms based on which rclone backend the user selected. **Why:** Adding support for a new backend means adding one entry to the registry, not writing new UI components. Keeps UI code backend-agnostic. **Shape:** ```typescript interface FieldDef { key: string; // matches rclone config key exactly label: string; inputType: 'text' | 'password' | 'select' | 'toggle'; required: boolean; placeholder?: string; helpText?: string; options?: { value: string; label: string }[]; // for select } type BackendSchema = Record; ``` ### Pattern 3: Pure Builder Functions **What:** Config and script builders are pure functions — they receive wizard state and return a string. No side effects, no DOM access, no async. **When:** Always — this is the core generation logic. **Why:** Easily testable (unit test: given state X, output matches expected string). Deterministic. Separates concerns cleanly. ### Pattern 4: Blob URL Download **What:** To trigger a file download in the browser without a server, create a `Blob` from the string content, generate an object URL, attach it to an `` element, and programmatically click it. **When:** Single file download. ```typescript function downloadTextFile(filename: string, content: string): void { const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } ``` **Confidence: HIGH** — Blob URL download is a standard, well-supported browser API (all modern browsers, no library needed for single files). ### Pattern 5: ZIP Bundle via JSZip **What:** When the user wants all generated files in one download, use JSZip to assemble a ZIP in-memory and trigger download. **When:** Multi-file download (rclone.conf + one or more .ps1 files). ```typescript import JSZip from 'jszip'; async function downloadZip(files: { name: string; content: string }[]): Promise { const zip = new JSZip(); files.forEach(f => zip.file(f.name, f.content)); const blob = await zip.generateAsync({ type: 'blob' }); downloadTextFile('ready2blob-deployment.zip', URL.createObjectURL(blob)); } ``` **Confidence: HIGH** — JSZip is the established library for client-side ZIP creation. FileSaver.js is an optional companion for older browser compatibility but not required with the Blob URL pattern above. --- ## Anti-Patterns to Avoid ### Anti-Pattern 1: Per-Step Local State **What:** Each wizard step manages its own form state with no shared store. **Why bad:** When the user navigates back to step 2 from step 4, their inputs are gone. The config builder cannot access step 2 data from step 4. Breaks the "review before download" pattern. **Instead:** Lift all state to the wizard root. Steps only control their own UI (focus, error display), not their data. ### Anti-Pattern 2: Generating Files Only at Download Time from DOM **What:** Reading form field values directly from the DOM to build the config string at download time. **Why bad:** Bypasses validation, couples the builder to the DOM structure, cannot unit-test without a browser. Fragile. **Instead:** Always read from wizard state, which is the validated, typed representation of user input. ### Anti-Pattern 3: Hardcoding Backend Fields in Step Components **What:** Writing a dedicated ``, ``, `` component for every backend. **Why bad:** Adding a new backend requires a new component. Does not scale. Leads to duplication of validation logic. **Instead:** Use the Backend Schema Registry to drive a single generic `` that renders fields from schema definitions. ### Anti-Pattern 4: Storing Secrets Beyond Session **What:** Persisting wizard state to `localStorage`, `sessionStorage`, or any cache. **Why bad:** Credentials (storage keys, SAS tokens) would persist on the machine after the browser tab is closed. Security risk explicitly called out in project constraints. **Instead:** Wizard state lives only in in-memory React/Vue/Svelte state. Closing the tab is the only "logout". ### Anti-Pattern 5: Using a Backend for File Generation **What:** Sending form data to a server to generate files, which returns them as downloads. **Why bad:** Server receives credentials in plaintext. Violates the project's explicit no-backend constraint. Creates data retention risk. **Instead:** All generation is client-side (builders are pure TS/JS functions). --- ## Suggested Build Order (Component Dependencies) Dependencies flow from foundational to dependent. Build in this order: ``` Phase 1 — Foundation └── Wizard State shape definition (TypeScript types + store setup) └── Backend Schema Registry (static data, no UI) Phase 2 — Core Generators (no UI needed yet, fully testable) └── rclone.conf Builder (pure function, unit-testable immediately) └── PowerShell Script Builder — Intune variant └── PowerShell Script Builder — RMM variant └── Download Manager (Blob URL + JSZip wrapper) Phase 3 — Wizard UI Shell └── Step navigation (stepper, next/back, step completion tracking) └── Wizard State wired to UI (reads/writes) Phase 4 — Dynamic Step Forms └── Backend type selector (step 1) └── Dynamic backend fields step (step 2, driven by Schema Registry) └── Deployment options step (step 3) └── Review + Download step (step 4, calls builders + download manager) Phase 5 — Polish └── Per-step validation with user-visible errors └── Security warning modal before download └── Preview pane (show generated file content before download) ``` **Rationale for this order:** - Builders and the schema registry have zero UI dependencies — build and test them first - The wizard UI shell (navigation only) can be built against mock/empty state - Dynamic forms are built last because they depend on both state wiring AND the schema registry being final - The download step can only be meaningfully built once all builders exist --- ## Scalability Considerations | Concern | At MVP (5 backends) | At Growth (20+ backends) | Notes | |---------|---------------------|--------------------------|-------| | Backend support | Hardcode schema for top 5 | Schema registry makes adding trivial | Registry pattern is the key enabler | | Bundle size | Single JS bundle is fine | Consider lazy-loading backend schemas | Each schema is tiny; not a real concern until 50+ backends | | State complexity | Flat wizard state struct | No change needed | Wizard is inherently linear; state stays simple | | Testing | Unit tests on builders | Add snapshot tests for generated files | Builders are pure functions — easiest thing to test | | Localization | Not needed v1 | Field labels/help text in schema enables i18n | Plan label strings as separate keys in schema if i18n is future | --- ## Key Technical Decisions | Decision | Recommended Choice | Rationale | |----------|-------------------|-----------| | State management | React Context + useReducer OR Zustand (lightweight store) | No server state; no need for React Query or Redux. Zustand reduces boilerplate vs Context for this use case. | | ZIP library | JSZip v3 | De facto standard for browser ZIP. No alternatives with meaningful adoption. | | File download | Native Blob URL API | No library needed. FileSaver.js unnecessary for modern browsers. | | Config generation | Plain string templates (template literals) | rclone.conf is simple enough that a template engine adds no value. | | Script generation | Template literal function per script type | Same reasoning. Mustache/Handlebars would be overkill. | | Backend schema | Static TypeScript object (no database, no fetch) | Schemas are known at build time. Static data = zero loading time. | --- ## Sources - rclone.conf format: training data (stable since rclone v1.x; format has not changed); confidence HIGH - Azure Blob rclone backend fields: training data; confidence MEDIUM (specific field names should be cross-checked against https://rclone.org/azureblob/ before implementing the schema registry) - Intune PowerShell script deployment: training data; confidence MEDIUM (execution context and timeout limits are well-documented but should be verified for current Intune behavior) - JSZip client-side ZIP: training data; confidence HIGH (library API is stable) - Blob URL download pattern: training data (standard browser API, MDN-documented); confidence HIGH - Web search and WebFetch unavailable during this research session — claims marked MEDIUM should be verified against official docs during implementation phases