Files
Ready2Blob/.planning/research/ARCHITECTURE.md
T
kawaandClaude Sonnet 4.6 0b72904e36 docs: complete project research
Add STACK, FEATURES, ARCHITECTURE, PITFALLS, and SUMMARY research files for Ready2Blob.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 09:46:22 +01:00

441 lines
19 KiB
Markdown

# 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<string, string>; // 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<BackendType, FieldDef[]>;
```
### 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 `<a>` 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<void> {
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 `<AzureBlobStep />`, `<S3Step />`, `<OneDriveStep />` 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 `<DynamicBackendStep />` 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