docs(13): research phase domain — rclone backend expansion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 15:49:15 +02:00
co-authored by Claude Sonnet 4.6
parent 9437d6c9b5
commit 5228ae2a0a
@@ -0,0 +1,606 @@
# Phase 13: Add Remaining RClone Remotes - Research
**Researched:** 2026-04-01
**Domain:** rclone backend registry expansion, React component architecture, UX search/categorization
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- Comprehensive coverage — all rclone backends that make sense for unattended Windows deployment
- OAuth-requiring backends included via token-paste approach (same as OneDrive: user runs `rclone authorize` locally, pastes resulting JSON token)
- All new backends shipped in a single phase (no sub-phases by auth type)
- Claude picks the specific backend list based on rclone documentation and deployment viability
- Reuse per-backend AuthToggle component pattern for backends with mutually-exclusive auth fields (e.g., FTP password vs key file) — same proven pattern as AzureAuthToggle/SftpAuthToggle
- Build a new reusable OAuthInstructions component for OAuth-token backends — collapsible step-by-step guide showing how to run `rclone authorize` for that specific backend
- OAuthInstructions takes backend-specific command and steps as props, keeps RemoteConfigStep clean
- Categorized sections with headings: Cloud Object Storage, Cloud Drives, Protocol-based — grouped by storage type
- Search/filter bar at the top that filters instantly as user types
- Search matches across all fields: displayName, description, category, and field labels
- Categories with no matching backends collapse when search is active
- Add `category` field to each BACKEND_REGISTRY entry — single source of truth stays single
- Add inline SVG icons per backend card — hand-picked SVGs bundled in the app, no external dependency
### Claude's Discretion
- BackendType implementation: keep explicit union type or derive dynamically from registry keys — pick whichever balances type safety and maintainability best at 20+ backends
- Specific backend list selection based on rclone docs and unattended deployment viability
- Field validation depth per backend (regex patterns vs simple required/optional)
- Icon design approach (monochrome vs brand colors, sizing)
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>
---
## Summary
Phase 13 expands the BACKEND_REGISTRY from 7 to approximately 18-20 entries, grouping them into three categories (Cloud Object Storage, Cloud Drives, Protocol-based). Each new backend requires: a registry entry, a RCLONE_TYPE_MAP entry, a `buildZodSchema()` call, and a `backendLabel` entry. Backends with OAuth tokens get an `OAuthInstructions` collapsible component. Backends with mutually-exclusive auth get a new AuthToggle component. The BackendSelectionStep gets a complete UX overhaul with category headings, instant search, and inline SVG icons.
The critical architectural decision is whether `BackendType` stays as an explicit union literal or is derived from registry keys. At 20+ backends, explicit union becomes a maintenance burden (every new backend requires editing 6 files). Deriving from registry keys reduces that to 4 files and eliminates the "forgot to update BackendType" class of bug.
**Primary recommendation:** Derive `BackendType` from `keyof typeof BACKEND_REGISTRY` to reduce per-backend boilerplate from 6 touch-points to 4. Use `as const` assertion on the registry object for full TypeScript narrowing.
---
## Recommended Backend List
Based on rclone documentation and suitability for unattended Windows deployment via RMM/Intune, the following backends should be added. Each entry shows the rclone `type` string and auth approach.
### Category: Cloud Object Storage (S3-like)
These all use static API credentials — ideal for unattended deployment.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `azureblob` | Azure Blob Storage | `azureblob` | Key or SAS URL | Existing |
| `azure-files` | Azure Files Storage | `azurefiles` | Account + Key | New — uses `account` + `key` fields |
| `s3` | Amazon S3 | `s3` | Access Key + Secret | Existing |
| `s3-compatible` | S3-Compatible | `s3` (provider=Other) | Access Key + Secret + Endpoint | Existing |
| `gcs` | Google Cloud Storage | `google cloud storage` | Service Account JSON | Existing |
| `b2` | Backblaze B2 | `b2` | Application Key ID + Key | Existing |
| `swift` | OpenStack Swift | `swift` | User + Key + Auth URL + Tenant | New — API credential |
### Category: Cloud Drives (OAuth token-paste)
These require OAuth — user runs `rclone authorize` locally, pastes JSON token.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `onedrive` | OneDrive | `onedrive` | OAuth token + drive_id + drive_type | Existing |
| `gdrive` | Google Drive | `drive` | OAuth token + service account option | New — token or service_account_credentials |
| `dropbox` | Dropbox | `dropbox` | OAuth token (JSON blob) | New — token field only |
| `box` | Box | `box` | OAuth token (JSON blob) | New — token + box_sub_type |
| `pcloud` | pCloud | `pcloud` | OAuth token (JSON blob) | New — token + hostname |
### Category: Protocol-based
These use server credentials — IP/hostname, user, password.
| Backend ID | displayName | rclone type | Auth approach | Notes |
|------------|-------------|-------------|---------------|-------|
| `sftp` | SFTP | `sftp` | Password or Key PEM (AuthToggle) | Existing |
| `ftp` | FTP | `ftp` | Host + User + Password | New — also FTPS via `tls` select |
| `webdav` | WebDAV | `webdav` | URL + User + Password + Vendor select | New — includes NextCloud, SharePoint, OwnCloud, etc. |
| `smb` | SMB / Windows Share | `smb` | Host + User + Pass + Domain | New — ideal for Windows shops |
| `http` | HTTP (read-only) | `http` | URL only | New — simple, read-only |
| `seafile` | Seafile | `seafile` | URL + User + Password | New — self-hosted cloud |
**Total:** 7 existing + 10 new = 17 backends
**Excluded from scope (not suitable for unattended Windows deployment):**
- HDFS — Hadoop/enterprise Linux ecosystem, not Windows-native
- Google Photos — read-only, consumer
- iCloud Drive — macOS-only tooling
- Mega, Jottacloud, Yandex, Mail.ru — consumer-grade, poor enterprise adoption
- pCloud — included only if token-paste approach is feasible (MEDIUM confidence)
- Internet Archive, Pixeldrain, Uloz.to, Gofile — public/consumer file sharing
---
## Standard Stack
### Core (no new dependencies)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| react | ^18.3.1 | UI rendering | Existing |
| react-hook-form | ^7.72.0 | Form state + validation | Existing — all forms use it |
| zod | ^4.3.6 | Schema validation | Existing — `buildZodSchema()` auto-derives |
| @hookform/resolvers | ^5.2.2 | Zod <-> RHF bridge | Existing |
**No new npm dependencies.** Inline SVG icons are bundled as TSX components. No icon library needed.
### Installation
```bash
# Nothing new to install — all new code uses existing stack
```
---
## Architecture Patterns
### Recommended Project Structure (additions only)
```
src/
├── schemas/
│ └── registry.ts # Add ~10 new backend entries + category field
├── generators/
│ └── rclone-conf.ts # Add ~10 entries to RCLONE_TYPE_MAP
├── components/
│ ├── wizard/
│ │ ├── BackendSelectionStep.tsx # Full overhaul: categories, search, icons
│ │ ├── RemoteConfigStep.tsx # Add auth-toggle branches for FTP/WebDAV
│ │ ├── FtpAuthToggle.tsx # New: password vs no-auth (anonymous) toggle
│ │ ├── OAuthInstructions.tsx # New: reusable collapsible rclone authorize guide
│ │ └── [backend]AuthToggle.tsx # Only if backend needs mutually-exclusive auth
│ └── icons/
│ └── BackendIcons.tsx # New: inline SVG icon map, keyed by BackendType
```
### Pattern 1: Deriving BackendType from Registry (RECOMMENDED)
The current explicit union `'azureblob' | 's3' | ...` must be touched in `registry.ts` every time a backend is added. At 17+ backends, derive it instead:
```typescript
// src/schemas/registry.ts
export const BACKEND_REGISTRY = {
azureblob: { ... },
gdrive: { ... },
// ... all entries
} as const; // <-- CRITICAL: 'as const' needed for keyof narrowing
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Result: 'azureblob' | 's3' | 'gdrive' | ... (auto-updated when registry grows)
```
**What changes:** `BackendType` no longer lives in `registry.ts` as a separate literal union — it is derived. The `src/store/types.ts` re-export (`export type { BackendType }`) continues to work unchanged. All consumers importing `BackendType` from `./registry` or `../store/types` require zero changes.
**What must be verified:** The `BACKEND_REGISTRY` cast at the bottom currently uses `as Record<BackendType, ...>` which would become circular. Remove that cast — with `as const` the registry is already fully typed by inference.
**Confidence:** HIGH — standard TypeScript pattern, verified against codebase.
### Pattern 2: BACKEND_REGISTRY Entry with `category` Field
Add `category` to the registry entry — single source of truth for categorization:
```typescript
// src/schemas/registry.ts
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
export interface BackendMeta {
displayName: string;
description: string;
category: BackendCategory; // NEW field
fields: FieldDef[];
}
export const BACKEND_REGISTRY = {
azureblob: {
displayName: 'Azure Blob Storage',
description: 'Microsoft Azure cloud storage',
category: 'cloud-object-storage', // NEW
fields: [ ... ],
},
gdrive: {
displayName: 'Google Drive',
description: 'Google Drive (paste pre-obtained rclone token)',
category: 'cloud-drives',
fields: [ ... ],
},
} as const;
```
### Pattern 3: OAuthInstructions Component
Collapsible component, collapsed by default. Takes backend-specific props:
```typescript
// src/components/wizard/OAuthInstructions.tsx
interface OAuthInstructionsProps {
backendName: string; // e.g. "Google Drive"
authorizeCommand: string; // e.g. 'rclone authorize "drive"'
steps?: string[]; // optional custom step descriptions
}
export function OAuthInstructions({ backendName, authorizeCommand, steps }: OAuthInstructionsProps) {
const [expanded, setExpanded] = useState(false);
// Renders a collapsible details/summary or button-controlled div
// Shows numbered steps: install rclone → run command → browser opens → paste JSON
}
```
Used in RemoteConfigStep for `gdrive`, `dropbox`, `box`, `pcloud` alongside the `token` PasswordField:
```typescript
// In RemoteConfigStep, new OAuth branch:
} : backendType === 'gdrive' ? (
<>
<OAuthInstructions
backendName="Google Drive"
authorizeCommand='rclone authorize "drive"'
/>
<FieldRenderer field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token')!} ... />
<FieldRenderer field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'root_folder_id')!} ... />
</>
)
```
### Pattern 4: BackendSelectionStep Category + Search
The current flat grid becomes a category-grouped, searchable layout:
```typescript
// Category display order (stable, not alphabetical)
const CATEGORY_ORDER: BackendCategory[] = [
'cloud-object-storage',
'cloud-drives',
'protocol-based',
];
const CATEGORY_LABELS: Record<BackendCategory, string> = {
'cloud-object-storage': 'Cloud Object Storage',
'cloud-drives': 'Cloud Drives',
'protocol-based': 'Protocol-based',
};
// Search filter logic — matches displayName, description, category label, and field labels
function matchesSearch(entry: BackendMeta, query: string): boolean {
const q = query.toLowerCase();
if (entry.displayName.toLowerCase().includes(q)) return true;
if (entry.description.toLowerCase().includes(q)) return true;
if (CATEGORY_LABELS[entry.category].toLowerCase().includes(q)) return true;
if (entry.fields.some(f => f.label.toLowerCase().includes(q))) return true;
return false;
}
```
Search bar uses the existing `TextFieldMD3` component. The search query is stored in local `useState` — no global state needed.
### Pattern 5: Inline SVG Icon Component
Icons are TSX components returning `<svg>` elements directly — no external dependency, full styling control:
```typescript
// src/components/icons/BackendIcons.tsx
interface IconProps { className?: string; }
export const AzureIcon = ({ className }: IconProps) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
{/* Azure blob SVG path */}
</svg>
);
// Icon map keyed by BackendType
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>> = {
azureblob: AzureIcon,
gdrive: GoogleDriveIcon,
// ...
};
```
`BackendCard` receives an optional `icon` prop rendered at the top-left of the card.
### Anti-Patterns to Avoid
- **Maintaining explicit BackendType union:** At 17+ backends, a forgotten update causes silent TypeScript errors in `backendLabel` exhaustiveness checks. Derive from registry keys instead.
- **Splitting `category` into a separate map:** Keeping it in `BACKEND_REGISTRY` means one file to edit per new backend. A separate `BACKEND_CATEGORIES` record creates two sources of truth.
- **Using `display: none` CSS for search-hidden categories:** The category heading itself must be hidden when its backends are all filtered out — use conditional rendering, not CSS hiding (CSS hiding leaves empty heading wrappers in DOM).
- **Eager OAuthInstructions expansion:** Default expanded state wastes screen space and is intimidating. Default collapsed, user expands when they need help.
- **Placeholder prop on `TextFieldMD3`:** TextFieldMD3 uses `placeholder=" "` (space) internally for the CSS-only floating label. Do NOT pass a real placeholder string — it will break the `peer-[:not(:placeholder-shown)]` selector. Use `helpText` for descriptive hints instead.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| OAuth token paste textarea | Multi-line textarea component | `PasswordField` (inputType: 'password') | Existing component with show/hide, tooltip, validation — textarea breaks form validation contract |
| Search debouncing | `useDebounce` hook | Direct `useState` + filter on render | 17 backends filter in <1ms; debounce is premature optimization |
| Icon library | npm icon library install | Inline TSX SVG components in `BackendIcons.tsx` | Zero dependency, full Tailwind/CSS control, no bundle bloat |
| BackendType exhaustiveness check | Manual `assertNever()` | `keyof typeof BACKEND_REGISTRY` derivation | TypeScript already errors if a key is missing from `backendLabel` record |
| Category data store | Separate `CATEGORY_REGISTRY` map | `category` field on `BACKEND_REGISTRY` entry | Single source of truth already established |
| rclone type mapping override | Per-backend conditional logic | `RCLONE_TYPE_MAP` entry (existing pattern) | Already used for `s3-compatible``s3` |
**Key insight:** The registry-driven pattern already handles 95% of new-backend complexity. Most new backends (FTP, Seafile, Koofr) need zero custom React components — just a registry entry, a RCLONE_TYPE_MAP entry, a schema call, and a backendLabel string.
---
## Common Pitfalls
### Pitfall 1: BackendType Cast Conflicts with `as const`
**What goes wrong:** The current registry.ts bottom has `} as Record<BackendType, { displayName: string; description: string; fields: FieldDef[] }>`. When `BackendType` is derived from `keyof typeof BACKEND_REGISTRY`, this cast creates a circular dependency (`BackendType` depends on the registry type, which is cast to use `BackendType`).
**Why it happens:** TypeScript cannot resolve the type when both sides of the `as` cast reference each other.
**How to avoid:** Remove the cast entirely. With `as const` on the registry object, TypeScript infers the full precise type. The `BackendMeta` interface should be used for individual entries, not as a cast on the whole object.
**Warning signs:** TypeScript error like "Type alias 'BackendType' circularly references itself."
### Pitfall 2: `backendLabel` Record in RemoteConfigStep Breaks Exhaustiveness
**What goes wrong:** `backendLabel` is typed as `Record<NonNullable<typeof backendType>, string>` — adding a new `BackendType` without adding to `backendLabel` causes a TypeScript error at this record. This is desirable (forced exhaustiveness) but easy to miss during development.
**How to avoid:** Add `backendLabel` entry as the FIRST thing when adding a new backend. Use `displayName` from registry directly instead of duplicating: `const backendLabel = Object.fromEntries(Object.entries(BACKEND_REGISTRY).map(([k, v]) => [k, v.displayName]))` — eliminates the duplication entirely.
**Warning signs:** TS2741 "Property X is missing in type Record<...>"
### Pitfall 3: `BACKEND_SCHEMAS` Explicit Object Misses New Backends
**What goes wrong:** `BACKEND_SCHEMAS` in `src/schemas/index.ts` is an explicit object with one `buildZodSchema()` call per backend. Adding a backend to the registry but forgetting `BACKEND_SCHEMAS` causes a runtime crash in `RemoteConfigStep` when the schema lookup returns `undefined`.
**How to avoid:** Auto-generate `BACKEND_SCHEMAS` from the registry:
```typescript
export const BACKEND_SCHEMAS = Object.fromEntries(
(Object.keys(BACKEND_REGISTRY) as BackendType[]).map(t => [t, buildZodSchema(t)])
) as Record<BackendType, ReturnType<typeof buildZodSchema>>;
```
This makes `BACKEND_SCHEMAS` auto-expand whenever the registry grows.
**Warning signs:** Runtime error "Cannot read properties of undefined (reading 'parse')" in RemoteConfigStep.
### Pitfall 4: FTP/WebDAV Toggle Auth — Zod Schema for Hidden Fields
**What goes wrong:** AzureAuthToggle and SftpAuthToggle register BOTH auth fields always (CSS-hide the inactive one) so values are preserved on tab switch. The Zod schema marks both as optional. This works for Azure (key/sas_url are both optional) but FTP's `pass` field may be expected to be required — making it optional in Zod allows submitting with no password, which silently creates a broken config.
**Why it happens:** Toggle pattern requires both fields to be optional in Zod regardless of visual state.
**How to avoid:** For FTP, both `pass` (required: false) and the anonymous-access toggle approach is fine since FTP often legitimately uses anonymous auth. Mark both toggle fields as `required: false` in the registry. Document in helpText that the inactive field is ignored.
### Pitfall 5: Search Filter Leaves Empty Category Headings in DOM
**What goes wrong:** If search is implemented with CSS `hidden` on non-matching cards but the category heading is always rendered, an empty `<h3>Cloud Drives</h3>` appears when all Cloud Drive backends are filtered out.
**How to avoid:** Filter backends per category before render, skip the entire category block (heading + cards) when `filteredBackends.length === 0` for that category.
### Pitfall 6: rclone Type Strings Are Exact and Case-Sensitive
**What goes wrong:** `RCLONE_TYPE_MAP` maps backend IDs to rclone type strings. Errors here produce a broken `rclone.conf` that rclone silently rejects.
**Verified mappings (HIGH confidence — from rclone official docs):**
| BackendType | rclone type string |
|-------------|-------------------|
| `gdrive` | `drive` |
| `dropbox` | `dropbox` |
| `box` | `box` |
| `pcloud` | `pcloud` |
| `ftp` | `ftp` |
| `webdav` | `webdav` |
| `smb` | `smb` |
| `http` | `http` |
| `swift` | `swift` |
| `seafile` | `seafile` |
| `azure-files` | `azurefiles` |
**Warning signs:** rclone error "didn't find section in config file" or "unknown backend type."
### Pitfall 7: Google Drive Has Two Auth Paths — Service Account vs OAuth Token
**What goes wrong:** Google Drive supports both service account JSON (for unattended, like GCS) and OAuth token paste (like OneDrive). Exposing both options requires an AuthToggle, or a decision to support only one path.
**Recommendation:** Support both via a `GdriveAuthToggle` component:
- Tab 1: "OAuth Token" — token field + OAuthInstructions component
- Tab 2: "Service Account" — service_account_credentials field (reuses GCS pattern)
This matches the unattended deployment goal: domain admins can use service accounts; personal Drive setups use OAuth token paste.
---
## Code Examples
Verified patterns from codebase:
### Auto-derived BackendType (replaces explicit union)
```typescript
// src/schemas/registry.ts
export const BACKEND_REGISTRY = {
azureblob: { displayName: '...', description: '...', category: 'cloud-object-storage', fields: [...] },
gdrive: { displayName: '...', description: '...', category: 'cloud-drives', fields: [...] },
// ...
} as const; // REQUIRED for keyof narrowing
export type BackendCategory = 'cloud-object-storage' | 'cloud-drives' | 'protocol-based';
export type BackendType = keyof typeof BACKEND_REGISTRY;
// Automatically includes every key in the registry
```
### Auto-generated BACKEND_SCHEMAS (replaces explicit per-backend calls)
```typescript
// src/schemas/index.ts
export const BACKEND_SCHEMAS = Object.fromEntries(
(Object.keys(BACKEND_REGISTRY) as BackendType[]).map(t => [t, buildZodSchema(t)])
) as Record<BackendType, ReturnType<typeof buildZodSchema>>;
```
### backendLabel derived from registry (removes duplicate displayNames)
```typescript
// In RemoteConfigStep.tsx
const backendLabel = Object.fromEntries(
Object.entries(BACKEND_REGISTRY).map(([k, v]) => [k, v.displayName])
) as Record<BackendType, string>;
```
### Category-grouped backend rendering in BackendSelectionStep
```typescript
const CATEGORY_ORDER: BackendCategory[] = ['cloud-object-storage', 'cloud-drives', 'protocol-based'];
// Inside component:
const filteredEntries = Object.entries(BACKEND_REGISTRY).filter(
([, entry]) => !query || matchesSearch(entry, query)
);
{CATEGORY_ORDER.map(cat => {
const backends = filteredEntries.filter(([, e]) => e.category === cat);
if (backends.length === 0) return null; // collapse empty categories
return (
<section key={cat}>
<h3>{CATEGORY_LABELS[cat]}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{backends.map(([type, entry]) => (
<BackendCard key={type} ... icon={BACKEND_ICONS[type as BackendType]} />
))}
</div>
</section>
);
})}
```
### OAuthInstructions usage in RemoteConfigStep
```typescript
// For gdrive OAuth path in RemoteConfigStep:
<OAuthInstructions
backendName="Google Drive"
authorizeCommand='rclone authorize "drive"'
/>
<FieldRenderer
field={BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token')!}
register={register}
error={errors.token as FieldError | undefined}
/>
```
### FTP registry entry (example of new backend with optional FTPS)
```typescript
ftp: {
displayName: 'FTP',
description: 'File Transfer Protocol (supports FTPS)',
category: 'protocol-based',
fields: [
{ key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'ftp.example.com' },
{ key: 'user', label: 'Username', inputType: 'text', required: false, placeholder: 'anonymous', helpText: 'Leave blank for anonymous FTP' },
{ key: 'pass', label: 'Password', inputType: 'password', required: false },
{ key: 'port', label: 'Port', inputType: 'text', required: false, placeholder: '21' },
{ key: 'explicit_tls', label: 'TLS Mode', inputType: 'select', required: false,
options: [
{ value: '', label: 'Plain FTP (no encryption)' },
{ value: 'true', label: 'Explicit FTPS (STARTTLS)' },
],
helpText: 'Use Explicit FTPS for encrypted FTP connections (port 21 + STARTTLS)',
},
],
},
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Explicit `BackendType` union literal (7 entries) | Derive from `keyof typeof BACKEND_REGISTRY` | Phase 13 | Eliminates one of 6 touch-points per new backend |
| Explicit per-backend `buildZodSchema()` calls | Auto-generate from registry keys | Phase 13 | Eliminates one of 6 touch-points per new backend |
| Manual `backendLabel` record duplication | Derive from `entry.displayName` | Phase 13 | Eliminates last duplication of displayName |
| Flat card grid (no categories) | Category-grouped with search | Phase 13 | Required UX for 17+ backends |
| No OAuth guidance | OAuthInstructions collapsible component | Phase 13 | New backends (gdrive, dropbox, box) need this |
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest ^4.1.1 |
| Config file | `vite.config.ts` (test.environment: jsdom) + `vitest.config.ts` (node env for generator tests) |
| Quick run command | `npx vitest run` |
| Full suite command | `npx vitest run` |
### Phase Requirements → Test Map
| Behavior | Test Type | Automated Command | File Exists? |
|----------|-----------|-------------------|-------------|
| All 17 backend types present in BACKEND_REGISTRY | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ |
| Each new backend has required fields | unit | `npx vitest run src/schemas/registry.test.ts` | Update existing ✅ |
| BACKEND_SCHEMAS auto-covers new backends (no undefined) | unit | `npx vitest run src/schemas/index.test.ts` | Wave 0 gap ❌ |
| RCLONE_TYPE_MAP has entry for each new backend | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ |
| BackendSelectionStep renders all 17 cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Update existing ✅ |
| Search filter hides non-matching cards | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 gap ❌ |
| Category headings collapse when all items filtered | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | Wave 0 gap ❌ |
| OAuthInstructions renders collapsed by default | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 gap ❌ |
| OAuthInstructions expands on button click | unit | `npx vitest run src/components/wizard/OAuthInstructions.test.tsx` | Wave 0 gap ❌ |
| RemoteConfigStep renders gdrive form fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ |
| RemoteConfigStep renders ftp form fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | Update existing ✅ |
| buildRcloneConf outputs correct type for gdrive | unit | `npx vitest run src/generators/rclone-conf.test.ts` | Update existing ✅ |
### Sampling Rate
- **Per task commit:** `npx vitest run`
- **Per wave merge:** `npx vitest run`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `src/schemas/index.test.ts` — covers BACKEND_SCHEMAS auto-generation (verify no undefined for new types)
- [ ] `src/components/wizard/OAuthInstructions.test.tsx` — covers collapsed default + expand behavior
- [ ] New test cases in `BackendSelectionStep.test.tsx` — search filter and category collapse behavior
---
## Open Questions
1. **Google Drive: OAuth token vs service account — single form or toggle?**
- What we know: GCS already uses service_account_credentials. Google Drive supports both approaches.
- What's unclear: Whether IT pros deploying via RMM will have service accounts for Drive (less common than for GCS).
- Recommendation: Implement `GdriveAuthToggle` with two tabs (OAuth token + Service Account) to cover both. If that adds too much complexity, default to OAuth token path only (simpler, more universally applicable).
2. **Azure Files vs Azure Blob — same `azurefiles` type string?**
- What we know: rclone docs list `azurefiles` as the type for Azure Files Storage (distinct from `azureblob`).
- What's unclear: Required fields for `azurefiles` — need to verify account/key vs connection_string approach.
- Recommendation: Verify against https://rclone.org/azurefiles/ during plan execution before writing registry fields. HIGH confidence on type string, MEDIUM confidence on exact field names.
3. **pCloud hostname requirement for EU users**
- What we know: pCloud has region servers; EU users must set `hostname` to `eapi.pcloud.com`.
- What's unclear: Can we default `hostname` to `api.pcloud.com` and let users override, or is a select dropdown better?
- Recommendation: Add `hostname` as a select with two options (US/EU) — clearer than asking IT pros to know the API hostname.
4. **BackendCard icon sizing and color**
- What we know: User decided "inline SVG, no npm library."
- What's unclear: Monochrome (currentColor, works in dark mode) vs brand colors.
- Recommendation: Monochrome (`fill="currentColor"`) for consistency with MD3 token system and dark mode compatibility. Brand-colored icons require hardcoded hex values that ignore the MD3 color token system.
---
## Sources
### Primary (HIGH confidence)
- rclone.org/drive/ — Google Drive type string (`drive`), service account fields, OAuth token structure
- rclone.org/dropbox/ — Dropbox type string (`dropbox`), token field
- rclone.org/box/ — Box type string (`box`), token + box_sub_type fields
- rclone.org/pcloud/ — pCloud type string (`pcloud`), token + hostname fields
- rclone.org/ftp/ — FTP type string (`ftp`), host/user/pass/tls fields
- rclone.org/webdav/ — WebDAV type string (`webdav`), url/user/pass/vendor fields + vendor options
- rclone.org/smb/ — SMB type string (`smb`), host/user/pass/domain fields
- rclone.org/swift/ — OpenStack Swift type string (`swift`), required fields
- rclone.org/seafile/ — Seafile type string (`seafile`), url/user/pass fields
- rclone.org/koofr/ — Koofr type string (`koofr`), user/password fields
- rclone.org/s3/ — S3-compatible provider enum (Wasabi, Cloudflare, MinIO, etc.)
- Existing codebase (registry.ts, BackendSelectionStep.tsx, RemoteConfigStep.tsx) — HIGH confidence on patterns
### Secondary (MEDIUM confidence)
- rclone.org/overview/ — full backend list confirmed
- rclone.org/docs/ — comprehensive backend list verified
### Tertiary (LOW confidence)
- Azure Files (`azurefiles`) field names — type string confirmed, exact credential fields need verification against https://rclone.org/azurefiles/
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — no new dependencies, all existing patterns
- Backend list selection: HIGH — rclone official docs confirmed for all 10 new backends
- rclone type strings: HIGH — verified against official docs for all backends
- Architecture patterns (BackendType derivation): HIGH — standard TypeScript `keyof typeof` pattern
- Architecture patterns (OAuthInstructions component): HIGH — directly mirrors existing AzureAuthToggle/SftpAuthToggle patterns
- BackendSelectionStep overhaul: HIGH — filter/group/search is standard React pattern, no novel abstractions
- Azure Files field names: MEDIUM — type string confirmed, credential fields need docs verification
- pCloud EU hostname handling: MEDIUM — documented in rclone docs, implementation approach is recommendation
**Research date:** 2026-04-01
**Valid until:** 2026-05-01 (rclone docs are stable; backend type strings rarely change)