'array', 'installed_at' => 'datetime', 'last_updated_at' => 'datetime', 'progress' => 'integer', 'total_steps' => 'integer', ]; public const APP_WORDPRESS = 'wordpress'; public const APP_JOOMLA = 'joomla'; public const APP_DRUPAL = 'drupal'; public const APP_OPENCART = 'opencart'; public const APP_MAGENTO = 'magento'; public function site(): BelongsTo { return $this->belongsTo(HostedSite::class, 'hosted_site_id'); } public function isActive(): bool { return $this->status === 'active'; } public function getAdminPasswordAttribute(): ?string { return $this->admin_password_encrypted ? decrypt($this->admin_password_encrypted) : null; } public function getDatabasePasswordAttribute(): ?string { return $this->database_password_encrypted ? decrypt($this->database_password_encrypted) : null; } public function getAdminUrlAttribute(): ?string { if (!$this->install_url || !$this->admin_path) { return null; } return rtrim($this->install_url, '/') . '/' . ltrim($this->admin_path, '/'); } public function updateProgress(int $step, int $totalSteps, string $message, ?string $currentStep = null): void { $progress = $totalSteps > 0 ? (int) round(($step / $totalSteps) * 100) : 0; $this->update([ 'progress' => min($progress, 100), 'progress_message' => $message, 'current_step' => $currentStep, 'total_steps' => $totalSteps, ]); } public function markFailed(string $errorMessage): void { $this->update([ 'status' => 'failed', 'error_message' => $errorMessage, 'progress_message' => 'Installation failed', ]); } public function markCompleted(): void { $this->update([ 'status' => 'active', 'progress' => 100, 'progress_message' => 'Installation complete', 'current_step' => null, 'installed_at' => now(), ]); } public function isInstalling(): bool { return $this->status === 'installing'; } public function hasFailed(): bool { return $this->status === 'failed'; } }