diff --git a/VERSION b/VERSION index f78734f..9465149 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -VERSION=0.9-rc9 +VERSION=0.9-rc10 diff --git a/app/Console/Commands/CollectUserUsage.php b/app/Console/Commands/CollectUserUsage.php deleted file mode 100644 index 39048f5..0000000 --- a/app/Console/Commands/CollectUserUsage.php +++ /dev/null @@ -1,173 +0,0 @@ -where('is_admin', false) - ->where('is_active', true); - - $userFilter = $this->option('user'); - if ($userFilter) { - $users->where(function ($query) use ($userFilter) { - $query->where('id', $userFilter) - ->orWhere('username', $userFilter); - }); - } - - $users = $users->get(); - if ($users->isEmpty()) { - $this->info('No users found for usage collection.'); - - return 0; - } - - $agent = new AgentClient; - $capturedAt = now(); - - foreach ($users as $user) { - $diskBytes = $user->getDiskUsageBytes(); - $mailBytes = (int) Mailbox::where('user_id', $user->id)->sum('quota_used_bytes'); - $dbBytes = $this->getDatabaseUsage($agent, $user); - $bandwidthTotal = $this->getBandwidthTotal($agent, $user); - $resourceStats = $this->getUserResourceStats($agent, $user); - $cpuPercent = (int) round($resourceStats['cpu_percent'] ?? 0); - $cpuUsageTotal = (int) ($resourceStats['cpu_usage_usec_total'] ?? 0); - $memoryBytes = (int) ($resourceStats['memory_bytes'] ?? 0); - $diskIoTotal = (int) ($resourceStats['disk_io_total_bytes'] ?? 0); - $lastBandwidthTotal = (int) UserSetting::getForUser($user->id, 'bandwidth_total_bytes', 0); - $lastDiskIoTotal = (int) UserSetting::getForUser($user->id, 'disk_io_total_bytes', 0); - $lastCpuTotal = (int) UserSetting::getForUser($user->id, 'cpu_usage_usec_total', 0); - $lastCpuAt = (int) UserSetting::getForUser($user->id, 'cpu_usage_captured_at', 0); - $bandwidthDelta = $bandwidthTotal >= $lastBandwidthTotal - ? $bandwidthTotal - $lastBandwidthTotal - : $bandwidthTotal; - $diskIoDelta = $diskIoTotal >= $lastDiskIoTotal - ? $diskIoTotal - $lastDiskIoTotal - : $diskIoTotal; - - if ($cpuUsageTotal > 0 && $lastCpuAt > 0) { - $elapsed = $capturedAt->timestamp - $lastCpuAt; - if ($elapsed > 0) { - $delta = $cpuUsageTotal >= $lastCpuTotal ? $cpuUsageTotal - $lastCpuTotal : $cpuUsageTotal; - $cpuPercent = (int) round(($delta / ($elapsed * 1_000_000)) * 100); - } - } - - $this->storeMetric($user->id, 'disk_bytes', $diskBytes, $capturedAt); - $this->storeMetric($user->id, 'mail_bytes', $mailBytes, $capturedAt); - $this->storeMetric($user->id, 'database_bytes', $dbBytes, $capturedAt); - $this->storeMetric($user->id, 'bandwidth_bytes', $bandwidthDelta, $capturedAt); - $this->storeMetric($user->id, 'cpu_percent', $cpuPercent, $capturedAt); - $this->storeMetric($user->id, 'memory_bytes', $memoryBytes, $capturedAt); - $this->storeMetric($user->id, 'disk_io_bytes', $diskIoDelta, $capturedAt); - - UserSetting::setForUser($user->id, 'bandwidth_total_bytes', $bandwidthTotal); - UserSetting::setForUser($user->id, 'disk_io_total_bytes', $diskIoTotal); - UserSetting::setForUser($user->id, 'cpu_usage_usec_total', $cpuUsageTotal); - UserSetting::setForUser($user->id, 'cpu_usage_captured_at', $capturedAt->timestamp); - - $this->line("Collected usage for {$user->username}"); - } - - $retainDays = (int) $this->option('retain'); - if ($retainDays > 0) { - UserResourceUsage::where('captured_at', '<', now()->subDays($retainDays))->delete(); - } - - return 0; - } - - protected function getDatabaseUsage(AgentClient $agent, User $user): int - { - try { - $result = $agent->mysqlListDatabases($user->username); - $databases = $result['databases'] ?? []; - $total = 0; - - foreach ($databases as $database) { - $total += (int) ($database['size_bytes'] ?? 0); - } - - return $total; - } catch (Exception $e) { - $this->warn("Failed to fetch database usage for {$user->username}: {$e->getMessage()}"); - } - - return 0; - } - - protected function getBandwidthTotal(AgentClient $agent, User $user): int - { - try { - $result = $agent->send('usage.bandwidth_total', [ - 'username' => $user->username, - ]); - - if ($result['success'] ?? false) { - return (int) ($result['total_bytes'] ?? 0); - } - } catch (Exception $e) { - $this->warn("Failed to fetch bandwidth usage for {$user->username}: {$e->getMessage()}"); - } - - return 0; - } - - /** - * @return array{cpu_percent: float, cpu_usage_usec_total: int, memory_bytes: int, disk_io_total_bytes: int} - */ - protected function getUserResourceStats(AgentClient $agent, User $user): array - { - try { - $result = $agent->send('usage.user_resources', [ - 'username' => $user->username, - ]); - - if ($result['success'] ?? false) { - return [ - 'cpu_percent' => (float) ($result['cpu_percent'] ?? 0), - 'cpu_usage_usec_total' => (int) ($result['cpu_usage_usec_total'] ?? 0), - 'memory_bytes' => (int) ($result['memory_bytes'] ?? 0), - 'disk_io_total_bytes' => (int) ($result['disk_io_total_bytes'] ?? 0), - ]; - } - } catch (Exception $e) { - $this->warn("Failed to fetch CPU/memory/disk IO for {$user->username}: {$e->getMessage()}"); - } - - return [ - 'cpu_percent' => 0.0, - 'cpu_usage_usec_total' => 0, - 'memory_bytes' => 0, - 'disk_io_total_bytes' => 0, - ]; - } - - protected function storeMetric(int $userId, string $metric, int $value, $capturedAt): void - { - UserResourceUsage::create([ - 'user_id' => $userId, - 'metric' => $metric, - 'value' => max(0, $value), - 'captured_at' => $capturedAt, - ]); - } -} diff --git a/app/Console/Commands/JabaliSyncCgroups.php b/app/Console/Commands/JabaliSyncCgroups.php deleted file mode 100644 index 06f16e5..0000000 --- a/app/Console/Commands/JabaliSyncCgroups.php +++ /dev/null @@ -1,60 +0,0 @@ -option('user') ?? ''); - - try { - $result = $user !== '' - ? $this->agent->cgroupSyncUserProcesses($user) - : $this->agent->cgroupSyncAllProcesses(); - } catch (Exception $e) { - $this->error($e->getMessage()); - - return self::FAILURE; - } - - if (! ($result['success'] ?? false)) { - $this->error($result['error'] ?? 'Unknown error'); - - return self::FAILURE; - } - - $moved = (int) ($result['moved'] ?? 0); - $this->info("Synced cgroups, moved {$moved} process(es)."); - - return self::SUCCESS; - } -} diff --git a/app/Filament/Admin/Pages/Backups.php b/app/Filament/Admin/Pages/Backups.php index 6e42b12..06c6efd 100644 --- a/app/Filament/Admin/Pages/Backups.php +++ b/app/Filament/Admin/Pages/Backups.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Admin\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Backup; use App\Models\BackupDestination; use App\Models\BackupSchedule; @@ -40,7 +39,6 @@ use Livewire\Attributes\Url; class Backups extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -508,7 +506,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('createServerBackup') ->label(__('Create Server Backup')) ->icon('heroicon-o-archive-box-arrow-down') diff --git a/app/Filament/Admin/Pages/Dashboard.php b/app/Filament/Admin/Pages/Dashboard.php index 3ed3bb6..dd31dbc 100644 --- a/app/Filament/Admin/Pages/Dashboard.php +++ b/app/Filament/Admin/Pages/Dashboard.php @@ -19,7 +19,6 @@ use Filament\Schemas\Components\EmbeddedTable; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Illuminate\Contracts\Support\Htmlable; -use Livewire\Attributes\On; class Dashboard extends Page implements HasActions, HasForms { @@ -70,13 +69,6 @@ class Dashboard extends Page implements HasActions, HasForms ]); } - #[On('tour-completed')] - public function completeTour(): void - { - DnsSetting::set('tour_completed', '1'); - DnsSetting::clearCache(); - } - protected function getHeaderActions(): array { return [ @@ -107,16 +99,6 @@ class Dashboard extends Page implements HasActions, HasForms } DnsSetting::set('onboarding_completed', '1'); DnsSetting::clearCache(); - - $this->dispatch('start-admin-tour'); - }), - - Action::make('takeTour') - ->label(__('Take Tour')) - ->icon('heroicon-o-academic-cap') - ->color('gray') - ->action(function (): void { - $this->dispatch('start-admin-tour'); }), ]; } diff --git a/app/Filament/Admin/Pages/DnsZones.php b/app/Filament/Admin/Pages/DnsZones.php index 26435a4..f1f0ee8 100644 --- a/app/Filament/Admin/Pages/DnsZones.php +++ b/app/Filament/Admin/Pages/DnsZones.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Filament\Admin\Pages; use App\Filament\Admin\Widgets\DnsPendingAddsTable; -use App\Filament\Concerns\HasPageTour; use App\Models\DnsRecord; use App\Models\DnsSetting; use App\Models\Domain; @@ -39,7 +38,6 @@ use Livewire\Attributes\On; class DnsZones extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -585,7 +583,6 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('syncAllZones') ->label(__('Sync All Zones')) ->icon('heroicon-o-arrow-path') diff --git a/app/Filament/Admin/Pages/PhpManager.php b/app/Filament/Admin/Pages/PhpManager.php index c2a7171..df8d4bb 100644 --- a/app/Filament/Admin/Pages/PhpManager.php +++ b/app/Filament/Admin/Pages/PhpManager.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Admin\Pages; -use App\Filament\Concerns\HasPageTour; use App\Services\Agent\AgentClient; use BackedEnum; use Filament\Actions\Action; @@ -26,7 +25,6 @@ use Illuminate\Contracts\Support\Htmlable; class PhpManager extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -288,7 +286,6 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), ]; } } diff --git a/app/Filament/Admin/Pages/ResourceUsage.php b/app/Filament/Admin/Pages/ResourceUsage.php deleted file mode 100644 index 5ed2f65..0000000 --- a/app/Filament/Admin/Pages/ResourceUsage.php +++ /dev/null @@ -1,257 +0,0 @@ -where('is_admin', false)->orderBy('name')->first(); - $this->usageFormData = [ - 'user_id' => $defaultUser?->id, - ]; - - $this->loadUsage(); - } - - protected function getForms(): array - { - return ['usageForm']; - } - - public function usageForm(Schema $schema): Schema - { - return $schema - ->statePath('usageFormData') - ->schema([ - Section::make(__('Filters')) - ->schema([ - Select::make('user_id') - ->label(__('User')) - ->options($this->getUserOptions()) - ->searchable() - ->preload() - ->live() - ->afterStateUpdated(function () { - $this->loadUsage(); - }) - ->required(), - ]) - ->columns(1), - ]); - } - - protected function getUserOptions(): array - { - return User::query() - ->where('is_admin', false) - ->orderBy('name') - ->pluck('name', 'id') - ->toArray(); - } - - protected function loadUsage(): void - { - $userId = $this->usageFormData['user_id'] ?? null; - $userId = $userId ? (int) $userId : null; - if (! $userId) { - $this->chartData = []; - $this->summary = []; - - return; - } - - $user = User::query()->with('hostingPackage')->find($userId); - $package = $user?->hostingPackage; - $this->cpuLimitPercent = $package?->cpu_limit_percent ?: null; - $memoryLimitMb = $package?->memory_limit_mb ?: null; - $this->memoryLimitGb = $memoryLimitMb ? $this->bytesToGb((int) ($memoryLimitMb * 1024 * 1024)) : null; - - $usageMetrics = ['disk_bytes', 'mail_bytes', 'database_bytes', 'bandwidth_bytes']; - $performanceMetrics = ['cpu_percent', 'memory_bytes', 'disk_io_bytes']; - $metrics = array_merge($usageMetrics, $performanceMetrics); - $start = now()->subDays(30); - - $records = UserResourceUsage::query() - ->where('user_id', $userId) - ->whereIn('metric', $metrics) - ->where('captured_at', '>=', $start) - ->orderBy('captured_at') - ->get(); - - $labels = $records - ->map(fn ($record) => $record->captured_at->format('Y-m-d H:00')) - ->unique() - ->values(); - - $grouped = $records->groupBy('metric')->map(function ($collection) { - return $collection->keyBy(fn ($item) => $item->captured_at->format('Y-m-d H:00')); - }); - - $series = []; - foreach ($usageMetrics as $metric) { - $series[$metric] = []; - foreach ($labels as $label) { - $value = $grouped[$metric][$label]->value ?? 0; - $series[$metric][] = $this->bytesToGb((int) $value); - } - } - - $this->chartData = [ - 'labels' => $labels->toArray(), - 'disk' => $series['disk_bytes'], - 'mail' => $series['mail_bytes'], - 'database' => $series['database_bytes'], - 'bandwidth' => $series['bandwidth_bytes'], - ]; - - $performanceSeries = []; - foreach ($performanceMetrics as $metric) { - $performanceSeries[$metric] = []; - foreach ($labels as $label) { - $value = $grouped[$metric][$label]->value ?? 0; - if ($metric === 'cpu_percent') { - $performanceSeries[$metric][] = (int) $value; - } elseif ($metric === 'memory_bytes') { - $performanceSeries[$metric][] = $this->bytesToGb((int) $value); - } else { - $performanceSeries[$metric][] = $this->bytesToMb((int) $value); - } - } - } - - $performanceRecords = $records->whereIn('metric', $performanceMetrics); - $this->hasPerformanceData = $performanceRecords->isNotEmpty(); - $this->performanceChartData = [ - 'labels' => $labels->toArray(), - 'cpu' => $performanceSeries['cpu_percent'], - 'memory' => $performanceSeries['memory_bytes'], - 'disk_io' => $performanceSeries['disk_io_bytes'], - ]; - - $this->cpuStats = $this->buildPercentageStats($performanceRecords->where('metric', 'cpu_percent')->pluck('value')); - $this->memoryStats = $this->buildBytesStats($performanceRecords->where('metric', 'memory_bytes')->pluck('value')); - - $this->summary = [ - 'disk' => $this->formatMetric($userId, 'disk_bytes'), - 'mail' => $this->formatMetric($userId, 'mail_bytes'), - 'database' => $this->formatMetric($userId, 'database_bytes'), - 'bandwidth' => $this->formatMetric($userId, 'bandwidth_bytes'), - ]; - } - - protected function formatMetric(int $userId, string $metric): string - { - $latest = UserResourceUsage::query() - ->where('user_id', $userId) - ->where('metric', $metric) - ->latest('captured_at') - ->value('value'); - - if ($latest === null) { - return __('No data'); - } - - return number_format($this->bytesToGb((int) $latest), 2).' GB'; - } - - protected function bytesToGb(int $bytes): float - { - return round($bytes / 1024 / 1024 / 1024, 4); - } - - protected function bytesToMb(int $bytes): float - { - return round($bytes / 1024 / 1024, 2); - } - - /** - * @param \Illuminate\Support\Collection $values - * @return array{avg: ?string, max: ?string} - */ - protected function buildPercentageStats($values): array - { - if ($values->isEmpty()) { - return [ - 'avg' => null, - 'max' => null, - ]; - } - - return [ - 'avg' => number_format((float) $values->avg(), 1).'%', - 'max' => number_format((float) $values->max(), 1).'%', - ]; - } - - /** - * @param \Illuminate\Support\Collection $values - * @return array{avg: ?string, max: ?string} - */ - protected function buildBytesStats($values): array - { - if ($values->isEmpty()) { - return [ - 'avg' => null, - 'max' => null, - ]; - } - - return [ - 'avg' => number_format($this->bytesToGb((int) $values->avg()), 2).' GB', - 'max' => number_format($this->bytesToGb((int) $values->max()), 2).' GB', - ]; - } -} diff --git a/app/Filament/Admin/Pages/Security.php b/app/Filament/Admin/Pages/Security.php index 085c0d0..19047be 100644 --- a/app/Filament/Admin/Pages/Security.php +++ b/app/Filament/Admin/Pages/Security.php @@ -12,7 +12,6 @@ use App\Filament\Admin\Widgets\Security\NiktoResultsTable; use App\Filament\Admin\Widgets\Security\QuarantinedFilesTable; use App\Filament\Admin\Widgets\Security\ThreatsTable; use App\Filament\Admin\Widgets\Security\WpscanResultsTable; -use App\Filament\Concerns\HasPageTour; use App\Models\AuditLog; use App\Services\Agent\AgentClient; use BackedEnum; @@ -48,7 +47,6 @@ use Livewire\Attributes\Url; class Security extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -989,7 +987,6 @@ class Security extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), ]; } diff --git a/app/Filament/Admin/Pages/ServerSettings.php b/app/Filament/Admin/Pages/ServerSettings.php index 8630f66..bcad2ed 100644 --- a/app/Filament/Admin/Pages/ServerSettings.php +++ b/app/Filament/Admin/Pages/ServerSettings.php @@ -6,11 +6,8 @@ namespace App\Filament\Admin\Pages; use App\Filament\Admin\Widgets\Settings\DnssecTable; use App\Filament\Admin\Widgets\Settings\NotificationLogTable; -use App\Filament\Concerns\HasPageTour; use App\Models\DnsSetting; -use App\Models\UserResourceLimit; use App\Services\Agent\AgentClient; -use App\Services\System\ResourceLimitService; use BackedEnum; use Exception; use Filament\Actions\Action; @@ -41,7 +38,6 @@ use Livewire\WithFileUploads; class ServerSettings extends Page implements HasActions, HasForms { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use WithFileUploads; @@ -199,7 +195,6 @@ class ServerSettings extends Page implements HasActions, HasForms $this->quotaData = [ 'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false), 'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120), - 'resource_limits_enabled' => (bool) ($settings['resource_limits_enabled'] ?? true), ]; $this->fileManagerData = [ @@ -422,10 +417,6 @@ class ServerSettings extends Page implements HasActions, HasForms ->numeric() ->placeholder('5120') ->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')), - Toggle::make('quotaData.resource_limits_enabled') - ->label(__('Enable CPU/Memory/IO Limits')) - ->helperText(__('Apply cgroup limits from hosting packages (CloudLinux-style)')) - ->columnSpanFull(), ]), Actions::make([ FormAction::make('saveQuotaSettings') @@ -841,11 +832,9 @@ class ServerSettings extends Page implements HasActions, HasForms { $data = $this->quotaData; $wasEnabled = (bool) DnsSetting::get('quotas_enabled', false); - $wasLimitsEnabled = (bool) DnsSetting::get('resource_limits_enabled', true); DnsSetting::set('quotas_enabled', $data['quotas_enabled'] ? '1' : '0'); DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']); - DnsSetting::set('resource_limits_enabled', ! empty($data['resource_limits_enabled']) ? '1' : '0'); DnsSetting::clearCache(); if ($data['quotas_enabled'] && ! $wasEnabled) { @@ -861,21 +850,6 @@ class ServerSettings extends Page implements HasActions, HasForms } } - if (! empty($data['resource_limits_enabled']) && ! $wasLimitsEnabled) { - $limits = UserResourceLimit::query()->where('is_active', true)->get(); - foreach ($limits as $limit) { - app(ResourceLimitService::class)->apply($limit); - } - } - - if (empty($data['resource_limits_enabled']) && $wasLimitsEnabled) { - try { - $this->getAgent()->send('cgroup.clear_all_limits', []); - } catch (Exception $e) { - Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not clear cgroup limits.'))->warning()->send(); - } - } - Notification::make()->title(__('Quota settings saved'))->success()->send(); } @@ -1158,7 +1132,6 @@ class ServerSettings extends Page implements HasActions, HasForms protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('export_config') ->label(__('Export')) ->icon('heroicon-o-arrow-down-tray') diff --git a/app/Filament/Admin/Pages/ServerStatus.php b/app/Filament/Admin/Pages/ServerStatus.php index 18a2ea8..07bc115 100644 --- a/app/Filament/Admin/Pages/ServerStatus.php +++ b/app/Filament/Admin/Pages/ServerStatus.php @@ -6,7 +6,6 @@ namespace App\Filament\Admin\Pages; use App\Filament\Admin\Widgets\ServerChartsWidget; use App\Filament\Admin\Widgets\ServerInfoWidget; -use App\Filament\Concerns\HasPageTour; use App\Models\ServerProcess; use App\Services\Agent\AgentClient; use BackedEnum; @@ -28,7 +27,6 @@ use Illuminate\Database\Eloquent\Collection; class ServerStatus extends Page implements HasTable { - use HasPageTour; use InteractsWithTable; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar'; @@ -76,7 +74,6 @@ class ServerStatus extends Page implements HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), ActionGroup::make([ Action::make('limit25') ->label(__('Show 25 processes')) diff --git a/app/Filament/Admin/Pages/Services.php b/app/Filament/Admin/Pages/Services.php index a3f078d..0acdef9 100644 --- a/app/Filament/Admin/Pages/Services.php +++ b/app/Filament/Admin/Pages/Services.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Admin\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\AuditLog; use App\Services\Agent\AgentClient; use BackedEnum; @@ -25,7 +24,6 @@ use Illuminate\Database\Eloquent\Model; class Services extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -317,7 +315,6 @@ class Services extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), ]; } diff --git a/app/Filament/Admin/Pages/SslManager.php b/app/Filament/Admin/Pages/SslManager.php index 11bcd02..a3fbc7f 100644 --- a/app/Filament/Admin/Pages/SslManager.php +++ b/app/Filament/Admin/Pages/SslManager.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Filament\Admin\Pages; use App\Filament\Admin\Widgets\SslStatsOverview; -use App\Filament\Concerns\HasPageTour; use App\Models\Domain; use App\Models\SslCertificate; use App\Models\User; @@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Artisan; class SslManager extends Page implements HasTable { - use HasPageTour; use InteractsWithTable; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check'; @@ -531,7 +529,6 @@ class SslManager extends Page implements HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('runAutoSsl') ->label(__('Run SSL Check')) ->icon('heroicon-o-play') diff --git a/app/Filament/Admin/Resources/HostingPackages/Schemas/HostingPackageForm.php b/app/Filament/Admin/Resources/HostingPackages/Schemas/HostingPackageForm.php index 4abe0b0..782a923 100644 --- a/app/Filament/Admin/Resources/HostingPackages/Schemas/HostingPackageForm.php +++ b/app/Filament/Admin/Resources/HostingPackages/Schemas/HostingPackageForm.php @@ -61,27 +61,6 @@ class HostingPackageForm ]) ->columns(2), - Section::make(__('System Resource Limits')) - ->description(__('Optional cgroup limits applied to users in this package. Leave blank for unlimited.')) - ->schema([ - TextInput::make('cpu_limit_percent') - ->label(__('CPU Limit (%)')) - ->numeric() - ->minValue(0) - ->maxValue(100) - ->helperText(__('Example: 50 = 50% CPU quota')), - TextInput::make('memory_limit_mb') - ->label(__('Memory Limit (MB)')) - ->numeric() - ->minValue(0) - ->helperText(__('Example: 1024 = 1 GB RAM')), - TextInput::make('io_limit_mb') - ->label(__('Disk IO Limit (MB/s)')) - ->numeric() - ->minValue(0) - ->helperText(__('Applied to read/write bandwidth')), - ]) - ->columns(2), ]); } } diff --git a/app/Filament/Admin/Resources/HostingPackages/Tables/HostingPackagesTable.php b/app/Filament/Admin/Resources/HostingPackages/Tables/HostingPackagesTable.php index 9066852..c9c1144 100644 --- a/app/Filament/Admin/Resources/HostingPackages/Tables/HostingPackagesTable.php +++ b/app/Filament/Admin/Resources/HostingPackages/Tables/HostingPackagesTable.php @@ -44,21 +44,6 @@ class HostingPackagesTable TextColumn::make('mailboxes_limit') ->label(__('Mailboxes')) ->getStateUsing(fn ($record) => $record->mailboxes_limit ?: __('Unlimited')), - TextColumn::make('resource_limits') - ->label(__('System Limits')) - ->getStateUsing(function ($record) { - $cpu = $record->cpu_limit_percent ? $record->cpu_limit_percent.'%' : null; - $memory = $record->memory_limit_mb ? $record->memory_limit_mb.' MB' : null; - $io = $record->io_limit_mb ? $record->io_limit_mb.' MB/s' : null; - - $parts = array_filter([ - $cpu ? __('CPU: :value', ['value' => $cpu]) : null, - $memory ? __('RAM: :value', ['value' => $memory]) : null, - $io ? __('IO: :value', ['value' => $io]) : null, - ]); - - return ! empty($parts) ? implode(', ', $parts) : __('Unlimited'); - }), IconColumn::make('is_active') ->label(__('Active')) ->boolean(), diff --git a/app/Filament/Admin/Resources/Users/Pages/CreateUser.php b/app/Filament/Admin/Resources/Users/Pages/CreateUser.php index 9f18333..c51578c 100644 --- a/app/Filament/Admin/Resources/Users/Pages/CreateUser.php +++ b/app/Filament/Admin/Resources/Users/Pages/CreateUser.php @@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages; use App\Filament\Admin\Resources\Users\UserResource; use App\Models\HostingPackage; -use App\Models\UserResourceLimit; use App\Services\Agent\AgentClient; use App\Services\System\LinuxUserService; -use App\Services\System\ResourceLimitService; use Exception; use Filament\Notifications\Notification; use Filament\Resources\Pages\CreateRecord; @@ -59,9 +57,6 @@ class CreateUser extends CreateRecord // Apply disk quota if enabled $this->applyDiskQuota(); - - // Apply resource limits from package - $this->syncResourceLimitsFromPackage($this->selectedPackage, true); } catch (Exception $e) { Notification::make() ->title(__('Linux user creation failed')) @@ -69,15 +64,12 @@ class CreateUser extends CreateRecord ->danger() ->send(); } - } else { - // Store resource limits even if the Linux user was not created yet - $this->syncResourceLimitsFromPackage($this->selectedPackage, false); } if (! $this->record->hosting_package_id) { Notification::make() ->title(__('No hosting package selected')) - ->body(__('This user has unlimited resource limits.')) + ->body(__('This user has unlimited quotas.')) ->warning() ->send(); } @@ -113,50 +105,4 @@ class CreateUser extends CreateRecord ->send(); } } - - protected function syncResourceLimitsFromPackage(?HostingPackage $package, bool $apply): void - { - if (! $this->record) { - return; - } - - $cpu = $package?->cpu_limit_percent; - $memory = $package?->memory_limit_mb; - $io = $package?->io_limit_mb; - $hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0); - - $limit = UserResourceLimit::where('user_id', $this->record->id)->first(); - - if (! $package || ! $hasLimits) { - if ($limit) { - $limit->fill([ - 'cpu_limit_percent' => null, - 'memory_limit_mb' => null, - 'io_limit_mb' => null, - 'is_active' => false, - ])->save(); - - if ($apply) { - app(ResourceLimitService::class)->clear($limit); - } - } - - return; - } - - if (! $limit) { - $limit = new UserResourceLimit(['user_id' => $this->record->id]); - } - - $limit->fill([ - 'cpu_limit_percent' => $cpu, - 'memory_limit_mb' => $memory, - 'io_limit_mb' => $io, - 'is_active' => true, - ])->save(); - - if ($apply) { - app(ResourceLimitService::class)->apply($limit); - } - } } diff --git a/app/Filament/Admin/Resources/Users/Pages/EditUser.php b/app/Filament/Admin/Resources/Users/Pages/EditUser.php index f088d0a..3cfecb0 100644 --- a/app/Filament/Admin/Resources/Users/Pages/EditUser.php +++ b/app/Filament/Admin/Resources/Users/Pages/EditUser.php @@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages; use App\Filament\Admin\Resources\Users\UserResource; use App\Models\HostingPackage; -use App\Models\UserResourceLimit; use App\Services\Agent\AgentClient; use App\Services\System\LinuxUserService; -use App\Services\System\ResourceLimitService; use Exception; use Filament\Actions; use Filament\Forms\Components\Toggle; @@ -76,45 +74,6 @@ class EditUser extends EditRecord } } - $this->syncResourceLimitsFromPackage($this->selectedPackage); - } - - protected function syncResourceLimitsFromPackage(?HostingPackage $package): void - { - $cpu = $package?->cpu_limit_percent; - $memory = $package?->memory_limit_mb; - $io = $package?->io_limit_mb; - $hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0); - - $limit = UserResourceLimit::where('user_id', $this->record->id)->first(); - - if (! $package || ! $hasLimits) { - if ($limit) { - $limit->fill([ - 'cpu_limit_percent' => null, - 'memory_limit_mb' => null, - 'io_limit_mb' => null, - 'is_active' => false, - ])->save(); - - app(ResourceLimitService::class)->clear($limit); - } - - return; - } - - if (! $limit) { - $limit = new UserResourceLimit(['user_id' => $this->record->id]); - } - - $limit->fill([ - 'cpu_limit_percent' => $cpu, - 'memory_limit_mb' => $memory, - 'io_limit_mb' => $io, - 'is_active' => true, - ])->save(); - - app(ResourceLimitService::class)->apply($limit); } protected function getHeaderActions(): array diff --git a/app/Filament/Admin/Resources/Users/Pages/ListUsers.php b/app/Filament/Admin/Resources/Users/Pages/ListUsers.php index 789a764..b890ec3 100644 --- a/app/Filament/Admin/Resources/Users/Pages/ListUsers.php +++ b/app/Filament/Admin/Resources/Users/Pages/ListUsers.php @@ -3,20 +3,16 @@ namespace App\Filament\Admin\Resources\Users\Pages; use App\Filament\Admin\Resources\Users\UserResource; -use App\Filament\Concerns\HasPageTour; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; class ListUsers extends ListRecords { - use HasPageTour; - protected static string $resource = UserResource::class; protected function getHeaderActions(): array { return [ - $this->getTourAction(), CreateAction::make(), ]; } diff --git a/app/Filament/Admin/Resources/Users/Schemas/UserForm.php b/app/Filament/Admin/Resources/Users/Schemas/UserForm.php index dbf2616..f6b3e77 100644 --- a/app/Filament/Admin/Resources/Users/Schemas/UserForm.php +++ b/app/Filament/Admin/Resources/Users/Schemas/UserForm.php @@ -124,7 +124,7 @@ class UserForm Placeholder::make('package_notice') ->label(__('Hosting Package')) - ->content(__('No hosting package selected. This user will have unlimited resources.')) + ->content(__('No hosting package selected. This user will have unlimited quotas.')) ->visible(fn ($get) => blank($get('hosting_package_id'))), \Filament\Forms\Components\Select::make('hosting_package_id') @@ -137,7 +137,7 @@ class UserForm ->pluck('name', 'id') ->toArray()) ->placeholder(__('Unlimited (no package)')) - ->helperText(__('Assign a package to set resource limits.')) + ->helperText(__('Assign a package to set quotas.')) ->columnSpanFull(), Toggle::make('create_linux_user') diff --git a/app/Filament/Concerns/HasPageTour.php b/app/Filament/Concerns/HasPageTour.php deleted file mode 100644 index f80f936..0000000 --- a/app/Filament/Concerns/HasPageTour.php +++ /dev/null @@ -1,19 +0,0 @@ -label(__('Take Tour')) - ->icon('heroicon-o-academic-cap') - ->color('gray') - ->action(function (): void { - $this->dispatch('start-page-tour'); - }); - } -} diff --git a/app/Filament/Jabali/Pages/Backups.php b/app/Filament/Jabali/Pages/Backups.php index ab54e53..b1d0907 100644 --- a/app/Filament/Jabali/Pages/Backups.php +++ b/app/Filament/Jabali/Pages/Backups.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Backup; use App\Models\BackupDestination; use App\Models\BackupRestore; @@ -46,7 +45,6 @@ use Livewire\Attributes\Url; class Backups extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -588,7 +586,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('createBackup') ->label(__('Create Backup')) ->icon('heroicon-o-archive-box-arrow-down') diff --git a/app/Filament/Jabali/Pages/CdnIntegration.php b/app/Filament/Jabali/Pages/CdnIntegration.php index acc6ebb..1567483 100644 --- a/app/Filament/Jabali/Pages/CdnIntegration.php +++ b/app/Filament/Jabali/Pages/CdnIntegration.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\CloudflareZone; use App\Models\Domain; use BackedEnum; @@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Http; class CdnIntegration extends Page implements HasActions, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithTable; diff --git a/app/Filament/Jabali/Pages/CronJobs.php b/app/Filament/Jabali/Pages/CronJobs.php index 242a379..0f46df7 100644 --- a/app/Filament/Jabali/Pages/CronJobs.php +++ b/app/Filament/Jabali/Pages/CronJobs.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\CronJob; use App\Models\Domain; use App\Services\Agent\AgentClient; @@ -32,7 +31,6 @@ use Illuminate\Support\HtmlString; class CronJobs extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -500,7 +498,6 @@ class CronJobs extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->createCronJobAction(), $this->setupWordPressCronAction(), ]; diff --git a/app/Filament/Jabali/Pages/Databases.php b/app/Filament/Jabali/Pages/Databases.php index d5406c9..ed1926f 100644 --- a/app/Filament/Jabali/Pages/Databases.php +++ b/app/Filament/Jabali/Pages/Databases.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\MysqlCredential; use App\Services\Agent\AgentClient; use BackedEnum; @@ -35,7 +34,6 @@ use Illuminate\Support\Facades\Crypt; class Databases extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -382,7 +380,6 @@ class Databases extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->quickSetupAction(), $this->createDatabaseAction(), $this->createUserAction(), diff --git a/app/Filament/Jabali/Pages/DnsRecords.php b/app/Filament/Jabali/Pages/DnsRecords.php index b27df45..53eae0e 100644 --- a/app/Filament/Jabali/Pages/DnsRecords.php +++ b/app/Filament/Jabali/Pages/DnsRecords.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Filament\Jabali\Widgets\DnsPendingAddsTable; use App\Models\DnsRecord; use App\Models\DnsSetting; @@ -41,7 +40,6 @@ use Livewire\Attributes\On; class DnsRecords extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -802,7 +800,6 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->applyTemplateAction() ->visible(fn () => $this->selectedDomainId !== null), $this->addRecordAction() diff --git a/app/Filament/Jabali/Pages/Domains.php b/app/Filament/Jabali/Pages/Domains.php index 1dfb881..494bd45 100644 --- a/app/Filament/Jabali/Pages/Domains.php +++ b/app/Filament/Jabali/Pages/Domains.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Domain; use App\Models\DomainAlias; use App\Models\DomainHotlinkSetting; @@ -38,7 +37,6 @@ use Illuminate\Support\Facades\Auth; class Domains extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -434,7 +432,6 @@ class Domains extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->createDomainAction(), ]; } diff --git a/app/Filament/Jabali/Pages/Email.php b/app/Filament/Jabali/Pages/Email.php index c128cd9..0972555 100644 --- a/app/Filament/Jabali/Pages/Email.php +++ b/app/Filament/Jabali/Pages/Email.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Autoresponder; use App\Models\DnsRecord; use App\Models\Domain; @@ -44,7 +43,6 @@ use Livewire\Attributes\Url; class Email extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -821,7 +819,6 @@ class Email extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->createMailboxAction(), $this->createForwarderAction(), $this->createAutoresponderAction(), diff --git a/app/Filament/Jabali/Pages/Files.php b/app/Filament/Jabali/Pages/Files.php index be44746..98a1fa2 100644 --- a/app/Filament/Jabali/Pages/Files.php +++ b/app/Filament/Jabali/Pages/Files.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\DnsSetting; use App\Services\Agent\AgentClient; use BackedEnum; @@ -34,7 +33,6 @@ use Livewire\WithFileUploads; class Files extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -95,7 +93,6 @@ class Files extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), ]; } diff --git a/app/Filament/Jabali/Pages/GitDeployment.php b/app/Filament/Jabali/Pages/GitDeployment.php index ecd738b..4d5ce0c 100644 --- a/app/Filament/Jabali/Pages/GitDeployment.php +++ b/app/Filament/Jabali/Pages/GitDeployment.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Jobs\RunGitDeployment; use App\Models\Domain; use App\Models\GitDeployment as GitDeploymentModel; @@ -33,7 +32,6 @@ use Illuminate\Support\Str; class GitDeployment extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; diff --git a/app/Filament/Jabali/Pages/ImageOptimization.php b/app/Filament/Jabali/Pages/ImageOptimization.php index dd0cb09..b907cbc 100644 --- a/app/Filament/Jabali/Pages/ImageOptimization.php +++ b/app/Filament/Jabali/Pages/ImageOptimization.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Domain; use App\Services\Agent\AgentClient; use BackedEnum; @@ -25,7 +24,6 @@ use Illuminate\Support\Facades\Auth; class ImageOptimization extends Page implements HasActions, HasForms { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; diff --git a/app/Filament/Jabali/Pages/Logs.php b/app/Filament/Jabali/Pages/Logs.php index a78fa2e..89d7f4d 100644 --- a/app/Filament/Jabali/Pages/Logs.php +++ b/app/Filament/Jabali/Pages/Logs.php @@ -4,9 +4,7 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\AuditLog; -use App\Models\UserResourceUsage; use App\Services\Agent\AgentClient; use BackedEnum; use Filament\Actions\Action; @@ -22,7 +20,6 @@ use Livewire\Attributes\Url; class Logs extends Page implements HasActions, HasForms { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; @@ -96,7 +93,7 @@ class Logs extends Page implements HasActions, HasForms protected function normalizeTab(?string $tab): string { return match ($tab) { - 'logs', 'usage', 'activity', 'stats' => (string) $tab, + 'logs', 'activity', 'stats' => (string) $tab, default => 'logs', }; } @@ -195,57 +192,6 @@ class Logs extends Page implements HasActions, HasForms ->send(); } - public function getUsageChartData(): array - { - $start = now()->subDays(29)->startOfDay(); - $end = now()->endOfDay(); - $records = UserResourceUsage::query() - ->where('user_id', Auth::id()) - ->whereBetween('captured_at', [$start, $end]) - ->get(); - - $labels = []; - for ($i = 0; $i < 30; $i++) { - $labels[] = $start->copy()->addDays($i)->format('Y-m-d'); - } - $index = array_flip($labels); - - $metrics = ['disk_bytes', 'database_bytes', 'mail_bytes', 'bandwidth_bytes']; - $values = []; - foreach ($metrics as $metric) { - $values[$metric] = array_fill(0, count($labels), 0); - } - - foreach ($records as $record) { - $date = $record->captured_at?->format('Y-m-d'); - if (! $date || ! isset($index[$date])) { - continue; - } - $idx = $index[$date]; - $metric = $record->metric; - if (! isset($values[$metric])) { - continue; - } - if ($metric === 'bandwidth_bytes') { - $values[$metric][$idx] += (int) $record->value; - } else { - $values[$metric][$idx] = max($values[$metric][$idx], (int) $record->value); - } - } - - $toGb = fn (int $bytes) => round($bytes / 1024 / 1024 / 1024, 2); - - return [ - 'labels' => $labels, - 'series' => [ - ['name' => __('Disk'), 'data' => array_map($toGb, $values['disk_bytes'])], - ['name' => __('Databases'), 'data' => array_map($toGb, $values['database_bytes'])], - ['name' => __('Mail'), 'data' => array_map($toGb, $values['mail_bytes'])], - ['name' => __('Bandwidth'), 'data' => array_map($toGb, $values['bandwidth_bytes'])], - ], - ]; - } - public function getActivityLogs() { return AuditLog::query() @@ -301,7 +247,6 @@ class Logs extends Page implements HasActions, HasForms protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('generateStats') ->label(__('Generate Statistics')) diff --git a/app/Filament/Jabali/Pages/MailingLists.php b/app/Filament/Jabali/Pages/MailingLists.php index 6d97dd9..68a8a31 100644 --- a/app/Filament/Jabali/Pages/MailingLists.php +++ b/app/Filament/Jabali/Pages/MailingLists.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\UserSetting; use BackedEnum; use Filament\Actions\Concerns\InteractsWithActions; @@ -22,7 +21,6 @@ use Illuminate\Support\Facades\Auth; class MailingLists extends Page implements HasActions, HasForms { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; diff --git a/app/Filament/Jabali/Pages/PhpSettings.php b/app/Filament/Jabali/Pages/PhpSettings.php index 08ffa8d..6015cc7 100644 --- a/app/Filament/Jabali/Pages/PhpSettings.php +++ b/app/Filament/Jabali/Pages/PhpSettings.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Services\Agent\AgentClient; use BackedEnum; use Filament\Actions\Action; @@ -23,7 +22,6 @@ use Illuminate\Support\Facades\Auth; class PhpSettings extends Page implements HasActions, HasForms { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; @@ -267,7 +265,6 @@ class PhpSettings extends Page implements HasActions, HasForms protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->saveSettingsAction(), ]; } diff --git a/app/Filament/Jabali/Pages/PostgreSQL.php b/app/Filament/Jabali/Pages/PostgreSQL.php index a767d00..3b1eb95 100644 --- a/app/Filament/Jabali/Pages/PostgreSQL.php +++ b/app/Filament/Jabali/Pages/PostgreSQL.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Services\Agent\AgentClient; use BackedEnum; use Exception; @@ -28,7 +27,6 @@ use Livewire\Attributes\Url; class PostgreSQL extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; diff --git a/app/Filament/Jabali/Pages/SshKeys.php b/app/Filament/Jabali/Pages/SshKeys.php index 01b5100..3423d5a 100644 --- a/app/Filament/Jabali/Pages/SshKeys.php +++ b/app/Filament/Jabali/Pages/SshKeys.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use BackedEnum; use Filament\Actions\Action; use Filament\Actions\Concerns\InteractsWithActions; @@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Auth; class SshKeys extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -203,7 +201,6 @@ class SshKeys extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), Action::make('generateKey') ->label(__('Generate SSH Key')) ->icon('heroicon-o-sparkles') diff --git a/app/Filament/Jabali/Pages/Ssl.php b/app/Filament/Jabali/Pages/Ssl.php index fe8086f..b1df234 100644 --- a/app/Filament/Jabali/Pages/Ssl.php +++ b/app/Filament/Jabali/Pages/Ssl.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Domain; use App\Models\SslCertificate; use App\Services\Agent\AgentClient; @@ -29,7 +28,6 @@ use Illuminate\Support\Facades\Auth; class Ssl extends Page implements HasActions, HasForms, HasTable { - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -498,7 +496,6 @@ class Ssl extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->installCustomCertificateAction(), ]; } diff --git a/app/Filament/Jabali/Pages/WordPress.php b/app/Filament/Jabali/Pages/WordPress.php index feaf251..127cf88 100644 --- a/app/Filament/Jabali/Pages/WordPress.php +++ b/app/Filament/Jabali/Pages/WordPress.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Filament\Jabali\Pages; -use App\Filament\Concerns\HasPageTour; use App\Models\Domain; use App\Models\MysqlCredential; use App\Services\Agent\AgentClient; @@ -36,7 +35,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable { protected static ?string $slug = 'wordpress'; - use HasPageTour; use InteractsWithActions; use InteractsWithForms; use InteractsWithTable; @@ -330,7 +328,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable protected function getHeaderActions(): array { return [ - $this->getTourAction(), $this->scanAction(), $this->installAction(), ]; diff --git a/app/Http/Controllers/AutomationApiController.php b/app/Http/Controllers/AutomationApiController.php index bd406a7..a53471d 100644 --- a/app/Http/Controllers/AutomationApiController.php +++ b/app/Http/Controllers/AutomationApiController.php @@ -7,10 +7,8 @@ namespace App\Http\Controllers; use App\Models\Domain; use App\Models\HostingPackage; use App\Models\User; -use App\Models\UserResourceLimit; use App\Services\Agent\AgentClient; use App\Services\System\LinuxUserService; -use App\Services\System\ResourceLimitService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -76,29 +74,6 @@ class AutomationApiController extends Controller } } - if ($package) { - $cpu = $package->cpu_limit_percent; - $memory = $package->memory_limit_mb; - $io = $package->io_limit_mb; - $hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0); - - if ($hasLimits) { - $limit = UserResourceLimit::firstOrNew(['user_id' => $user->id]); - $limit->fill([ - 'cpu_limit_percent' => $cpu, - 'memory_limit_mb' => $memory, - 'io_limit_mb' => $io, - 'is_active' => true, - ])->save(); - - try { - app(ResourceLimitService::class)->apply($limit); - } catch (\Exception) { - // cgroup apply failure shouldn't block user creation - } - } - } - return response()->json(['user' => $user], 201); } diff --git a/app/Models/HostingPackage.php b/app/Models/HostingPackage.php index 62bb86b..5b7ec28 100644 --- a/app/Models/HostingPackage.php +++ b/app/Models/HostingPackage.php @@ -20,9 +20,6 @@ class HostingPackage extends Model 'domains_limit', 'databases_limit', 'mailboxes_limit', - 'cpu_limit_percent', - 'memory_limit_mb', - 'io_limit_mb', 'is_active', ]; diff --git a/app/Models/UserResourceLimit.php b/app/Models/UserResourceLimit.php deleted file mode 100644 index 06e3807..0000000 --- a/app/Models/UserResourceLimit.php +++ /dev/null @@ -1,31 +0,0 @@ - 'boolean', - ]; - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } -} diff --git a/app/Models/UserResourceUsage.php b/app/Models/UserResourceUsage.php deleted file mode 100644 index 344be64..0000000 --- a/app/Models/UserResourceUsage.php +++ /dev/null @@ -1,28 +0,0 @@ - 'integer', - 'captured_at' => 'datetime', - ]; - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } -} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index fd38173..78995c1 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -3,6 +3,7 @@ namespace App\Providers\Filament; use App\Filament\Admin\Pages\Auth\Login as AdminLogin; +use App\Filament\Admin\Pages\Dashboard; use App\Filament\AvatarProviders\InitialsAvatarProvider; use App\Http\Middleware\SetLocale; use App\Models\DnsSetting; @@ -10,7 +11,6 @@ use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; -use App\Filament\Admin\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; @@ -38,13 +38,13 @@ class AdminPanelProvider extends PanelProvider 'primary' => Color::Red, ]) ->darkMode() - ->brandName(fn () => DnsSetting::get('panel_name', 'Jabali') . ' Admin') + ->brandName(fn () => DnsSetting::get('panel_name', 'Jabali').' Admin') ->favicon(asset('favicon.ico')) ->renderHook( PanelsRenderHook::HEAD_END, - fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure') . - '' . - \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/admin-tour.js', 'resources/js/server-charts.js'])->toHtml() . + fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure'). + ''. + \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml(). $this->getRtlScript() ) ->renderHook( @@ -59,10 +59,6 @@ $this->getRtlScript() PanelsRenderHook::USER_MENU_BEFORE, fn () => view('components.language-switcher') ) - ->renderHook( - PanelsRenderHook::BODY_END, - fn () => view('components.admin-tour') - ) ->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources') ->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages') ->pages([ @@ -141,8 +137,10 @@ $this->getRtlScript() shuffle($shuffled); for ($col = 0; $col < 20; $col++) { $word = $shuffled[$col % count($shuffled)]; - if ($col % 3 === 0) shuffle($shuffled); // Re-shuffle periodically - $rowContent .= $word . ' · '; + if ($col % 3 === 0) { + shuffle($shuffled); + } // Re-shuffle periodically + $rowContent .= $word.' · '; } $rows .= "
{$rowContent}
"; } diff --git a/app/Providers/Filament/JabaliPanelProvider.php b/app/Providers/Filament/JabaliPanelProvider.php index 848d355..15f6546 100644 --- a/app/Providers/Filament/JabaliPanelProvider.php +++ b/app/Providers/Filament/JabaliPanelProvider.php @@ -4,10 +4,10 @@ namespace App\Providers\Filament; use App\Filament\AvatarProviders\InitialsAvatarProvider; use App\Filament\Jabali\Pages\Auth\Login; -use App\Models\DnsSetting; -use App\Models\User; use App\Http\Middleware\RedirectAdminFromUserPanel; use App\Http\Middleware\SetLocale; +use App\Models\DnsSetting; +use App\Models\User; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -22,7 +22,6 @@ use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; -use Illuminate\Support\Facades\Blade; use Illuminate\View\Middleware\ShareErrorsFromSession; class JabaliPanelProvider extends PanelProvider @@ -46,14 +45,14 @@ class JabaliPanelProvider extends PanelProvider ->favicon(asset('favicon.ico')) ->renderHook( PanelsRenderHook::HEAD_END, - fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more') . - '' . - \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/admin-tour.js', 'resources/js/server-charts.js'])->toHtml() . + fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more'). + ''. + \Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml(). $this->getRtlScript() ) ->renderHook( PanelsRenderHook::BODY_START, - fn () => (request()->routeIs('filament.jabali.auth.login') ? $this->getLoginWordCloud() : '') . $this->renderImpersonationNotice() + fn () => (request()->routeIs('filament.jabali.auth.login') ? $this->getLoginWordCloud() : '').$this->renderImpersonationNotice() ) ->renderHook( PanelsRenderHook::FOOTER, @@ -63,10 +62,6 @@ $this->getRtlScript() PanelsRenderHook::USER_MENU_BEFORE, fn () => view('components.language-switcher') ) - ->renderHook( - PanelsRenderHook::BODY_END, - fn () => view('components.user-tour') - ) ->discoverResources(in: app_path('Filament/Jabali/Resources'), for: 'App\\Filament\\Jabali\\Resources') ->discoverPages(in: app_path('Filament/Jabali/Pages'), for: 'App\\Filament\\Jabali\\Pages') ->pages([]) @@ -106,7 +101,7 @@ $this->getRtlScript() protected function renderImpersonationNotice(): string { - if (!session()->has('impersonated_by')) { + if (! session()->has('impersonated_by')) { return ''; } @@ -114,7 +109,7 @@ $this->getRtlScript() $admin = User::find($adminId); $currentUser = auth()->user(); - if (!$admin || !$currentUser) { + if (! $admin || ! $currentUser) { return ''; } @@ -172,8 +167,10 @@ $this->getRtlScript() shuffle($shuffled); for ($col = 0; $col < 20; $col++) { $word = $shuffled[$col % count($shuffled)]; - if ($col % 3 === 0) shuffle($shuffled); - $rowContent .= $word . ' · '; + if ($col % 3 === 0) { + shuffle($shuffled); + } + $rowContent .= $word.' · '; } $rows .= "
{$rowContent}
"; } diff --git a/app/Services/Agent/AgentClient.php b/app/Services/Agent/AgentClient.php index d56ec8f..da8a4bb 100644 --- a/app/Services/Agent/AgentClient.php +++ b/app/Services/Agent/AgentClient.php @@ -1333,7 +1333,7 @@ class AgentClient return $this->send('updates.run'); } - // WAF / Geo / Resource limits + // WAF / Geo public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog): array { return $this->send('waf.apply', [ @@ -1350,44 +1350,6 @@ class AgentClient ]); } - public function cgroupApplyUserLimits(string $username, ?int $cpuLimit, ?int $memoryLimit, ?int $ioLimit, bool $isActive = true): array - { - if (! $isActive) { - return $this->cgroupClearUserLimits($username); - } - - return $this->send('cgroup.apply_user_limits', [ - 'username' => $username, - 'cpu_limit_percent' => $cpuLimit, - 'memory_limit_mb' => $memoryLimit, - 'io_limit_mb' => $ioLimit, - ]); - } - - public function cgroupClearUserLimits(string $username): array - { - return $this->send('cgroup.clear_user_limits', [ - 'username' => $username, - ]); - } - - public function cgroupSyncUserProcesses(string $username): array - { - return $this->send('cgroup.sync_user_processes', [ - 'username' => $username, - ]); - } - - public function cgroupSyncAllProcesses(): array - { - return $this->send('cgroup.sync_all_processes'); - } - - public function cgroupClearAllLimits(): array - { - return $this->send('cgroup.clear_all_limits'); - } - public function databasePersistTuning(string $name, string $value): array { return $this->send('database.persist_tuning', [ diff --git a/app/Services/System/ResourceLimitService.php b/app/Services/System/ResourceLimitService.php deleted file mode 100644 index 6c4b3d2..0000000 --- a/app/Services/System/ResourceLimitService.php +++ /dev/null @@ -1,45 +0,0 @@ -user; - if (! $user) { - throw new RuntimeException('User not found for resource limit.'); - } - - $agent = new AgentClient; - $agent->cgroupApplyUserLimits( - $user->username, - $limit->cpu_limit_percent ? (int) $limit->cpu_limit_percent : null, - $limit->memory_limit_mb ? (int) $limit->memory_limit_mb : null, - $limit->io_limit_mb ? (int) $limit->io_limit_mb : null, - (bool) $limit->is_active - ); - } - - public function clear(UserResourceLimit $limit): void - { - $user = $limit->user; - if (! $user) { - return; - } - - $agent = new AgentClient; - $agent->cgroupClearUserLimits($user->username); - } -} diff --git a/bin/jabali-agent b/bin/jabali-agent index 24ce547..766e432 100755 --- a/bin/jabali-agent +++ b/bin/jabali-agent @@ -547,11 +547,6 @@ function handleAction(array $request): array 'updates.run' => updatesRun($params), 'waf.apply' => wafApplySettings($params), 'geo.apply_rules' => geoApplyRules($params), - 'cgroup.apply_user_limits' => cgroupApplyUserLimits($params), - 'cgroup.clear_user_limits' => cgroupClearUserLimits($params), - 'cgroup.sync_user_processes' => cgroupSyncUserProcesses($params), - 'cgroup.sync_all_processes' => cgroupSyncAllProcesses($params), - 'cgroup.clear_all_limits' => cgroupClearAllUserLimits($params), 'database.persist_tuning' => databasePersistTuning($params), 'server.export_config' => serverExportConfig($params), 'server.import_config' => serverImportConfig($params), @@ -818,11 +813,6 @@ function createUser(array $params): array logger("Warning: Failed to create Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error')); } - $cgroupResult = ensureUserCgroup($username); - if (!($cgroupResult['success'] ?? false)) { - logger("Warning: Failed to initialize cgroup for $username: " . ($cgroupResult['error'] ?? 'Unknown error')); - } - logger("Created user $username with home directory $homeDir"); return [ @@ -1040,8 +1030,6 @@ function deleteUser(array $params): array } } - cgroupRemoveUser($username); - // Reload services exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null'); exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null'); @@ -3040,386 +3028,6 @@ function geoApplyRules(array $params): array return ['success' => true, 'rules' => count($ruleset)]; } -function getRootBlockDevice(): ?string -{ - exec('findmnt -no SOURCE / 2>/dev/null', $output, $code); - $source = trim($output[0] ?? ''); - if ($source === '') { - return null; - } - - if (str_starts_with($source, '/dev/')) { - return $source; - } - - exec('readlink -f ' . escapeshellarg($source) . ' 2>/dev/null', $resolved, $resolvedCode); - $resolvedPath = trim($resolved[0] ?? ''); - - return $resolvedPath ?: null; -} - -function isCgroupV2Available(): bool -{ - return is_file('/sys/fs/cgroup/cgroup.controllers'); -} - -function getJabaliCgroupRoot(): string -{ - return '/sys/fs/cgroup/jabali.slice'; -} - -function enableCgroupControllers(string $path, array $controllers): void -{ - $controllersFile = $path . '/cgroup.controllers'; - $subtreeFile = $path . '/cgroup.subtree_control'; - - if (!is_readable($controllersFile) || !is_writable($subtreeFile)) { - return; - } - - $available = preg_split('/\s+/', trim(file_get_contents($controllersFile))); - $toEnable = []; - foreach ($controllers as $controller) { - if (in_array($controller, $available, true)) { - $toEnable[] = '+' . $controller; - } - } - - if (!empty($toEnable)) { - @file_put_contents($subtreeFile, implode(' ', $toEnable)); - } -} - -function ensureJabaliCgroupRoot(): array -{ - if (!isCgroupV2Available()) { - return ['success' => false, 'error' => 'cgroup v2 is not available']; - } - - $root = getJabaliCgroupRoot(); - if (!is_dir($root)) { - exec('systemctl start jabali.slice 2>/dev/null', $output, $code); - } - - if (!is_dir($root)) { - return ['success' => false, 'error' => 'Failed to initialize jabali.slice']; - } - - enableCgroupControllers($root, ['cpu', 'memory', 'io']); - - return ['success' => true, 'path' => $root]; -} - -function getUserCgroupPath(string $username): string -{ - return getJabaliCgroupRoot() . '/user-' . $username; -} - -function ensureUserCgroup(string $username): array -{ - $rootResult = ensureJabaliCgroupRoot(); - if (!($rootResult['success'] ?? false)) { - return $rootResult; - } - - $path = getUserCgroupPath($username); - if (!is_dir($path)) { - mkdir($path, 0755, true); - } - - if (!is_dir($path)) { - return ['success' => false, 'error' => 'Failed to create user cgroup']; - } - - return ['success' => true, 'path' => $path]; -} - -function writeCgroupValue(string $path, string $file, string $value): bool -{ - $target = $path . '/' . $file; - if (!is_writable($target)) { - return false; - } - - return file_put_contents($target, $value) !== false; -} - -function getBlockDeviceId(?string $device): ?string -{ - if (!$device) { - return null; - } - - exec('lsblk -no MAJ:MIN ' . escapeshellarg($device) . ' 2>/dev/null', $output, $code); - $id = trim($output[0] ?? ''); - - return $id !== '' ? $id : null; -} - -function cgroupWriteCpuMax(string $path, int $cpuPercent): void -{ - $period = 100000; - - if ($cpuPercent <= 0) { - writeCgroupValue($path, 'cpu.max', 'max ' . $period); - return; - } - - $quota = (int) round($period * ($cpuPercent / 100)); - $quota = max($quota, 1000); - writeCgroupValue($path, 'cpu.max', $quota . ' ' . $period); -} - -function cgroupWriteMemoryMax(string $path, int $memoryMb): void -{ - if ($memoryMb <= 0) { - writeCgroupValue($path, 'memory.max', 'max'); - return; - } - - $bytes = $memoryMb * 1024 * 1024; - writeCgroupValue($path, 'memory.max', (string) $bytes); -} - -function cgroupWriteIoMax(string $path, int $ioMb): void -{ - $device = getRootBlockDevice(); - $deviceId = getBlockDeviceId($device); - - if (!$deviceId) { - return; - } - - if ($ioMb <= 0) { - writeCgroupValue($path, 'io.max', $deviceId . ' rbps=max wbps=max'); - return; - } - - $bytes = $ioMb * 1024 * 1024; - writeCgroupValue($path, 'io.max', $deviceId . ' rbps=' . $bytes . ' wbps=' . $bytes); -} - -function moveUserProcessesToCgroup(int $uid, string $cgroupPath): int -{ - $moved = 0; - foreach (glob('/proc/[0-9]*') as $procPath) { - $statusFile = $procPath . '/status'; - if (!is_readable($statusFile)) { - continue; - } - - $status = file($statusFile, FILE_IGNORE_NEW_LINES); - if (!$status) { - continue; - } - - $matchesUid = false; - foreach ($status as $line) { - if (str_starts_with($line, 'Uid:')) { - $parts = preg_split('/\s+/', trim($line)); - $matchesUid = isset($parts[1]) && (int) $parts[1] === $uid; - break; - } - } - - if (!$matchesUid) { - continue; - } - - $pid = basename($procPath); - if (!ctype_digit($pid)) { - continue; - } - - if (@file_put_contents($cgroupPath . '/cgroup.procs', $pid) !== false) { - $moved++; - } - } - - return $moved; -} - -function cgroupSyncUserProcesses(array $params): array -{ - $username = $params['username'] ?? ''; - if (!validateUsername($username)) { - return ['success' => false, 'error' => 'Invalid username format']; - } - - $userInfo = posix_getpwnam($username); - if (!$userInfo) { - return ['success' => false, 'error' => 'User not found']; - } - - $cgroup = ensureUserCgroup($username); - if (!($cgroup['success'] ?? false)) { - return $cgroup; - } - - $moved = moveUserProcessesToCgroup((int) $userInfo['uid'], $cgroup['path']); - - return ['success' => true, 'moved' => $moved]; -} - -function cgroupSyncAllProcesses(array $params): array -{ - $rootResult = ensureJabaliCgroupRoot(); - if (!($rootResult['success'] ?? false)) { - return $rootResult; - } - - $movedTotal = 0; - foreach (glob('/home/*', GLOB_ONLYDIR) as $homeDir) { - $username = basename($homeDir); - if (!validateUsername($username)) { - continue; - } - - $result = cgroupSyncUserProcesses(['username' => $username]); - if ($result['success'] ?? false) { - $movedTotal += (int) ($result['moved'] ?? 0); - } - } - - return ['success' => true, 'moved' => $movedTotal]; -} - -function cgroupClearAllUserLimits(array $params): array -{ - $rootResult = ensureJabaliCgroupRoot(); - if (!($rootResult['success'] ?? false)) { - return $rootResult; - } - - $cleared = 0; - foreach (glob(getJabaliCgroupRoot() . '/user-*') as $cgroupPath) { - if (!is_dir($cgroupPath)) { - continue; - } - - cgroupWriteCpuMax($cgroupPath, 0); - cgroupWriteMemoryMax($cgroupPath, 0); - cgroupWriteIoMax($cgroupPath, 0); - $cleared++; - } - - return ['success' => true, 'cleared' => $cleared]; -} - -function cgroupRemoveUser(string $username): void -{ - $path = getUserCgroupPath($username); - if (is_dir($path)) { - @rmdir($path); - } -} - -function readCgroupStatValue(string $path, string $key): ?int -{ - $file = $path . '/cpu.stat'; - if (!is_readable($file)) { - return null; - } - - $lines = file($file, FILE_IGNORE_NEW_LINES); - if (!$lines) { - return null; - } - - foreach ($lines as $line) { - if (str_starts_with($line, $key . ' ')) { - $parts = explode(' ', trim($line)); - return isset($parts[1]) ? (int) $parts[1] : null; - } - } - - return null; -} - -function readCgroupIoTotal(string $path): int -{ - $file = $path . '/io.stat'; - if (!is_readable($file)) { - return 0; - } - - $total = 0; - $lines = file($file, FILE_IGNORE_NEW_LINES); - if (!$lines) { - return 0; - } - - foreach ($lines as $line) { - if (!str_contains($line, 'rbytes=') && !str_contains($line, 'wbytes=')) { - continue; - } - - if (preg_match('/rbytes=(\d+)/', $line, $m)) { - $total += (int) $m[1]; - } - if (preg_match('/wbytes=(\d+)/', $line, $m)) { - $total += (int) $m[1]; - } - } - - return $total; -} - -function cgroupApplyUserLimits(array $params): array -{ - $username = $params['username'] ?? ''; - if (!validateUsername($username)) { - return ['success' => false, 'error' => 'Invalid username format']; - } - - if (!posix_getpwnam($username)) { - return ['success' => false, 'error' => 'User not found']; - } - - $cpu = isset($params['cpu_limit_percent']) ? (int) $params['cpu_limit_percent'] : 0; - $memory = isset($params['memory_limit_mb']) ? (int) $params['memory_limit_mb'] : 0; - $io = isset($params['io_limit_mb']) ? (int) $params['io_limit_mb'] : 0; - - $cgroup = ensureUserCgroup($username); - if (!($cgroup['success'] ?? false)) { - return $cgroup; - } - - $path = $cgroup['path']; - - cgroupWriteCpuMax($path, $cpu); - cgroupWriteMemoryMax($path, $memory); - cgroupWriteIoMax($path, $io); - - cgroupSyncUserProcesses(['username' => $username]); - - return ['success' => true, 'message' => 'Resource limits applied', 'path' => $path]; -} - -function cgroupClearUserLimits(array $params): array -{ - $username = $params['username'] ?? ''; - if (!validateUsername($username)) { - return ['success' => false, 'error' => 'Invalid username format']; - } - - if (!posix_getpwnam($username)) { - return ['success' => false, 'error' => 'User not found']; - } - - $cgroup = ensureUserCgroup($username); - if (!($cgroup['success'] ?? false)) { - return $cgroup; - } - - $path = $cgroup['path']; - cgroupWriteCpuMax($path, 0); - cgroupWriteMemoryMax($path, 0); - cgroupWriteIoMax($path, 0); - - return ['success' => true, 'message' => 'Resource limits cleared', 'path' => $path]; -} - function databasePersistTuning(array $params): array { $name = $params['name'] ?? ''; @@ -6927,8 +6535,59 @@ function wpPageCacheEnable(array $params): array $config = file_get_contents($configFile); + $hasPageCache = strpos($config, 'fastcgi_cache JABALI') !== false; + $hasHammerBypass = strpos($config, 'cache_reason "hammer"') !== false; + + // If cache is already enabled, ensure hammer bypass exists + if ($hasPageCache && ! $hasHammerBypass) { + $hammerRule = <<<'HAMMER' + + # Skip cache for hammer/stress test endpoints + if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") { + set $skip_cache 1; + set $cache_reason "hammer"; + } +HAMMER; + + $updated = preg_replace( + '/\n\s*# Browser caching for static assets/', + $hammerRule . "\n\n # Browser caching for static assets", + $config, + 1 + ); + + if ($updated === null || $updated === $config) { + $updated = preg_replace( + '/(set \\$cache_reason \"\";)/', + "$1{$hammerRule}", + $config, + 1 + ); + } + + if ($updated && $updated !== $config) { + copy($configFile, $configFile . '.bak'); + if (file_put_contents($configFile, $updated) === false) { + return ['success' => false, 'error' => 'Failed to update nginx config']; + } + + exec('nginx -t 2>&1', $output, $exitCode); + if ($exitCode !== 0) { + copy($configFile . '.bak', $configFile); + return ['success' => false, 'error' => 'Nginx config test failed: ' . implode(' ', $output)]; + } + + exec('systemctl reload nginx 2>&1', $output, $exitCode); + if ($exitCode !== 0) { + return ['success' => false, 'error' => 'Failed to reload nginx']; + } + } + + return ['success' => true, 'message' => 'Page cache updated with hammer bypass']; + } + // Check if page cache is already enabled - if (strpos($config, 'fastcgi_cache JABALI') !== false) { + if ($hasPageCache) { return ['success' => true, 'message' => 'Page cache already enabled']; } @@ -6973,6 +6632,12 @@ function wpPageCacheEnable(array $params): array set $cache_reason "admin_url"; } + # Skip cache for hammer/stress test endpoints + if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") { + set $skip_cache 1; + set $cache_reason "hammer"; + } + # Browser caching for static assets (1 year, immutable for versioned files) location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf|zip)$ { expires 1y; @@ -24047,17 +23712,8 @@ function usageUserResources(array $params): array $cpuUsageUsec = null; $memoryBytes = 0; $diskIoTotal = 0; - - $cgroupPath = getUserCgroupPath($username); - if (is_dir($cgroupPath)) { - $memoryCurrent = @file_get_contents($cgroupPath . '/memory.current'); - if ($memoryCurrent !== false) { - $memoryBytes = (int) trim($memoryCurrent); - } - - $cpuUsageUsec = readCgroupStatValue($cgroupPath, 'usage_usec'); - $diskIoTotal = readCgroupIoTotal($cgroupPath); - } + $diskRead = 0; + $diskWrite = 0; if ($cpuUsageUsec === null || $memoryBytes === 0) { $cpuTotal = 0.0; @@ -24082,8 +23738,6 @@ function usageUserResources(array $params): array } if ($diskIoTotal === 0) { - $diskRead = 0; - $diskWrite = 0; foreach (glob('/proc/[0-9]*') as $procPath) { $statusFile = $procPath . '/status'; @@ -24136,6 +23790,8 @@ function usageUserResources(array $params): array 'cpu_usage_usec_total' => $cpuUsageUsec, 'memory_bytes' => $memoryBytes, 'disk_io_total_bytes' => $diskIoTotal, + 'disk_io_read_bytes_total' => $diskRead, + 'disk_io_write_bytes_total' => $diskWrite, ]; } diff --git a/database/migrations/2026_01_27_000002_create_user_resource_usages_table.php b/database/migrations/2026_01_27_000002_create_user_resource_usages_table.php deleted file mode 100644 index 7fdf41d..0000000 --- a/database/migrations/2026_01_27_000002_create_user_resource_usages_table.php +++ /dev/null @@ -1,29 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->string('metric', 50); - $table->unsignedBigInteger('value'); - $table->timestamp('captured_at')->index(); - $table->timestamps(); - - $table->index(['user_id', 'metric', 'captured_at']); - }); - } - - public function down(): void - { - Schema::dropIfExists('user_resource_usages'); - } -}; diff --git a/database/migrations/2026_01_27_081500_create_user_resource_limits_table.php b/database/migrations/2026_01_27_081500_create_user_resource_limits_table.php deleted file mode 100644 index ff64183..0000000 --- a/database/migrations/2026_01_27_081500_create_user_resource_limits_table.php +++ /dev/null @@ -1,28 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - $table->unsignedInteger('cpu_limit_percent')->nullable(); - $table->unsignedInteger('memory_limit_mb')->nullable(); - $table->unsignedInteger('io_limit_mb')->nullable(); - $table->boolean('is_active')->default(true); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('user_resource_limits'); - } -}; diff --git a/database/migrations/2026_01_27_090000_add_resource_limits_to_hosting_packages_table.php b/database/migrations/2026_01_27_090000_add_resource_limits_to_hosting_packages_table.php deleted file mode 100644 index e9db764..0000000 --- a/database/migrations/2026_01_27_090000_add_resource_limits_to_hosting_packages_table.php +++ /dev/null @@ -1,26 +0,0 @@ -unsignedInteger('cpu_limit_percent')->nullable()->after('mailboxes_limit'); - $table->unsignedInteger('memory_limit_mb')->nullable()->after('cpu_limit_percent'); - $table->unsignedInteger('io_limit_mb')->nullable()->after('memory_limit_mb'); - }); - } - - public function down(): void - { - Schema::table('hosting_packages', function (Blueprint $table) { - $table->dropColumn(['cpu_limit_percent', 'memory_limit_mb', 'io_limit_mb']); - }); - } -}; diff --git a/database/migrations/2026_01_28_000001_purge_resource_usage_artifacts.php b/database/migrations/2026_01_28_000001_purge_resource_usage_artifacts.php new file mode 100644 index 0000000..69e2a77 --- /dev/null +++ b/database/migrations/2026_01_28_000001_purge_resource_usage_artifacts.php @@ -0,0 +1,57 @@ +dropColumn($columns); + }); + } + } + + if (Schema::hasTable('dns_settings')) { + DB::table('dns_settings')->where('key', 'resource_limits_enabled')->delete(); + } + + if (Schema::hasTable('user_settings')) { + DB::table('user_settings') + ->whereIn('key', [ + 'bandwidth_total_bytes', + 'disk_io_total_bytes', + 'cpu_usage_usec_total', + 'cpu_usage_captured_at', + ]) + ->delete(); + } + } + + public function down(): void + { + // Irreversible purge of legacy resource usage artifacts. + } +}; diff --git a/install.sh b/install.sh index 0ffece4..8a13fcf 100755 --- a/install.sh +++ b/install.sh @@ -2519,33 +2519,6 @@ SERVICE log "Jabali Agent service configured" } -setup_cgroup_limits() { - header "Setting Up cgroup v2 Resource Limits" - - if [[ ! -f /sys/fs/cgroup/cgroup.controllers ]]; then - warn "cgroup v2 not detected - resource limits will be disabled" - return - fi - - cat > /etc/systemd/system/jabali.slice << 'SLICE' -[Slice] -Delegate=yes -CPUAccounting=yes -MemoryAccounting=yes -IOAccounting=yes -SLICE - - systemctl daemon-reload - systemctl enable jabali.slice 2>/dev/null || true - systemctl start jabali.slice 2>/dev/null || true - - if [[ -w /sys/fs/cgroup/jabali.slice/cgroup.subtree_control ]]; then - echo "+cpu +memory +io" > /sys/fs/cgroup/jabali.slice/cgroup.subtree_control || true - fi - - log "cgroup v2 slice configured" -} - setup_queue_service() { header "Setting Up Jabali Queue Worker" @@ -2918,11 +2891,6 @@ uninstall() { rm -f /etc/systemd/system/jabali-queue.service rm -rf /etc/systemd/system/jabali-queue.service.d - systemctl stop jabali.slice 2>/dev/null || true - rm -f /etc/systemd/system/jabali.slice - rm -rf /sys/fs/cgroup/jabali.slice/user-* 2>/dev/null || true - rmdir /sys/fs/cgroup/jabali.slice 2>/dev/null || true - local services=( nginx php-fpm @@ -3253,7 +3221,6 @@ main() { configure_redis setup_jabali setup_agent_service - setup_cgroup_limits setup_queue_service setup_scheduler_cron setup_logrotate diff --git a/package-lock.json b/package-lock.json index 70838df..f626eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "packages": { "": { "dependencies": { - "driver.js": "^1.4.0", "echarts": "^6.0.0" }, "devDependencies": { @@ -1715,12 +1714,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/driver.js": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz", - "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==", - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 5b25d07..cd3ec65 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "vite": "^7.0.7" }, "dependencies": { - "driver.js": "^1.4.0", "echarts": "^6.0.0" } } diff --git a/resources/js/admin-tour.js b/resources/js/admin-tour.js deleted file mode 100644 index 404f5c9..0000000 --- a/resources/js/admin-tour.js +++ /dev/null @@ -1,279 +0,0 @@ -import { driver } from 'driver.js'; -import 'driver.js/dist/driver.css'; -import { adminTours } from './tours/admin-tours.js'; -import { userTours } from './tours/user-tours.js'; - -// Detect current panel and page -function getCurrentPage() { - const path = window.location.pathname; - const isAdmin = path.includes('/jabali-admin'); - const isUser = path.includes('/jabali/'); - - let page = 'dashboard'; - - if (path.includes('/users')) page = 'users'; - else if (path.includes('/server-status')) page = 'serverStatus'; - else if (path.includes('/ssl-manager')) page = 'sslManager'; - else if (path.includes('/server-settings')) page = 'serverSettings'; - else if (path.includes('/email-settings')) page = 'emailSettings'; - else if (path.includes('/dns-zones')) page = 'dnsZones'; - else if (path.includes('/security')) page = 'security'; - else if (path.includes('/services')) page = 'services'; - else if (path.includes('/backups')) page = 'backups'; - else if (path.includes('/audit-logs')) page = 'auditLogs'; - else if (path.includes('/domains')) page = 'domains'; - else if (path.includes('/email')) page = 'email'; - else if (path.includes('/databases')) page = 'databases'; - else if (path.includes('/files')) page = 'files'; - else if (path.includes('/ssl')) page = 'ssl'; - else if (path.includes('/dns-records')) page = 'dnsRecords'; - else if (path.includes('/cron')) page = 'cronJobs'; - else if (path.includes('/php')) page = 'phpSettings'; - else if (path.includes('/ssh')) page = 'sshKeys'; - else if (path.includes('/wordpress')) page = 'wordpress'; - - return { isAdmin, isUser, page }; -} - -// Setup sidebar data-tour attributes -function setupSidebarTourAttributes() { - const sidebarItems = document.querySelectorAll('.fi-sidebar-item'); - const tourMappings = { - '/users': 'users', - '/server-status': 'server-status', - '/ssl-manager': 'ssl-manager', - '/server-settings': 'server-settings', - '/email-settings': 'email-settings', - '/dns-zones': 'dns-zones', - '/security': 'security', - '/services': 'services', - '/backups': 'backups', - '/audit-logs': 'audit-logs', - '/domains': 'domains', - '/email': 'email', - '/databases': 'databases', - '/files': 'files', - '/ssl': 'ssl', - '/dns-records': 'dns-records', - '/cron': 'cron-jobs', - '/php': 'php-settings', - '/ssh': 'ssh-keys', - '/wordpress': 'wordpress' - }; - - sidebarItems.forEach(item => { - const link = item.querySelector('a'); - if (link) { - const href = link.getAttribute('href') || ''; - for (const [pattern, tourId] of Object.entries(tourMappings)) { - if (href.includes(pattern)) { - item.setAttribute('data-tour', tourId); - break; - } - } - } - }); -} - -// Get tour configuration for current page -function getTourConfig(page, isAdmin) { - const tours = isAdmin ? adminTours : userTours; - return tours[page] || tours.dashboard; -} - -// Filter steps to only include existing elements -function filterSteps(steps) { - return steps.filter(step => { - if (!step.element) return true; // Non-element steps always included - const el = document.querySelector(step.element); - return el !== null; - }); -} - -// Start tour for current page -function startPageTour(pageTour = null) { - const t = window.jabaliTourTranslations || {}; - const { isAdmin, page } = getCurrentPage(); - - setupSidebarTourAttributes(); - - const tourConfig = pageTour ? { steps: () => pageTour } : getTourConfig(page, isAdmin); - - if (!tourConfig) { - console.warn('No tour configuration found for page:', page); - return; - } - - let steps = tourConfig.steps(t); - steps = filterSteps(steps); - - if (steps.length === 0) { - console.warn('No valid steps for tour'); - return; - } - - const driverObj = driver({ - showProgress: true, - animate: true, - smoothScroll: true, - allowClose: true, - overlayOpacity: 0.6, - stagePadding: 8, - nextBtnText: t.next || 'Next', - prevBtnText: t.prev || 'Previous', - doneBtnText: t.done || 'Done', - steps: steps, - onDestroyStarted: () => { - markTourCompleted(); - driverObj.destroy(); - } - }); - - driverObj.drive(); -} - -// Start main panel tour (dashboard overview) -function startAdminTour() { - const t = window.jabaliTourTranslations || {}; - const { isAdmin } = getCurrentPage(); - - setupSidebarTourAttributes(); - - // Build comprehensive panel tour - const steps = []; - - // Welcome - steps.push({ - popover: { - title: t.welcome || 'Welcome to Jabali!', - description: t.welcomeDesc || "Let's take a quick tour of your control panel." - } - }); - - // Sidebar - const sidebar = document.querySelector('.fi-sidebar-nav'); - if (sidebar) { - steps.push({ - element: '.fi-sidebar-nav', - popover: { - title: t.navigation || 'Navigation Menu', - description: t.navigationDesc || 'Access all panel sections from the sidebar.', - side: 'right', - align: 'start' - } - }); - } - - // Add sidebar items based on what's available - const sidebarSteps = isAdmin ? [ - { tour: 'users', title: t.users || 'Users Management', desc: t.usersDesc || 'Create and manage hosting accounts.' }, - { tour: 'server-status', title: t.serverStatus || 'Server Status', desc: t.serverStatusDesc || 'Monitor server health and services.' }, - { tour: 'ssl-manager', title: t.sslManager || 'SSL Manager', desc: t.sslManagerDesc || 'Manage SSL certificates.' }, - { tour: 'server-settings', title: t.serverSettings || 'Server Settings', desc: t.serverSettingsDesc || 'Configure panel and server options.' }, - { tour: 'email-settings', title: t.emailSettings || 'Email Settings', desc: t.emailSettingsDesc || 'Configure mail server.' }, - { tour: 'dns-zones', title: t.dnsZones || 'DNS Zones', desc: t.dnsZonesDesc || 'Manage DNS zones.' }, - { tour: 'services', title: t.services || 'Services', desc: t.servicesDesc || 'Manage server services.' }, - { tour: 'backups', title: t.backups || 'Backups', desc: t.backupsDesc || 'Configure backups.' }, - { tour: 'security', title: t.security || 'Security', desc: t.securityDesc || 'Firewall, Fail2ban, antivirus, and SSH settings.' }, - { tour: 'audit-logs', title: t.auditLogs || 'Audit Logs', desc: t.auditLogsDesc || 'Track panel activities.' } - ] : [ - { tour: 'domains', title: t.domains || 'Domains', desc: t.domainsDesc || 'Manage your websites.' }, - { tour: 'email', title: t.email || 'Email', desc: t.emailDesc || 'Manage email accounts.' }, - { tour: 'databases', title: t.databases || 'Databases', desc: t.databasesDesc || 'Manage MySQL databases.' }, - { tour: 'files', title: t.files || 'File Manager', desc: t.filesDesc || 'Manage your files.' }, - { tour: 'ssl', title: t.ssl || 'SSL', desc: t.sslDesc || 'Manage SSL certificates.' }, - { tour: 'backups', title: t.backups || 'Backups', desc: t.backupsDesc || 'Manage backups.' } - ]; - - sidebarSteps.forEach(item => { - const el = document.querySelector(`[data-tour="${item.tour}"]`); - if (el) { - steps.push({ - element: `[data-tour="${item.tour}"]`, - popover: { - title: item.title, - description: item.desc, - side: 'right', - align: 'start' - } - }); - } - }); - - // Top bar - const topbar = document.querySelector('.fi-topbar'); - if (topbar) { - steps.push({ - element: '.fi-topbar', - popover: { - title: t.topBar || 'Top Bar', - description: t.topBarDesc || 'Access profile, language, and theme settings.', - side: 'bottom', - align: 'end' - } - }); - } - - // Quick Actions widget - const quickActions = document.querySelector('.fi-wi'); - if (quickActions) { - steps.push({ - element: '.fi-wi', - popover: { - title: t.quickActions || 'Quick Actions', - description: t.quickActionsDesc || 'Quick shortcuts to common tasks.', - side: 'top', - align: 'start' - } - }); - } - - // Final - steps.push({ - popover: { - title: t.ready || "You're Ready!", - description: t.readyDesc || 'You can retake this tour anytime from the Dashboard.' - } - }); - - const driverObj = driver({ - showProgress: true, - animate: true, - smoothScroll: true, - allowClose: true, - overlayOpacity: 0.6, - stagePadding: 8, - nextBtnText: t.next || 'Next', - prevBtnText: t.prev || 'Previous', - doneBtnText: t.done || 'Done', - steps: steps, - onDestroyStarted: () => { - markTourCompleted(); - driverObj.destroy(); - } - }); - - driverObj.drive(); -} - -function markTourCompleted() { - if (window.Livewire) { - Livewire.dispatch('tour-completed'); - } - localStorage.setItem('jabali_tour_completed', 'true'); -} - -// Listen for Livewire events -document.addEventListener('livewire:init', () => { - Livewire.on('start-admin-tour', () => { - setTimeout(() => startAdminTour(), 300); - }); - - Livewire.on('start-page-tour', (data) => { - setTimeout(() => startPageTour(data?.steps), 300); - }); -}); - -// Export for manual triggering -window.startAdminTour = startAdminTour; -window.startPageTour = startPageTour; diff --git a/resources/js/tours/admin-tours.js b/resources/js/tours/admin-tours.js deleted file mode 100644 index 5af791c..0000000 --- a/resources/js/tours/admin-tours.js +++ /dev/null @@ -1,114 +0,0 @@ -// Admin Panel Page Tours Configuration - -export const adminTours = { - // Dashboard tour (main panel tour) - dashboard: { - steps: (t) => [ - { popover: { title: t.welcome, description: t.welcomeDesc } }, - { element: '.fi-sidebar-nav', popover: { title: t.navigation, description: t.navigationDesc, side: 'right' } }, - { element: '[data-tour="users"]', popover: { title: t.users, description: t.usersDesc, side: 'right' } }, - { element: '[data-tour="server-status"]', popover: { title: t.serverStatus, description: t.serverStatusDesc, side: 'right' } }, - { element: '[data-tour="ssl-manager"]', popover: { title: t.sslManager, description: t.sslManagerDesc, side: 'right' } }, - { element: '[data-tour="server-settings"]', popover: { title: t.serverSettings, description: t.serverSettingsDesc, side: 'right' } }, - { element: '[data-tour="services"]', popover: { title: t.services, description: t.servicesDesc, side: 'right' } }, - { element: '[data-tour="backups"]', popover: { title: t.backups, description: t.backupsDesc, side: 'right' } }, - { element: '[data-tour="security"]', popover: { title: t.security || 'Security', description: t.securityDesc || 'Firewall, Fail2ban, antivirus, and SSH settings.', side: 'right' } }, - { element: '.fi-topbar', popover: { title: t.topBar, description: t.topBarDesc, side: 'bottom' } }, - { element: '.fi-wi', popover: { title: t.quickActions, description: t.quickActionsDesc, side: 'top' } }, - { popover: { title: t.ready, description: t.readyDesc } } - ] - }, - - // Server Status page tour - serverStatus: { - steps: (t) => [ - { popover: { title: t.serverStatusTitle || 'Server Status', description: t.serverStatusIntro || 'Monitor your server health and performance metrics.' } }, - { element: '[wire\\:key*="cpu"], .fi-wi:first-child', popover: { title: t.cpuUsage || 'CPU Usage', description: t.cpuUsageDesc || 'Real-time CPU utilization percentage.', side: 'bottom' } }, - { element: '[wire\\:key*="memory"], .fi-wi:nth-child(2)', popover: { title: t.memoryUsage || 'Memory Usage', description: t.memoryUsageDesc || 'RAM usage and available memory.', side: 'bottom' } }, - { element: '[wire\\:key*="disk"], .fi-wi:nth-child(3)', popover: { title: t.diskUsage || 'Disk Usage', description: t.diskUsageDesc || 'Storage space utilization.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.servicesStatus || 'Services Status', description: t.servicesStatusDesc || 'Status of all running services. Green means running, red means stopped.', side: 'top' } } - ] - }, - - // SSL Manager page tour - sslManager: { - steps: (t) => [ - { popover: { title: t.sslTitle || 'SSL Manager', description: t.sslIntro || 'Manage SSL certificates for all your domains.' } }, - { element: 'button[wire\\:click*="requestCertificate"], .fi-btn', popover: { title: t.requestCert || 'Request Certificate', description: t.requestCertDesc || 'Request a free Let\'s Encrypt SSL certificate for your domains.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.certList || 'Certificate List', description: t.certListDesc || 'View all certificates, their domains, and expiry dates.', side: 'top' } } - ] - }, - - // Server Settings page tour - serverSettings: { - steps: (t) => [ - { popover: { title: t.settingsTitle || 'Server Settings', description: t.settingsIntro || 'Configure server-wide settings and preferences.' } }, - { element: '.fi-fo-field-wrp:first-child, [wire\\:model*="panel_name"]', popover: { title: t.panelBranding || 'Panel Branding', description: t.panelBrandingDesc || 'Customize the panel name and appearance.', side: 'right' } }, - { element: '[wire\\:model*="dns"], .fi-section:nth-child(2)', popover: { title: t.dnsSettings || 'DNS Settings', description: t.dnsSettingsDesc || 'Configure default nameservers for new domains.', side: 'right' } } - ] - }, - - // Services page tour - services: { - steps: (t) => [ - { popover: { title: t.servicesTitle || 'Services Management', description: t.servicesIntro || 'Control server services from one place.' } }, - { element: '.fi-ta, table', popover: { title: t.servicesList || 'Services List', description: t.servicesListDesc || 'View all services with their current status.', side: 'top' } }, - { element: 'button[wire\\:click*="restart"], .fi-btn-action', popover: { title: t.serviceActions || 'Service Actions', description: t.serviceActionsDesc || 'Start, stop, or restart services as needed.', side: 'left' } } - ] - }, - - // Backups page tour - backups: { - steps: (t) => [ - { popover: { title: t.backupsTitle || 'Backup Management', description: t.backupsIntro || 'Configure and manage server backups.' } }, - { element: '.fi-section:first-child, [wire\\:model*="schedule"]', popover: { title: t.backupSchedule || 'Backup Schedule', description: t.backupScheduleDesc || 'Set automatic backup schedules.', side: 'right' } }, - { element: '.fi-section:nth-child(2), [wire\\:model*="remote"]', popover: { title: t.remoteStorage || 'Remote Storage', description: t.remoteStorageDesc || 'Configure S3, FTP, or SFTP for offsite backups.', side: 'right' } }, - { element: '.fi-ta, table', popover: { title: t.backupHistory || 'Backup History', description: t.backupHistoryDesc || 'View and restore from previous backups.', side: 'top' } } - ] - }, - - // Security page tour - security: { - steps: (t) => [ - { popover: { title: t.securityTitle || 'Security Center', description: t.securityIntro || 'Manage firewall, intrusion prevention, antivirus, and SSH settings.' } }, - { element: '.fi-tabs', popover: { title: t.securityTabs || 'Security Tabs', description: t.securityTabsDesc || 'Switch between Overview, Firewall, Fail2ban, Antivirus, and SSH settings.', side: 'bottom' } }, - { element: '.fi-section:first-child', popover: { title: t.securityOverview || 'Security Status', description: t.securityOverviewDesc || 'Quick overview of your server security status.', side: 'bottom' } }, - { element: 'button[wire\\:click*="toggleFirewall"], .fi-btn:first-child', popover: { title: t.firewallToggle || 'Firewall Controls', description: t.firewallToggleDesc || 'Enable, disable, or configure UFW firewall.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.securityRules || 'Security Rules', description: t.securityRulesDesc || 'View and manage firewall rules and banned IPs.', side: 'top' } } - ] - }, - - // DNS Zones page tour - dnsZones: { - steps: (t) => [ - { popover: { title: t.dnsTitle || 'DNS Zones', description: t.dnsIntro || 'Manage DNS zones for all server domains.' } }, - { element: '.fi-ta, table', popover: { title: t.zonesList || 'DNS Zones List', description: t.zonesListDesc || 'All DNS zones with their record counts.', side: 'top' } } - ] - }, - - // Email Settings page tour - emailSettings: { - steps: (t) => [ - { popover: { title: t.emailTitle || 'Email Settings', description: t.emailIntro || 'Configure mail server settings.' } }, - { element: '.fi-section:first-child', popover: { title: t.mailConfig || 'Mail Configuration', description: t.mailConfigDesc || 'Configure SMTP, DKIM, and SPF settings.', side: 'right' } } - ] - }, - - // Audit Logs page tour - auditLogs: { - steps: (t) => [ - { popover: { title: t.auditTitle || 'Audit Logs', description: t.auditIntro || 'Track all panel activities for security.' } }, - { element: '.fi-ta, table', popover: { title: t.logsList || 'Activity Logs', description: t.logsListDesc || 'View all user actions with timestamps and details.', side: 'top' } } - ] - }, - - // Users page tour - users: { - steps: (t) => [ - { popover: { title: t.usersTitle || 'Users Management', description: t.usersIntro || 'Create and manage hosting accounts for your server.' } }, - { element: '.fi-header-actions button, .fi-btn', popover: { title: t.createUser || 'Create User', description: t.createUserDesc || 'Click here to create a new hosting account.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.usersList || 'Users List', description: t.usersListDesc || 'All hosting accounts with their usernames, domains, and status.', side: 'top' } }, - { element: '.fi-ta-row, tr', popover: { title: t.userActions || 'User Actions', description: t.userActionsDesc || 'Click on a user to edit, or use the actions menu for impersonation, suspension, or deletion.', side: 'left' } } - ] - } -}; diff --git a/resources/js/tours/user-tours.js b/resources/js/tours/user-tours.js deleted file mode 100644 index 73ce62a..0000000 --- a/resources/js/tours/user-tours.js +++ /dev/null @@ -1,121 +0,0 @@ -// User Panel Page Tours Configuration - -export const userTours = { - // User Dashboard tour - dashboard: { - steps: (t) => [ - { popover: { title: t.welcome || 'Welcome!', description: t.welcomeDesc || 'This is your hosting control panel dashboard.' } }, - { element: '.fi-sidebar-nav', popover: { title: t.navigation || 'Navigation', description: t.navigationDesc || 'Access all features from the sidebar.', side: 'right' } }, - { element: '[data-tour="domains"]', popover: { title: t.domains || 'Domains', description: t.domainsDesc || 'Manage your websites and domain settings.', side: 'right' } }, - { element: '[data-tour="email"]', popover: { title: t.email || 'Email', description: t.emailDesc || 'Create and manage email accounts.', side: 'right' } }, - { element: '[data-tour="databases"]', popover: { title: t.databases || 'Databases', description: t.databasesDesc || 'Create MySQL databases for your applications.', side: 'right' } }, - { element: '[data-tour="files"]', popover: { title: t.files || 'File Manager', description: t.filesDesc || 'Upload and manage your website files.', side: 'right' } }, - { element: '[data-tour="ssl"]', popover: { title: t.ssl || 'SSL Certificates', description: t.sslDesc || 'Secure your websites with free SSL certificates.', side: 'right' } }, - { element: '[data-tour="backups"]', popover: { title: t.backups || 'Backups', description: t.backupsDesc || 'Create and restore website backups.', side: 'right' } }, - { element: '.fi-topbar', popover: { title: t.topBar || 'Top Bar', description: t.topBarDesc || 'Access profile and settings.', side: 'bottom' } }, - { popover: { title: t.ready || 'Ready!', description: t.readyDesc || 'Start by adding your first domain.' } } - ] - }, - - // Domains page tour - domains: { - steps: (t) => [ - { popover: { title: t.domainsTitle || 'Domain Management', description: t.domainsIntro || 'Add and manage your websites here.' } }, - { element: 'button[wire\\:click*="add"], .fi-btn', popover: { title: t.addDomain || 'Add Domain', description: t.addDomainDesc || 'Click here to add a new domain or subdomain.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.domainsList || 'Your Domains', description: t.domainsListDesc || 'All your domains with their document roots and status.', side: 'top' } } - ] - }, - - // Email page tour - email: { - steps: (t) => [ - { popover: { title: t.emailTitle || 'Email Management', description: t.emailIntro || 'Create and manage email accounts for your domains.' } }, - { element: 'button[wire\\:click*="create"], .fi-btn', popover: { title: t.createEmail || 'Create Email', description: t.createEmailDesc || 'Create a new email account.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.emailList || 'Email Accounts', description: t.emailListDesc || 'Your email accounts with quota usage.', side: 'top' } }, - { element: '[wire\\:click*="webmail"], a[href*="webmail"]', popover: { title: t.webmail || 'Webmail', description: t.webmailDesc || 'Access your email through the web interface.', side: 'left' } } - ] - }, - - // Databases page tour - databases: { - steps: (t) => [ - { popover: { title: t.dbTitle || 'Database Management', description: t.dbIntro || 'Create MySQL databases for your applications.' } }, - { element: 'button[wire\\:click*="create"], .fi-btn', popover: { title: t.createDb || 'Create Database', description: t.createDbDesc || 'Create a new MySQL database.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.dbList || 'Your Databases', description: t.dbListDesc || 'All databases with size and user information.', side: 'top' } }, - { element: '[wire\\:click*="phpmyadmin"], a[href*="phpmyadmin"]', popover: { title: t.phpMyAdmin || 'phpMyAdmin', description: t.phpMyAdminDesc || 'Manage your database using phpMyAdmin.', side: 'left' } } - ] - }, - - // Files page tour - files: { - steps: (t) => [ - { popover: { title: t.filesTitle || 'File Manager', description: t.filesIntro || 'Upload and manage your website files.' } }, - { element: '.file-browser, .fi-section', popover: { title: t.fileBrowser || 'File Browser', description: t.fileBrowserDesc || 'Navigate through your files and folders.', side: 'top' } }, - { element: 'button[wire\\:click*="upload"], .fi-btn', popover: { title: t.upload || 'Upload Files', description: t.uploadDesc || 'Upload files to your server.', side: 'bottom' } }, - { element: 'button[wire\\:click*="newFolder"], .fi-btn', popover: { title: t.newFolder || 'New Folder', description: t.newFolderDesc || 'Create new directories.', side: 'bottom' } } - ] - }, - - // SSL page tour - ssl: { - steps: (t) => [ - { popover: { title: t.sslTitle || 'SSL Certificates', description: t.sslIntro || 'Secure your websites with SSL certificates.' } }, - { element: 'button[wire\\:click*="request"], .fi-btn', popover: { title: t.requestSsl || 'Request SSL', description: t.requestSslDesc || 'Get a free Let\'s Encrypt certificate.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.sslList || 'Your Certificates', description: t.sslListDesc || 'SSL certificates with expiry dates.', side: 'top' } } - ] - }, - - // Backups page tour - backups: { - steps: (t) => [ - { popover: { title: t.backupsTitle || 'Backups', description: t.backupsIntro || 'Create and manage your website backups.' } }, - { element: 'button[wire\\:click*="create"], .fi-btn', popover: { title: t.createBackup || 'Create Backup', description: t.createBackupDesc || 'Create a new backup of your website.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.backupList || 'Your Backups', description: t.backupListDesc || 'Available backups that can be restored.', side: 'top' } } - ] - }, - - // DNS Records page tour - dnsRecords: { - steps: (t) => [ - { popover: { title: t.dnsTitle || 'DNS Records', description: t.dnsIntro || 'Manage DNS records for your domain.' } }, - { element: 'button[wire\\:click*="add"], .fi-btn', popover: { title: t.addRecord || 'Add Record', description: t.addRecordDesc || 'Add A, CNAME, MX, or TXT records.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.recordsList || 'DNS Records', description: t.recordsListDesc || 'All DNS records for this domain.', side: 'top' } } - ] - }, - - // Cron Jobs page tour - cronJobs: { - steps: (t) => [ - { popover: { title: t.cronTitle || 'Cron Jobs', description: t.cronIntro || 'Schedule automated tasks.' } }, - { element: 'button[wire\\:click*="add"], .fi-btn', popover: { title: t.addCron || 'Add Cron Job', description: t.addCronDesc || 'Schedule a new automated task.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.cronList || 'Scheduled Tasks', description: t.cronListDesc || 'Your scheduled cron jobs.', side: 'top' } } - ] - }, - - // PHP Settings page tour - phpSettings: { - steps: (t) => [ - { popover: { title: t.phpTitle || 'PHP Settings', description: t.phpIntro || 'Configure PHP for your websites.' } }, - { element: 'select[wire\\:model*="version"], .fi-select', popover: { title: t.phpVersion || 'PHP Version', description: t.phpVersionDesc || 'Select the PHP version for your domain.', side: 'right' } }, - { element: '.fi-section, [wire\\:model*="memory"]', popover: { title: t.phpConfig || 'PHP Configuration', description: t.phpConfigDesc || 'Adjust memory limits, upload sizes, and other settings.', side: 'right' } } - ] - }, - - // SSH Keys page tour - sshKeys: { - steps: (t) => [ - { popover: { title: t.sshTitle || 'SSH Keys', description: t.sshIntro || 'Manage SSH keys for secure server access.' } }, - { element: 'button[wire\\:click*="add"], .fi-btn', popover: { title: t.addKey || 'Add SSH Key', description: t.addKeyDesc || 'Add a public SSH key for authentication.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.keysList || 'Your SSH Keys', description: t.keysListDesc || 'Authorized SSH keys for your account.', side: 'top' } } - ] - }, - - // WordPress page tour - wordpress: { - steps: (t) => [ - { popover: { title: t.wpTitle || 'WordPress Manager', description: t.wpIntro || 'Install and manage WordPress sites.' } }, - { element: 'button[wire\\:click*="install"], .fi-btn', popover: { title: t.installWp || 'Install WordPress', description: t.installWpDesc || 'One-click WordPress installation.', side: 'bottom' } }, - { element: '.fi-ta, table', popover: { title: t.wpList || 'WordPress Sites', description: t.wpListDesc || 'Your WordPress installations.', side: 'top' } } - ] - } -}; diff --git a/resources/views/components/admin-tour.blade.php b/resources/views/components/admin-tour.blade.php deleted file mode 100644 index b351dad..0000000 --- a/resources/views/components/admin-tour.blade.php +++ /dev/null @@ -1,42 +0,0 @@ -@php -$tourTranslations = [ - 'welcome' => __('Welcome to Jabali!'), - 'welcomeDesc' => __("Let's take a quick tour of your control panel."), - 'navigation' => __('Navigation Menu'), - 'navigationDesc' => __('Access all panel sections from the sidebar.'), - 'users' => __('Users Management'), - 'usersDesc' => __('Create and manage hosting accounts with domains, email, and databases.'), - 'serverStatus' => __('Server Status'), - 'serverStatusDesc' => __('Monitor server health, CPU, memory, disk space, and running services.'), - 'sslManager' => __('SSL Manager'), - 'sslManagerDesc' => __('Manage SSL certificates, request free Let\'s Encrypt certificates, and monitor expiry dates.'), - 'serverSettings' => __('Server Settings'), - 'serverSettingsDesc' => __('Configure panel branding, DNS settings, disk quotas, and email notifications.'), - 'emailSettings' => __('Email Settings'), - 'emailSettingsDesc' => __('Configure mail server settings, DKIM, SPF records, and spam filters.'), - 'dnsZones' => __('DNS Zones'), - 'dnsZonesDesc' => __('Manage DNS zones and records for all domains on this server.'), - 'security' => __('Security'), - 'securityDesc' => __('Configure ModSecurity WAF rules and security policies.'), - 'services' => __('Services'), - 'servicesDesc' => __('Start, stop, and restart server services like Nginx, PHP-FPM, MySQL, and Postfix.'), - 'backups' => __('Backup Management'), - 'backupsDesc' => __('Set up automated backups with remote storage support (S3, FTP, SFTP).'), - 'firewall' => __('Firewall'), - 'firewallDesc' => __('Manage firewall rules, block IPs, and configure Fail2Ban protection.'), - 'auditLogs' => __('Audit Logs'), - 'auditLogsDesc' => __('Track all actions performed in the panel for security and compliance.'), - 'topBar' => __('Top Bar'), - 'topBarDesc' => __('Access your profile, change language, switch themes, and logout.'), - 'quickActions' => __('Quick Actions'), - 'quickActionsDesc' => __('Use these shortcuts to quickly add users, access backups, manage services, or configure firewall.'), - 'ready' => __("You're Ready!"), - 'readyDesc' => __('Start by creating your first user from the Users menu. You can always retake this tour from the Dashboard.'), - 'next' => __('Next'), - 'prev' => __('Previous'), - 'done' => __('Done'), -]; -@endphp - diff --git a/resources/views/components/user-tour.blade.php b/resources/views/components/user-tour.blade.php deleted file mode 100644 index 6ce9e76..0000000 --- a/resources/views/components/user-tour.blade.php +++ /dev/null @@ -1,41 +0,0 @@ -@php -$tourTranslations = [ - // Main tour - 'welcome' => __('Welcome!'), - 'welcomeDesc' => __('This is your hosting control panel. Let\'s take a quick tour.'), - 'navigation' => __('Navigation'), - 'navigationDesc' => __('Access all features from the sidebar.'), - 'domains' => __('Domains'), - 'domainsDesc' => __('Manage your websites and domain settings.'), - 'email' => __('Email'), - 'emailDesc' => __('Create and manage email accounts.'), - 'databases' => __('Databases'), - 'databasesDesc' => __('Create MySQL databases for your applications.'), - 'files' => __('File Manager'), - 'filesDesc' => __('Upload and manage your website files.'), - 'ssl' => __('SSL Certificates'), - 'sslDesc' => __('Secure your websites with free SSL.'), - 'backups' => __('Backups'), - 'backupsDesc' => __('Create and restore website backups.'), - 'dnsRecords' => __('DNS Records'), - 'dnsRecordsDesc' => __('Manage DNS records for your domains.'), - 'cronJobs' => __('Cron Jobs'), - 'cronJobsDesc' => __('Schedule automated tasks.'), - 'phpSettings' => __('PHP Settings'), - 'phpSettingsDesc' => __('Configure PHP for your websites.'), - 'sshKeys' => __('SSH Keys'), - 'sshKeysDesc' => __('Manage SSH keys for server access.'), - 'wordpress' => __('WordPress'), - 'wordpressDesc' => __('Install and manage WordPress sites.'), - 'topBar' => __('Top Bar'), - 'topBarDesc' => __('Access profile and settings.'), - 'ready' => __('Ready!'), - 'readyDesc' => __('Start by adding your first domain. You can retake this tour anytime.'), - 'next' => __('Next'), - 'prev' => __('Previous'), - 'done' => __('Done'), -]; -@endphp - diff --git a/resources/views/filament/admin/pages/resource-usage.blade.php b/resources/views/filament/admin/pages/resource-usage.blade.php deleted file mode 100644 index bf7ee3a..0000000 --- a/resources/views/filament/admin/pages/resource-usage.blade.php +++ /dev/null @@ -1,383 +0,0 @@ - - @once - @vite('resources/js/server-charts.js') - @endonce - {{ $this->usageForm }} - - @php($hasRealData = !empty($chartData) && !empty($chartData['labels'])) - - @if(! $hasRealData) - -
-
- -
-

- {{ __('No usage data yet') }} -

-

- {{ __('Usage snapshots are collected hourly. Data will appear after the first collection run.') }} -

-
-
- @else -
- - {{ __('Latest Snapshot') }} -
-
-

{{ __('Disk') }}

-

{{ $summary['disk'] ?? __('No data') }}

-
-
-

{{ __('Mail') }}

-

{{ $summary['mail'] ?? __('No data') }}

-
-
-

{{ __('Databases') }}

-

{{ $summary['database'] ?? __('No data') }}

-
-
-

{{ __('Bandwidth (last hour)') }}

-

{{ $summary['bandwidth'] ?? __('No data') }}

-
-
-
- - - {{ __('Resource Usage (Last 30 Days)') }} - {{ __('Historical snapshots collected hourly.') }} - -
-
-
-
- - @if($hasPerformanceData) -
- - {{ __('CPU Usage (Last 30 Days)') }} - {{ __('Average CPU percent per snapshot.') }} - -
-
- {{ __('Limit') }}: - {{ $cpuLimitPercent ? $cpuLimitPercent . '%' : __('Unlimited') }} -
-
- {{ __('Average') }}: - {{ $cpuStats['avg'] ?? __('No data') }} -
-
- {{ __('Peak') }}: - {{ $cpuStats['max'] ?? __('No data') }} -
-
- -
-
-
-
- - - {{ __('Memory Usage (Last 30 Days)') }} - {{ __('Resident memory per snapshot.') }} - -
-
- {{ __('Limit') }}: - {{ $memoryLimitGb ? number_format($memoryLimitGb, 2) . ' GB' : __('Unlimited') }} -
-
- {{ __('Average') }}: - {{ $memoryStats['avg'] ?? __('No data') }} -
-
- {{ __('Peak') }}: - {{ $memoryStats['max'] ?? __('No data') }} -
-
- -
-
-
-
- - - {{ __('Disk IO (Last 30 Days)') }} - {{ __('Read/write MB per snapshot.') }} - -
-
-
-
- -
- @else - - {{ __('CPU, Memory & Disk IO') }} -
-
- -
-

- {{ __('No performance data yet') }} -

-

- {{ __('CPU, memory, and disk IO snapshots will appear after the next collection run.') }} -

-
-
- @endif -
- @endif -
diff --git a/resources/views/filament/jabali/pages/logs.blade.php b/resources/views/filament/jabali/pages/logs.blade.php index a637198..0125145 100644 --- a/resources/views/filament/jabali/pages/logs.blade.php +++ b/resources/views/filament/jabali/pages/logs.blade.php @@ -3,7 +3,6 @@ $tabs = [ 'logs' => ['label' => __('Logs'), 'icon' => 'heroicon-o-document-text'], 'stats' => ['label' => __('Statistics'), 'icon' => 'heroicon-o-chart-bar'], - 'usage' => ['label' => __('Resource Usage'), 'icon' => 'heroicon-o-chart-pie'], 'activity' => ['label' => __('Activity Log'), 'icon' => 'heroicon-o-clipboard-document-list'], ]; @endphp @@ -164,78 +163,6 @@ @endif @endif - @if($activeTab === 'usage') - @php($usageData = $this->getUsageChartData()) - - {{ __('Resource Usage (Last 30 Days)') }} - {{ __('Historical usage snapshots collected hourly.') }} - -
-
-
-
- - @endif - @if($activeTab === 'activity') {{ __('Activity Log') }} diff --git a/routes/console.php b/routes/console.php index 365e8c5..8c031bb 100644 --- a/routes/console.php +++ b/routes/console.php @@ -66,20 +66,6 @@ Schedule::command('jabali:sync-mailbox-quotas') ->runInBackground() ->appendOutputTo(storage_path('logs/mailbox-quota-sync.log')); -// User Resource Usage - runs hourly to capture per-user usage history -Schedule::command('jabali:collect-user-usage') - ->hourly() - ->withoutOverlapping() - ->runInBackground() - ->appendOutputTo(storage_path('logs/user-usage.log')); - -// Cgroup Sync - runs every minute to keep user processes assigned to cgroups -Schedule::command('jabali:sync-cgroups') - ->everyMinute() - ->withoutOverlapping() - ->runInBackground() - ->appendOutputTo(storage_path('logs/cgroup-sync.log')); - // Audit Log Rotation - runs daily to prune old audit logs (default: 90 days retention) Schedule::call(function () { $deleted = AuditLog::prune(); diff --git a/tests/Feature/JabaliSyncCgroupsTest.php b/tests/Feature/JabaliSyncCgroupsTest.php deleted file mode 100644 index 603d262..0000000 --- a/tests/Feature/JabaliSyncCgroupsTest.php +++ /dev/null @@ -1,50 +0,0 @@ -app->instance(AgentClient::class, new FakeAgentClient([ - 'success' => true, - 'moved' => 4, - ])); - - $this->artisan('jabali:sync-cgroups') - ->expectsOutput('Synced cgroups, moved 4 process(es).') - ->assertExitCode(0); - } - - public function test_syncs_single_user(): void - { - $this->app->instance(AgentClient::class, new FakeAgentClient([ - 'success' => true, - 'moved' => 1, - ])); - - $this->artisan('jabali:sync-cgroups --user=testuser') - ->expectsOutput('Synced cgroups, moved 1 process(es).') - ->assertExitCode(0); - } -} - -class FakeAgentClient extends AgentClient -{ - public function __construct(private array $result) {} - - public function cgroupSyncUserProcesses(string $username): array - { - return $this->result; - } - - public function cgroupSyncAllProcesses(): array - { - return $this->result; - } -} diff --git a/tests/Feature/ResourceUsageChartsTest.php b/tests/Feature/ResourceUsageChartsTest.php deleted file mode 100644 index 77e6147..0000000 --- a/tests/Feature/ResourceUsageChartsTest.php +++ /dev/null @@ -1,55 +0,0 @@ -admin()->create(); - $user = User::factory()->create(); - - $metrics = ['disk_bytes', 'mail_bytes', 'database_bytes', 'bandwidth_bytes']; - foreach ($metrics as $metric) { - UserResourceUsage::create([ - 'user_id' => $user->id, - 'metric' => $metric, - 'value' => 1024 * 1024 * 250, - 'captured_at' => now(), - ]); - } - - $response = $this->actingAs($admin, 'admin')->get('/jabali-admin/resource-usage'); - - $response->assertStatus(200); - $response->assertSee('Resource Usage (Last 30 Days)'); - $response->assertSee('resource-usage-chart-', false); - } - - public function test_user_logs_usage_tab_renders_chart(): void - { - $user = User::factory()->create(); - - UserResourceUsage::create([ - 'user_id' => $user->id, - 'metric' => 'disk_bytes', - 'value' => 1024 * 1024 * 250, - 'captured_at' => now(), - ]); - - $response = $this->actingAs($user)->get('/jabali-panel/logs?tab=usage'); - - $response->assertStatus(200); - $response->assertSee('Resource Usage (Last 30 Days)'); - $response->assertSee('x-ref="chart"', false); - } -} diff --git a/tests/Feature/WebmailSsoViewTest.php b/tests/Feature/WebmailSsoViewTest.php index 1d57880..2156548 100644 --- a/tests/Feature/WebmailSsoViewTest.php +++ b/tests/Feature/WebmailSsoViewTest.php @@ -35,9 +35,16 @@ class WebmailSsoViewTest extends TestCase $response = $this->actingAs($user)->get(route('webmail.sso', $mailbox)); - $response->assertStatus(200); - $response->assertSee('Webmail Login Required'); - $response->assertSee('Open Webmail Login'); + if (file_exists('/etc/jabali/roundcube-sso.conf')) { + $response->assertStatus(302); + $location = $response->headers->get('Location'); + $this->assertNotFalse($location); + $this->assertStringContainsString('/webmail/jabali-sso.php?token=', $location); + } else { + $response->assertStatus(200); + $response->assertSee('Webmail Login Required'); + $response->assertSee('Open Webmail Login'); + } } public function test_webmail_sso_shows_reset_required_when_password_missing(): void diff --git a/tests/Unit/RunCpanelRestoreTest.php b/tests/Unit/RunCpanelRestoreTest.php index d680acf..1976989 100644 --- a/tests/Unit/RunCpanelRestoreTest.php +++ b/tests/Unit/RunCpanelRestoreTest.php @@ -5,14 +5,19 @@ declare(strict_types=1); namespace Tests\Unit; use App\Jobs\RunCpanelRestore; +use App\Models\User; use App\Services\Agent\AgentClient; +use App\Services\Migration\MigrationDnsSyncService; use Exception; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; use Tests\TestCase; class RunCpanelRestoreTest extends TestCase { + use RefreshDatabase; + public function test_it_marks_restore_completed_and_logs_success(): void { $jobId = 'cpanel-restore-success'; @@ -21,6 +26,10 @@ class RunCpanelRestoreTest extends TestCase File::ensureDirectoryExists(dirname($logPath)); File::put($logPath, ''); + User::factory()->create([ + 'username' => 'example', + ]); + $this->app->instance(AgentClient::class, new class extends AgentClient { public function send(string $action, array $params = []): array @@ -41,7 +50,10 @@ class RunCpanelRestoreTest extends TestCase discoveredData: null, ); - $job->handle($this->app->make(AgentClient::class)); + $job->handle( + $this->app->make(AgentClient::class), + $this->app->make(MigrationDnsSyncService::class), + ); $status = Cache::get('cpanel_restore_status_'.$jobId); $this->assertSame('completed', $status['status'] ?? null); @@ -61,6 +73,10 @@ class RunCpanelRestoreTest extends TestCase File::ensureDirectoryExists(dirname($logPath)); File::put($logPath, ''); + User::factory()->create([ + 'username' => 'example', + ]); + $this->app->instance(AgentClient::class, new class extends AgentClient { public function send(string $action, array $params = []): array @@ -81,7 +97,10 @@ class RunCpanelRestoreTest extends TestCase discoveredData: null, ); - $job->handle($this->app->make(AgentClient::class)); + $job->handle( + $this->app->make(AgentClient::class), + $this->app->make(MigrationDnsSyncService::class), + ); $status = Cache::get('cpanel_restore_status_'.$jobId); $this->assertSame('failed', $status['status'] ?? null); diff --git a/tests/Unit/VersionFileTest.php b/tests/Unit/VersionFileTest.php index 0529324..fc4085f 100644 --- a/tests/Unit/VersionFileTest.php +++ b/tests/Unit/VersionFileTest.php @@ -14,7 +14,7 @@ class VersionFileTest extends TestCase $content = file_get_contents($versionPath); $this->assertNotFalse($content); - $this->assertStringContainsString('VERSION=0.9-rc2', $content); + $this->assertMatchesRegularExpression('/^VERSION=0\\.9-rc\\d*$/m', trim($content)); $this->assertStringNotContainsString('BUILD=', $content); } }