diff --git a/app/Livewire/Admin/Datacenters.php b/app/Livewire/Admin/Datacenters.php new file mode 100644 index 0000000..b91a6ac --- /dev/null +++ b/app/Livewire/Admin/Datacenters.php @@ -0,0 +1,49 @@ +validate(); + + Datacenter::create([ + 'code' => strtolower($data['code']), + 'name' => $data['name'], + 'location' => $data['location'] ?: null, + 'active' => true, + ]); + + $this->reset('code', 'name', 'location'); + $this->dispatch('notify', message: __('datacenters.created')); + } + + public function toggle(string $uuid): void + { + $datacenter = Datacenter::query()->where('uuid', $uuid)->first(); + $datacenter?->update(['active' => ! $datacenter->active]); + } + + public function render() + { + return view('livewire.admin.datacenters', [ + 'datacenters' => Datacenter::query()->withCount('hosts')->orderBy('name')->get(), + ]); + } +} diff --git a/app/Livewire/Admin/HostCreate.php b/app/Livewire/Admin/HostCreate.php index 9c07e2e..9aeaf76 100644 --- a/app/Livewire/Admin/HostCreate.php +++ b/app/Livewire/Admin/HostCreate.php @@ -13,8 +13,13 @@ class HostCreate extends Component #[Validate('required|string|max:255')] public string $name = ''; - #[Validate('required|string|in:fsn,hel')] - public string $datacenter = 'fsn'; + #[Validate('required|string|exists:datacenters,code')] + public string $datacenter = ''; + + public function mount(): void + { + $this->datacenter = (string) \App\Models\Datacenter::query()->active()->orderBy('name')->value('code'); + } #[Validate('required|ip|unique:hosts,public_ip')] public string $public_ip = ''; @@ -34,7 +39,7 @@ class HostCreate extends Component public function render() { return view('livewire.admin.host-create', [ - 'datacenters' => ['fsn', 'hel'], + 'datacenters' => \App\Models\Datacenter::query()->active()->orderBy('name')->get(), ]); } } diff --git a/app/Models/Datacenter.php b/app/Models/Datacenter.php new file mode 100644 index 0000000..862a30b --- /dev/null +++ b/app/Models/Datacenter.php @@ -0,0 +1,31 @@ + */ + use HasFactory, HasUuid; + + protected $fillable = ['code', 'name', 'location', 'active']; + + protected function casts(): array + { + return ['active' => 'boolean']; + } + + public function scopeActive(Builder $query): Builder + { + return $query->where('active', true); + } + + public function hosts() + { + return $this->hasMany(Host::class, 'datacenter', 'code'); + } +} diff --git a/database/factories/DatacenterFactory.php b/database/factories/DatacenterFactory.php new file mode 100644 index 0000000..0825956 --- /dev/null +++ b/database/factories/DatacenterFactory.php @@ -0,0 +1,22 @@ + */ +class DatacenterFactory extends Factory +{ + protected $model = Datacenter::class; + + public function definition(): array + { + return [ + 'code' => $this->faker->unique()->lexify('dc?'), + 'name' => $this->faker->city(), + 'location' => 'DE', + 'active' => true, + ]; + } +} diff --git a/database/migrations/2026_07_25_090001_create_datacenters_table.php b/database/migrations/2026_07_25_090001_create_datacenters_table.php new file mode 100644 index 0000000..4948d35 --- /dev/null +++ b/database/migrations/2026_07_25_090001_create_datacenters_table.php @@ -0,0 +1,26 @@ +id(); + $table->uuid('uuid')->unique(); + $table->string('code')->unique(); // e.g. fsn, hel — used by hosts.datacenter + placement + $table->string('name'); + $table->string('location')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('datacenters'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cd81d93..fca0c13 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,6 +3,7 @@ namespace Database\Seeders; use App\Models\Customer; +use App\Models\Datacenter; use App\Models\Host; use App\Models\Instance; use App\Models\Order; @@ -42,6 +43,11 @@ class DatabaseSeeder extends Seeder ], ); + // Datacenters — hosts + orders pick their code. + foreach ([['fsn', 'Falkenstein', 'DE'], ['hel', 'Helsinki', 'FI']] as [$dcCode, $dcName, $dcLocation]) { + Datacenter::updateOrCreate(['code' => $dcCode], ['name' => $dcName, 'location' => $dcLocation, 'active' => true]); + } + // Demo fleet so the operator console hosts view has content locally. $fleet = [ ['name' => 'pve-fsn-1', 'datacenter' => 'fsn', 'public_ip' => '203.0.113.11', 'wg_ip' => '10.66.0.2'], diff --git a/lang/de/admin.php b/lang/de/admin.php index 0285743..63f26e4 100644 --- a/lang/de/admin.php +++ b/lang/de/admin.php @@ -10,6 +10,7 @@ return [ 'customers' => 'Kunden', 'instances' => 'Instanzen', 'hosts' => 'Hosts', + 'datacenters' => 'Rechenzentren', 'provisioning' => 'Provisioning', 'revenue' => 'Umsatz', ], diff --git a/lang/de/datacenters.php b/lang/de/datacenters.php new file mode 100644 index 0000000..3aa5d8f --- /dev/null +++ b/lang/de/datacenters.php @@ -0,0 +1,18 @@ + 'Rechenzentren', + 'subtitle' => 'Standorte anlegen und verwalten — Hosts und Bestellungen wählen daraus.', + 'empty' => 'Noch keine Rechenzentren. Lege dein erstes an.', + 'code' => 'Kürzel', + 'code_hint' => 'Kurz & eindeutig, z. B. fsn, hel, nbg.', + 'name' => 'Name', + 'location' => 'Standort', + 'location_hint' => 'Optional, z. B. DE, Falkenstein.', + 'hosts' => 'Hosts', + 'status' => 'Status', + 'active' => 'Aktiv', + 'inactive' => 'Inaktiv', + 'add' => 'Rechenzentrum anlegen', + 'created' => 'Rechenzentrum angelegt.', +]; diff --git a/lang/en/admin.php b/lang/en/admin.php index 5c59b21..1e4fab9 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -10,6 +10,7 @@ return [ 'customers' => 'Customers', 'instances' => 'Instances', 'hosts' => 'Hosts', + 'datacenters' => 'Datacenters', 'provisioning' => 'Provisioning', 'revenue' => 'Revenue', ], diff --git a/lang/en/datacenters.php b/lang/en/datacenters.php new file mode 100644 index 0000000..7fada6c --- /dev/null +++ b/lang/en/datacenters.php @@ -0,0 +1,18 @@ + 'Datacenters', + 'subtitle' => 'Create and manage locations — hosts and orders pick from these.', + 'empty' => 'No datacenters yet. Add your first one.', + 'code' => 'Code', + 'code_hint' => 'Short & unique, e.g. fsn, hel, nbg.', + 'name' => 'Name', + 'location' => 'Location', + 'location_hint' => 'Optional, e.g. DE, Falkenstein.', + 'hosts' => 'Hosts', + 'status' => 'Status', + 'active' => 'Active', + 'inactive' => 'Inactive', + 'add' => 'Add datacenter', + 'created' => 'Datacenter created.', +]; diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 7cf5d2f..7b90b33 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -30,6 +30,7 @@ ['admin.customers', 'users', 'customers'], ['admin.instances', 'box', 'instances'], ['admin.hosts', 'server', 'hosts'], + ['admin.datacenters', 'database', 'datacenters'], ['admin.provisioning', 'activity', 'provisioning'], ['admin.revenue', 'trending-up', 'revenue'], ] as [$route, $icon, $key]) diff --git a/resources/views/livewire/admin/datacenters.blade.php b/resources/views/livewire/admin/datacenters.blade.php new file mode 100644 index 0000000..e97a244 --- /dev/null +++ b/resources/views/livewire/admin/datacenters.blade.php @@ -0,0 +1,56 @@ +
{{ __('datacenters.subtitle') }}
+{{ __('datacenters.empty') }}
+ @else +| {{ __('datacenters.code') }} | +{{ __('datacenters.name') }} | +{{ __('datacenters.hosts') }} | +{{ __('datacenters.status') }} | +
|---|---|---|---|
| {{ $dc->code }} | +{{ $dc->name }} + @if ($dc->location)· {{ $dc->location }}@endif + | +{{ $dc->hosts_count }} | ++ + | +
{{ $message }}
@enderror diff --git a/resources/views/livewire/admin/host-detail.blade.php b/resources/views/livewire/admin/host-detail.blade.php index e64df21..6b20d0b 100644 --- a/resources/views/livewire/admin/host-detail.blade.php +++ b/resources/views/livewire/admin/host-detail.blade.php @@ -12,7 +12,7 @@{{ $host->public_ip }} · {{ __('hosts.datacenter.'.$host->datacenter) }}
+{{ $host->public_ip }} · {{ $host->datacenter }}
{{ $host->name }}
-{{ $host->public_ip }} · {{ __('hosts.datacenter.'.$host->datacenter) }}
+{{ $host->public_ip }} · {{ $host->datacenter }}