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>
This commit is contained in:
2026-04-26 22:07:07 -04:00
parent bb04a5e3b9
commit b5a4ba531c
2 changed files with 33 additions and 71 deletions

View File

@@ -464,23 +464,44 @@ class ConfigOptionSeeder extends Seeder
// FreeBSD 15.0 (Dec 2025), 14.4 (Mar 2026)
// Proxmox VE 9.1 (Nov 2025), 8.4 (security through Aug 2026)
// Windows Server 2025 (Nov 2024), 2022, 2019
// Each Linux distro has a "Minimal" variant where one is published
// upstream — stripped-down image with no GUI / dev tools / docs,
// intended for cloud / container / appliance workloads. Slugs use
// the `-min` suffix; family grouping in the picker keeps the pair
// adjacent. Proxmox / Windows / "No OS" don't have minimal variants.
$this->seedValues($gen14OsOption, [
['label' => 'AlmaLinux 10', 'value' => 'alma10', 'monthly' => 0, 'is_default' => true],
['label' => 'AlmaLinux 9', 'value' => 'alma9', 'monthly' => 0],
['label' => 'AlmaLinux 10', 'value' => 'alma10', 'monthly' => 0],
['label' => 'AlmaLinux 10 Minimal', 'value' => 'alma10-min', 'monthly' => 0],
['label' => 'AlmaLinux 9', 'value' => 'alma9', 'monthly' => 0, 'is_default' => true],
['label' => 'AlmaLinux 9 Minimal', 'value' => 'alma9-min', 'monthly' => 0],
['label' => 'AlmaLinux 8', 'value' => 'alma8', 'monthly' => 0],
['label' => 'AlmaLinux 8 Minimal', 'value' => 'alma8-min', 'monthly' => 0],
['label' => 'Rocky Linux 10', 'value' => 'rocky10', 'monthly' => 0],
['label' => 'Rocky Linux 10 Minimal', 'value' => 'rocky10-min', 'monthly' => 0],
['label' => 'Rocky Linux 9', 'value' => 'rocky9', 'monthly' => 0],
['label' => 'Rocky Linux 9 Minimal', 'value' => 'rocky9-min', 'monthly' => 0],
['label' => 'Rocky Linux 8', 'value' => 'rocky8', 'monthly' => 0],
['label' => 'Rocky Linux 8 Minimal', 'value' => 'rocky8-min', 'monthly' => 0],
['label' => 'Ubuntu 26.04 LTS', 'value' => 'ubuntu26', 'monthly' => 0],
['label' => 'Ubuntu 26.04 LTS Minimal', 'value' => 'ubuntu26-min', 'monthly' => 0],
['label' => 'Ubuntu 24.04 LTS', 'value' => 'ubuntu24', 'monthly' => 0],
['label' => 'Ubuntu 24.04 LTS Minimal', 'value' => 'ubuntu24-min', 'monthly' => 0],
['label' => 'Ubuntu 22.04 LTS', 'value' => 'ubuntu22', 'monthly' => 0],
['label' => 'Ubuntu 22.04 LTS Minimal', 'value' => 'ubuntu22-min', 'monthly' => 0],
['label' => 'Debian 13 (Trixie)', 'value' => 'debian13', 'monthly' => 0],
['label' => 'Debian 13 Minimal', 'value' => 'debian13-min', 'monthly' => 0],
['label' => 'Debian 12 (Bookworm)', 'value' => 'debian12', 'monthly' => 0],
['label' => 'Debian 12 Minimal', 'value' => 'debian12-min', 'monthly' => 0],
['label' => 'Fedora Server 44', 'value' => 'fedora44', 'monthly' => 0],
['label' => 'Fedora Server 44 Minimal', 'value' => 'fedora44-min', 'monthly' => 0],
['label' => 'Fedora Server 43', 'value' => 'fedora43', 'monthly' => 0],
['label' => 'Fedora Server 43 Minimal', 'value' => 'fedora43-min', 'monthly' => 0],
['label' => 'openSUSE Leap 16.0', 'value' => 'opensuse-leap-160', 'monthly' => 0],
['label' => 'openSUSE Leap 16.0 Minimal', 'value' => 'opensuse-leap-160-min', 'monthly' => 0],
['label' => 'FreeBSD 15.0', 'value' => 'freebsd15', 'monthly' => 0],
['label' => 'FreeBSD 15.0 Minimal', 'value' => 'freebsd15-min', 'monthly' => 0],
['label' => 'FreeBSD 14.4', 'value' => 'freebsd14', 'monthly' => 0],
['label' => 'FreeBSD 14.4 Minimal', 'value' => 'freebsd14-min', 'monthly' => 0],
['label' => 'Proxmox VE 9.1', 'value' => 'proxmox9', 'monthly' => 0],
['label' => 'Proxmox VE 8.4', 'value' => 'proxmox8', 'monthly' => 0],
['label' => 'Windows Server 2025 (BYOL)', 'value' => 'windows-2025-byol', 'monthly' => 0],

View File

@@ -56,34 +56,28 @@ const familyGroups = computed<FamilyGroup[]>(() => {
return Array.from(map.values()).sort((a, b) => a.rank - b.rank)
})
// Set of family names that are currently expanded. By default we open
// only the family that contains the current selection — keeps the picker
// compact while still showing the active choice.
const openFamilies = ref<Set<string>>(new Set())
// Single-open accordion: only one family expanded at a time. Defaults to
// whichever family contains the current selection so the user always sees
// their pick on first paint. Click the open family's header to close it
// (no family expanded), or click another to swap.
const openFamily = ref<string>('')
watch(
() => props.selected,
(slug) => {
if (!slug) return
const meta = metaFor(slug)
if (!openFamilies.value.has(meta.family)) {
const next = new Set(openFamilies.value)
next.add(meta.family)
openFamilies.value = next
}
openFamily.value = meta.family
},
{ immediate: true },
)
function toggleFamily(name: string): void {
const next = new Set(openFamilies.value)
if (next.has(name)) next.delete(name)
else next.add(name)
openFamilies.value = next
openFamily.value = openFamily.value === name ? '' : name
}
function isFamilyOpen(name: string): boolean {
return openFamilies.value.has(name)
return openFamily.value === name
}
function selectedInFamily(fam: FamilyGroup): DedicatedConfigValue | null {
@@ -120,38 +114,12 @@ function priceLabel(v: DedicatedConfigValue): string {
function logoFor(slug: string): string {
return metaFor(slug).logo
}
function expandAll(): void {
openFamilies.value = new Set(familyGroups.value.map(f => f.family))
}
function collapseAll(): void {
// Keep the family with the selection open so the user always sees their pick.
const meta = props.selected ? metaFor(props.selected) : null
openFamilies.value = new Set(meta ? [meta.family] : [])
}
const allOpen = computed<boolean>(() => openFamilies.value.size === familyGroups.value.length)
</script>
<template>
<div class="os-group">
<div class="os-group__head">
<div class="os-group__head-row">
<h4 class="os-group__title">{{ shortGroupLabel(group.name) }}</h4>
<button
type="button"
class="os-group__expand-toggle"
@click="allOpen ? collapseAll() : expandAll()"
>
<VIcon
:icon="allOpen ? 'tabler-fold' : 'tabler-fold-up'"
size="14"
class="me-1"
/>
{{ allOpen ? 'Collapse all' : 'Expand all' }}
</button>
</div>
<h4 class="os-group__title">{{ shortGroupLabel(group.name) }}</h4>
<p v-if="group.description" class="os-group__desc">{{ group.description }}</p>
</div>
@@ -227,38 +195,11 @@ const allOpen = computed<boolean>(() => openFamilies.value.size === familyGroups
margin-bottom: 14px;
}
.os-group__head-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.os-group__title {
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
margin-bottom: 0;
}
.os-group__expand-toggle {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
color: rgba(var(--v-theme-on-surface), 0.6);
background: transparent;
border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background-color 0.15s;
&:hover {
color: rgba(var(--v-theme-on-surface), 0.95);
border-color: rgba(var(--v-theme-primary), 0.45);
background: rgba(var(--v-theme-primary), 0.06);
}
margin-bottom: 4px;
}
.os-group__desc {