Domain models: Cluster/Server/Site/Backup/TeamMember/Invitation/SecurityEvent/Certificate/Activity/Cron

Full Eloquent foundation matching template data:
- 10 migrations (clusters, servers, sites, backups, team_members,
  invitations, security_events, certificates, activities, crons).
- 10 models with relationships + casts + accessors.
- 10 factories.
- DatabaseSeeder seeds matching template stats:
  - Marie Weber (m.weber@acme-cluster.de / clupilot) as owner
  - Admin (admin@clupilot.test / clupilot)
  - 1 Acme Cluster, 6 named servers (eu-prod-01/-02, edge-ams,
    us-east-01, staging, ap-bom-01) with CPU/RAM/disk loads from template
  - 87 sites distributed [23,19,14,18,6,7] per server
  - 102 backups, 87 SSL certificates, 47 cronjobs
  - 128 security events (matches template 'Blocked / 24h')
  - 5 team members + 3 pending invitations
  - 30 activity feed entries.
master
Boban Blaskovic 2026-05-31 22:33:09 +02:00
parent 78edcef3da
commit 7276e27408
31 changed files with 1058 additions and 17 deletions

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Activity extends Model
{
use HasFactory;
protected $fillable = [
'user_id', 'type', 'level',
'message', 'subject', 'meta', 'occurred_at',
];
protected $casts = [
'meta' => 'array',
'occurred_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

31
app/app/Models/Backup.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Backup extends Model
{
use HasFactory;
protected $fillable = [
'site_id', 'type', 'size_bytes', 'status',
'storage_location', 'completed_at',
];
protected $casts = [
'completed_at' => 'datetime',
];
public function site(): BelongsTo
{
return $this->belongsTo(Site::class);
}
public function getSizeMbAttribute(): float
{
return round($this->size_bytes / 1024 / 1024, 1);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Certificate extends Model
{
use HasFactory;
protected $fillable = [
'site_id', 'domain', 'issuer',
'issued_at', 'expires_at', 'auto_renew',
];
protected $casts = [
'issued_at' => 'datetime',
'expires_at' => 'datetime',
'auto_renew' => 'bool',
];
public function site(): BelongsTo
{
return $this->belongsTo(Site::class);
}
public function getDaysToExpiryAttribute(): int
{
return $this->expires_at ? max(0, (int) now()->diffInDays($this->expires_at, false)) : 0;
}
public function getStatusAttribute(): string
{
$days = $this->days_to_expiry;
return match (true) {
$days <= 7 => 'critical',
$days <= 30 => 'warning',
default => 'ok',
};
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Cluster extends Model
{
use HasFactory;
protected $fillable = [
'name', 'slug', 'region', 'env', 'monthly_price_cents',
'node_count', 'backups_enabled',
];
protected $casts = [
'backups_enabled' => 'bool',
];
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function getMonthlyPriceAttribute(): float
{
return $this->monthly_price_cents / 100;
}
}

27
app/app/Models/Cron.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Cron extends Model
{
use HasFactory;
protected $fillable = [
'site_id', 'name', 'expression', 'command',
'last_run_at', 'status', 'enabled',
];
protected $casts = [
'last_run_at' => 'datetime',
'enabled' => 'bool',
];
public function site(): BelongsTo
{
return $this->belongsTo(Site::class);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class Invitation extends Model
{
use HasFactory;
protected $fillable = [
'email', 'role', 'invited_by_user_id',
'token', 'sent_at', 'accepted_at', 'expires_at',
];
protected $casts = [
'sent_at' => 'datetime',
'accepted_at' => 'datetime',
'expires_at' => 'datetime',
];
protected static function booted(): void
{
static::creating(function (Invitation $i) {
$i->token = $i->token ?: Str::random(48);
$i->expires_at = $i->expires_at ?: now()->addDays(7);
});
}
public function invitedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by_user_id');
}
public function getIsPendingAttribute(): bool
{
return is_null($this->accepted_at) && (! $this->expires_at || $this->expires_at->isFuture());
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SecurityEvent extends Model
{
use HasFactory;
protected $fillable = [
'site_id', 'type', 'severity', 'blocked',
'source_ip', 'country', 'message', 'meta', 'occurred_at',
];
protected $casts = [
'blocked' => 'bool',
'meta' => 'array',
'occurred_at' => 'datetime',
];
public function site(): BelongsTo
{
return $this->belongsTo(Site::class);
}
}

30
app/app/Models/Server.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Server extends Model
{
use HasFactory;
protected $fillable = [
'cluster_id', 'name', 'region', 'country_flag', 'env',
'cpu_cores', 'ram_gb', 'disk_gb',
'cpu_load_pct', 'ram_used_pct', 'disk_used_pct',
'status', 'ip',
];
public function cluster(): BelongsTo
{
return $this->belongsTo(Cluster::class);
}
public function sites(): HasMany
{
return $this->hasMany(Site::class);
}
}

64
app/app/Models/Site.php Normal file
View File

@ -0,0 +1,64 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Site extends Model
{
use HasFactory;
protected $fillable = [
'server_id', 'domain', 'name',
'wp_version', 'php_version', 'status',
'visitors_24h', 'response_ms',
'https_enabled', 'cache_enabled', 'cdn_enabled', 'maintenance_mode',
'pagespeed_score', 'updates_pending',
];
protected $casts = [
'https_enabled' => 'bool',
'cache_enabled' => 'bool',
'cdn_enabled' => 'bool',
'maintenance_mode' => 'bool',
];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function backups(): HasMany
{
return $this->hasMany(Backup::class);
}
public function latestBackup(): HasOne
{
return $this->hasOne(Backup::class)->latestOfMany('completed_at');
}
public function certificate(): HasOne
{
return $this->hasOne(Certificate::class);
}
public function crons(): HasMany
{
return $this->hasMany(Cron::class);
}
public function securityEvents(): HasMany
{
return $this->hasMany(SecurityEvent::class);
}
public function getInitialAttribute(): string
{
return mb_strtoupper(mb_substr($this->name ?? $this->domain, 0, 1));
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TeamMember extends Model
{
use HasFactory;
protected $fillable = [
'user_id', 'role', 'scope', 'two_fa_enabled', 'last_active_at',
];
protected $casts = [
'two_fa_enabled' => 'bool',
'last_active_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use App\Models\Activity;
use Illuminate\Database\Eloquent\Factories\Factory;
class ActivityFactory extends Factory
{
protected $model = Activity::class;
public function definition(): array
{
return [
'user_id' => null,
'type' => $this->faker->randomElement(['deploy', 'backup', 'update', 'security', 'system']),
'level' => $this->faker->randomElement(['info', 'success', 'success', 'warning', 'error']),
'message' => $this->faker->sentence(6),
'subject' => $this->faker->domainName(),
'meta' => null,
'occurred_at' => $this->faker->dateTimeBetween('-3 days', 'now'),
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Models\Backup;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
class BackupFactory extends Factory
{
protected $model = Backup::class;
public function definition(): array
{
$completedAt = $this->faker->dateTimeBetween('-7 days', 'now');
return [
'site_id' => Site::factory(),
'type' => $this->faker->randomElement(['hourly', 'daily', 'manual', 'snapshot']),
'size_bytes' => $this->faker->numberBetween(50_000_000, 2_000_000_000),
'status' => $this->faker->randomElement(['ok', 'ok', 'ok', 'failed']),
'storage_location' => 's3://clupilot-backups',
'completed_at' => $completedAt,
];
}
public function failed(): self
{
return $this->state(['status' => 'failed']);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Models\Certificate;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
class CertificateFactory extends Factory
{
protected $model = Certificate::class;
public function definition(): array
{
$issued = $this->faker->dateTimeBetween('-60 days', '-1 days');
return [
'site_id' => Site::factory(),
'domain' => $this->faker->domainName(),
'issuer' => "Let's Encrypt",
'issued_at' => $issued,
'expires_at' => (clone $issued)->modify('+90 days'),
'auto_renew' => true,
];
}
public function expiringSoon(): self
{
return $this->state(['expires_at' => now()->addDays(5)]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Models\Cluster;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class ClusterFactory extends Factory
{
protected $model = Cluster::class;
public function definition(): array
{
$name = $this->faker->unique()->company();
return [
'name' => $name,
'slug' => Str::slug($name),
'region' => $this->faker->randomElement(['eu-central-1', 'eu-west-1', 'us-east-1', 'ap-south-1']),
'env' => $this->faker->randomElement(['prod', 'staging', 'edge', 'dev']),
'monthly_price_cents' => $this->faker->numberBetween(2900, 49900),
'node_count' => $this->faker->numberBetween(1, 6),
'backups_enabled' => true,
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Cron;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
class CronFactory extends Factory
{
protected $model = Cron::class;
public function definition(): array
{
return [
'site_id' => Site::factory(),
'name' => $this->faker->randomElement(['Backup nightly', 'Cache flush', 'WP cron', 'Sitemap rebuild']),
'expression' => $this->faker->randomElement(['0 * * * *', '*/15 * * * *', '0 2 * * *', '*/5 * * * *']),
'command' => 'php /var/www/html/artisan ' . $this->faker->randomElement(['queue:work', 'cache:clear', 'schedule:run']),
'last_run_at' => $this->faker->dateTimeBetween('-1 hour', 'now'),
'status' => $this->faker->randomElement(['ok', 'ok', 'ok', 'failing']),
'enabled' => true,
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\Invitation;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class InvitationFactory extends Factory
{
protected $model = Invitation::class;
public function definition(): array
{
return [
'email' => $this->faker->unique()->safeEmail(),
'role' => $this->faker->randomElement(['admin', 'developer', 'viewer']),
'invited_by_user_id' => User::factory(),
'sent_at' => now(),
'expires_at' => now()->addDays(7),
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\SecurityEvent;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
class SecurityEventFactory extends Factory
{
protected $model = SecurityEvent::class;
public function definition(): array
{
return [
'site_id' => Site::factory(),
'type' => $this->faker->randomElement(['brute_force', 'tor_exit', 'malware', 'firewall', 'scan']),
'severity' => $this->faker->randomElement(['info', 'low', 'medium', 'high', 'critical']),
'blocked' => true,
'source_ip' => $this->faker->ipv4(),
'country' => $this->faker->countryCode(),
'message' => $this->faker->sentence(8),
'meta' => null,
'occurred_at' => $this->faker->dateTimeBetween('-24 hours', 'now'),
];
}
public function critical(): self
{
return $this->state(['severity' => 'critical']);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\Cluster;
use App\Models\Server;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServerFactory extends Factory
{
protected $model = Server::class;
public function definition(): array
{
$env = $this->faker->randomElement(['prod', 'staging', 'edge']);
return [
'cluster_id' => Cluster::factory(),
'name' => $env . '-' . $this->faker->unique()->numerify('node-##'),
'region' => $this->faker->randomElement(['eu-central-1', 'eu-west-1', 'us-east-1', 'ap-south-1']),
'country_flag' => $this->faker->randomElement(['🇩🇪', '🇳🇱', '🇮🇪', '🇺🇸', '🇮🇳']),
'env' => $env,
'cpu_cores' => $this->faker->randomElement([4, 8, 16]),
'ram_gb' => $this->faker->randomElement([16, 32, 64, 128]),
'disk_gb' => $this->faker->randomElement([160, 320, 640, 1280]),
'cpu_load_pct' => $this->faker->numberBetween(8, 78),
'ram_used_pct' => $this->faker->numberBetween(15, 80),
'disk_used_pct' => $this->faker->numberBetween(20, 75),
'status' => $this->faker->randomElement(['ok', 'ok', 'ok', 'warn']),
'ip' => $this->faker->ipv4(),
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
class SiteFactory extends Factory
{
protected $model = Site::class;
public function definition(): array
{
$domain = $this->faker->unique()->domainName();
return [
'server_id' => Server::factory(),
'domain' => $domain,
'name' => ucfirst(explode('.', $domain)[0]),
'wp_version' => $this->faker->randomElement(['6.4', '6.5', '6.6']),
'php_version' => $this->faker->randomElement(['8.1', '8.2', '8.3', '8.4']),
'status' => $this->faker->randomElement(['ok', 'ok', 'ok', 'warn', 'bad']),
'visitors_24h' => $this->faker->numberBetween(40, 9800),
'response_ms' => $this->faker->numberBetween(80, 480),
'https_enabled' => true,
'cache_enabled' => $this->faker->boolean(85),
'cdn_enabled' => $this->faker->boolean(45),
'maintenance_mode' => false,
'pagespeed_score' => $this->faker->numberBetween(60, 99),
'updates_pending' => $this->faker->numberBetween(0, 6),
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\TeamMember;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class TeamMemberFactory extends Factory
{
protected $model = TeamMember::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'role' => $this->faker->randomElement(['admin', 'developer', 'developer', 'viewer']),
'scope' => $this->faker->randomElement(['All clusters', 'eu-prod-*', 'edge-*', 'staging-*']),
'two_fa_enabled' => $this->faker->boolean(70),
'last_active_at' => $this->faker->dateTimeBetween('-7 days', 'now'),
];
}
}

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('clusters', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('region', 32); // e.g. eu-central-1
$table->enum('env', ['prod', 'staging', 'edge', 'dev'])->default('prod');
$table->unsignedInteger('monthly_price_cents')->default(0);
$table->unsignedSmallInteger('node_count')->default(1);
$table->boolean('backups_enabled')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('clusters');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('servers', function (Blueprint $table) {
$table->id();
$table->foreignId('cluster_id')->nullable()->constrained()->nullOnDelete();
$table->string('name', 64); // eu-prod-01
$table->string('region', 32); // eu-central-1
$table->string('country_flag', 4)->default('🇩🇪'); // emoji
$table->enum('env', ['prod', 'staging', 'edge', 'dev'])->default('prod');
$table->unsignedSmallInteger('cpu_cores')->default(4);
$table->unsignedSmallInteger('ram_gb')->default(16);
$table->unsignedInteger('disk_gb')->default(160);
$table->unsignedTinyInteger('cpu_load_pct')->default(0);
$table->unsignedTinyInteger('ram_used_pct')->default(0);
$table->unsignedTinyInteger('disk_used_pct')->default(0);
$table->enum('status', ['ok', 'warn', 'bad', 'maintenance'])->default('ok');
$table->string('ip', 45)->nullable();
$table->timestamps();
$table->index(['env', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('servers');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('sites', function (Blueprint $table) {
$table->id();
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
$table->string('domain')->unique();
$table->string('name')->nullable();
$table->string('wp_version', 16)->default('6.4');
$table->string('php_version', 8)->default('8.3');
$table->enum('status', ['ok', 'warn', 'bad', 'maintenance'])->default('ok');
$table->unsignedInteger('visitors_24h')->default(0);
$table->unsignedInteger('response_ms')->default(0);
$table->boolean('https_enabled')->default(true);
$table->boolean('cache_enabled')->default(true);
$table->boolean('cdn_enabled')->default(false);
$table->boolean('maintenance_mode')->default(false);
$table->unsignedTinyInteger('pagespeed_score')->default(85);
$table->unsignedSmallInteger('updates_pending')->default(0);
$table->timestamps();
$table->index(['status']);
$table->index(['server_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('sites');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('backups', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['hourly', 'daily', 'manual', 'snapshot'])->default('daily');
$table->unsignedBigInteger('size_bytes')->default(0);
$table->enum('status', ['queued', 'running', 'ok', 'failed'])->default('queued');
$table->string('storage_location')->nullable(); // e.g. s3://clupilot-backups
$table->timestamp('completed_at')->nullable();
$table->timestamps();
$table->index(['site_id', 'completed_at']);
$table->index(['status']);
});
}
public function down(): void
{
Schema::dropIfExists('backups');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('team_members', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('role', ['owner', 'admin', 'developer', 'viewer'])->default('viewer');
$table->string('scope')->nullable(); // e.g. "All clusters", "eu-prod-*"
$table->boolean('two_fa_enabled')->default(false);
$table->timestamp('last_active_at')->nullable();
$table->timestamps();
$table->unique('user_id');
$table->index(['role']);
});
}
public function down(): void
{
Schema::dropIfExists('team_members');
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('invitations', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->enum('role', ['admin', 'developer', 'viewer'])->default('developer');
$table->foreignId('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('token', 64)->unique();
$table->timestamp('sent_at')->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->index(['email']);
});
}
public function down(): void
{
Schema::dropIfExists('invitations');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('security_events', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->nullable()->constrained()->cascadeOnDelete();
$table->enum('type', ['brute_force', 'tor_exit', 'malware', 'cert_expiring', 'firewall', 'scan'])
->default('firewall');
$table->enum('severity', ['info', 'low', 'medium', 'high', 'critical'])->default('low');
$table->boolean('blocked')->default(true);
$table->string('source_ip', 45)->nullable();
$table->string('country', 4)->nullable();
$table->string('message')->nullable();
$table->json('meta')->nullable();
$table->timestamp('occurred_at');
$table->timestamps();
$table->index(['severity', 'occurred_at']);
$table->index(['site_id']);
});
}
public function down(): void
{
Schema::dropIfExists('security_events');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('certificates', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->constrained()->cascadeOnDelete();
$table->string('domain');
$table->string('issuer', 64)->default("Let's Encrypt");
$table->timestamp('issued_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->boolean('auto_renew')->default(true);
$table->timestamps();
$table->index(['expires_at']);
});
}
public function down(): void
{
Schema::dropIfExists('certificates');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('activities', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->enum('type', ['deploy', 'backup', 'update', 'security', 'system', 'login'])->default('system');
$table->enum('level', ['info', 'success', 'warning', 'error'])->default('info');
$table->string('message');
$table->string('subject')->nullable(); // domain / server name
$table->json('meta')->nullable();
$table->timestamp('occurred_at');
$table->timestamps();
$table->index(['occurred_at']);
$table->index(['level']);
});
}
public function down(): void
{
Schema::dropIfExists('activities');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('crons', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('expression', 64)->default('0 * * * *'); // cron expr
$table->string('command');
$table->timestamp('last_run_at')->nullable();
$table->enum('status', ['ok', 'failing', 'paused'])->default('ok');
$table->boolean('enabled')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('crons');
}
};

View File

@ -2,26 +2,44 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Activity;
use App\Models\Backup;
use App\Models\Certificate;
use App\Models\Cluster;
use App\Models\Cron;
use App\Models\Invitation;
use App\Models\SecurityEvent;
use App\Models\Server;
use App\Models\Site;
use App\Models\TeamMember;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void public function run(): void
{ {
// User::factory(10)->create(); /* ───── primary user (matches template) ───── */
$owner = User::updateOrCreate(
['email' => 'm.weber@acme-cluster.de'],
[
'name' => 'Marie Weber',
'password' => bcrypt('clupilot'),
'email_verified_at' => now(),
]
);
User::factory()->create([ TeamMember::updateOrCreate(
'name' => 'Test User', ['user_id' => $owner->id],
'email' => 'test@example.com', [
]); 'role' => 'owner',
'scope' => 'All clusters',
'two_fa_enabled' => true,
'last_active_at' => now(),
]
);
/* ───── dev login (kept from previous seed) ───── */
User::updateOrCreate( User::updateOrCreate(
['email' => 'admin@clupilot.test'], ['email' => 'admin@clupilot.test'],
[ [
@ -30,5 +48,106 @@ class DatabaseSeeder extends Seeder
'email_verified_at' => now(), 'email_verified_at' => now(),
] ]
); );
/* ───── 1 cluster: Acme Cluster ───── */
$cluster = Cluster::firstOrCreate(
['slug' => 'acme-cluster'],
[
'name' => 'Acme Cluster',
'region' => 'eu-central-1',
'env' => 'prod',
'monthly_price_cents' => 24900,
'node_count' => 6,
'backups_enabled' => true,
]
);
/* ───── 6 servers ───── */
$servers = [
['name' => 'eu-prod-01', 'region' => 'eu-central-1', 'flag' => '🇩🇪', 'env' => 'prod', 'status' => 'ok', 'cpu' => 42, 'ram' => 58, 'disk' => 47],
['name' => 'eu-prod-02', 'region' => 'eu-central-1', 'flag' => '🇩🇪', 'env' => 'prod', 'status' => 'ok', 'cpu' => 36, 'ram' => 64, 'disk' => 51],
['name' => 'edge-ams', 'region' => 'eu-west-1', 'flag' => '🇳🇱', 'env' => 'edge', 'status' => 'ok', 'cpu' => 21, 'ram' => 45, 'disk' => 38],
['name' => 'us-east-01', 'region' => 'us-east-1', 'flag' => '🇺🇸', 'env' => 'prod', 'status' => 'warn', 'cpu' => 78, 'ram' => 71, 'disk' => 54],
['name' => 'staging', 'region' => 'eu-central-1', 'flag' => '🇩🇪', 'env' => 'staging', 'status' => 'ok', 'cpu' => 18, 'ram' => 32, 'disk' => 22],
['name' => 'ap-bom-01', 'region' => 'ap-south-1', 'flag' => '🇮🇳', 'env' => 'prod', 'status' => 'ok', 'cpu' => 28, 'ram' => 52, 'disk' => 43],
];
$serverModels = [];
foreach ($servers as $s) {
$serverModels[] = Server::firstOrCreate(
['name' => $s['name']],
[
'cluster_id' => $cluster->id,
'region' => $s['region'],
'country_flag' => $s['flag'],
'env' => $s['env'],
'cpu_cores' => 8,
'ram_gb' => 32,
'disk_gb' => 320,
'cpu_load_pct' => $s['cpu'],
'ram_used_pct' => $s['ram'],
'disk_used_pct' => $s['disk'],
'status' => $s['status'],
'ip' => fake()->ipv4(),
]
);
}
/* ───── 87 sites distributed across servers ───── */
$distribution = [23, 19, 14, 18, 6, 7]; // sums to 87
foreach ($serverModels as $i => $server) {
$target = $distribution[$i] ?? 5;
$existing = $server->sites()->count();
if ($existing < $target) {
Site::factory()->count($target - $existing)
->create(['server_id' => $server->id]);
}
}
$allSites = Site::query()->get();
/* ───── Certificates ───── */
foreach ($allSites as $site) {
if (! $site->certificate) {
Certificate::factory()->create([
'site_id' => $site->id,
'domain' => '*.' . $site->domain,
]);
}
}
/* ───── Backups (sample) ───── */
$backupSites = $allSites->random(min(28, $allSites->count()));
foreach ($backupSites as $site) {
Backup::factory()->count(rand(2, 6))->create(['site_id' => $site->id]);
}
/* ───── Crons (sample) ───── */
foreach ($allSites->random(min(20, $allSites->count())) as $site) {
Cron::factory()->count(rand(1, 4))->create(['site_id' => $site->id]);
}
/* ───── Security events: 128 in last 24h (matches template stat) ───── */
SecurityEvent::factory()->count(128)
->create(['site_id' => $allSites->random()->id]);
/* ───── Team members (4 + owner) ───── */
if (TeamMember::count() < 5) {
User::factory()->count(4)->create()->each(function (User $u) {
TeamMember::factory()->create(['user_id' => $u->id]);
});
}
/* ───── Pending invitations ───── */
if (Invitation::count() < 3) {
Invitation::factory()->count(3)->create([
'invited_by_user_id' => $owner->id,
'accepted_at' => null,
]);
}
/* ───── Activity feed (30 recent items) ───── */
Activity::factory()->count(30)->create(['user_id' => $owner->id]);
} }
} }