Added disks selection

This commit is contained in:
Raphaël Numbus
2026-03-29 16:12:49 +02:00
parent 068bf78cfb
commit 19b3588b2b
+141 -27
View File
@@ -385,6 +385,100 @@
</div>
</div>
<!-- Step 12: Disks -->
<div x-show="step === 12" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Disks</h2>
<p class="text-slate-400 mt-2 text-lg">Select where NixOS will be installed and where your data will live.</p>
</div>
<!-- Critical Warning -->
<div class="bg-red-500/10 border border-red-500/50 p-6 rounded-2xl flex gap-6 items-start">
<div class="bg-red-500 rounded-full p-2 mt-1 shrink-0">
<i class="mdi mdi-alert-octagon text-white text-xl"></i>
</div>
<div>
<h4 class="text-red-400 font-bold text-lg">Data Wipe Warning</h4>
<p class="text-sm text-red-200/80 leading-relaxed">Selecting a disk for Numbus will <strong>completely erase all existing data</strong> on that drive. Ensure you have backed up any important information before proceeding.</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
<!-- Boot Selection -->
<div class="space-y-6">
<h3 class="text-xl font-bold text-white flex items-center gap-2">
<i class="mdi mdi-boot text-sky-400"></i> Boot Disks
</h3>
<p class="text-sm text-slate-400">Choose 1 (Stripe) or 2 (Mirror) disks for the operating system.</p>
<div class="space-y-3">
<template x-for="disk in hardwareData.disks" :key="disk.id">
<button @click="toggleDisk(disk.id, 'boot')"
x-show="!formData.disks.data.includes(disk.id)"
:class="formData.disks.boot.includes(disk.id) ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700 opacity-60 hover:opacity-100'"
class="w-full p-4 border rounded-xl text-left transition-all group">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<i class="mdi mdi-harddisk text-2xl" :class="formData.disks.boot.includes(disk.id) ? 'text-white' : 'text-slate-500'"></i>
<div>
<div class="font-bold text-sm" x-text="disk.name"></div>
<div class="text-[10px] uppercase text-slate-500 font-bold" x-text="disk.size + ' • ' + disk.type"></div>
</div>
</div>
<div class="text-right">
<div class="text-[10px] font-bold" :class="disk.health === 'PASSED' ? 'text-green-500' : 'text-amber-500'" x-text="'Health: ' + disk.health"></div>
</div>
</div>
</button>
</template>
</div>
<!-- Boot Safety Warning -->
<div x-show="formData.disks.boot.length === 1" class="bg-amber-500/5 border border-amber-500/20 p-4 rounded-xl">
<p class="text-xs text-amber-200/70"><i class="mdi mdi-shield-alert mr-1"></i> <strong>Striped Config:</strong> Using only one boot disk provides no hardware redundancy. If this drive fails, the system will not boot.</p>
</div>
</div>
<!-- Data Selection -->
<div class="space-y-6">
<h3 class="text-xl font-bold text-white flex items-center gap-2">
<i class="mdi mdi-database text-sky-400"></i> Data Disks
</h3>
<p class="text-sm text-slate-400">Select disks for storage. SnapRAID parity is used if >1 disk is chosen.</p>
<div class="space-y-3">
<template x-for="disk in hardwareData.disks" :key="disk.id">
<button @click="toggleDisk(disk.id, 'data')"
x-show="!formData.disks.boot.includes(disk.id)"
:class="formData.disks.data.includes(disk.id) ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700 opacity-60 hover:opacity-100'"
class="w-full p-4 border rounded-xl text-left transition-all group">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3">
<i class="mdi mdi-server text-2xl" :class="formData.disks.data.includes(disk.id) ? 'text-white' : 'text-slate-500'"></i>
<div>
<div class="font-bold text-sm" x-text="disk.name"></div>
<div class="text-[10px] uppercase text-slate-500 font-bold" x-text="disk.size + ' • ' + disk.type"></div>
</div>
</div>
<div class="text-right">
<div class="text-[10px] font-bold" :class="disk.health === 'PASSED' ? 'text-green-500' : 'text-amber-500'" x-text="'Health: ' + disk.health"></div>
</div>
</div>
</button>
</template>
</div>
<!-- Data Safety Warning -->
<div x-show="formData.disks.data.length === 1" class="bg-amber-500/5 border border-amber-500/20 p-4 rounded-xl">
<p class="text-xs text-amber-200/70"><i class="mdi mdi-shield-alert mr-1"></i> <strong>Striped Config:</strong> One data disk provides no parity. Data loss will occur if this drive fails.</p>
</div>
<div x-show="formData.disks.data.length > 1" class="bg-green-500/5 border border-green-500/20 p-4 rounded-xl">
<p class="text-xs text-green-400/70"><i class="mdi mdi-shield-check mr-1"></i> <strong>SnapRAID Active:</strong> Your data is protected by parity drives.</p>
</div>
</div>
</div>
</div>
<!-- Step 6: Network -->
<div x-show="step === 6" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
@@ -672,8 +766,8 @@
</div>
</div>
<!-- Step 12: Non-Interactive Git Config -->
<div x-show="step === 12" class="space-y-8">
<!-- Step 13: Non-Interactive Git Config -->
<div x-show="step === 13" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Configuration Source</h2>
<p class="text-slate-400 mt-2 text-lg">Link your Git repository containing the numbus.yaml file.</p>
@@ -696,8 +790,8 @@
</div>
</div>
<!-- Step 13: Setup Type & Restore -->
<div x-show="step === 13" class="space-y-8">
<!-- Step 14: Setup Type & Restore -->
<div x-show="step === 14" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Setup Type</h2>
<p class="text-slate-400 mt-2 text-lg">Are we creating a new server instance or restoring an existing one?</p>
@@ -728,8 +822,8 @@
</div>
</div>
<!-- Step 14: Tweaks -->
<div x-show="step === 14" class="space-y-8">
<!-- Step 15: Tweaks -->
<div x-show="step === 15" class="space-y-8">
<div class="border-b border-slate-700 pb-4">
<h2 class="text-4xl font-bold text-sky-400">Environment Overrides</h2>
<p class="text-slate-400 mt-2 text-lg">Apply specific tweaks to this local deployment.</p>
@@ -831,8 +925,8 @@
</div>
</div>
<!-- Step 15: Installation Progress -->
<div x-show="step === 15" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<!-- Step 16: Installation Progress -->
<div x-show="step === 16" class="flex flex-col items-center justify-center py-20 text-center space-y-8">
<div class="w-full max-w-md bg-slate-900 rounded-full h-4 overflow-hidden border border-slate-700">
<div class="bg-fuchsia-600 h-full animate-pulse" style="width: 35%"></div>
</div>
@@ -851,8 +945,8 @@
</div>
</div>
<!-- Final Review (Step 16) -->
<div x-show="step === 16" class="space-y-8 text-center py-10">
<!-- Final Review (Step 17) -->
<div x-show="step === 17" class="space-y-8 text-center py-10">
<div class="w-20 h-20 bg-green-500/20 text-green-500 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>
</div>
@@ -874,7 +968,7 @@
>Back</button>
<button
x-show="step > 0 && step < maxSteps && ![3,4,15].includes(step)"
x-show="step > 0 && step < maxSteps && ![3,4,16].includes(step)"
@click="if(canGoNext()) step++"
:disabled="!canGoNext()"
:class="!canGoNext() ? 'opacity-30 cursor-not-allowed' : ''"
@@ -913,12 +1007,13 @@
function configurator() {
return {
step: 0,
maxSteps: 15,
maxSteps: 16,
hardwareData: { disks: [] },
init() {
// Watch the step and reset scroll position of the main content area
this.updateNavigation();
this.$watch('step', () => {
const container = this.$el.querySelector('.flex-grow.overflow-y-auto');
const container = this.$el.querySelector('.flex-grow');
if (container) {
container.scrollTop = 0;
}
@@ -935,10 +1030,10 @@
{ label: 'Live Setup', step: 3 },
{ label: 'Discovery', step: 4 }
]},
{ title: 'Server Configuration', steps: [5,6,7,8,9,10,11,12,13,14], minors: [
{ label: 'Configuration', step: 12 }
{ title: 'Server Configuration', steps: [5,6,7,8,9,10,11,12,13,14,15], minors: [
{ label: 'Configuration', step: 13 }
]},
{ title: 'Installation', steps: [15], minors: [
{ title: 'Installation', steps: [16], minors: [
{ label: 'Progress', step: 15 }
]}
],
@@ -960,11 +1055,14 @@
if (isServer) {
configMinors.push({ label: 'Alerts', step: 11 });
}
if (isServer) {
configMinors.push({ label: 'Disks', step: 12 });
}
} else {
configMinors = [
{ label: 'Source Repo', step: 12 },
{ label: 'Setup Type', step: 13 },
{ label: 'Environment Tweaks', step: 14 }
{ label: 'Source Repo', step: 13 },
{ label: 'Setup Type', step: 14 },
{ label: 'Environment Tweaks', step: 15 }
];
}
@@ -981,13 +1079,13 @@
},
{
title: 'Configuration',
steps: isInteractive ? [5, 6, 7, 8, 9, 10, 11] : [12, 13, 14],
steps: isInteractive ? [5, 6, 7, 8, 9, 10, 11, 12] : [13, 14, 15],
minors: configMinors
},
{
title: 'Finalize',
steps: [15, 16],
minors: [{ label: 'Deployment Status', step: 15 }]
steps: [16, 17],
minors: [{ label: 'Deployment Status', step: 16 }]
}
];
},
@@ -1064,6 +1162,10 @@
ssl: {
domain: '',
cloudflare_token: ''
},
disks: {
boot: [],
data: []
},
gitConfig: {
url: '',
@@ -1090,6 +1192,17 @@
}
}
},
toggleDisk(id, category) {
const list = this.formData.disks[category];
const index = list.indexOf(id);
if (index > -1) {
list.splice(index, 1);
} else {
if (category === 'boot' && list.length >= 2) return;
if (category === 'data' && list.length >= 9) return;
list.push(id);
}
},
addGroup() {
const name = this.newGroupName.trim().toLowerCase().replaceAll(' ', '_');
if (name && !this.formData.groups[name]) {
@@ -1165,7 +1278,7 @@
if (res.ok) {
this.hardwareData = await res.json();
clearInterval(timer);
this.step = (this.formData.deploymentMode === 'interactive') ? 5 : 12;
this.step = (this.formData.deploymentMode === 'interactive') ? 5 : 13;
}
} catch (e) { /* Bridge might not have written file yet */ }
}, 2000);
@@ -1184,10 +1297,11 @@
9: () => this.formData.security.ssh_keys.trim().length > 10 && this.formData.ssl.cloudflare_token.length > 10,
10: () => !!(this.formData.users[0].username && this.formData.users[0].name && this.validators.email(this.formData.users[0].email)),
11: () => this.validators.domain(this.formData.mail.smtp_host) && this.validators.port(this.formData.mail.smtp_port),
12: () => this.formData.gitConfig.url.length > 5,
13: () => !this.formData.restoreConfig.restore_data || (this.formData.restoreConfig.backup_host && this.formData.restoreConfig.setup_key),
14: () => true,
15: () => false
12: () => this.formData.disks.boot.length >= 1,
13: () => this.formData.gitConfig.url.length > 5,
14: () => !this.formData.restoreConfig.restore_data || (this.formData.restoreConfig.backup_host && this.formData.restoreConfig.setup_key),
15: () => true,
16: () => false
};
if (rules[this.step]) {