Remove resource usage collection and tours
This commit is contained in:
@@ -1,173 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\Mailbox;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserResourceUsage;
|
|
||||||
use App\Models\UserSetting;
|
|
||||||
use App\Services\Agent\AgentClient;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class CollectUserUsage extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'jabali:collect-user-usage {--user=} {--retain=90}';
|
|
||||||
|
|
||||||
protected $description = 'Collect per-user resource usage snapshots';
|
|
||||||
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$users = User::query()
|
|
||||||
->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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Services\Agent\AgentClient;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class JabaliSyncCgroups extends Command
|
|
||||||
{
|
|
||||||
public function __construct(private AgentClient $agent)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'jabali:sync-cgroups {--user=}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Sync user processes into Jabali cgroups';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$user = (string) ($this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Admin\Pages;
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use App\Models\BackupDestination;
|
use App\Models\BackupDestination;
|
||||||
use App\Models\BackupSchedule;
|
use App\Models\BackupSchedule;
|
||||||
@@ -40,7 +39,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class Backups extends Page implements HasActions, HasForms, HasTable
|
class Backups extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -508,7 +506,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('createServerBackup')
|
Action::make('createServerBackup')
|
||||||
->label(__('Create Server Backup'))
|
->label(__('Create Server Backup'))
|
||||||
->icon('heroicon-o-archive-box-arrow-down')
|
->icon('heroicon-o-archive-box-arrow-down')
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use Filament\Schemas\Components\EmbeddedTable;
|
|||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Illuminate\Contracts\Support\Htmlable;
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
use Livewire\Attributes\On;
|
|
||||||
|
|
||||||
class Dashboard extends Page implements HasActions, HasForms
|
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
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -107,16 +99,6 @@ class Dashboard extends Page implements HasActions, HasForms
|
|||||||
}
|
}
|
||||||
DnsSetting::set('onboarding_completed', '1');
|
DnsSetting::set('onboarding_completed', '1');
|
||||||
DnsSetting::clearCache();
|
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');
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Admin\Pages;
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
use App\Filament\Admin\Widgets\DnsPendingAddsTable;
|
use App\Filament\Admin\Widgets\DnsPendingAddsTable;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\DnsRecord;
|
use App\Models\DnsRecord;
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
@@ -39,7 +38,6 @@ use Livewire\Attributes\On;
|
|||||||
|
|
||||||
class DnsZones extends Page implements HasActions, HasForms, HasTable
|
class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -585,7 +583,6 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('syncAllZones')
|
Action::make('syncAllZones')
|
||||||
->label(__('Sync All Zones'))
|
->label(__('Sync All Zones'))
|
||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Admin\Pages;
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -26,7 +25,6 @@ use Illuminate\Contracts\Support\Htmlable;
|
|||||||
|
|
||||||
class PhpManager extends Page implements HasActions, HasForms, HasTable
|
class PhpManager extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -288,7 +286,6 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Admin\Pages;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserResourceUsage;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Concerns\InteractsWithForms;
|
|
||||||
use Filament\Forms\Contracts\HasForms;
|
|
||||||
use Filament\Pages\Page;
|
|
||||||
use Filament\Schemas\Components\Section;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Icons\Heroicon;
|
|
||||||
use Illuminate\Contracts\Support\Htmlable;
|
|
||||||
|
|
||||||
class ResourceUsage extends Page implements HasForms
|
|
||||||
{
|
|
||||||
use InteractsWithForms;
|
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChartBar;
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 14;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'resource-usage';
|
|
||||||
|
|
||||||
protected string $view = 'filament.admin.pages.resource-usage';
|
|
||||||
|
|
||||||
public array $usageFormData = [];
|
|
||||||
|
|
||||||
public array $chartData = [];
|
|
||||||
|
|
||||||
public array $performanceChartData = [];
|
|
||||||
|
|
||||||
public bool $hasPerformanceData = false;
|
|
||||||
|
|
||||||
public ?int $cpuLimitPercent = null;
|
|
||||||
|
|
||||||
public ?float $memoryLimitGb = null;
|
|
||||||
|
|
||||||
public array $cpuStats = [];
|
|
||||||
|
|
||||||
public array $memoryStats = [];
|
|
||||||
|
|
||||||
public array $summary = [];
|
|
||||||
|
|
||||||
public function getTitle(): string|Htmlable
|
|
||||||
{
|
|
||||||
return __('Resource Usage Reports');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getNavigationLabel(): string
|
|
||||||
{
|
|
||||||
return __('Resource Usage');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mount(): void
|
|
||||||
{
|
|
||||||
$defaultUser = User::query()->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<int, int|float> $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<int, int|float> $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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ use App\Filament\Admin\Widgets\Security\NiktoResultsTable;
|
|||||||
use App\Filament\Admin\Widgets\Security\QuarantinedFilesTable;
|
use App\Filament\Admin\Widgets\Security\QuarantinedFilesTable;
|
||||||
use App\Filament\Admin\Widgets\Security\ThreatsTable;
|
use App\Filament\Admin\Widgets\Security\ThreatsTable;
|
||||||
use App\Filament\Admin\Widgets\Security\WpscanResultsTable;
|
use App\Filament\Admin\Widgets\Security\WpscanResultsTable;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -48,7 +47,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class Security extends Page implements HasActions, HasForms, HasTable
|
class Security extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -989,7 +987,6 @@ class Security extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,8 @@ namespace App\Filament\Admin\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Widgets\Settings\DnssecTable;
|
use App\Filament\Admin\Widgets\Settings\DnssecTable;
|
||||||
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
use App\Models\UserResourceLimit;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use App\Services\System\ResourceLimitService;
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -41,7 +38,6 @@ use Livewire\WithFileUploads;
|
|||||||
|
|
||||||
class ServerSettings extends Page implements HasActions, HasForms
|
class ServerSettings extends Page implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
@@ -199,7 +195,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
$this->quotaData = [
|
$this->quotaData = [
|
||||||
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
||||||
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
||||||
'resource_limits_enabled' => (bool) ($settings['resource_limits_enabled'] ?? true),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->fileManagerData = [
|
$this->fileManagerData = [
|
||||||
@@ -422,10 +417,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
->numeric()
|
->numeric()
|
||||||
->placeholder('5120')
|
->placeholder('5120')
|
||||||
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
->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([
|
Actions::make([
|
||||||
FormAction::make('saveQuotaSettings')
|
FormAction::make('saveQuotaSettings')
|
||||||
@@ -841,11 +832,9 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
{
|
{
|
||||||
$data = $this->quotaData;
|
$data = $this->quotaData;
|
||||||
$wasEnabled = (bool) DnsSetting::get('quotas_enabled', false);
|
$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('quotas_enabled', $data['quotas_enabled'] ? '1' : '0');
|
||||||
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
||||||
DnsSetting::set('resource_limits_enabled', ! empty($data['resource_limits_enabled']) ? '1' : '0');
|
|
||||||
DnsSetting::clearCache();
|
DnsSetting::clearCache();
|
||||||
|
|
||||||
if ($data['quotas_enabled'] && ! $wasEnabled) {
|
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();
|
Notification::make()->title(__('Quota settings saved'))->success()->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,7 +1132,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('export_config')
|
Action::make('export_config')
|
||||||
->label(__('Export'))
|
->label(__('Export'))
|
||||||
->icon('heroicon-o-arrow-down-tray')
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Filament\Admin\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Widgets\ServerChartsWidget;
|
use App\Filament\Admin\Widgets\ServerChartsWidget;
|
||||||
use App\Filament\Admin\Widgets\ServerInfoWidget;
|
use App\Filament\Admin\Widgets\ServerInfoWidget;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\ServerProcess;
|
use App\Models\ServerProcess;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -28,7 +27,6 @@ use Illuminate\Database\Eloquent\Collection;
|
|||||||
|
|
||||||
class ServerStatus extends Page implements HasTable
|
class ServerStatus extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
|
||||||
@@ -76,7 +74,6 @@ class ServerStatus extends Page implements HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
ActionGroup::make([
|
ActionGroup::make([
|
||||||
Action::make('limit25')
|
Action::make('limit25')
|
||||||
->label(__('Show 25 processes'))
|
->label(__('Show 25 processes'))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Admin\Pages;
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -25,7 +24,6 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
class Services extends Page implements HasActions, HasForms, HasTable
|
class Services extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -317,7 +315,6 @@ class Services extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Admin\Pages;
|
namespace App\Filament\Admin\Pages;
|
||||||
|
|
||||||
use App\Filament\Admin\Widgets\SslStatsOverview;
|
use App\Filament\Admin\Widgets\SslStatsOverview;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\SslCertificate;
|
use App\Models\SslCertificate;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Artisan;
|
|||||||
|
|
||||||
class SslManager extends Page implements HasTable
|
class SslManager extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||||
@@ -531,7 +529,6 @@ class SslManager extends Page implements HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('runAutoSsl')
|
Action::make('runAutoSsl')
|
||||||
->label(__('Run SSL Check'))
|
->label(__('Run SSL Check'))
|
||||||
->icon('heroicon-o-play')
|
->icon('heroicon-o-play')
|
||||||
|
|||||||
@@ -61,27 +61,6 @@ class HostingPackageForm
|
|||||||
])
|
])
|
||||||
->columns(2),
|
->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),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,21 +44,6 @@ class HostingPackagesTable
|
|||||||
TextColumn::make('mailboxes_limit')
|
TextColumn::make('mailboxes_limit')
|
||||||
->label(__('Mailboxes'))
|
->label(__('Mailboxes'))
|
||||||
->getStateUsing(fn ($record) => $record->mailboxes_limit ?: __('Unlimited')),
|
->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')
|
IconColumn::make('is_active')
|
||||||
->label(__('Active'))
|
->label(__('Active'))
|
||||||
->boolean(),
|
->boolean(),
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Resources\Users\UserResource;
|
use App\Filament\Admin\Resources\Users\UserResource;
|
||||||
use App\Models\HostingPackage;
|
use App\Models\HostingPackage;
|
||||||
use App\Models\UserResourceLimit;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use App\Services\System\LinuxUserService;
|
use App\Services\System\LinuxUserService;
|
||||||
use App\Services\System\ResourceLimitService;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
@@ -59,9 +57,6 @@ class CreateUser extends CreateRecord
|
|||||||
|
|
||||||
// Apply disk quota if enabled
|
// Apply disk quota if enabled
|
||||||
$this->applyDiskQuota();
|
$this->applyDiskQuota();
|
||||||
|
|
||||||
// Apply resource limits from package
|
|
||||||
$this->syncResourceLimitsFromPackage($this->selectedPackage, true);
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title(__('Linux user creation failed'))
|
->title(__('Linux user creation failed'))
|
||||||
@@ -69,15 +64,12 @@ class CreateUser extends CreateRecord
|
|||||||
->danger()
|
->danger()
|
||||||
->send();
|
->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) {
|
if (! $this->record->hosting_package_id) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title(__('No hosting package selected'))
|
->title(__('No hosting package selected'))
|
||||||
->body(__('This user has unlimited resource limits.'))
|
->body(__('This user has unlimited quotas.'))
|
||||||
->warning()
|
->warning()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
@@ -113,50 +105,4 @@ class CreateUser extends CreateRecord
|
|||||||
->send();
|
->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages;
|
|||||||
|
|
||||||
use App\Filament\Admin\Resources\Users\UserResource;
|
use App\Filament\Admin\Resources\Users\UserResource;
|
||||||
use App\Models\HostingPackage;
|
use App\Models\HostingPackage;
|
||||||
use App\Models\UserResourceLimit;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use App\Services\System\LinuxUserService;
|
use App\Services\System\LinuxUserService;
|
||||||
use App\Services\System\ResourceLimitService;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Forms\Components\Toggle;
|
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
|
protected function getHeaderActions(): array
|
||||||
|
|||||||
@@ -3,20 +3,16 @@
|
|||||||
namespace App\Filament\Admin\Resources\Users\Pages;
|
namespace App\Filament\Admin\Resources\Users\Pages;
|
||||||
|
|
||||||
use App\Filament\Admin\Resources\Users\UserResource;
|
use App\Filament\Admin\Resources\Users\UserResource;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use Filament\Actions\CreateAction;
|
use Filament\Actions\CreateAction;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListUsers extends ListRecords
|
class ListUsers extends ListRecords
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
|
|
||||||
protected static string $resource = UserResource::class;
|
protected static string $resource = UserResource::class;
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
CreateAction::make(),
|
CreateAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class UserForm
|
|||||||
|
|
||||||
Placeholder::make('package_notice')
|
Placeholder::make('package_notice')
|
||||||
->label(__('Hosting Package'))
|
->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'))),
|
->visible(fn ($get) => blank($get('hosting_package_id'))),
|
||||||
|
|
||||||
\Filament\Forms\Components\Select::make('hosting_package_id')
|
\Filament\Forms\Components\Select::make('hosting_package_id')
|
||||||
@@ -137,7 +137,7 @@ class UserForm
|
|||||||
->pluck('name', 'id')
|
->pluck('name', 'id')
|
||||||
->toArray())
|
->toArray())
|
||||||
->placeholder(__('Unlimited (no package)'))
|
->placeholder(__('Unlimited (no package)'))
|
||||||
->helperText(__('Assign a package to set resource limits.'))
|
->helperText(__('Assign a package to set quotas.'))
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
Toggle::make('create_linux_user')
|
Toggle::make('create_linux_user')
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Concerns;
|
|
||||||
|
|
||||||
use Filament\Actions\Action;
|
|
||||||
|
|
||||||
trait HasPageTour
|
|
||||||
{
|
|
||||||
protected function getTourAction(): Action
|
|
||||||
{
|
|
||||||
return Action::make('takeTour')
|
|
||||||
->label(__('Take Tour'))
|
|
||||||
->icon('heroicon-o-academic-cap')
|
|
||||||
->color('gray')
|
|
||||||
->action(function (): void {
|
|
||||||
$this->dispatch('start-page-tour');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use App\Models\BackupDestination;
|
use App\Models\BackupDestination;
|
||||||
use App\Models\BackupRestore;
|
use App\Models\BackupRestore;
|
||||||
@@ -46,7 +45,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class Backups extends Page implements HasActions, HasForms, HasTable
|
class Backups extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -588,7 +586,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('createBackup')
|
Action::make('createBackup')
|
||||||
->label(__('Create Backup'))
|
->label(__('Create Backup'))
|
||||||
->icon('heroicon-o-archive-box-arrow-down')
|
->icon('heroicon-o-archive-box-arrow-down')
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\CloudflareZone;
|
use App\Models\CloudflareZone;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Http;
|
|||||||
|
|
||||||
class CdnIntegration extends Page implements HasActions, HasTable
|
class CdnIntegration extends Page implements HasActions, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\CronJob;
|
use App\Models\CronJob;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
@@ -32,7 +31,6 @@ use Illuminate\Support\HtmlString;
|
|||||||
|
|
||||||
class CronJobs extends Page implements HasActions, HasForms, HasTable
|
class CronJobs extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -500,7 +498,6 @@ class CronJobs extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->createCronJobAction(),
|
$this->createCronJobAction(),
|
||||||
$this->setupWordPressCronAction(),
|
$this->setupWordPressCronAction(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\MysqlCredential;
|
use App\Models\MysqlCredential;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -35,7 +34,6 @@ use Illuminate\Support\Facades\Crypt;
|
|||||||
|
|
||||||
class Databases extends Page implements HasActions, HasForms, HasTable
|
class Databases extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -382,7 +380,6 @@ class Databases extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->quickSetupAction(),
|
$this->quickSetupAction(),
|
||||||
$this->createDatabaseAction(),
|
$this->createDatabaseAction(),
|
||||||
$this->createUserAction(),
|
$this->createUserAction(),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Filament\Jabali\Widgets\DnsPendingAddsTable;
|
use App\Filament\Jabali\Widgets\DnsPendingAddsTable;
|
||||||
use App\Models\DnsRecord;
|
use App\Models\DnsRecord;
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
@@ -41,7 +40,6 @@ use Livewire\Attributes\On;
|
|||||||
|
|
||||||
class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -802,7 +800,6 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->applyTemplateAction()
|
$this->applyTemplateAction()
|
||||||
->visible(fn () => $this->selectedDomainId !== null),
|
->visible(fn () => $this->selectedDomainId !== null),
|
||||||
$this->addRecordAction()
|
$this->addRecordAction()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\DomainAlias;
|
use App\Models\DomainAlias;
|
||||||
use App\Models\DomainHotlinkSetting;
|
use App\Models\DomainHotlinkSetting;
|
||||||
@@ -38,7 +37,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class Domains extends Page implements HasActions, HasForms, HasTable
|
class Domains extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -434,7 +432,6 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->createDomainAction(),
|
$this->createDomainAction(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Autoresponder;
|
use App\Models\Autoresponder;
|
||||||
use App\Models\DnsRecord;
|
use App\Models\DnsRecord;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
@@ -44,7 +43,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class Email extends Page implements HasActions, HasForms, HasTable
|
class Email extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -821,7 +819,6 @@ class Email extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->createMailboxAction(),
|
$this->createMailboxAction(),
|
||||||
$this->createForwarderAction(),
|
$this->createForwarderAction(),
|
||||||
$this->createAutoresponderAction(),
|
$this->createAutoresponderAction(),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -34,7 +33,6 @@ use Livewire\WithFileUploads;
|
|||||||
|
|
||||||
class Files extends Page implements HasActions, HasForms, HasTable
|
class Files extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -95,7 +93,6 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Jobs\RunGitDeployment;
|
use App\Jobs\RunGitDeployment;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\GitDeployment as GitDeploymentModel;
|
use App\Models\GitDeployment as GitDeploymentModel;
|
||||||
@@ -33,7 +32,6 @@ use Illuminate\Support\Str;
|
|||||||
|
|
||||||
class GitDeployment extends Page implements HasActions, HasForms, HasTable
|
class GitDeployment extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@@ -25,7 +24,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class ImageOptimization extends Page implements HasActions, HasForms
|
class ImageOptimization extends Page implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\UserResourceUsage;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -22,7 +20,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class Logs extends Page implements HasActions, HasForms
|
class Logs extends Page implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
@@ -96,7 +93,7 @@ class Logs extends Page implements HasActions, HasForms
|
|||||||
protected function normalizeTab(?string $tab): string
|
protected function normalizeTab(?string $tab): string
|
||||||
{
|
{
|
||||||
return match ($tab) {
|
return match ($tab) {
|
||||||
'logs', 'usage', 'activity', 'stats' => (string) $tab,
|
'logs', 'activity', 'stats' => (string) $tab,
|
||||||
default => 'logs',
|
default => 'logs',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -195,57 +192,6 @@ class Logs extends Page implements HasActions, HasForms
|
|||||||
->send();
|
->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()
|
public function getActivityLogs()
|
||||||
{
|
{
|
||||||
return AuditLog::query()
|
return AuditLog::query()
|
||||||
@@ -301,7 +247,6 @@ class Logs extends Page implements HasActions, HasForms
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
|
|
||||||
Action::make('generateStats')
|
Action::make('generateStats')
|
||||||
->label(__('Generate Statistics'))
|
->label(__('Generate Statistics'))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\UserSetting;
|
use App\Models\UserSetting;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Concerns\InteractsWithActions;
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
@@ -22,7 +21,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class MailingLists extends Page implements HasActions, HasForms
|
class MailingLists extends Page implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -23,7 +22,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class PhpSettings extends Page implements HasActions, HasForms
|
class PhpSettings extends Page implements HasActions, HasForms
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
|
|
||||||
@@ -267,7 +265,6 @@ class PhpSettings extends Page implements HasActions, HasForms
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->saveSettingsAction(),
|
$this->saveSettingsAction(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -28,7 +27,6 @@ use Livewire\Attributes\Url;
|
|||||||
|
|
||||||
class PostgreSQL extends Page implements HasActions, HasForms, HasTable
|
class PostgreSQL extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\Concerns\InteractsWithActions;
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class SshKeys extends Page implements HasActions, HasForms, HasTable
|
class SshKeys extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -203,7 +201,6 @@ class SshKeys extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
Action::make('generateKey')
|
Action::make('generateKey')
|
||||||
->label(__('Generate SSH Key'))
|
->label(__('Generate SSH Key'))
|
||||||
->icon('heroicon-o-sparkles')
|
->icon('heroicon-o-sparkles')
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\SslCertificate;
|
use App\Models\SslCertificate;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
@@ -29,7 +28,6 @@ use Illuminate\Support\Facades\Auth;
|
|||||||
|
|
||||||
class Ssl extends Page implements HasActions, HasForms, HasTable
|
class Ssl extends Page implements HasActions, HasForms, HasTable
|
||||||
{
|
{
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -498,7 +496,6 @@ class Ssl extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->installCustomCertificateAction(),
|
$this->installCustomCertificateAction(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Jabali\Pages;
|
namespace App\Filament\Jabali\Pages;
|
||||||
|
|
||||||
use App\Filament\Concerns\HasPageTour;
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\MysqlCredential;
|
use App\Models\MysqlCredential;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
@@ -36,7 +35,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
|
|||||||
{
|
{
|
||||||
protected static ?string $slug = 'wordpress';
|
protected static ?string $slug = 'wordpress';
|
||||||
|
|
||||||
use HasPageTour;
|
|
||||||
use InteractsWithActions;
|
use InteractsWithActions;
|
||||||
use InteractsWithForms;
|
use InteractsWithForms;
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
@@ -330,7 +328,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
$this->getTourAction(),
|
|
||||||
$this->scanAction(),
|
$this->scanAction(),
|
||||||
$this->installAction(),
|
$this->installAction(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\HostingPackage;
|
use App\Models\HostingPackage;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserResourceLimit;
|
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use App\Services\System\LinuxUserService;
|
use App\Services\System\LinuxUserService;
|
||||||
use App\Services\System\ResourceLimitService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Validator;
|
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);
|
return response()->json(['user' => $user], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ class HostingPackage extends Model
|
|||||||
'domains_limit',
|
'domains_limit',
|
||||||
'databases_limit',
|
'databases_limit',
|
||||||
'mailboxes_limit',
|
'mailboxes_limit',
|
||||||
'cpu_limit_percent',
|
|
||||||
'memory_limit_mb',
|
|
||||||
'io_limit_mb',
|
|
||||||
'is_active',
|
'is_active',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class UserResourceLimit extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'cpu_limit_percent',
|
|
||||||
'memory_limit_mb',
|
|
||||||
'io_limit_mb',
|
|
||||||
'is_active',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class UserResourceUsage extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = [
|
|
||||||
'user_id',
|
|
||||||
'metric',
|
|
||||||
'value',
|
|
||||||
'captured_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'value' => 'integer',
|
|
||||||
'captured_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
use App\Filament\Admin\Pages\Auth\Login as AdminLogin;
|
use App\Filament\Admin\Pages\Auth\Login as AdminLogin;
|
||||||
|
use App\Filament\Admin\Pages\Dashboard;
|
||||||
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
||||||
use App\Http\Middleware\SetLocale;
|
use App\Http\Middleware\SetLocale;
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
@@ -10,7 +11,6 @@ use Filament\Http\Middleware\Authenticate;
|
|||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||||
use App\Filament\Admin\Pages\Dashboard;
|
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Filament\PanelProvider;
|
use Filament\PanelProvider;
|
||||||
use Filament\Support\Colors\Color;
|
use Filament\Support\Colors\Color;
|
||||||
@@ -44,7 +44,7 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
PanelsRenderHook::HEAD_END,
|
PanelsRenderHook::HEAD_END,
|
||||||
fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure').
|
fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure').
|
||||||
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'">'.
|
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'">'.
|
||||||
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/admin-tour.js', 'resources/js/server-charts.js'])->toHtml() .
|
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
|
||||||
$this->getRtlScript()
|
$this->getRtlScript()
|
||||||
)
|
)
|
||||||
->renderHook(
|
->renderHook(
|
||||||
@@ -59,10 +59,6 @@ $this->getRtlScript()
|
|||||||
PanelsRenderHook::USER_MENU_BEFORE,
|
PanelsRenderHook::USER_MENU_BEFORE,
|
||||||
fn () => view('components.language-switcher')
|
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')
|
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
||||||
->pages([
|
->pages([
|
||||||
@@ -141,7 +137,9 @@ $this->getRtlScript()
|
|||||||
shuffle($shuffled);
|
shuffle($shuffled);
|
||||||
for ($col = 0; $col < 20; $col++) {
|
for ($col = 0; $col < 20; $col++) {
|
||||||
$word = $shuffled[$col % count($shuffled)];
|
$word = $shuffled[$col % count($shuffled)];
|
||||||
if ($col % 3 === 0) shuffle($shuffled); // Re-shuffle periodically
|
if ($col % 3 === 0) {
|
||||||
|
shuffle($shuffled);
|
||||||
|
} // Re-shuffle periodically
|
||||||
$rowContent .= $word.' · ';
|
$rowContent .= $word.' · ';
|
||||||
}
|
}
|
||||||
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ namespace App\Providers\Filament;
|
|||||||
|
|
||||||
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
||||||
use App\Filament\Jabali\Pages\Auth\Login;
|
use App\Filament\Jabali\Pages\Auth\Login;
|
||||||
use App\Models\DnsSetting;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Http\Middleware\RedirectAdminFromUserPanel;
|
use App\Http\Middleware\RedirectAdminFromUserPanel;
|
||||||
use App\Http\Middleware\SetLocale;
|
use App\Http\Middleware\SetLocale;
|
||||||
|
use App\Models\DnsSetting;
|
||||||
|
use App\Models\User;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@@ -22,7 +22,6 @@ use Illuminate\Cookie\Middleware\EncryptCookies;
|
|||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\Support\Facades\Blade;
|
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
|
||||||
class JabaliPanelProvider extends PanelProvider
|
class JabaliPanelProvider extends PanelProvider
|
||||||
@@ -48,7 +47,7 @@ class JabaliPanelProvider extends PanelProvider
|
|||||||
PanelsRenderHook::HEAD_END,
|
PanelsRenderHook::HEAD_END,
|
||||||
fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more').
|
fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more').
|
||||||
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'?v='.filemtime(public_path('css/filament-custom.css')).'">'.
|
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'?v='.filemtime(public_path('css/filament-custom.css')).'">'.
|
||||||
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/admin-tour.js', 'resources/js/server-charts.js'])->toHtml() .
|
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
|
||||||
$this->getRtlScript()
|
$this->getRtlScript()
|
||||||
)
|
)
|
||||||
->renderHook(
|
->renderHook(
|
||||||
@@ -63,10 +62,6 @@ $this->getRtlScript()
|
|||||||
PanelsRenderHook::USER_MENU_BEFORE,
|
PanelsRenderHook::USER_MENU_BEFORE,
|
||||||
fn () => view('components.language-switcher')
|
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')
|
->discoverResources(in: app_path('Filament/Jabali/Resources'), for: 'App\\Filament\\Jabali\\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Jabali/Pages'), for: 'App\\Filament\\Jabali\\Pages')
|
->discoverPages(in: app_path('Filament/Jabali/Pages'), for: 'App\\Filament\\Jabali\\Pages')
|
||||||
->pages([])
|
->pages([])
|
||||||
@@ -172,7 +167,9 @@ $this->getRtlScript()
|
|||||||
shuffle($shuffled);
|
shuffle($shuffled);
|
||||||
for ($col = 0; $col < 20; $col++) {
|
for ($col = 0; $col < 20; $col++) {
|
||||||
$word = $shuffled[$col % count($shuffled)];
|
$word = $shuffled[$col % count($shuffled)];
|
||||||
if ($col % 3 === 0) shuffle($shuffled);
|
if ($col % 3 === 0) {
|
||||||
|
shuffle($shuffled);
|
||||||
|
}
|
||||||
$rowContent .= $word.' · ';
|
$rowContent .= $word.' · ';
|
||||||
}
|
}
|
||||||
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
||||||
|
|||||||
@@ -1333,7 +1333,7 @@ class AgentClient
|
|||||||
return $this->send('updates.run');
|
return $this->send('updates.run');
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAF / Geo / Resource limits
|
// WAF / Geo
|
||||||
public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog): array
|
public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog): array
|
||||||
{
|
{
|
||||||
return $this->send('waf.apply', [
|
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
|
public function databasePersistTuning(string $name, string $value): array
|
||||||
{
|
{
|
||||||
return $this->send('database.persist_tuning', [
|
return $this->send('database.persist_tuning', [
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\System;
|
|
||||||
|
|
||||||
use App\Models\DnsSetting;
|
|
||||||
use App\Models\UserResourceLimit;
|
|
||||||
use App\Services\Agent\AgentClient;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class ResourceLimitService
|
|
||||||
{
|
|
||||||
public function apply(UserResourceLimit $limit): void
|
|
||||||
{
|
|
||||||
if (! DnsSetting::get('resource_limits_enabled', true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $limit->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
468
bin/jabali-agent
468
bin/jabali-agent
@@ -547,11 +547,6 @@ function handleAction(array $request): array
|
|||||||
'updates.run' => updatesRun($params),
|
'updates.run' => updatesRun($params),
|
||||||
'waf.apply' => wafApplySettings($params),
|
'waf.apply' => wafApplySettings($params),
|
||||||
'geo.apply_rules' => geoApplyRules($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),
|
'database.persist_tuning' => databasePersistTuning($params),
|
||||||
'server.export_config' => serverExportConfig($params),
|
'server.export_config' => serverExportConfig($params),
|
||||||
'server.import_config' => serverImportConfig($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'));
|
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");
|
logger("Created user $username with home directory $homeDir");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -1040,8 +1030,6 @@ function deleteUser(array $params): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cgroupRemoveUser($username);
|
|
||||||
|
|
||||||
// Reload services
|
// Reload services
|
||||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
|
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
|
||||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 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)];
|
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
|
function databasePersistTuning(array $params): array
|
||||||
{
|
{
|
||||||
$name = $params['name'] ?? '';
|
$name = $params['name'] ?? '';
|
||||||
@@ -6927,8 +6535,59 @@ function wpPageCacheEnable(array $params): array
|
|||||||
|
|
||||||
$config = file_get_contents($configFile);
|
$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
|
// Check if page cache is already enabled
|
||||||
if (strpos($config, 'fastcgi_cache JABALI') !== false) {
|
if ($hasPageCache) {
|
||||||
return ['success' => true, 'message' => 'Page cache already enabled'];
|
return ['success' => true, 'message' => 'Page cache already enabled'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6973,6 +6632,12 @@ function wpPageCacheEnable(array $params): array
|
|||||||
set $cache_reason "admin_url";
|
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)
|
# 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)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf|zip)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
@@ -24047,17 +23712,8 @@ function usageUserResources(array $params): array
|
|||||||
$cpuUsageUsec = null;
|
$cpuUsageUsec = null;
|
||||||
$memoryBytes = 0;
|
$memoryBytes = 0;
|
||||||
$diskIoTotal = 0;
|
$diskIoTotal = 0;
|
||||||
|
$diskRead = 0;
|
||||||
$cgroupPath = getUserCgroupPath($username);
|
$diskWrite = 0;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cpuUsageUsec === null || $memoryBytes === 0) {
|
if ($cpuUsageUsec === null || $memoryBytes === 0) {
|
||||||
$cpuTotal = 0.0;
|
$cpuTotal = 0.0;
|
||||||
@@ -24082,8 +23738,6 @@ function usageUserResources(array $params): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($diskIoTotal === 0) {
|
if ($diskIoTotal === 0) {
|
||||||
$diskRead = 0;
|
|
||||||
$diskWrite = 0;
|
|
||||||
|
|
||||||
foreach (glob('/proc/[0-9]*') as $procPath) {
|
foreach (glob('/proc/[0-9]*') as $procPath) {
|
||||||
$statusFile = $procPath . '/status';
|
$statusFile = $procPath . '/status';
|
||||||
@@ -24136,6 +23790,8 @@ function usageUserResources(array $params): array
|
|||||||
'cpu_usage_usec_total' => $cpuUsageUsec,
|
'cpu_usage_usec_total' => $cpuUsageUsec,
|
||||||
'memory_bytes' => $memoryBytes,
|
'memory_bytes' => $memoryBytes,
|
||||||
'disk_io_total_bytes' => $diskIoTotal,
|
'disk_io_total_bytes' => $diskIoTotal,
|
||||||
|
'disk_io_read_bytes_total' => $diskRead,
|
||||||
|
'disk_io_write_bytes_total' => $diskWrite,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('user_resource_usages', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('user_resource_limits', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('hosting_packages', function (Blueprint $table) {
|
|
||||||
$table->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']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (Schema::hasTable('user_resource_usages')) {
|
||||||
|
Schema::drop('user_resource_usages');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('user_resource_limits')) {
|
||||||
|
Schema::drop('user_resource_limits');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('hosting_packages')) {
|
||||||
|
$columns = [];
|
||||||
|
foreach (['cpu_limit_percent', 'memory_limit_mb', 'io_limit_mb'] as $column) {
|
||||||
|
if (Schema::hasColumn('hosting_packages', $column)) {
|
||||||
|
$columns[] = $column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($columns)) {
|
||||||
|
Schema::table('hosting_packages', function (Blueprint $table) use ($columns) {
|
||||||
|
$table->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.
|
||||||
|
}
|
||||||
|
};
|
||||||
33
install.sh
33
install.sh
@@ -2519,33 +2519,6 @@ SERVICE
|
|||||||
log "Jabali Agent service configured"
|
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() {
|
setup_queue_service() {
|
||||||
header "Setting Up Jabali Queue Worker"
|
header "Setting Up Jabali Queue Worker"
|
||||||
|
|
||||||
@@ -2918,11 +2891,6 @@ uninstall() {
|
|||||||
rm -f /etc/systemd/system/jabali-queue.service
|
rm -f /etc/systemd/system/jabali-queue.service
|
||||||
rm -rf /etc/systemd/system/jabali-queue.service.d
|
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=(
|
local services=(
|
||||||
nginx
|
nginx
|
||||||
php-fpm
|
php-fpm
|
||||||
@@ -3253,7 +3221,6 @@ main() {
|
|||||||
configure_redis
|
configure_redis
|
||||||
setup_jabali
|
setup_jabali
|
||||||
setup_agent_service
|
setup_agent_service
|
||||||
setup_cgroup_limits
|
|
||||||
setup_queue_service
|
setup_queue_service
|
||||||
setup_scheduler_cron
|
setup_scheduler_cron
|
||||||
setup_logrotate
|
setup_logrotate
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -5,7 +5,6 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"driver.js": "^1.4.0",
|
|
||||||
"echarts": "^6.0.0"
|
"echarts": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1715,12 +1714,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"driver.js": "^1.4.0",
|
|
||||||
"echarts": "^6.0.0"
|
"echarts": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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
|
|
||||||
<script>
|
|
||||||
window.jabaliTourTranslations = {!! json_encode($tourTranslations) !!};
|
|
||||||
</script>
|
|
||||||
@@ -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
|
|
||||||
<script>
|
|
||||||
window.jabaliTourTranslations = {!! json_encode($tourTranslations) !!};
|
|
||||||
</script>
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
<x-filament-panels::page>
|
|
||||||
@once
|
|
||||||
@vite('resources/js/server-charts.js')
|
|
||||||
@endonce
|
|
||||||
{{ $this->usageForm }}
|
|
||||||
|
|
||||||
@php($hasRealData = !empty($chartData) && !empty($chartData['labels']))
|
|
||||||
|
|
||||||
@if(! $hasRealData)
|
|
||||||
<x-filament::section class="mt-6">
|
|
||||||
<div class="flex flex-col items-center justify-center py-10">
|
|
||||||
<div class="mb-3 rounded-full bg-gray-100 p-3 dark:bg-gray-500/20">
|
|
||||||
<x-filament::icon icon="heroicon-o-chart-bar" class="h-6 w-6 text-gray-500 dark:text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-base font-semibold text-gray-950 dark:text-white">
|
|
||||||
{{ __('No usage data yet') }}
|
|
||||||
</h3>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ __('Usage snapshots are collected hourly. Data will appear after the first collection run.') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
@else
|
|
||||||
<div class="mt-6 space-y-6">
|
|
||||||
<x-filament::section icon="heroicon-o-square-3-stack-3d" icon-color="primary">
|
|
||||||
<x-slot name="heading">{{ __('Latest Snapshot') }}</x-slot>
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Disk') }}</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['disk'] ?? __('No data') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Mail') }}</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['mail'] ?? __('No data') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Databases') }}</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['database'] ?? __('No data') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Bandwidth (last hour)') }}</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['bandwidth'] ?? __('No data') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
<x-filament::section icon="heroicon-o-chart-bar" icon-color="success">
|
|
||||||
<x-slot name="heading">{{ __('Resource Usage (Last 30 Days)') }}</x-slot>
|
|
||||||
<x-slot name="description">{{ __('Historical snapshots collected hourly.') }}</x-slot>
|
|
||||||
|
|
||||||
<div
|
|
||||||
wire:key="resource-usage-chart-{{ $usageFormData['user_id'] ?? 'none' }}"
|
|
||||||
x-data="{
|
|
||||||
chart: null,
|
|
||||||
init() {
|
|
||||||
let data = @js($chartData);
|
|
||||||
|
|
||||||
const boot = () => {
|
|
||||||
const element = this.$refs.chart ?? this.$el;
|
|
||||||
if (!window.echarts || !data.labels?.length || !element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = window.echarts.init(element);
|
|
||||||
this.chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis' },
|
|
||||||
legend: {
|
|
||||||
data: ['Disk', 'Mail', 'Databases', 'Bandwidth'],
|
|
||||||
bottom: 0,
|
|
||||||
},
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: data.labels,
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value) => value.slice(5),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: { formatter: '{value} GB' },
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{ name: 'Disk', type: 'line', smooth: true, data: data.disk, areaStyle: {} },
|
|
||||||
{ name: 'Mail', type: 'line', smooth: true, data: data.mail, areaStyle: {} },
|
|
||||||
{ name: 'Databases', type: 'line', smooth: true, data: data.database, areaStyle: {} },
|
|
||||||
{ name: 'Bandwidth', type: 'line', smooth: true, data: data.bandwidth, areaStyle: {} },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', () => this.chart?.resize());
|
|
||||||
requestAnimationFrame(() => this.chart?.resize());
|
|
||||||
setTimeout(() => this.chart?.resize(), 150);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!boot()) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (boot()) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
x-init="init"
|
|
||||||
class="w-full"
|
|
||||||
wire:ignore
|
|
||||||
>
|
|
||||||
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
@if($hasPerformanceData)
|
|
||||||
<div class="grid gap-6 lg:grid-cols-2">
|
|
||||||
<x-filament::section icon="heroicon-o-cpu-chip" icon-color="info">
|
|
||||||
<x-slot name="heading">{{ __('CPU Usage (Last 30 Days)') }}</x-slot>
|
|
||||||
<x-slot name="description">{{ __('Average CPU percent per snapshot.') }}</x-slot>
|
|
||||||
|
|
||||||
<div class="mb-4 flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Limit') }}:</span>
|
|
||||||
{{ $cpuLimitPercent ? $cpuLimitPercent . '%' : __('Unlimited') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Average') }}:</span>
|
|
||||||
{{ $cpuStats['avg'] ?? __('No data') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Peak') }}:</span>
|
|
||||||
{{ $cpuStats['max'] ?? __('No data') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-data="{
|
|
||||||
chart: null,
|
|
||||||
init() {
|
|
||||||
const data = @js($performanceChartData);
|
|
||||||
const limit = @js($cpuLimitPercent);
|
|
||||||
const boot = () => {
|
|
||||||
const element = this.$refs.chart ?? this.$el;
|
|
||||||
if (!window.echarts || !data.labels?.length || !element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
const series = [
|
|
||||||
{ name: 'CPU', type: 'line', smooth: true, data: data.cpu, areaStyle: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
series.push({
|
|
||||||
name: 'Limit',
|
|
||||||
type: 'line',
|
|
||||||
data: data.labels.map(() => limit),
|
|
||||||
lineStyle: { type: 'dashed', width: 2, color: '#ef4444' },
|
|
||||||
symbol: 'none',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = window.echarts.init(element);
|
|
||||||
this.chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis' },
|
|
||||||
legend: { data: series.map(item => item.name), bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: data.labels,
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value) => value.slice(5),
|
|
||||||
margin: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: { formatter: '{value}%' },
|
|
||||||
},
|
|
||||||
series,
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', () => this.chart?.resize());
|
|
||||||
requestAnimationFrame(() => this.chart?.resize());
|
|
||||||
setTimeout(() => this.chart?.resize(), 150);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!boot()) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (boot()) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
x-init="init"
|
|
||||||
class="w-full"
|
|
||||||
wire:ignore
|
|
||||||
>
|
|
||||||
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
<x-filament::section icon="heroicon-o-circle-stack" icon-color="warning">
|
|
||||||
<x-slot name="heading">{{ __('Memory Usage (Last 30 Days)') }}</x-slot>
|
|
||||||
<x-slot name="description">{{ __('Resident memory per snapshot.') }}</x-slot>
|
|
||||||
|
|
||||||
<div class="mb-4 flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Limit') }}:</span>
|
|
||||||
{{ $memoryLimitGb ? number_format($memoryLimitGb, 2) . ' GB' : __('Unlimited') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Average') }}:</span>
|
|
||||||
{{ $memoryStats['avg'] ?? __('No data') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Peak') }}:</span>
|
|
||||||
{{ $memoryStats['max'] ?? __('No data') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-data="{
|
|
||||||
chart: null,
|
|
||||||
init() {
|
|
||||||
const data = @js($performanceChartData);
|
|
||||||
const limit = @js($memoryLimitGb);
|
|
||||||
const boot = () => {
|
|
||||||
const element = this.$refs.chart ?? this.$el;
|
|
||||||
if (!window.echarts || !data.labels?.length || !element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
const series = [
|
|
||||||
{ name: 'Memory', type: 'line', smooth: true, data: data.memory, areaStyle: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
series.push({
|
|
||||||
name: 'Limit',
|
|
||||||
type: 'line',
|
|
||||||
data: data.labels.map(() => limit),
|
|
||||||
lineStyle: { type: 'dashed', width: 2, color: '#ef4444' },
|
|
||||||
symbol: 'none',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = window.echarts.init(element);
|
|
||||||
this.chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis' },
|
|
||||||
legend: { data: series.map(item => item.name), bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: data.labels,
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value) => value.slice(5),
|
|
||||||
margin: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: { formatter: '{value} GB' },
|
|
||||||
},
|
|
||||||
series,
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', () => this.chart?.resize());
|
|
||||||
requestAnimationFrame(() => this.chart?.resize());
|
|
||||||
setTimeout(() => this.chart?.resize(), 150);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!boot()) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (boot()) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
x-init="init"
|
|
||||||
class="w-full"
|
|
||||||
wire:ignore
|
|
||||||
>
|
|
||||||
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
<x-filament::section icon="heroicon-o-arrow-path-rounded-square" icon-color="primary">
|
|
||||||
<x-slot name="heading">{{ __('Disk IO (Last 30 Days)') }}</x-slot>
|
|
||||||
<x-slot name="description">{{ __('Read/write MB per snapshot.') }}</x-slot>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-data="{
|
|
||||||
chart: null,
|
|
||||||
init() {
|
|
||||||
const data = @js($performanceChartData);
|
|
||||||
const boot = () => {
|
|
||||||
const element = this.$refs.chart ?? this.$el;
|
|
||||||
if (!window.echarts || !data.labels?.length || !element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = window.echarts.init(element);
|
|
||||||
this.chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis' },
|
|
||||||
legend: { data: ['Disk IO'], bottom: 0 },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: data.labels,
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value) => value.slice(5),
|
|
||||||
margin: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: { formatter: '{value} MB' },
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{ name: 'Disk IO', type: 'line', smooth: true, data: data.disk_io, areaStyle: {} },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', () => this.chart?.resize());
|
|
||||||
requestAnimationFrame(() => this.chart?.resize());
|
|
||||||
setTimeout(() => this.chart?.resize(), 150);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!boot()) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (boot()) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
x-init="init"
|
|
||||||
class="w-full"
|
|
||||||
wire:ignore
|
|
||||||
>
|
|
||||||
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<x-filament::section icon="heroicon-o-cpu-chip" icon-color="info">
|
|
||||||
<x-slot name="heading">{{ __('CPU, Memory & Disk IO') }}</x-slot>
|
|
||||||
<div class="flex flex-col items-center justify-center py-10 text-center">
|
|
||||||
<div class="mb-3 rounded-full bg-gray-100 p-3 dark:bg-gray-500/20">
|
|
||||||
<x-filament::icon icon="heroicon-o-cpu-chip" class="h-6 w-6 text-gray-500 dark:text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-base font-semibold text-gray-950 dark:text-white">
|
|
||||||
{{ __('No performance data yet') }}
|
|
||||||
</h3>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ __('CPU, memory, and disk IO snapshots will appear after the next collection run.') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</x-filament-panels::page>
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
$tabs = [
|
$tabs = [
|
||||||
'logs' => ['label' => __('Logs'), 'icon' => 'heroicon-o-document-text'],
|
'logs' => ['label' => __('Logs'), 'icon' => 'heroicon-o-document-text'],
|
||||||
'stats' => ['label' => __('Statistics'), 'icon' => 'heroicon-o-chart-bar'],
|
'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'],
|
'activity' => ['label' => __('Activity Log'), 'icon' => 'heroicon-o-clipboard-document-list'],
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
@@ -164,78 +163,6 @@
|
|||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($activeTab === 'usage')
|
|
||||||
@php($usageData = $this->getUsageChartData())
|
|
||||||
<x-filament::section class="mt-4" icon="heroicon-o-chart-pie">
|
|
||||||
<x-slot name="heading">{{ __('Resource Usage (Last 30 Days)') }}</x-slot>
|
|
||||||
<x-slot name="description">{{ __('Historical usage snapshots collected hourly.') }}</x-slot>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-data="{
|
|
||||||
chart: null,
|
|
||||||
init() {
|
|
||||||
const data = @js($usageData);
|
|
||||||
const isDemo = Boolean(data.demo);
|
|
||||||
const boot = () => {
|
|
||||||
const element = this.$refs.chart ?? this.$el;
|
|
||||||
if (!window.echarts || !element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = window.echarts.init(element);
|
|
||||||
this.chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis' },
|
|
||||||
legend: { data: data.series.map(s => s.name) },
|
|
||||||
grid: { left: '3%', right: '3%', bottom: 50, containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: data.labels,
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value) => value.slice(5),
|
|
||||||
margin: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
axisLabel: { formatter: '{value} GB' },
|
|
||||||
},
|
|
||||||
series: data.series.map((series) => ({
|
|
||||||
name: series.name,
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
areaStyle: {},
|
|
||||||
data: series.data,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', () => this.chart?.resize());
|
|
||||||
requestAnimationFrame(() => this.chart?.resize());
|
|
||||||
setTimeout(() => this.chart?.resize(), 150);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!boot()) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (boot()) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
x-init="init"
|
|
||||||
class="w-full"
|
|
||||||
wire:ignore
|
|
||||||
>
|
|
||||||
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($activeTab === 'activity')
|
@if($activeTab === 'activity')
|
||||||
<x-filament::section class="mt-4" icon="heroicon-o-clipboard-document-list">
|
<x-filament::section class="mt-4" icon="heroicon-o-clipboard-document-list">
|
||||||
<x-slot name="heading">{{ __('Activity Log') }}</x-slot>
|
<x-slot name="heading">{{ __('Activity Log') }}</x-slot>
|
||||||
|
|||||||
@@ -66,20 +66,6 @@ Schedule::command('jabali:sync-mailbox-quotas')
|
|||||||
->runInBackground()
|
->runInBackground()
|
||||||
->appendOutputTo(storage_path('logs/mailbox-quota-sync.log'));
|
->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)
|
// Audit Log Rotation - runs daily to prune old audit logs (default: 90 days retention)
|
||||||
Schedule::call(function () {
|
Schedule::call(function () {
|
||||||
$deleted = AuditLog::prune();
|
$deleted = AuditLog::prune();
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Services\Agent\AgentClient;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class JabaliSyncCgroupsTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_syncs_all_processes(): void
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Feature;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\UserResourceUsage;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class ResourceUsageChartsTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_admin_resource_usage_page_renders_chart_with_data(): void
|
|
||||||
{
|
|
||||||
$admin = User::factory()->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,10 +35,17 @@ class WebmailSsoViewTest extends TestCase
|
|||||||
|
|
||||||
$response = $this->actingAs($user)->get(route('webmail.sso', $mailbox));
|
$response = $this->actingAs($user)->get(route('webmail.sso', $mailbox));
|
||||||
|
|
||||||
|
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->assertStatus(200);
|
||||||
$response->assertSee('Webmail Login Required');
|
$response->assertSee('Webmail Login Required');
|
||||||
$response->assertSee('Open Webmail Login');
|
$response->assertSee('Open Webmail Login');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function test_webmail_sso_shows_reset_required_when_password_missing(): void
|
public function test_webmail_sso_shows_reset_required_when_password_missing(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Unit;
|
namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Jobs\RunCpanelRestore;
|
use App\Jobs\RunCpanelRestore;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
|
use App\Services\Migration\MigrationDnsSyncService;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class RunCpanelRestoreTest extends TestCase
|
class RunCpanelRestoreTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_it_marks_restore_completed_and_logs_success(): void
|
public function test_it_marks_restore_completed_and_logs_success(): void
|
||||||
{
|
{
|
||||||
$jobId = 'cpanel-restore-success';
|
$jobId = 'cpanel-restore-success';
|
||||||
@@ -21,6 +26,10 @@ class RunCpanelRestoreTest extends TestCase
|
|||||||
File::ensureDirectoryExists(dirname($logPath));
|
File::ensureDirectoryExists(dirname($logPath));
|
||||||
File::put($logPath, '');
|
File::put($logPath, '');
|
||||||
|
|
||||||
|
User::factory()->create([
|
||||||
|
'username' => 'example',
|
||||||
|
]);
|
||||||
|
|
||||||
$this->app->instance(AgentClient::class, new class extends AgentClient
|
$this->app->instance(AgentClient::class, new class extends AgentClient
|
||||||
{
|
{
|
||||||
public function send(string $action, array $params = []): array
|
public function send(string $action, array $params = []): array
|
||||||
@@ -41,7 +50,10 @@ class RunCpanelRestoreTest extends TestCase
|
|||||||
discoveredData: null,
|
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);
|
$status = Cache::get('cpanel_restore_status_'.$jobId);
|
||||||
$this->assertSame('completed', $status['status'] ?? null);
|
$this->assertSame('completed', $status['status'] ?? null);
|
||||||
@@ -61,6 +73,10 @@ class RunCpanelRestoreTest extends TestCase
|
|||||||
File::ensureDirectoryExists(dirname($logPath));
|
File::ensureDirectoryExists(dirname($logPath));
|
||||||
File::put($logPath, '');
|
File::put($logPath, '');
|
||||||
|
|
||||||
|
User::factory()->create([
|
||||||
|
'username' => 'example',
|
||||||
|
]);
|
||||||
|
|
||||||
$this->app->instance(AgentClient::class, new class extends AgentClient
|
$this->app->instance(AgentClient::class, new class extends AgentClient
|
||||||
{
|
{
|
||||||
public function send(string $action, array $params = []): array
|
public function send(string $action, array $params = []): array
|
||||||
@@ -81,7 +97,10 @@ class RunCpanelRestoreTest extends TestCase
|
|||||||
discoveredData: null,
|
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);
|
$status = Cache::get('cpanel_restore_status_'.$jobId);
|
||||||
$this->assertSame('failed', $status['status'] ?? null);
|
$this->assertSame('failed', $status['status'] ?? null);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class VersionFileTest extends TestCase
|
|||||||
$content = file_get_contents($versionPath);
|
$content = file_get_contents($versionPath);
|
||||||
|
|
||||||
$this->assertNotFalse($content);
|
$this->assertNotFalse($content);
|
||||||
$this->assertStringContainsString('VERSION=0.9-rc2', $content);
|
$this->assertMatchesRegularExpression('/^VERSION=0\\.9-rc\\d*$/m', trim($content));
|
||||||
$this->assertStringNotContainsString('BUILD=', $content);
|
$this->assertStringNotContainsString('BUILD=', $content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user