Commit Graph

133 Commits

Author SHA256 Message Date
548fc5f1ee fix(docker): drop git from runtime, remove orphaned opcache.preload_user
Production runtime image doesn't need git (composer install runs in a
separate stage); cuts a non-trivial CVE surface. opcache.preload_user
without opcache.preload produces a startup warning — drop it; we don't
have a preload script.

Image still builds cleanly and php-fpm boots without warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:07:59 -04:00
e264326b0b feat(ci): Gitea Actions release workflow on v* tags
Builds and pushes three images per tag, then runs helm upgrade
against us-prod. Cache-from/cache-to layers reuse buildx cache
across runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:57:15 -04:00
d94a196300 docs(helm): chart README + APP_KEY/Passport bootstrap procedure
Spells out the one-time secret generation that must NEVER be re-run.
Documents local k3d setup and operations runbooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:56:58 -04:00
1c1cc0681d feat(helm): values-local + values-us-prod
Local: in-cluster MariaDB + Valkey, port-forward instead of ingress,
chart-generated APP_KEY (dev only).
Prod: external MariaDB (ezscale ns), Longhorn-backed Valkey, Traefik
IngressRoute with cloudflarewarp + cert-manager TLS, image.tag set
at deploy time, secret pre-created out-of-band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:56:25 -04:00
7238095a77 feat(helm): Traefik IngressRoute + cert-manager Certificate
Two IngressRoutes (web → http-to-https redirect, websecure → app)
covering all configured hosts. Certificate covers all hosts as SANs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:55:52 -04:00
b0f0cd2c16 feat(helm): in-cluster Valkey StatefulSet (toggleable)
AOF persistence + LRU eviction + optional password. PVC for the
queue data so Horizon doesn't lose pending jobs on pod restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:55:32 -04:00
f4ec009840 feat(helm): in-cluster MariaDB CR (toggleable for dev)
Renders only when mariadb.enabled=true. Generates a random root
password Secret with helm.sh/resource-policy=keep so uninstall
doesn't orphan the data volume.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:55:17 -04:00
c46f02bca5 feat(helm): mariadb-operator Database/User/Grant CRDs
When mariadb.enabled=true, references the in-cluster MariaDB this
chart deploys. When false, references an external CR via
mariadb.externalRef. Privileges scoped to the website's database
only — no global ALL PRIVILEGES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:55:06 -04:00
3c2cb907d3 feat(helm): pre-install/pre-upgrade migration Job
Helm hook runs migrate (and optionally seed) before any pod rolls.
If the Job fails, helm upgrade aborts and the previous ReplicaSet
keeps serving traffic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:54:44 -04:00
67cd6f243a feat(helm): scheduler deployment (single replica, schedule:work)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:54:31 -04:00
2d633e37ab feat(helm): Horizon deployment (Recreate strategy, 60s grace)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:54:21 -04:00
4b63fec032 feat(helm): HPA for app deployment (toggleable, CPU-based)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:54:08 -04:00
02c8abb67b feat(helm): app Deployment (nginx + php-fpm sidecar)
Two-container pod sharing source via emptyDir populated by init
container. Nginx vhost in a separate ConfigMap. OAuth keys mounted
from the chart Secret as files under /var/www/html/secrets/, copied
into storage/ by the prod entrypoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:53:56 -04:00
fb50dae658 feat(helm): Service template (ClusterIP, port 80 → http)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:53:25 -04:00
a4b65e61d2 feat(helm): ConfigMap + Secret templates
ConfigMap renders all non-secret env vars including dynamic DB_HOST
and REDIS_HOST. Secret template only renders when secret.create=true
(dev convenience); production references an existing Secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:53:17 -04:00
9222c8e973 feat(helm): chart skeleton (Chart.yaml, values, helpers)
Initial scaffold for the ezscale-website chart. Defaults assume
self-contained local dev (in-cluster MariaDB + Valkey). Production
overrides will live in values-us-prod.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:52:53 -04:00
22d1ce3102 feat(docker): production multi-stage Dockerfile
Three named targets (app, horizon, scheduler) sharing a runtime-base
with PHP 8.3-FPM, opcache, redis, and pinned php-fpm pool config.
Composer + Node build stages are separate so vendor/ and public/build/
are baked into the runtime image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:50:18 -04:00
efe3fa53a4 docs: remove ipv4-outreach-tickets.txt; refresh GETTING_STARTED
Outreach notes don't belong in the repo. GETTING_STARTED reconciled
against current composer/npm scripts: fix Gitea clone URL, drop Vuexy
references, remove Redis requirement, replace multi-terminal startup
with `composer run dev`, update PHP/Node versions to 8.3/24, fix
branch workflow to main. TASKS.md: mark multi-currency and KB as done,
fix CI/CD reference from GitHub to Gitea Actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:40:18 -04:00
ab3a195e85 docs(readme): reconcile against current reality
Update codebase counts to live values, fix Gitea repo URL (was GitHub),
move multi-currency/KB from 'not yet implemented' to 'implemented',
refresh footer date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:37:53 -04:00
4a8a6f7564 docs(plan): k8s deployment implementation plan
19 bite-sized tasks covering README/docs cleanup, multi-stage prod
Dockerfile, Helm chart with all templates, values-local + values-us-prod,
Gitea Actions release workflow, and a local k3d e2e smoke test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:31:39 -04:00
bd7d99b8d1 docs(spec): k8s deployment design (Helm chart + production Dockerfile)
Locks in the production deployment shape: Helm chart matching sister
ezscale-api pattern, multi-stage Dockerfile with three targets
(app/horizon/scheduler), operator-managed MariaDB CRDs that plug into
the existing ezscale-namespace MariaDB instance, per-app Valkey,
Traefik IngressRoute + cert-manager TLS, Storj for file storage.

Critical invariant captured: APP_KEY and Passport keys are bootstrapped
once and never regenerated by the chart.

Two environments: local (k3d/minikube) and us-prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:23:56 -04:00
8a984f86ad docs(claude): expand strategic direction + sister-project context
Adds two sections to the project-level CLAUDE.md:

- "Strategic Direction: WHMCS Replacement" — scopes the long-arc plan
  (storefront, billing, provisioning, customer area, marketing site,
  admin RBAC, notifications, marketing email automation, future
  registrar / SSL / multi-tenant / partner API) so future sessions
  don't accidentally re-litigate decisions.

- "Sister Projects (Reference)" — calls out infrastructure/ (Ezra +
  capacity / costs source-of-truth) and ezscale_api/ (Battlelog/ACP
  SaaS) as separate Gitea repos with their own CLAUDE.md, plus the
  catalog-data flow (website pulls hardware inventory + IP
  availability from infrastructure/).

Also clarifies cross-repo conventions: Gitea (not GitHub), no shared
DBs, design docs under docs/superpowers/specs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:11:11 -04:00
39149f0b30 feat(dedicated): plan feature flags for cpu_premium + max_ram_gb
Adds two boolean/numeric feature flags on each 14th-gen plan:
- cpu_premium: chassis supports the Platinum 8280 CPU upgrade
  (R440/R540 false; R640/R740/R740xd true)
- max_ram_gb: maximum RAM the chassis can physically host
  (16-DIMM chassis cap at 1 TB; 24-DIMM at 1.5 TB)

These drive the route-level config-option filters in
routes/marketing.php so customers never see Platinum CPU on R440 or
the 1.5 TB RAM tier on a 16-slot chassis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:11:04 -04:00
dfdef3d7f4 feat: docker compose dev environment
Replaces the bare-metal `composer run dev` workflow with a fully
containerized 9-service stack orchestrated by docker compose. Single
command brings up the full app — three subdomains (marketing /
account / admin) reachable via Traefik with TLS, MariaDB + Valkey
+ Mailpit + Vite HMR + Horizon + scheduler all wired in.

Components:
- docker-compose.yml: traefik, app (php-fpm), web (nginx), mariadb,
  valkey, mailpit, vite, horizon, scheduler.
- docker/: Dockerfiles, nginx config, entrypoint scripts.
- Makefile: convenience targets (up / down / logs / shell / migrate
  / seed / test / pint / etc).
- .env.docker.example: template for Docker-stack environment vars
  (separate from website/.env so bare-metal devs aren't disrupted).
- website/vite.config.ts: server.host / origin / hmr / cors hooks
  driven by VITE_HOST / VITE_ORIGIN / VITE_HMR_HOST so the same
  config serves both bare-metal and Docker.
- website/bootstrap/app.php: redirectGuestsTo() now uses
  request()->getScheme() so http: dev hosts don't get force-https
  redirects.
- composer.json: drops laravel/sail (replaced by this stack).
- docs/superpowers/specs/2026-04-25-docker-compose-dev-environment-design.md:
  full design spec.

Bare-metal `composer run dev` workflow stays usable for anyone who
prefers it — Docker stack reads .env.docker, doesn't fight
website/.env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:10:53 -04:00
4b98e52043 chore: gitignore screenshots, scheduled-tasks lock, playwright cache
Working tree was accumulating debris across sessions:
- Headless-Chrome / visual-companion screenshots in repo root (dozens
  of throwaway PNGs from design iteration)
- .claude/scheduled_tasks.lock from autonomous-loop runs
- .playwright-mcp/ cache from the Playwright MCP server

Adds explicit ignores so these stay local. Intentionally archived
screenshots belong under docs/; the /*.png pattern only catches
root-level debris.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:10:27 -04:00
b5a4ba531c feat(dedicated): minimal OS variants, single-open accordion, default Alma 9
Three asks shipped together:

1. Default flipped from AlmaLinux 10 → AlmaLinux 9. Alma 10 stays in
   the picker but isn't pre-selected; 9 is the more battle-tested
   choice for production dedicated workloads.

2. Single-open accordion: openFamilies (Set<string>) → openFamily
   (string). Opening any family closes whichever was previously
   open. Click the open family's header to fully collapse. Watch
   on `props.selected` keeps the active selection's family open on
   first paint and on programmatic selection changes (URL hydration).
   Removed the "Expand all / Collapse all" toggle in the title row —
   redundant under single-open semantics.

3. "Minimal" image variants added for every distro that publishes one
   upstream: AlmaLinux / Rocky / Ubuntu / Debian / Fedora / openSUSE /
   FreeBSD. New labels add a clear "Minimal" suffix; new slugs use
   `-min` suffix (e.g. alma9-min, ubuntu24-min). Proxmox / Windows /
   "No OS" deliberately have no minimal variant — Proxmox is a
   single-flavor hypervisor, Windows is BYOL, "No OS" is a no-op.

Total OS count: 22 → 38 (across 9 families). Reseeded the OS group;
20/20 dedicated tests still pass; npm run build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:07:07 -04:00
bb04a5e3b9 chore(dedicated): refresh OS catalog to April 2026 currents + fix Proxmox color
OS list updated to reflect each project's actual current and supported
versions as of 2026-04 (verified per project release calendars):

Added:
- AlmaLinux 10 (RHEL 10 rebuild, default) — was missing
- Rocky Linux 10 — was missing
- Ubuntu 26.04 LTS (Resolute Raccoon, released Apr 2026) — was missing
- Debian 13 Trixie (current stable since Aug 2025) — was missing;
  bumped from oldstable Debian 12
- openSUSE Leap 16.0 (Oct 2025) — replaces 15.6 (EOLs Apr 30 2026)
- FreeBSD 15.0 (Dec 2025) and 14.4 (Mar 2026) — was a generic "14"
- Proxmox VE 9.1 (Nov 2025) — added alongside 8.4 (security maint
  through Aug 2026)

Removed:
- Debian 11 (LTS ends Aug 2026, dropped to avoid offering near-EOL)
- openSUSE Leap 15.6 (EOLs in 4 days)

Default OS flipped from AlmaLinux 9 → AlmaLinux 10 (current major).

Proxmox logo color: the Wikimedia CoreUI Proxmox SVG was monochrome
(inheriting black). Added fill="#E57000" so the brand orange is
correct.

22 OS options total across 9 distro families. metaFor() in
OsGroupSelector already handles all families via slug prefix —
no component changes needed for the new versions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:56:26 -04:00
c5e9bf594f feat(dedicated): authentic OS logos from Wikimedia + expand to 17 distros
Logo source swap:
Replaced all 9 OS brand SVGs with the actual icon-only files mirrored
on Wikimedia Commons (each project's official press kit). The
simple-icons rendering of AlmaLinux specifically was a generic
circles pattern — Wikimedia carries the real multicolor flame mark.
Same correction for Rocky (real green wedge logo), Fedora (proper F
infinity), Debian (bare swirl), Ubuntu (Circle of Friends in orange
hex), FreeBSD (horned daemon mark), Proxmox (CoreUI icon-only),
Windows (4-square 2021 mark), and openSUSE (chameleon button).
Hand-drawn no-os.svg stays — it's a generic terminal indicator,
no brand to source.

OS list expanded 14 → 17 (latest non-EOL versions only):
- Added: AlmaLinux 8, Rocky Linux 8, Ubuntu 22.04 LTS, Debian 11,
  Fedora 43, Fedora 44, openSUSE Leap 15.6, Windows Server 2025,
  Windows Server 2019.
- Removed: Fedora 41 (EOL'd Nov 2025).
- Default flipped from "No OS" to AlmaLinux 9 in the previous commit;
  unchanged here.

OsGroupSelector metaFor() gains an openSUSE family rank between
Fedora and FreeBSD. Reseeded the OS group; 20/20 dedicated tests
still pass; npm run build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:50:00 -04:00
2c85eba156 feat(dedicated): official OS logos + collapsible distro families
Two changes to the OS picker that should be felt together:

1. Swapped my hand-authored SVGs for official brand marks pulled from
   simple-icons (CC0). These are the actual path geometries used in
   each distro's brand kit, just colored to read on dark navy.
   AlmaLinux uses their accent orange (#FA9001) instead of the brand
   navy (#0E1F3D) so it's visible on our dark background.
   Affected: ubuntu, debian, almalinux, rocky, fedora, freebsd,
   proxmox. windows.svg and no-os.svg unchanged (geometric / generic).

2. Each distro family is now a collapsible accordion. The family
   containing the current selection auto-expands on mount; everything
   else collapses to a one-line row showing logo + family name +
   option count + chevron. Header gets a primary-color "selected"
   chip + tinted border when its family contains the active choice.
   "Expand all" / "Collapse all" toggle in the title row for power
   users; collapseAll() keeps the active selection's family open.

Net effect: the picker is ~1/4 of its previous height when only one
family is in use, and the official logos replace my approximations
(AlmaLinux flame mark is now correct, FreeBSD daemon is correct,
Proxmox four-square crown is correct, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:32:53 -04:00
dd8f83a990 polish(dedicated): drive bay title, HDD/SSD optgroups, OS expansion + grouping
Four customer-copy / UX cleanups bundled together:

1. Drive bay title strip — shortGroupLabel() collapses "LFF/SFF/NVMe
   Drive Bays" to just "Drive Bays" everywhere it surfaces (rail
   anchor, configurator section title, BuildSummary line item).
   Each chassis only ever shows one drive bay group, so the
   form-factor prefix was redundant noise.

2. HDD/SSD optgroups in Drive Selection — VSelect now interleaves
   VListSubheader rows ("HDDs", "SSDs", "NVMe") between options.
   Sentinel header values (`__hdr_<cat>`) are filtered in
   onDriveChange so a stray header click can't propagate.

3. OS list expansion — went from 6 entries to 14: added AlmaLinux 8,
   Rocky 8, Ubuntu 22.04 LTS, Debian 11, Fedora Server 41, FreeBSD 14,
   Proxmox VE 8, Windows Server 2019 (BYOL). Default flipped from
   "No OS" → "AlmaLinux 9" (matching what most dedicated buyers
   actually want — flag and revert via seeder if you'd rather keep
   bare-metal as the default).

4. OS picker grouped by distro — OsGroupSelector renders family
   sections (AlmaLinux, Rocky Linux, Ubuntu, Debian, Fedora,
   FreeBSD, Proxmox VE, Windows Server, Other) with a small
   uppercase heading above each row of tiles. metaFor() helper
   maps slug → family + logo path. New SVG logos for fedora,
   freebsd, proxmox; refined geometry on almalinux + rocky + debian.

Reseeded the OS group (deleted old 6 values, recreated 14 with new
ordering). 20/20 dedicated tests still pass. `npm run build` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:21:50 -04:00
f0df110b47 feat(dedicated): 3-column anchor rail layout for configurator
Switches the dedicated server detail page from 2-column (configurator
+ sticky summary) to 3-column (anchor rail | configurator | sticky
summary) — option D from the visual companion exploration. Power-user
shape: every section visible at once, rail tracks completion state,
sticky-on-both-sides keeps navigation + price always in reach.

- New ConfigSectionRail.vue: vertical list of section anchors
  (CPU, RAM, OS, Storage, Network, etc.) with three states
  (untouched, touched ✓, active ●). Click to smooth-scroll.
- Configurator wraps each group in <section :id="cfg-<slug>"> with
  scroll-margin-top: 92px to clear the navbar on scroll-into-view.
- IntersectionObserver in DedicatedConfigurator/index.vue updates
  store.activeAnchorId as sections cross the upper viewport.
  rootMargin '-92px 0px -65% 0px' picks the section nearest the top.
- Store: activeAnchorId reactive ref, isGroupTouched() helper
  (compares selection against seeded default; drive bay groups
  also require quantity > 0 to count), groupAnchorId() and
  shortGroupLabel() helpers.
- Detail-grid CSS: 180px | 1fr | 380px on desktop. Rail hides at
  ≤1280px (tablet keeps the summary). Full stack at ≤1024px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:05:37 -04:00
a224051bde feat(dedicated): dropdowns for radio groups, card-grid OS picker
Replaces stacked radio cards with VSelect dropdowns across CPU, RAM,
Bandwidth, IPv4, Private Networking, PCIe NVMe Add-in, and the Drive
Selection control inside each drive bay group. Major space savings —
the LFF Drive Selection alone goes from 15 stacked cards to one row
on screen, with the active price still visible at a glance via the
selection slot's right-aligned chip.

OS group becomes a tile-grid picker (`OsGroupSelector.vue`): 6 cards
with brand logos, distro name, and price chip. Logos shipped as
hand-authored SVGs at public/img/os/{ubuntu,debian,almalinux,rocky,
windows,no-os}.svg — no new npm dependency.

- Synchronous Pinia store init: moved store.init() out of onMounted
  into setup so children's `selected` props are populated on first
  render. Without this VSelect's selection slot fires with a stub
  item before init completes and the whole tree throws on a
  defensive `.toFixed` access.
- Defensive priceLabel guards in both OptionGroupSelector and
  DriveBayGroupSelector for any future re-render where the slot's
  raw item is incomplete.
- isOperatingSystemGroup() helper alongside isDriveBayGroup() in the
  store; configurator switches OS → OsGroupSelector, drive bays →
  DriveBayGroupSelector, everything else → OptionGroupSelector.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:48:53 -04:00
8be088e22a chore(dedicated): drop carrier jargon and mixed-size ticket copy
The drive bay UX leaked two more pieces of internal detail customers
shouldn't care about:

- "(LFF carrier)" suffix on every LFF SSD label (it's just a tray
  adapter so a 2.5" drive fits a 3.5" bay — internal mechanical
  detail, irrelevant to the customer's storage choice).
- "Mixed-size setups via post-order ticket" tail on the LFF/SFF/NVMe
  group descriptions, plus the entire "Need a custom drive layout?"
  card at the bottom of the detail page. We're not actually offering
  per-bay custom drive layouts as a service, so pitching it as a
  workflow was misleading.

- "No drives — configure via ticket" → "No drives" on the default
  value across all three drive bay groups.

Reseeded with `Drive Selection` value labels deleted+recreated since
the seeder keys on `[option_id, label]`. Internal value slugs
unchanged so share URLs still resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:28:35 -04:00
8cf9526bfc chore(dedicated): scrub vendor names from PCIe NVMe Add-in copy
The PCIe NVMe Add-in description and value labels exposed our adapter
SKUs (EC-PCIE / EC-P4BF), the vendor's product line names (Rocket 4
Plus / Rocket 5 Gen5), and PCIe lane / bifurcation jargon — none of
which the customer needs to see, and our standing rule is no vendor
names on public pages.

- Description rewritten in customer terms: positions the group as
  "fast scratch space or 4-drive bundle for more capacity" instead of
  explaining the adapter card mechanism.
- Value labels collapsed to "1× N TB M.2 NVMe (Gen4)" / "(Gen5)" /
  "(Gen4 bundle)" — keeps the generation distinction (which matters
  to customers) and the count, drops the vendor product names and
  adapter SKUs.

Internal value slugs (`1x1tb-r4p-pcie`, `4x1tb-r4p-p4bf`) left intact
so any in-flight share URLs and the seeder's update path don't break;
they're not customer-visible UI text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:23:22 -04:00
4833d667e3 feat(dedicated): drive bay Option B restructure (per-drive × quantity)
Replaces the flat-radio combo pattern in LFF/SFF/NVMe drive bay groups
with a Drive Selection (radio, per-drive cost) + Drive Quantity
(stepper) composite. Adds SAS HDD/SSD variants on LFF and SFF, and
collapses NVMe to enterprise U.2 sizes only.

- Seeder: rewrites the 3 drive bay groups; LFF goes from 35 flat combo
  values to 15 per-drive selections, SFF 8 → 8, NVMe 7 → 4. Adds
  SAS HDD (12/16 TB) and SAS SSD (1.92/3.84/7.68 TB) on LFF, SAS SSD
  trio on SFF, and 7.68 TB SATA SSD on both.
- Store: selections become Record<string, string | {drive,quantity}>;
  driveBayCost computed as drive_selection × quantity.
- DriveBayGroupSelector.vue: new composite component with stepper.
- BuildSummary: renders drive bay rows as "N× <drive> = $Y".
- Route filter: clamps Drive Quantity max_qty to chassis bay_count
  instead of filtering value slugs.
- URL contract: drive bay groups serialize as <prefix>_drive +
  <prefix>_qty (lff/sff/nvme).
- Tests: rewrites bay-count filter test, adds 5 new tests covering
  the two-option structure, SAS variants on LFF/SFF, NVMe enterprise
  sizes, and per-drive pricing alignment with the spec table.

Implements docs/superpowers/specs/2026-04-26-dedicated-drive-bays-option-b-design.md.

20/20 dedicated tests pass; 30/30 marketing tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:19:41 -04:00
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
c74ca7f554 feat(dedicated): add PCIe NVMe Add-in group (Sabrent adapter combos)
New separate-category config group for stacking high-performance
M.2 NVMe storage in any free PCIe slot, on top of the bay-attached
drives. Doesn't compete with bay drives — uses PCIe slots
independently.

Two adapter paths:
- Sabrent EC-PCIE (\$17.98) — single M.2 NVMe in any PCIe x4+ slot
- Sabrent EC-P4BF (\$99.99) — 4× M.2 NVMe in one PCIe 4.0 x16 slot,
  requires PCIe bifurcation support (R440/R540/R640/R740 verified;
  R740xd LFF rear PCIe may need check at build time)

10 curated combos + "None" default. Pricing: 12-month payback ×
1.5x markup on (Sabrent retail drive cost + adapter cost).

Single-drive (EC-PCIE):
  1× 1 TB Rocket 4 Plus     +\$35
  1× 2 TB Rocket 4 Plus     +\$40
  1× 4 TB Rocket 4 Plus     +\$115
  1× 8 TB Rocket 4 Plus     +\$465
  1× 2 TB Rocket 5 Gen5     +\$65
  1× 4 TB Rocket 5 Gen5     +\$180

4-drive bifurcation (EC-P4BF):
  4× 1 TB Rocket 4 Plus     +\$140
  4× 2 TB Rocket 4 Plus     +\$160
  4× 4 TB Rocket 4 Plus     +\$455
  4× 8 TB Rocket 4 Plus     +\$1,865 (extreme density, ~32 TB raw)

Drive prices scraped from Sabrent's Shopify product JSON
(/products/{slug}.json) on 2026-04-26.

Attached to LFF + SFF chassis (R440 / R540 / R640 / R740 /
R740xd SFF / R740xd LFF). Skip on R640 NVMe / R740xd NVMe — those
chassis route all PCIe lanes to the U.2 backplane, so no slots
are free for add-in cards.

Tests: bumped group count to 11; added a test verifying chassis
attachment correctness. 15/15 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:03:34 -04:00
168b06cd7d feat(dedicated): expand LFF Drive Bays with high-cap HDD + SSD options
Extended the LFF Drive Bays group from 7 → 35 entries to surface
real high-density configurations customers actually order.

High-capacity HDDs (12 new entries, prices set at ~12-month payback
× 1.5x markup on Server Part Deals Seagate Exos pricing scraped
2026-04-26):
  12 TB Enterprise HDD: ×2 \$90, ×4 \$180, ×8 \$360, ×12 \$540
  20 TB Enterprise HDD: ×2 \$110, ×4 \$220, ×8 \$440, ×12 \$660
  24 TB Enterprise HDD: ×2 \$150, ×4 \$300, ×8 \$600, ×12 \$900

LFF SSDs in 3.5" carriers (16 new entries) — 2.5" SATA/SAS SSDs
mounted via SaveMyServer adapters into LFF trays. Same per-drive
pricing model as the SFF group:
  480 GB SATA SSD: ×2 \$20, ×4 \$40, ×8 \$80, ×12 \$120
  1.92 TB SATA SSD: ×2 \$36, ×4 \$72, ×8 \$144, ×12 \$216
  3.84 TB SATA SSD: ×2 \$90, ×4 \$180, ×8 \$360, ×12 \$540
  7.68 TB SAS SSD: ×2 \$200, ×4 \$400, ×8 \$800, ×12 \$1,200

Existing chassis-bay-count filter at the route level keys on the
leading number in each value slug (e.g. "8x12tb-hdd" → 8 bays),
so combos that don't fit a chassis stay hidden — no extra logic
needed for the new entries.

Group description updated to reflect HDDs + SSDs both supported.
14/14 dedicated tests pass; pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:48:32 -04:00
f464e1ad48 feat(dedicated): bandwidth + private networking restructure
Bandwidth: dropped the "10 Gbps unmetered (fair-use)" tier — at our
\$295/mo it was a margin trap (Evocative bills 95th percentile at
\$0.48/Mbps; saturated 10G = \~\$4,560/mo cost). Replaced with a
"10 Gbps + 500 TB" tier at \$345/mo. 500 TB covers 99% of legitimate
heavy use; abuse customers self-select into a metered package or
get billed for overage.

New bandwidth ladder:
  1 Gbps unmetered (baseline)            \$0
  10 Gbps + 10 TB                        \$45
  10 Gbps + 50 TB                        \$95
  10 Gbps + 100 TB                       \$175
  10 Gbps + 500 TB  (NEW, replaces ∞)    \$345

Private Networking: new group, separate from public Bandwidth.
Customer can pick a private intra-rack link speed independent of
their public uplink. Traffic stays on our internal switch fabric
and isn't metered. Flat monthly per the brainstorm decision (no
setup fees).

  1 Gbit private (included, default)     \$0
  10 Gbit private                        \$25
  40 Gbit private                        \$75

Attached to all 8 14th-gen plans.

Updated test count: now 10 Dedicated 14th Gen config groups (was 9).
14/14 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:00:50 -04:00
d09224c35c chore(dedicated): bump RAM tier prices to per-stick economics
Recalibrated against Hetzner DX153 configurator data ($78/64GB-DDR5
module monthly) and OVH SYS-5 baseline (~\$0.42/GB monthly, but on
cheaper Silver-tier chassis without iDRAC9 Enterprise / BOSS — not
a 1:1 comp for our Gold-tier line).

New tier pricing at \$65/64GB-equivalent — sits ~17% under Hetzner's
premium tier and ~50% above OVH's budget-floor on a $/GB basis. At
this rate, 1 TB upgrade recoups our $6,240 hardware cost (16x
\$390/stick DDR4-2400 LRDIMM at 2026 Q2 shortage prices) inside
6 months of customer rental, with ~30% ongoing margin.

Per-tier changes:
- 64 GB:    +\$35 → +\$65   (+\$30/mo,  ~85% bump)
- 128 GB:   +\$90 → +\$195  (+\$105/mo, ~115% bump)
- 256 GB:   +\$195 → +\$260 (+\$65/mo,  ~33% bump)
- 512 GB:   +\$380 → +\$520 (+\$140/mo, ~37% bump)
- 1 TB:     +\$580 → +\$1,040 (+\$460/mo, ~80% bump)
- 1.5 TB:   +\$780 → +\$1,560 (+\$780/mo, ~100% bump)

The biggest jumps are at the high-density LRDIMM tiers where DDR4
EOL shortage hits hardest. Original pricing (set during the
brainstorm before the shortage data was researched) would have
left us underwater on hardware-cost recovery.

14/14 dedicated tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:57:37 -04:00
2658576c5b feat(dedicated): filter drive bay options to chassis-compatible combos
Per-chassis page hides drive combos that require more bays than the
chassis has. R440 (4-bay) no longer shows 8× / 12× options; R640
(8-bay SFF) no longer shows 16× / 24×; R640 NVMe (10-bay) hides
16× NVMe.

Implementation:
- routes/marketing.php /dedicated-servers/{slug}: post-load filter
  on configGroups. For any group whose name contains "Drive Bays",
  parses the leading bay-count digit from each value's slug
  (e.g. "8x8tb-hdd" → 8) and drops values where bay_count >
  chassis features.bay_count. Non-quantity values (e.g. "none")
  always pass through.
- ConfigOptionSeeder: dropped the now-redundant "(R740xd LFF
  only)"/"(R740 16-bay only)" parentheticals from the labels —
  the filter handles compatibility, the labels stay clean.

New test asserts:
- R440 (4 bays) drops 8× and 12× LFF combos
- R740xd LFF (12 bays) keeps all combos
- R640 NVMe (10 bays) drops 16× NVMe combo

13/13 dedicated tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:36:57 -04:00
2a9d36270a chore(dedicated): cut setup fees in half — competitive recalibration
Original tiers ($349/$549/$799) drifted into Hetzner Dell US
setup-fee territory ($840-$1,020) — too aggressive for a small
Atlanta provider competing against ColoCrossing ($0 setup) and
OVH SYS ($60-$343). The competitor research at
infrastructure/docs/competitors-atlanta-2026.md and
dedicated-server-pricing-2026q2.md both flagged "no setup fees"
as a documented competitive advantage.

New tiers ($149/$249/$399) preserve a meaningful safety net on
monthly customers (~1 month of rental recoups the fee) while
sitting inside the OVH SYS price band. Annual / Semi-Annual
customers still pay $0 setup.

Per-plan changes:
- R440 / R640 SFF (Tier 2): $349 → $149
- R540 / R740 / R740xd SFF / R740xd LFF (Tier 3): $549 → $249
- R640 NVMe / R740xd NVMe (Tier 4): $799 → $399

Spec doc updated. Test expectations adjusted; 12/12 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:34:30 -04:00
61afa4ed14 feat(dedicated): drive bay configurator + sticky-summary fix + reword setup fee
Three changes bundled:

1. Drive bay configurator (3 new ConfigOptionSeeder groups):
   - LFF Drive Bays (3.5") — 7 starter combos from "None" to
     12× 8 TB SATA HDD; attached to R440 / R540 / R740xd LFF
   - SFF Drive Bays (2.5") — 8 starter combos from "None" to
     24× 1.92 TB SATA SSD; attached to R640 / R740 / R740xd SFF
   - NVMe Drive Bays (U.2) — 7 starter combos from "None" to
     16× 2 TB U.2 NVMe; attached to R640 NVMe / R740xd NVMe
   Combos enforce chassis bay-count constraints via labels
   ("R740xd LFF only"); customers wanting heterogeneous setups
   use the post-order ticket flow.

2. Sticky build-summary fix: previously the BuildSummary card
   slid under the Vuetify navbar at scroll. Moved sticky from
   the inner card to the .detail-grid__summary wrapper, removed
   align-items: start so the right grid cell stretches to the
   configurator column's height (giving sticky a tall enough
   container), and offset top by 80px (64px navbar + 16px
   breathing room). Mobile path drops sticky entirely.

3. Setup fee reword — "Hardware acquisition" was leaking our
   cost structure and making the fee feel like procurement
   passthrough. Now reads "Server provisioning & deployment"
   in BuildSummary, and the FAQ describes what the fee covers
   (build, racking, iDRAC config, deployment) without exposing
   margins. Same shift across the non-refundable note: "once
   your build starts" instead of "once hardware is purchased."

Detail page bay-strategy callout updated: drive selection IS now
self-serve, so the callout pivots to "need a custom drive layout?"
pointing customers with mixed-size / hot-spare / RAID-preference
needs to the contact form.

Tests: updated count assertion to 9 groups, added a new test
verifying drive bay groups attach to chassis by bay type.
22/22 of my session's Pest tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:25:33 -04:00
be3eaba2a1 feat(dedicated): build summary sidebar + expanded CPU/IPv4 options
CPU upgrade ladder (standard, R440/R540/R640/R740) now 6 tiers:
- Gold 6230 (baseline, included)
- Gold 6244 high-clock (16C / 3.6 GHz, +$25/mo) — for per-core
  licensed workloads (MS SQL / Oracle) and single-threaded compute
- Gold 6248 (40C / 2.5 GHz, +$35/mo)
- Gold 6230R sweet-spot (52C / 2.1 GHz, +$50/mo) — Cascade Lake
  Refresh of the 6230, more cores at same clock, bridges baseline
  and 6248R
- Gold 6248R (48C / 3.0 GHz, +$75/mo)
- Gold 6258R (56C / 2.7 GHz, +$145/mo)

R740xd CPU ladder unchanged (6230 / 6248R / 6258R / Platinum 8280).

IPv4 block options extended to /24:
- /29 ($12) · /28 ($36) · /27 ($80) · /26 ($145) · /25 ($275) ·
  /24 ($499). All blocks above /29 require ARIN justification —
  the group description explains the policy and each tier's label
  carries a "justification required" tag.

Build summary sidebar replaces the bottom sticky footer on the
per-chassis page. New 2-column layout (configurator left, summary
right, sticky); collapses to single-column on tablet/mobile with
the summary stacked above the configurator so total stays visible.

The summary fixes the original "Total $468 billed monthly /
includes $349 setup" wording confusion by splitting into clearly
labeled sections:
- RECURRING: per-line itemized breakdown (baseline + each upgrade
  with its actual cycle-priced cost), subtotal in /mo or /yr suffix
- ONE-TIME: setup fee with non-refundable note (or strikethrough +
  "waived" badge when cycle is semi/annual)
- TOTAL: "First invoice $X" + "Then $Y/mo recurring" framing on
  monthly/quarterly cycles; "Total due today" + renewal preview on
  semi/annual

Removed: ConfiguratorFooter.vue (replaced by BuildSummary).
Pinned to top via position:sticky with viewport-height clamp +
internal scroll for tall configs. Order CTA + Copy share link
moved into the summary card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:19:04 -04:00
017b8b54c1 fix(dedicated): remove vendor name from public FAQ copy
Vendor sourcing is an internal procurement detail. Customer-facing
copy now reads "we source and assemble the chassis at our Atlanta
datacenter" — accurate and protects the supplier relationship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:08:05 -04:00
2f985ab8f3 test(dedicated): phase 4 — Pest feature tests for the new lineup
11 tests covering:
- Landing page returns 16 plans (8 14th-gen + 8 legacy)
- All 8 14th-gen chassis have correct setup_fee per tier mapping
  (R440 $349, R540 $549, R640 $349, R740 $549, R740xd $549,
   R740xd LFF $549, R640 NVMe $799, R740xd NVMe $799)
- Legacy 12th/13th-gen plans are active with $0 setup fee
- Per-chassis detail page renders for every 14th-gen slug
- 404 for invalid slugs
- R740xd variants get the R740xd-specific CPU group; non-xd
  chassis get the standard CPU group
- All 6 dedicated 14th-gen config groups exist after seeding
- RAM upgrade group standardizes on DDR4-2400 (per Q5 brainstorm)
- Checkout setupFee prop exposed correctly on dedicated plans
- Checkout setupFee is 0 on VPS plans

All 22 of my session's Pest tests pass (11 dedicated + 11 VPS
estimator). Pre-existing project test failures (DOMAIN_* mismatch
between hardcoded test URLs and docker dev env) are unrelated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:02:46 -04:00
311a4e961c feat(dedicated): phase 3 — frontend lineup pages + configurator
New pages:
- /dedicated-servers (rewrite): chassis grid with generation filter
  (All / Build to Order / In Stock — Rack inventory), hardware band,
  ColoCrossing-vs-EZSCALE comparison table, 8-question FAQ, custom-
  build CTA. Uses ChassisCard + GenerationFilter components.
- /dedicated-servers/{slug}: per-chassis page with locked baseline
  spec sidebar, configurator section (4-cycle toggle, 6 option
  groups via OptionGroupSelector, sticky ConfiguratorFooter with
  setup-fee waiver display), bay-strategy reminder card.

New shared components:
- Components/Marketing/Dedicated/
  - ChassisCard.vue — uniform card for both build-to-order and
    in-stock variants; differentiates with badge + border tone
  - GenerationFilter.vue — 3-option chip toggle with counts
  - BuildStatusPanel.vue — 5-stage timeline (Ordered → Hardware
    acquired → Assembly → Racked → Deployed) with editable prop
    for admin use; reused on customer service detail page
  - DedicatedConfigurator/
    - index.vue — orchestrator, mounts store, debounces URL push
    - CycleToggle.vue — 4 cycles (Monthly/Quarterly/Semi/Annual)
      with setup-waived badges on 6+ month tiers
    - OptionGroupSelector.vue — generic radio for any config group
    - ConfiguratorFooter.vue — sticky total + share link + order CTA

Pinia store:
- stores/dedicatedConfigurator.ts: per-chassis state (selections
  keyed by group name, cycle), getters for all sub-totals, setup-
  fee waiver logic, hydrateFromUrl + shareUrl + checkoutUrl. URL
  param shape: ?cycle=&cpu=&ram=&os=&bw=&ipv4= (only non-default
  values serialized).

Comparison rows are sourced from the freshly-landed competitor
research at infrastructure/docs/json/competitors-2026q2.json
and ovh-2026q2.json — focuses on hardware transparency, iDRAC9
inclusion, BOSS boot, setup-fee policy, and engineer-first support.

Drive picker descoped to v1.1 (per design spec): the configurator
captures CPU/RAM/OS/Bandwidth/IPv4 self-serve; drive selection is
handled via post-order ticket in v1. Bay-strategy callout on the
detail page sets that expectation.

npm run build clean; DedicatedServers + DedicatedServerDetail
bundles are 14.7 / 16.3 kB (gzipped 5.8 each). Visually verified
in the docker dev stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:01:12 -04:00
9c178f289c chore(dedicated): standardize 14th-gen RAM on DDR4-2400 ECC
Procurement decision: simpler single-DIMM-SKU sourcing across all
RAM tiers. The Gold 6230 IMC supports up to DDR4-2666, but
standardizing on 2400 keeps RDIMM ↔ LRDIMM transitions on the same
speed and means we don't need two separate procurement streams.

The ~10-18% memory bandwidth penalty vs 2666 is invisible for
typical hosting workloads (general compute, web/db, virtualization).
Memory-bandwidth-bound workloads (in-memory caches, ML inference)
can request a custom 2666 build via the contact form.

Updates:
- All 8 14th-gen plan rows: features.ram now reads
  "32 GB DDR4-2400 ECC RDIMM" (was 2666).
- Dedicated 14th Gen — RAM Upgrade group: each value now carries
  its actual DIMM type — RDIMM up through 128 GB, LRDIMM at
  256 GB and above. All speeds DDR4-2400.

Note: this diverges from infrastructure/docs/dedicated-server-
configurations.md which still lists 2666 (matches what
SaveMyServer's configurator ships). Reconcile upstream when
procurement is finalized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:52:49 -04:00
c7e545601e feat(dedicated): phase 2 — routes, configurator data, checkout setup fee
Routes:
- /dedicated-servers/{slug} (per-chassis detail) added alongside the
  existing /dedicated-servers landing. Landing route now eager-loads
  prices; detail route eager-loads chassis-specific config groups.

ConfigOptionSeeder — 6 new dedicated 14th-gen groups:
- CPU Upgrade (4 tiers; attached to R440/R540/R640/R740)
- CPU Upgrade R740xd (4 tiers, higher-TDP; attached to R740xd variants)
- RAM Upgrade (7 tiers, 32GB → 1.5TB)
- Operating System (6 options incl. Windows BYOL)
- Bandwidth (5 tiers, 1G unmetered → 10G unmetered fair-use)
- IPv4 Block (4 tiers, single → /27)
All idempotent via updateOrCreate, attached per chassis.

HandleSubscriptionCreated listener: build-to-order dedicated
plans (those with features.lead_time_days set) auto-create the
'ordered' build milestone. Other service types unaffected.

CheckoutController + Checkout/Show.vue:
- Pass plan.setup_fee as 'setupFee' Inertia prop
- Vue computes effectiveSetupFee (waived on semi_annual/annual,
  charged on monthly/quarterly per the brainstorm) and adds it
  to total. Display line-item still pending v3 polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:49:31 -04:00
c5fd4bcc7e feat(dedicated): phase 1 — backend data layer for new lineup
Migrations:
- add_setup_fee_to_plans_table: new decimal(10,2) default 0
- create_service_build_milestones_table: id, subscription_id,
  stage, reached_at, note, updated_by, timestamps; unique
  (subscription_id, stage); indexed (subscription_id, reached_at)

PlanSeeder:
- Removes the "archive 12th/13th-gen → hidden" block; flips all 8
  legacy slugs (dell-r330-lff..dell-r730-lff) to status=active so
  they surface as in-stock rack inventory alongside the new line.
- Replaces the 5-row 14th-gen placeholder with 8 proper SKUs:
  r440-4lff ($119, $349 setup), r540-8lff ($159, $549),
  r640-8sff ($179, $349), r740-16sff ($229, $549),
  r740xd-24sff ($279, $549), r740xd-12lff ($249, $549),
  r640-10nvme ($239, $799), r740xd-24nvme ($279, $799).
  Setup fees per the brainstorm tier mapping.
- Each new SKU carries full features (chassis, form_factor,
  bay_count, bay_type, locked baseline cpu/ram/boot/idrac/
  network/bandwidth, lead_time_days '7-10', tier).
- All 8 new SKUs have per-cycle prices (M/Q/Semi/A) at 5/10/15%
  discounts.

ServiceBuildMilestone model with STAGES const (ordered →
hardware_acquired → assembly → racked → deployed) and
subscription/updatedBy relations. Queries from this side until
we extend Cashier's Subscription model in a later phase.

Spec: docs/superpowers/specs/2026-04-26-dedicated-server-lineup-design.md

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