Files
website/docs/superpowers/specs/2026-04-26-dedicated-drive-bays-option-b-design.md
Andrew c9e0c8826f docs(spec): drive bay Option B restructure design (deferred)
Captures the full design for splitting LFF/SFF/NVMe drive bay
groups from flat radios into Drive Selection + Drive Quantity
composite controls. Includes the SAS variants user requested,
per-drive pricing table, schema decisions (no migrations needed,
existing schema already supports multi-option groups), Pinia
store changes, new DriveBayGroupSelector component sketch, URL
param contract changes, and migration steps.

Implementation deferred to a focused next session — realistic
4-5 hour build (backend seeder + frontend component + store
rework + test rewrite). Phase A (PCIe NVMe Add-in) shipped
ahead of this in c74ca7f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:04:48 -04:00

199 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Drive Bay Configurator — Option B Restructure (deferred)
**Date:** 2026-04-26
**Status:** Approved (design only) — implementation deferred to dedicated session
**Owner:** Andrew
**Repo:** `EZSCALE/website`
**Related:** `2026-04-26-dedicated-server-lineup-design.md` (parent spec, already shipped)
## Why this exists
The dedicated 14th-gen drive bay configurator (LFF / SFF / NVMe) currently uses a flat radio per chassis-bay-type group. After the recent expansion (high-cap HDDs, LFF SSDs, PCIe NVMe add-in), the LFF group alone is 35 entries on a single radio. Adding SAS variants on top would push it past 50 — UX is breaking under its own weight.
Option B splits each drive bay group into composite controls so customers pick **media + capacity + quantity** independently, and the frontend computes total = `unit_cost × quantity`. Net: ~10 drive selections + a stepper instead of 35-50 flat combo radios.
## Scope (in)
- Restructure the three existing drive bay groups (LFF, SFF, NVMe) from "one option, many flat values" to "two options per group: Drive Selection + Drive Quantity"
- Add SAS HDD and SAS SSD variants to LFF and SFF Drive Selection lists (currently only SATA)
- Update Pinia store to track `{drive, quantity}` per drive-bay group
- New `DriveBayGroupSelector` component (or extend `OptionGroupSelector`) to render the two-option pattern
- Update route filter to clamp `Drive Quantity` max to chassis bay_count
- Update `BuildSummary` to render drive bay line items as `N× <drive type> @ $X/drive = $Y total`
- Migrate URL params: `lff_drive=`, `lff_qty=`, `sff_drive=`, `sff_qty=`, `nvme_drive=`, `nvme_qty=`
- Test rewrite: existing drive-bay tests need updating, add coverage for the new structure
## Scope (out / deferred)
- PCIe NVMe Add-in group keeps flat-radio pattern (combos bundle adapter+drive cost, hard to split cleanly into media+capacity+quantity). Already shipped as `c74ca7f`.
- Mixed-bay heterogeneous configs (e.g., 4× HDD + 4× SSD on R540) — still ticket-only post-order. Schema doesn't natively support multi-drive-type per group.
- Frontend animation polish on the new component
- Per-bay-position drive picker (was descoped in original spec)
## Schema decisions
**No migrations needed.** The existing `plan_config_groups` / `plan_config_options` / `plan_config_values` schema already supports multiple options per group; the seeder just wasn't using that capability for drive bays.
Each drive bay group becomes:
```php
PlanConfigGroup::updateOrCreate(['name' => 'Dedicated 14th Gen — LFF Drive Bays'], [...]);
// Option 1: Drive Selection (radio) — values store PER-DRIVE monthly cost
$driveSelection = $this->seedRadioOption($group, 'Drive Selection', false, 1);
$this->seedValues($driveSelection, [
['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true],
['label' => '4 TB SATA HDD', 'value' => 'sata-hdd-4tb', 'monthly' => 12.00],
['label' => '8 TB SATA HDD', 'value' => 'sata-hdd-8tb', 'monthly' => 20.00],
['label' => '12 TB Enterprise SATA HDD', 'value' => 'sata-hdd-12tb', 'monthly' => 45.00],
['label' => '20 TB Enterprise SATA HDD', 'value' => 'sata-hdd-20tb', 'monthly' => 55.00],
['label' => '24 TB Enterprise SATA HDD', 'value' => 'sata-hdd-24tb', 'monthly' => 75.00],
['label' => '12 TB Enterprise SAS HDD', 'value' => 'sas-hdd-12tb', 'monthly' => 50.00], // NEW SAS
['label' => '16 TB Enterprise SAS HDD', 'value' => 'sas-hdd-16tb', 'monthly' => 55.00], // NEW SAS
['label' => '480 GB SATA SSD (LFF carrier)','value' => 'sata-ssd-480gb-lff', 'monthly' => 10.00],
['label' => '1.92 TB SATA SSD (LFF carrier)','value' => 'sata-ssd-1920gb-lff', 'monthly' => 18.00],
['label' => '3.84 TB SATA SSD (LFF carrier)','value' => 'sata-ssd-3840gb-lff', 'monthly' => 45.00],
['label' => '7.68 TB SAS SSD (LFF carrier)','value' => 'sas-ssd-7680gb-lff', 'monthly' => 200.00], // NEW SAS
['label' => '1.92 TB SAS SSD (LFF carrier)','value' => 'sas-ssd-1920gb-lff', 'monthly' => 40.00], // NEW SAS
['label' => '3.84 TB SAS SSD (LFF carrier)','value' => 'sas-ssd-3840gb-lff', 'monthly' => 80.00], // NEW SAS
]);
// Option 2: Drive Quantity (quantity stepper)
$driveQuantity = $this->seedQuantityOption(
$group, 'Drive Quantity', 0, 12, 'drives', 0, 2
);
// monthly_price=0 on the option itself — actual cost computed
// in the frontend as drive_selection.monthly_price × quantity.
```
Note: `seedQuantityOption` already exists and supports `min_qty / max_qty / step`. Setting `monthly_price=0` on it lets us reuse the helper without it interfering with our cost math.
Same pattern for SFF (Drive Selection list with SATA + SAS SSD options + the high-cap HDDs that fit, max_qty=24) and NVMe (U.2 NVMe sizes only, max_qty=24).
## Per-drive pricing table (in monthly cost terms)
| Drive | Per-drive $/mo | Notes |
|---|---|---|
| 4 TB SATA HDD | $12 | Existing |
| 8 TB SATA HDD | $20 | Existing |
| 12 TB SATA HDD | $45 | Existing |
| 20 TB SATA HDD | $55 | Existing |
| 24 TB SATA HDD | $75 | Existing |
| 12 TB SAS HDD | $50 | NEW — ~10% premium over SATA for 12 Gb/s + dual-port |
| 16 TB SAS HDD | $55 | NEW |
| 480 GB SATA SSD | $10 | Existing |
| 1.92 TB SATA SSD | $18 | Existing |
| 3.84 TB SATA SSD | $45 | Existing |
| 7.68 TB SATA SSD | $100 | Existing — covers most "premium" cases without going SAS |
| 1.92 TB SAS SSD | $40 | NEW — Nytro 3350 / 2532 class, 12 Gb/s dual-port |
| 3.84 TB SAS SSD | $80 | NEW |
| 7.68 TB SAS SSD | $200 | NEW |
| 1.92 TB U.2 NVMe (SFF chassis only) | $30 | NEW SFF entry — fits in 2.5" SFF NVMe-capable bays where present |
| 3.84 TB U.2 NVMe | $70 | |
| 7.68 TB U.2 NVMe | $150 | |
## Frontend changes
**Pinia store** (`stores/dedicatedConfigurator.ts`):
```ts
// Selections shape becomes:
// non-drive groups: { groupName: 'value-slug' }
// drive bay groups: { groupName: { drive: 'value-slug', quantity: 0 } }
const selections = ref<Record<string, string | { drive: string; quantity: number }>>({})
function isDriveBayGroup(name: string): boolean {
return name.endsWith('Drive Bays')
}
const driveBayCost = computed<Record<string, number>>(() => {
const costs: Record<string, number> = {}
for (const [groupName, sel] of Object.entries(selections.value)) {
if (typeof sel !== 'object') continue
const group = findGroup(groupName)
const driveOption = group?.options.find(o => o.name === 'Drive Selection')
const drive = driveOption?.values.find(v => v.value === sel.drive)
if (!drive) continue
const perDriveCost = pickCyclePrice(drive, cycle.value)
costs[groupName] = perDriveCost * sel.quantity
}
return costs
})
```
**New component** `Components/Marketing/Dedicated/DedicatedConfigurator/DriveBayGroupSelector.vue`:
Renders both options of a drive bay group:
- Top: radio list of Drive Selection values (uses existing `OptionGroupSelector`-style cards)
- Bottom: quantity stepper (1 → max_qty), labeled `Drives in chassis`
- Shows live computed cost: `2× 1.92 TB SATA SSD = +$36.00/mo`
`DedicatedConfigurator/index.vue` switches between `OptionGroupSelector` (single-option groups) and `DriveBayGroupSelector` based on group name.
**BuildSummary** updates to render drive bay rows as:
```
LFF Drive Bays
4× 1.92 TB SATA SSD +$72.00
```
(Currently shows the flat-combo label which works the same way visually, but the data flow changes.)
## URL state contract
Drive bay groups now serialize as two params each:
| Old (flat radio) | New (split) |
|---|---|
| `?lff=4x1920gb-ssd` | `?lff_drive=sata-ssd-1920gb-lff&lff_qty=4` |
| `?sff=8x3840gb-ssd` | `?sff_drive=sata-ssd-3840gb&sff_qty=8` |
| `?nvme=2x2tb-nvme` | `?nvme_drive=u2-nvme-2tb&nvme_qty=2` |
`hydrateFromUrl` reads both params per group. Old URLs with single-key drive params are silently dropped (acceptable since drive bay configs were rarely shared as URLs and the structure changed entirely).
## Tests
- Replace existing drive-bay flat-combo tests with new tests for the multi-option structure
- Cover: drive selection persists, quantity persists, total = drive × qty, quantity max respects chassis bay_count
- New test: SAS variants are visible only on LFF/SFF (not NVMe-native chassis)
## Migration path
The seeder uses `updateOrCreate` keyed on `[option_id, label]`. Restructuring requires a one-time DB cleanup before re-seeding:
```sql
-- Before re-running ConfigOptionSeeder for the new structure
DELETE pco, pcv
FROM plan_config_options pco
LEFT JOIN plan_config_values pcv ON pcv.option_id = pco.id
JOIN plan_config_groups pcg ON pcg.id = pco.group_id
WHERE pcg.name IN (
'Dedicated 14th Gen — LFF Drive Bays',
'Dedicated 14th Gen — SFF Drive Bays',
'Dedicated 14th Gen — NVMe Drive Bays'
);
```
Then `php artisan db:seed --class=ConfigOptionSeeder` recreates with new shape.
## Acceptance
- [ ] LFF / SFF / NVMe groups each have exactly 2 options (Drive Selection radio + Drive Quantity stepper)
- [ ] LFF Drive Selection includes ≥3 SAS variants (12/16 TB SAS HDD; 1.92/3.84/7.68 TB SAS SSD)
- [ ] SFF Drive Selection includes ≥3 SAS SSD variants
- [ ] Per-chassis filter clamps `Drive Quantity.max_qty` to chassis `bay_count`
- [ ] BuildSummary shows drive bay line items as `N× <drive> = $Y` not `<combo label>`
- [ ] URL params use new `_drive=&_qty=` format
- [ ] Existing 14 dedicated tests rewrite cleanly — count stays in 14-16 range
- [ ] `npm run build` passes
- [ ] Mobile (< 768 px) renders the stepper without overflow
## Estimated effort
- Backend (seeder rewrite + DB cleanup migration): ~1 hour
- Frontend (new component + store rework): ~2 hours
- Tests rewrite: ~1 hour
- Verify + commit: ~30 min
**Total: ~4-5 hour focused session.**