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;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupDestination;
|
||||
use App\Models\BackupSchedule;
|
||||
@@ -40,7 +39,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -508,7 +506,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('createServerBackup')
|
||||
->label(__('Create Server Backup'))
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
|
||||
@@ -19,7 +19,6 @@ use Filament\Schemas\Components\EmbeddedTable;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class Dashboard extends Page implements HasActions, HasForms
|
||||
{
|
||||
@@ -70,13 +69,6 @@ class Dashboard extends Page implements HasActions, HasForms
|
||||
]);
|
||||
}
|
||||
|
||||
#[On('tour-completed')]
|
||||
public function completeTour(): void
|
||||
{
|
||||
DnsSetting::set('tour_completed', '1');
|
||||
DnsSetting::clearCache();
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -107,16 +99,6 @@ class Dashboard extends Page implements HasActions, HasForms
|
||||
}
|
||||
DnsSetting::set('onboarding_completed', '1');
|
||||
DnsSetting::clearCache();
|
||||
|
||||
$this->dispatch('start-admin-tour');
|
||||
}),
|
||||
|
||||
Action::make('takeTour')
|
||||
->label(__('Take Tour'))
|
||||
->icon('heroicon-o-academic-cap')
|
||||
->color('gray')
|
||||
->action(function (): void {
|
||||
$this->dispatch('start-admin-tour');
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Widgets\DnsPendingAddsTable;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\DnsSetting;
|
||||
use App\Models\Domain;
|
||||
@@ -39,7 +38,6 @@ use Livewire\Attributes\On;
|
||||
|
||||
class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -585,7 +583,6 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('syncAllZones')
|
||||
->label(__('Sync All Zones'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
@@ -26,7 +25,6 @@ use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class PhpManager extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -288,7 +286,6 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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\ThreatsTable;
|
||||
use App\Filament\Admin\Widgets\Security\WpscanResultsTable;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\AuditLog;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -48,7 +47,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class Security extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -989,7 +987,6 @@ class Security extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,8 @@ namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Widgets\Settings\DnssecTable;
|
||||
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\DnsSetting;
|
||||
use App\Models\UserResourceLimit;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\System\ResourceLimitService;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@@ -41,7 +38,6 @@ use Livewire\WithFileUploads;
|
||||
|
||||
class ServerSettings extends Page implements HasActions, HasForms
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use WithFileUploads;
|
||||
@@ -199,7 +195,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
$this->quotaData = [
|
||||
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
||||
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
||||
'resource_limits_enabled' => (bool) ($settings['resource_limits_enabled'] ?? true),
|
||||
];
|
||||
|
||||
$this->fileManagerData = [
|
||||
@@ -422,10 +417,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->numeric()
|
||||
->placeholder('5120')
|
||||
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
||||
Toggle::make('quotaData.resource_limits_enabled')
|
||||
->label(__('Enable CPU/Memory/IO Limits'))
|
||||
->helperText(__('Apply cgroup limits from hosting packages (CloudLinux-style)'))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Actions::make([
|
||||
FormAction::make('saveQuotaSettings')
|
||||
@@ -841,11 +832,9 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
{
|
||||
$data = $this->quotaData;
|
||||
$wasEnabled = (bool) DnsSetting::get('quotas_enabled', false);
|
||||
$wasLimitsEnabled = (bool) DnsSetting::get('resource_limits_enabled', true);
|
||||
|
||||
DnsSetting::set('quotas_enabled', $data['quotas_enabled'] ? '1' : '0');
|
||||
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
||||
DnsSetting::set('resource_limits_enabled', ! empty($data['resource_limits_enabled']) ? '1' : '0');
|
||||
DnsSetting::clearCache();
|
||||
|
||||
if ($data['quotas_enabled'] && ! $wasEnabled) {
|
||||
@@ -861,21 +850,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($data['resource_limits_enabled']) && ! $wasLimitsEnabled) {
|
||||
$limits = UserResourceLimit::query()->where('is_active', true)->get();
|
||||
foreach ($limits as $limit) {
|
||||
app(ResourceLimitService::class)->apply($limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data['resource_limits_enabled']) && $wasLimitsEnabled) {
|
||||
try {
|
||||
$this->getAgent()->send('cgroup.clear_all_limits', []);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not clear cgroup limits.'))->warning()->send();
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()->title(__('Quota settings saved'))->success()->send();
|
||||
}
|
||||
|
||||
@@ -1158,7 +1132,6 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('export_config')
|
||||
->label(__('Export'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Widgets\ServerChartsWidget;
|
||||
use App\Filament\Admin\Widgets\ServerInfoWidget;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\ServerProcess;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -28,7 +27,6 @@ use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ServerStatus extends Page implements HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-chart-bar';
|
||||
@@ -76,7 +74,6 @@ class ServerStatus extends Page implements HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
ActionGroup::make([
|
||||
Action::make('limit25')
|
||||
->label(__('Show 25 processes'))
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\AuditLog;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -25,7 +24,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Services extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -317,7 +315,6 @@ class Services extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Widgets\SslStatsOverview;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Domain;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Models\User;
|
||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class SslManager extends Page implements HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
||||
@@ -531,7 +529,6 @@ class SslManager extends Page implements HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('runAutoSsl')
|
||||
->label(__('Run SSL Check'))
|
||||
->icon('heroicon-o-play')
|
||||
|
||||
@@ -61,27 +61,6 @@ class HostingPackageForm
|
||||
])
|
||||
->columns(2),
|
||||
|
||||
Section::make(__('System Resource Limits'))
|
||||
->description(__('Optional cgroup limits applied to users in this package. Leave blank for unlimited.'))
|
||||
->schema([
|
||||
TextInput::make('cpu_limit_percent')
|
||||
->label(__('CPU Limit (%)'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(100)
|
||||
->helperText(__('Example: 50 = 50% CPU quota')),
|
||||
TextInput::make('memory_limit_mb')
|
||||
->label(__('Memory Limit (MB)'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->helperText(__('Example: 1024 = 1 GB RAM')),
|
||||
TextInput::make('io_limit_mb')
|
||||
->label(__('Disk IO Limit (MB/s)'))
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->helperText(__('Applied to read/write bandwidth')),
|
||||
])
|
||||
->columns(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,21 +44,6 @@ class HostingPackagesTable
|
||||
TextColumn::make('mailboxes_limit')
|
||||
->label(__('Mailboxes'))
|
||||
->getStateUsing(fn ($record) => $record->mailboxes_limit ?: __('Unlimited')),
|
||||
TextColumn::make('resource_limits')
|
||||
->label(__('System Limits'))
|
||||
->getStateUsing(function ($record) {
|
||||
$cpu = $record->cpu_limit_percent ? $record->cpu_limit_percent.'%' : null;
|
||||
$memory = $record->memory_limit_mb ? $record->memory_limit_mb.' MB' : null;
|
||||
$io = $record->io_limit_mb ? $record->io_limit_mb.' MB/s' : null;
|
||||
|
||||
$parts = array_filter([
|
||||
$cpu ? __('CPU: :value', ['value' => $cpu]) : null,
|
||||
$memory ? __('RAM: :value', ['value' => $memory]) : null,
|
||||
$io ? __('IO: :value', ['value' => $io]) : null,
|
||||
]);
|
||||
|
||||
return ! empty($parts) ? implode(', ', $parts) : __('Unlimited');
|
||||
}),
|
||||
IconColumn::make('is_active')
|
||||
->label(__('Active'))
|
||||
->boolean(),
|
||||
|
||||
@@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Users\UserResource;
|
||||
use App\Models\HostingPackage;
|
||||
use App\Models\UserResourceLimit;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\System\LinuxUserService;
|
||||
use App\Services\System\ResourceLimitService;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
@@ -59,9 +57,6 @@ class CreateUser extends CreateRecord
|
||||
|
||||
// Apply disk quota if enabled
|
||||
$this->applyDiskQuota();
|
||||
|
||||
// Apply resource limits from package
|
||||
$this->syncResourceLimitsFromPackage($this->selectedPackage, true);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(__('Linux user creation failed'))
|
||||
@@ -69,15 +64,12 @@ class CreateUser extends CreateRecord
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
} else {
|
||||
// Store resource limits even if the Linux user was not created yet
|
||||
$this->syncResourceLimitsFromPackage($this->selectedPackage, false);
|
||||
}
|
||||
|
||||
if (! $this->record->hosting_package_id) {
|
||||
Notification::make()
|
||||
->title(__('No hosting package selected'))
|
||||
->body(__('This user has unlimited resource limits.'))
|
||||
->body(__('This user has unlimited quotas.'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
@@ -113,50 +105,4 @@ class CreateUser extends CreateRecord
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function syncResourceLimitsFromPackage(?HostingPackage $package, bool $apply): void
|
||||
{
|
||||
if (! $this->record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cpu = $package?->cpu_limit_percent;
|
||||
$memory = $package?->memory_limit_mb;
|
||||
$io = $package?->io_limit_mb;
|
||||
$hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0);
|
||||
|
||||
$limit = UserResourceLimit::where('user_id', $this->record->id)->first();
|
||||
|
||||
if (! $package || ! $hasLimits) {
|
||||
if ($limit) {
|
||||
$limit->fill([
|
||||
'cpu_limit_percent' => null,
|
||||
'memory_limit_mb' => null,
|
||||
'io_limit_mb' => null,
|
||||
'is_active' => false,
|
||||
])->save();
|
||||
|
||||
if ($apply) {
|
||||
app(ResourceLimitService::class)->clear($limit);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $limit) {
|
||||
$limit = new UserResourceLimit(['user_id' => $this->record->id]);
|
||||
}
|
||||
|
||||
$limit->fill([
|
||||
'cpu_limit_percent' => $cpu,
|
||||
'memory_limit_mb' => $memory,
|
||||
'io_limit_mb' => $io,
|
||||
'is_active' => true,
|
||||
])->save();
|
||||
|
||||
if ($apply) {
|
||||
app(ResourceLimitService::class)->apply($limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ namespace App\Filament\Admin\Resources\Users\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Users\UserResource;
|
||||
use App\Models\HostingPackage;
|
||||
use App\Models\UserResourceLimit;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\System\LinuxUserService;
|
||||
use App\Services\System\ResourceLimitService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
@@ -76,45 +74,6 @@ class EditUser extends EditRecord
|
||||
}
|
||||
}
|
||||
|
||||
$this->syncResourceLimitsFromPackage($this->selectedPackage);
|
||||
}
|
||||
|
||||
protected function syncResourceLimitsFromPackage(?HostingPackage $package): void
|
||||
{
|
||||
$cpu = $package?->cpu_limit_percent;
|
||||
$memory = $package?->memory_limit_mb;
|
||||
$io = $package?->io_limit_mb;
|
||||
$hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0);
|
||||
|
||||
$limit = UserResourceLimit::where('user_id', $this->record->id)->first();
|
||||
|
||||
if (! $package || ! $hasLimits) {
|
||||
if ($limit) {
|
||||
$limit->fill([
|
||||
'cpu_limit_percent' => null,
|
||||
'memory_limit_mb' => null,
|
||||
'io_limit_mb' => null,
|
||||
'is_active' => false,
|
||||
])->save();
|
||||
|
||||
app(ResourceLimitService::class)->clear($limit);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $limit) {
|
||||
$limit = new UserResourceLimit(['user_id' => $this->record->id]);
|
||||
}
|
||||
|
||||
$limit->fill([
|
||||
'cpu_limit_percent' => $cpu,
|
||||
'memory_limit_mb' => $memory,
|
||||
'io_limit_mb' => $io,
|
||||
'is_active' => true,
|
||||
])->save();
|
||||
|
||||
app(ResourceLimitService::class)->apply($limit);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
|
||||
@@ -3,20 +3,16 @@
|
||||
namespace App\Filament\Admin\Resources\Users\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Users\UserResource;
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
use HasPageTour;
|
||||
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ class UserForm
|
||||
|
||||
Placeholder::make('package_notice')
|
||||
->label(__('Hosting Package'))
|
||||
->content(__('No hosting package selected. This user will have unlimited resources.'))
|
||||
->content(__('No hosting package selected. This user will have unlimited quotas.'))
|
||||
->visible(fn ($get) => blank($get('hosting_package_id'))),
|
||||
|
||||
\Filament\Forms\Components\Select::make('hosting_package_id')
|
||||
@@ -137,7 +137,7 @@ class UserForm
|
||||
->pluck('name', 'id')
|
||||
->toArray())
|
||||
->placeholder(__('Unlimited (no package)'))
|
||||
->helperText(__('Assign a package to set resource limits.'))
|
||||
->helperText(__('Assign a package to set quotas.'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('create_linux_user')
|
||||
|
||||
@@ -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;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupDestination;
|
||||
use App\Models\BackupRestore;
|
||||
@@ -46,7 +45,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -588,7 +586,6 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('createBackup')
|
||||
->label(__('Create Backup'))
|
||||
->icon('heroicon-o-archive-box-arrow-down')
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\CloudflareZone;
|
||||
use App\Models\Domain;
|
||||
use BackedEnum;
|
||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CdnIntegration extends Page implements HasActions, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithTable;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\CronJob;
|
||||
use App\Models\Domain;
|
||||
use App\Services\Agent\AgentClient;
|
||||
@@ -32,7 +31,6 @@ use Illuminate\Support\HtmlString;
|
||||
|
||||
class CronJobs extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -500,7 +498,6 @@ class CronJobs extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->createCronJobAction(),
|
||||
$this->setupWordPressCronAction(),
|
||||
];
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\MysqlCredential;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -35,7 +34,6 @@ use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Databases extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -382,7 +380,6 @@ class Databases extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->quickSetupAction(),
|
||||
$this->createDatabaseAction(),
|
||||
$this->createUserAction(),
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Filament\Jabali\Widgets\DnsPendingAddsTable;
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\DnsSetting;
|
||||
@@ -41,7 +40,6 @@ use Livewire\Attributes\On;
|
||||
|
||||
class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -802,7 +800,6 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->applyTemplateAction()
|
||||
->visible(fn () => $this->selectedDomainId !== null),
|
||||
$this->addRecordAction()
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Domain;
|
||||
use App\Models\DomainAlias;
|
||||
use App\Models\DomainHotlinkSetting;
|
||||
@@ -38,7 +37,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -434,7 +432,6 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->createDomainAction(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Autoresponder;
|
||||
use App\Models\DnsRecord;
|
||||
use App\Models\Domain;
|
||||
@@ -44,7 +43,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class Email extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -821,7 +819,6 @@ class Email extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->createMailboxAction(),
|
||||
$this->createForwarderAction(),
|
||||
$this->createAutoresponderAction(),
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\DnsSetting;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -34,7 +33,6 @@ use Livewire\WithFileUploads;
|
||||
|
||||
class Files extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -95,7 +93,6 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Jobs\RunGitDeployment;
|
||||
use App\Models\Domain;
|
||||
use App\Models\GitDeployment as GitDeploymentModel;
|
||||
@@ -33,7 +32,6 @@ use Illuminate\Support\Str;
|
||||
|
||||
class GitDeployment extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Domain;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
@@ -25,7 +24,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ImageOptimization extends Page implements HasActions, HasForms
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\UserResourceUsage;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
@@ -22,7 +20,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class Logs extends Page implements HasActions, HasForms
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
@@ -96,7 +93,7 @@ class Logs extends Page implements HasActions, HasForms
|
||||
protected function normalizeTab(?string $tab): string
|
||||
{
|
||||
return match ($tab) {
|
||||
'logs', 'usage', 'activity', 'stats' => (string) $tab,
|
||||
'logs', 'activity', 'stats' => (string) $tab,
|
||||
default => 'logs',
|
||||
};
|
||||
}
|
||||
@@ -195,57 +192,6 @@ class Logs extends Page implements HasActions, HasForms
|
||||
->send();
|
||||
}
|
||||
|
||||
public function getUsageChartData(): array
|
||||
{
|
||||
$start = now()->subDays(29)->startOfDay();
|
||||
$end = now()->endOfDay();
|
||||
$records = UserResourceUsage::query()
|
||||
->where('user_id', Auth::id())
|
||||
->whereBetween('captured_at', [$start, $end])
|
||||
->get();
|
||||
|
||||
$labels = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$labels[] = $start->copy()->addDays($i)->format('Y-m-d');
|
||||
}
|
||||
$index = array_flip($labels);
|
||||
|
||||
$metrics = ['disk_bytes', 'database_bytes', 'mail_bytes', 'bandwidth_bytes'];
|
||||
$values = [];
|
||||
foreach ($metrics as $metric) {
|
||||
$values[$metric] = array_fill(0, count($labels), 0);
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
$date = $record->captured_at?->format('Y-m-d');
|
||||
if (! $date || ! isset($index[$date])) {
|
||||
continue;
|
||||
}
|
||||
$idx = $index[$date];
|
||||
$metric = $record->metric;
|
||||
if (! isset($values[$metric])) {
|
||||
continue;
|
||||
}
|
||||
if ($metric === 'bandwidth_bytes') {
|
||||
$values[$metric][$idx] += (int) $record->value;
|
||||
} else {
|
||||
$values[$metric][$idx] = max($values[$metric][$idx], (int) $record->value);
|
||||
}
|
||||
}
|
||||
|
||||
$toGb = fn (int $bytes) => round($bytes / 1024 / 1024 / 1024, 2);
|
||||
|
||||
return [
|
||||
'labels' => $labels,
|
||||
'series' => [
|
||||
['name' => __('Disk'), 'data' => array_map($toGb, $values['disk_bytes'])],
|
||||
['name' => __('Databases'), 'data' => array_map($toGb, $values['database_bytes'])],
|
||||
['name' => __('Mail'), 'data' => array_map($toGb, $values['mail_bytes'])],
|
||||
['name' => __('Bandwidth'), 'data' => array_map($toGb, $values['bandwidth_bytes'])],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivityLogs()
|
||||
{
|
||||
return AuditLog::query()
|
||||
@@ -301,7 +247,6 @@ class Logs extends Page implements HasActions, HasForms
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
|
||||
Action::make('generateStats')
|
||||
->label(__('Generate Statistics'))
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\UserSetting;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
@@ -22,7 +21,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class MailingLists extends Page implements HasActions, HasForms
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
@@ -23,7 +22,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PhpSettings extends Page implements HasActions, HasForms
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
@@ -267,7 +265,6 @@ class PhpSettings extends Page implements HasActions, HasForms
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->saveSettingsAction(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
@@ -28,7 +27,6 @@ use Livewire\Attributes\Url;
|
||||
|
||||
class PostgreSQL extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
@@ -26,7 +25,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SshKeys extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -203,7 +201,6 @@ class SshKeys extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
Action::make('generateKey')
|
||||
->label(__('Generate SSH Key'))
|
||||
->icon('heroicon-o-sparkles')
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Domain;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Services\Agent\AgentClient;
|
||||
@@ -29,7 +28,6 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class Ssl extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -498,7 +496,6 @@ class Ssl extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->installCustomCertificateAction(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Jabali\Pages;
|
||||
|
||||
use App\Filament\Concerns\HasPageTour;
|
||||
use App\Models\Domain;
|
||||
use App\Models\MysqlCredential;
|
||||
use App\Services\Agent\AgentClient;
|
||||
@@ -36,7 +35,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
|
||||
{
|
||||
protected static ?string $slug = 'wordpress';
|
||||
|
||||
use HasPageTour;
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
@@ -330,7 +328,6 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getTourAction(),
|
||||
$this->scanAction(),
|
||||
$this->installAction(),
|
||||
];
|
||||
|
||||
@@ -7,10 +7,8 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Domain;
|
||||
use App\Models\HostingPackage;
|
||||
use App\Models\User;
|
||||
use App\Models\UserResourceLimit;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\System\LinuxUserService;
|
||||
use App\Services\System\ResourceLimitService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -76,29 +74,6 @@ class AutomationApiController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($package) {
|
||||
$cpu = $package->cpu_limit_percent;
|
||||
$memory = $package->memory_limit_mb;
|
||||
$io = $package->io_limit_mb;
|
||||
$hasLimits = ($cpu && $cpu > 0) || ($memory && $memory > 0) || ($io && $io > 0);
|
||||
|
||||
if ($hasLimits) {
|
||||
$limit = UserResourceLimit::firstOrNew(['user_id' => $user->id]);
|
||||
$limit->fill([
|
||||
'cpu_limit_percent' => $cpu,
|
||||
'memory_limit_mb' => $memory,
|
||||
'io_limit_mb' => $io,
|
||||
'is_active' => true,
|
||||
])->save();
|
||||
|
||||
try {
|
||||
app(ResourceLimitService::class)->apply($limit);
|
||||
} catch (\Exception) {
|
||||
// cgroup apply failure shouldn't block user creation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['user' => $user], 201);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ class HostingPackage extends Model
|
||||
'domains_limit',
|
||||
'databases_limit',
|
||||
'mailboxes_limit',
|
||||
'cpu_limit_percent',
|
||||
'memory_limit_mb',
|
||||
'io_limit_mb',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
use App\Filament\Admin\Pages\Auth\Login as AdminLogin;
|
||||
use App\Filament\Admin\Pages\Dashboard;
|
||||
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
||||
use App\Http\Middleware\SetLocale;
|
||||
use App\Models\DnsSetting;
|
||||
@@ -10,7 +11,6 @@ use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use App\Filament\Admin\Pages\Dashboard;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
@@ -38,13 +38,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
'primary' => Color::Red,
|
||||
])
|
||||
->darkMode()
|
||||
->brandName(fn () => DnsSetting::get('panel_name', 'Jabali') . ' Admin')
|
||||
->brandName(fn () => DnsSetting::get('panel_name', 'Jabali').' Admin')
|
||||
->favicon(asset('favicon.ico'))
|
||||
->renderHook(
|
||||
PanelsRenderHook::HEAD_END,
|
||||
fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure') .
|
||||
'<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() .
|
||||
fn () => $this->getOpenGraphTags('Jabali Admin', 'Server administration panel for Jabali - Manage your hosting infrastructure').
|
||||
'<link rel="stylesheet" href="'.asset('css/filament-custom.css').'">'.
|
||||
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
|
||||
$this->getRtlScript()
|
||||
)
|
||||
->renderHook(
|
||||
@@ -59,10 +59,6 @@ $this->getRtlScript()
|
||||
PanelsRenderHook::USER_MENU_BEFORE,
|
||||
fn () => view('components.language-switcher')
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn () => view('components.admin-tour')
|
||||
)
|
||||
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
||||
->pages([
|
||||
@@ -141,8 +137,10 @@ $this->getRtlScript()
|
||||
shuffle($shuffled);
|
||||
for ($col = 0; $col < 20; $col++) {
|
||||
$word = $shuffled[$col % count($shuffled)];
|
||||
if ($col % 3 === 0) shuffle($shuffled); // Re-shuffle periodically
|
||||
$rowContent .= $word . ' · ';
|
||||
if ($col % 3 === 0) {
|
||||
shuffle($shuffled);
|
||||
} // Re-shuffle periodically
|
||||
$rowContent .= $word.' · ';
|
||||
}
|
||||
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\AvatarProviders\InitialsAvatarProvider;
|
||||
use App\Filament\Jabali\Pages\Auth\Login;
|
||||
use App\Models\DnsSetting;
|
||||
use App\Models\User;
|
||||
use App\Http\Middleware\RedirectAdminFromUserPanel;
|
||||
use App\Http\Middleware\SetLocale;
|
||||
use App\Models\DnsSetting;
|
||||
use App\Models\User;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
@@ -22,7 +22,6 @@ use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class JabaliPanelProvider extends PanelProvider
|
||||
@@ -46,14 +45,14 @@ class JabaliPanelProvider extends PanelProvider
|
||||
->favicon(asset('favicon.ico'))
|
||||
->renderHook(
|
||||
PanelsRenderHook::HEAD_END,
|
||||
fn () => $this->getOpenGraphTags('Jabali Panel', 'Web hosting control panel - Manage your domains, emails, databases and more') .
|
||||
'<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() .
|
||||
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')).'">'.
|
||||
\Illuminate\Support\Facades\Vite::useBuildDirectory('build')->withEntryPoints(['resources/js/server-charts.js'])->toHtml().
|
||||
$this->getRtlScript()
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_START,
|
||||
fn () => (request()->routeIs('filament.jabali.auth.login') ? $this->getLoginWordCloud() : '') . $this->renderImpersonationNotice()
|
||||
fn () => (request()->routeIs('filament.jabali.auth.login') ? $this->getLoginWordCloud() : '').$this->renderImpersonationNotice()
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::FOOTER,
|
||||
@@ -63,10 +62,6 @@ $this->getRtlScript()
|
||||
PanelsRenderHook::USER_MENU_BEFORE,
|
||||
fn () => view('components.language-switcher')
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn () => view('components.user-tour')
|
||||
)
|
||||
->discoverResources(in: app_path('Filament/Jabali/Resources'), for: 'App\\Filament\\Jabali\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Jabali/Pages'), for: 'App\\Filament\\Jabali\\Pages')
|
||||
->pages([])
|
||||
@@ -106,7 +101,7 @@ $this->getRtlScript()
|
||||
|
||||
protected function renderImpersonationNotice(): string
|
||||
{
|
||||
if (!session()->has('impersonated_by')) {
|
||||
if (! session()->has('impersonated_by')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -114,7 +109,7 @@ $this->getRtlScript()
|
||||
$admin = User::find($adminId);
|
||||
$currentUser = auth()->user();
|
||||
|
||||
if (!$admin || !$currentUser) {
|
||||
if (! $admin || ! $currentUser) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -172,8 +167,10 @@ $this->getRtlScript()
|
||||
shuffle($shuffled);
|
||||
for ($col = 0; $col < 20; $col++) {
|
||||
$word = $shuffled[$col % count($shuffled)];
|
||||
if ($col % 3 === 0) shuffle($shuffled);
|
||||
$rowContent .= $word . ' · ';
|
||||
if ($col % 3 === 0) {
|
||||
shuffle($shuffled);
|
||||
}
|
||||
$rowContent .= $word.' · ';
|
||||
}
|
||||
$rows .= "<div class=\"pattern-row\">{$rowContent}</div>";
|
||||
}
|
||||
|
||||
@@ -1333,7 +1333,7 @@ class AgentClient
|
||||
return $this->send('updates.run');
|
||||
}
|
||||
|
||||
// WAF / Geo / Resource limits
|
||||
// WAF / Geo
|
||||
public function wafApplySettings(bool $enabled, string $paranoia, bool $auditLog): array
|
||||
{
|
||||
return $this->send('waf.apply', [
|
||||
@@ -1350,44 +1350,6 @@ class AgentClient
|
||||
]);
|
||||
}
|
||||
|
||||
public function cgroupApplyUserLimits(string $username, ?int $cpuLimit, ?int $memoryLimit, ?int $ioLimit, bool $isActive = true): array
|
||||
{
|
||||
if (! $isActive) {
|
||||
return $this->cgroupClearUserLimits($username);
|
||||
}
|
||||
|
||||
return $this->send('cgroup.apply_user_limits', [
|
||||
'username' => $username,
|
||||
'cpu_limit_percent' => $cpuLimit,
|
||||
'memory_limit_mb' => $memoryLimit,
|
||||
'io_limit_mb' => $ioLimit,
|
||||
]);
|
||||
}
|
||||
|
||||
public function cgroupClearUserLimits(string $username): array
|
||||
{
|
||||
return $this->send('cgroup.clear_user_limits', [
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function cgroupSyncUserProcesses(string $username): array
|
||||
{
|
||||
return $this->send('cgroup.sync_user_processes', [
|
||||
'username' => $username,
|
||||
]);
|
||||
}
|
||||
|
||||
public function cgroupSyncAllProcesses(): array
|
||||
{
|
||||
return $this->send('cgroup.sync_all_processes');
|
||||
}
|
||||
|
||||
public function cgroupClearAllLimits(): array
|
||||
{
|
||||
return $this->send('cgroup.clear_all_limits');
|
||||
}
|
||||
|
||||
public function databasePersistTuning(string $name, string $value): array
|
||||
{
|
||||
return $this->send('database.persist_tuning', [
|
||||
|
||||
@@ -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),
|
||||
'waf.apply' => wafApplySettings($params),
|
||||
'geo.apply_rules' => geoApplyRules($params),
|
||||
'cgroup.apply_user_limits' => cgroupApplyUserLimits($params),
|
||||
'cgroup.clear_user_limits' => cgroupClearUserLimits($params),
|
||||
'cgroup.sync_user_processes' => cgroupSyncUserProcesses($params),
|
||||
'cgroup.sync_all_processes' => cgroupSyncAllProcesses($params),
|
||||
'cgroup.clear_all_limits' => cgroupClearAllUserLimits($params),
|
||||
'database.persist_tuning' => databasePersistTuning($params),
|
||||
'server.export_config' => serverExportConfig($params),
|
||||
'server.import_config' => serverImportConfig($params),
|
||||
@@ -818,11 +813,6 @@ function createUser(array $params): array
|
||||
logger("Warning: Failed to create Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
$cgroupResult = ensureUserCgroup($username);
|
||||
if (!($cgroupResult['success'] ?? false)) {
|
||||
logger("Warning: Failed to initialize cgroup for $username: " . ($cgroupResult['error'] ?? 'Unknown error'));
|
||||
}
|
||||
|
||||
logger("Created user $username with home directory $homeDir");
|
||||
|
||||
return [
|
||||
@@ -1040,8 +1030,6 @@ function deleteUser(array $params): array
|
||||
}
|
||||
}
|
||||
|
||||
cgroupRemoveUser($username);
|
||||
|
||||
// Reload services
|
||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
|
||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null');
|
||||
@@ -3040,386 +3028,6 @@ function geoApplyRules(array $params): array
|
||||
return ['success' => true, 'rules' => count($ruleset)];
|
||||
}
|
||||
|
||||
function getRootBlockDevice(): ?string
|
||||
{
|
||||
exec('findmnt -no SOURCE / 2>/dev/null', $output, $code);
|
||||
$source = trim($output[0] ?? '');
|
||||
if ($source === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($source, '/dev/')) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
exec('readlink -f ' . escapeshellarg($source) . ' 2>/dev/null', $resolved, $resolvedCode);
|
||||
$resolvedPath = trim($resolved[0] ?? '');
|
||||
|
||||
return $resolvedPath ?: null;
|
||||
}
|
||||
|
||||
function isCgroupV2Available(): bool
|
||||
{
|
||||
return is_file('/sys/fs/cgroup/cgroup.controllers');
|
||||
}
|
||||
|
||||
function getJabaliCgroupRoot(): string
|
||||
{
|
||||
return '/sys/fs/cgroup/jabali.slice';
|
||||
}
|
||||
|
||||
function enableCgroupControllers(string $path, array $controllers): void
|
||||
{
|
||||
$controllersFile = $path . '/cgroup.controllers';
|
||||
$subtreeFile = $path . '/cgroup.subtree_control';
|
||||
|
||||
if (!is_readable($controllersFile) || !is_writable($subtreeFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$available = preg_split('/\s+/', trim(file_get_contents($controllersFile)));
|
||||
$toEnable = [];
|
||||
foreach ($controllers as $controller) {
|
||||
if (in_array($controller, $available, true)) {
|
||||
$toEnable[] = '+' . $controller;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($toEnable)) {
|
||||
@file_put_contents($subtreeFile, implode(' ', $toEnable));
|
||||
}
|
||||
}
|
||||
|
||||
function ensureJabaliCgroupRoot(): array
|
||||
{
|
||||
if (!isCgroupV2Available()) {
|
||||
return ['success' => false, 'error' => 'cgroup v2 is not available'];
|
||||
}
|
||||
|
||||
$root = getJabaliCgroupRoot();
|
||||
if (!is_dir($root)) {
|
||||
exec('systemctl start jabali.slice 2>/dev/null', $output, $code);
|
||||
}
|
||||
|
||||
if (!is_dir($root)) {
|
||||
return ['success' => false, 'error' => 'Failed to initialize jabali.slice'];
|
||||
}
|
||||
|
||||
enableCgroupControllers($root, ['cpu', 'memory', 'io']);
|
||||
|
||||
return ['success' => true, 'path' => $root];
|
||||
}
|
||||
|
||||
function getUserCgroupPath(string $username): string
|
||||
{
|
||||
return getJabaliCgroupRoot() . '/user-' . $username;
|
||||
}
|
||||
|
||||
function ensureUserCgroup(string $username): array
|
||||
{
|
||||
$rootResult = ensureJabaliCgroupRoot();
|
||||
if (!($rootResult['success'] ?? false)) {
|
||||
return $rootResult;
|
||||
}
|
||||
|
||||
$path = getUserCgroupPath($username);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_dir($path)) {
|
||||
return ['success' => false, 'error' => 'Failed to create user cgroup'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'path' => $path];
|
||||
}
|
||||
|
||||
function writeCgroupValue(string $path, string $file, string $value): bool
|
||||
{
|
||||
$target = $path . '/' . $file;
|
||||
if (!is_writable($target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_put_contents($target, $value) !== false;
|
||||
}
|
||||
|
||||
function getBlockDeviceId(?string $device): ?string
|
||||
{
|
||||
if (!$device) {
|
||||
return null;
|
||||
}
|
||||
|
||||
exec('lsblk -no MAJ:MIN ' . escapeshellarg($device) . ' 2>/dev/null', $output, $code);
|
||||
$id = trim($output[0] ?? '');
|
||||
|
||||
return $id !== '' ? $id : null;
|
||||
}
|
||||
|
||||
function cgroupWriteCpuMax(string $path, int $cpuPercent): void
|
||||
{
|
||||
$period = 100000;
|
||||
|
||||
if ($cpuPercent <= 0) {
|
||||
writeCgroupValue($path, 'cpu.max', 'max ' . $period);
|
||||
return;
|
||||
}
|
||||
|
||||
$quota = (int) round($period * ($cpuPercent / 100));
|
||||
$quota = max($quota, 1000);
|
||||
writeCgroupValue($path, 'cpu.max', $quota . ' ' . $period);
|
||||
}
|
||||
|
||||
function cgroupWriteMemoryMax(string $path, int $memoryMb): void
|
||||
{
|
||||
if ($memoryMb <= 0) {
|
||||
writeCgroupValue($path, 'memory.max', 'max');
|
||||
return;
|
||||
}
|
||||
|
||||
$bytes = $memoryMb * 1024 * 1024;
|
||||
writeCgroupValue($path, 'memory.max', (string) $bytes);
|
||||
}
|
||||
|
||||
function cgroupWriteIoMax(string $path, int $ioMb): void
|
||||
{
|
||||
$device = getRootBlockDevice();
|
||||
$deviceId = getBlockDeviceId($device);
|
||||
|
||||
if (!$deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ioMb <= 0) {
|
||||
writeCgroupValue($path, 'io.max', $deviceId . ' rbps=max wbps=max');
|
||||
return;
|
||||
}
|
||||
|
||||
$bytes = $ioMb * 1024 * 1024;
|
||||
writeCgroupValue($path, 'io.max', $deviceId . ' rbps=' . $bytes . ' wbps=' . $bytes);
|
||||
}
|
||||
|
||||
function moveUserProcessesToCgroup(int $uid, string $cgroupPath): int
|
||||
{
|
||||
$moved = 0;
|
||||
foreach (glob('/proc/[0-9]*') as $procPath) {
|
||||
$statusFile = $procPath . '/status';
|
||||
if (!is_readable($statusFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = file($statusFile, FILE_IGNORE_NEW_LINES);
|
||||
if (!$status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchesUid = false;
|
||||
foreach ($status as $line) {
|
||||
if (str_starts_with($line, 'Uid:')) {
|
||||
$parts = preg_split('/\s+/', trim($line));
|
||||
$matchesUid = isset($parts[1]) && (int) $parts[1] === $uid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$matchesUid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pid = basename($procPath);
|
||||
if (!ctype_digit($pid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (@file_put_contents($cgroupPath . '/cgroup.procs', $pid) !== false) {
|
||||
$moved++;
|
||||
}
|
||||
}
|
||||
|
||||
return $moved;
|
||||
}
|
||||
|
||||
function cgroupSyncUserProcesses(array $params): array
|
||||
{
|
||||
$username = $params['username'] ?? '';
|
||||
if (!validateUsername($username)) {
|
||||
return ['success' => false, 'error' => 'Invalid username format'];
|
||||
}
|
||||
|
||||
$userInfo = posix_getpwnam($username);
|
||||
if (!$userInfo) {
|
||||
return ['success' => false, 'error' => 'User not found'];
|
||||
}
|
||||
|
||||
$cgroup = ensureUserCgroup($username);
|
||||
if (!($cgroup['success'] ?? false)) {
|
||||
return $cgroup;
|
||||
}
|
||||
|
||||
$moved = moveUserProcessesToCgroup((int) $userInfo['uid'], $cgroup['path']);
|
||||
|
||||
return ['success' => true, 'moved' => $moved];
|
||||
}
|
||||
|
||||
function cgroupSyncAllProcesses(array $params): array
|
||||
{
|
||||
$rootResult = ensureJabaliCgroupRoot();
|
||||
if (!($rootResult['success'] ?? false)) {
|
||||
return $rootResult;
|
||||
}
|
||||
|
||||
$movedTotal = 0;
|
||||
foreach (glob('/home/*', GLOB_ONLYDIR) as $homeDir) {
|
||||
$username = basename($homeDir);
|
||||
if (!validateUsername($username)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = cgroupSyncUserProcesses(['username' => $username]);
|
||||
if ($result['success'] ?? false) {
|
||||
$movedTotal += (int) ($result['moved'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'moved' => $movedTotal];
|
||||
}
|
||||
|
||||
function cgroupClearAllUserLimits(array $params): array
|
||||
{
|
||||
$rootResult = ensureJabaliCgroupRoot();
|
||||
if (!($rootResult['success'] ?? false)) {
|
||||
return $rootResult;
|
||||
}
|
||||
|
||||
$cleared = 0;
|
||||
foreach (glob(getJabaliCgroupRoot() . '/user-*') as $cgroupPath) {
|
||||
if (!is_dir($cgroupPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cgroupWriteCpuMax($cgroupPath, 0);
|
||||
cgroupWriteMemoryMax($cgroupPath, 0);
|
||||
cgroupWriteIoMax($cgroupPath, 0);
|
||||
$cleared++;
|
||||
}
|
||||
|
||||
return ['success' => true, 'cleared' => $cleared];
|
||||
}
|
||||
|
||||
function cgroupRemoveUser(string $username): void
|
||||
{
|
||||
$path = getUserCgroupPath($username);
|
||||
if (is_dir($path)) {
|
||||
@rmdir($path);
|
||||
}
|
||||
}
|
||||
|
||||
function readCgroupStatValue(string $path, string $key): ?int
|
||||
{
|
||||
$file = $path . '/cpu.stat';
|
||||
if (!is_readable($file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||
if (!$lines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with($line, $key . ' ')) {
|
||||
$parts = explode(' ', trim($line));
|
||||
return isset($parts[1]) ? (int) $parts[1] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readCgroupIoTotal(string $path): int
|
||||
{
|
||||
$file = $path . '/io.stat';
|
||||
if (!is_readable($file)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||
if (!$lines) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (!str_contains($line, 'rbytes=') && !str_contains($line, 'wbytes=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/rbytes=(\d+)/', $line, $m)) {
|
||||
$total += (int) $m[1];
|
||||
}
|
||||
if (preg_match('/wbytes=(\d+)/', $line, $m)) {
|
||||
$total += (int) $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
function cgroupApplyUserLimits(array $params): array
|
||||
{
|
||||
$username = $params['username'] ?? '';
|
||||
if (!validateUsername($username)) {
|
||||
return ['success' => false, 'error' => 'Invalid username format'];
|
||||
}
|
||||
|
||||
if (!posix_getpwnam($username)) {
|
||||
return ['success' => false, 'error' => 'User not found'];
|
||||
}
|
||||
|
||||
$cpu = isset($params['cpu_limit_percent']) ? (int) $params['cpu_limit_percent'] : 0;
|
||||
$memory = isset($params['memory_limit_mb']) ? (int) $params['memory_limit_mb'] : 0;
|
||||
$io = isset($params['io_limit_mb']) ? (int) $params['io_limit_mb'] : 0;
|
||||
|
||||
$cgroup = ensureUserCgroup($username);
|
||||
if (!($cgroup['success'] ?? false)) {
|
||||
return $cgroup;
|
||||
}
|
||||
|
||||
$path = $cgroup['path'];
|
||||
|
||||
cgroupWriteCpuMax($path, $cpu);
|
||||
cgroupWriteMemoryMax($path, $memory);
|
||||
cgroupWriteIoMax($path, $io);
|
||||
|
||||
cgroupSyncUserProcesses(['username' => $username]);
|
||||
|
||||
return ['success' => true, 'message' => 'Resource limits applied', 'path' => $path];
|
||||
}
|
||||
|
||||
function cgroupClearUserLimits(array $params): array
|
||||
{
|
||||
$username = $params['username'] ?? '';
|
||||
if (!validateUsername($username)) {
|
||||
return ['success' => false, 'error' => 'Invalid username format'];
|
||||
}
|
||||
|
||||
if (!posix_getpwnam($username)) {
|
||||
return ['success' => false, 'error' => 'User not found'];
|
||||
}
|
||||
|
||||
$cgroup = ensureUserCgroup($username);
|
||||
if (!($cgroup['success'] ?? false)) {
|
||||
return $cgroup;
|
||||
}
|
||||
|
||||
$path = $cgroup['path'];
|
||||
cgroupWriteCpuMax($path, 0);
|
||||
cgroupWriteMemoryMax($path, 0);
|
||||
cgroupWriteIoMax($path, 0);
|
||||
|
||||
return ['success' => true, 'message' => 'Resource limits cleared', 'path' => $path];
|
||||
}
|
||||
|
||||
function databasePersistTuning(array $params): array
|
||||
{
|
||||
$name = $params['name'] ?? '';
|
||||
@@ -6927,8 +6535,59 @@ function wpPageCacheEnable(array $params): array
|
||||
|
||||
$config = file_get_contents($configFile);
|
||||
|
||||
$hasPageCache = strpos($config, 'fastcgi_cache JABALI') !== false;
|
||||
$hasHammerBypass = strpos($config, 'cache_reason "hammer"') !== false;
|
||||
|
||||
// If cache is already enabled, ensure hammer bypass exists
|
||||
if ($hasPageCache && ! $hasHammerBypass) {
|
||||
$hammerRule = <<<'HAMMER'
|
||||
|
||||
# Skip cache for hammer/stress test endpoints
|
||||
if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") {
|
||||
set $skip_cache 1;
|
||||
set $cache_reason "hammer";
|
||||
}
|
||||
HAMMER;
|
||||
|
||||
$updated = preg_replace(
|
||||
'/\n\s*# Browser caching for static assets/',
|
||||
$hammerRule . "\n\n # Browser caching for static assets",
|
||||
$config,
|
||||
1
|
||||
);
|
||||
|
||||
if ($updated === null || $updated === $config) {
|
||||
$updated = preg_replace(
|
||||
'/(set \\$cache_reason \"\";)/',
|
||||
"$1{$hammerRule}",
|
||||
$config,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
if ($updated && $updated !== $config) {
|
||||
copy($configFile, $configFile . '.bak');
|
||||
if (file_put_contents($configFile, $updated) === false) {
|
||||
return ['success' => false, 'error' => 'Failed to update nginx config'];
|
||||
}
|
||||
|
||||
exec('nginx -t 2>&1', $output, $exitCode);
|
||||
if ($exitCode !== 0) {
|
||||
copy($configFile . '.bak', $configFile);
|
||||
return ['success' => false, 'error' => 'Nginx config test failed: ' . implode(' ', $output)];
|
||||
}
|
||||
|
||||
exec('systemctl reload nginx 2>&1', $output, $exitCode);
|
||||
if ($exitCode !== 0) {
|
||||
return ['success' => false, 'error' => 'Failed to reload nginx'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'Page cache updated with hammer bypass'];
|
||||
}
|
||||
|
||||
// Check if page cache is already enabled
|
||||
if (strpos($config, 'fastcgi_cache JABALI') !== false) {
|
||||
if ($hasPageCache) {
|
||||
return ['success' => true, 'message' => 'Page cache already enabled'];
|
||||
}
|
||||
|
||||
@@ -6973,6 +6632,12 @@ function wpPageCacheEnable(array $params): array
|
||||
set $cache_reason "admin_url";
|
||||
}
|
||||
|
||||
# Skip cache for hammer/stress test endpoints
|
||||
if ($request_uri ~* "/hammer|/io-hammer|/hammer-all") {
|
||||
set $skip_cache 1;
|
||||
set $cache_reason "hammer";
|
||||
}
|
||||
|
||||
# Browser caching for static assets (1 year, immutable for versioned files)
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif|mp4|webm|ogg|mp3|wav|pdf|zip)$ {
|
||||
expires 1y;
|
||||
@@ -24047,17 +23712,8 @@ function usageUserResources(array $params): array
|
||||
$cpuUsageUsec = null;
|
||||
$memoryBytes = 0;
|
||||
$diskIoTotal = 0;
|
||||
|
||||
$cgroupPath = getUserCgroupPath($username);
|
||||
if (is_dir($cgroupPath)) {
|
||||
$memoryCurrent = @file_get_contents($cgroupPath . '/memory.current');
|
||||
if ($memoryCurrent !== false) {
|
||||
$memoryBytes = (int) trim($memoryCurrent);
|
||||
}
|
||||
|
||||
$cpuUsageUsec = readCgroupStatValue($cgroupPath, 'usage_usec');
|
||||
$diskIoTotal = readCgroupIoTotal($cgroupPath);
|
||||
}
|
||||
$diskRead = 0;
|
||||
$diskWrite = 0;
|
||||
|
||||
if ($cpuUsageUsec === null || $memoryBytes === 0) {
|
||||
$cpuTotal = 0.0;
|
||||
@@ -24082,8 +23738,6 @@ function usageUserResources(array $params): array
|
||||
}
|
||||
|
||||
if ($diskIoTotal === 0) {
|
||||
$diskRead = 0;
|
||||
$diskWrite = 0;
|
||||
|
||||
foreach (glob('/proc/[0-9]*') as $procPath) {
|
||||
$statusFile = $procPath . '/status';
|
||||
@@ -24136,6 +23790,8 @@ function usageUserResources(array $params): array
|
||||
'cpu_usage_usec_total' => $cpuUsageUsec,
|
||||
'memory_bytes' => $memoryBytes,
|
||||
'disk_io_total_bytes' => $diskIoTotal,
|
||||
'disk_io_read_bytes_total' => $diskRead,
|
||||
'disk_io_write_bytes_total' => $diskWrite,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
setup_cgroup_limits() {
|
||||
header "Setting Up cgroup v2 Resource Limits"
|
||||
|
||||
if [[ ! -f /sys/fs/cgroup/cgroup.controllers ]]; then
|
||||
warn "cgroup v2 not detected - resource limits will be disabled"
|
||||
return
|
||||
fi
|
||||
|
||||
cat > /etc/systemd/system/jabali.slice << 'SLICE'
|
||||
[Slice]
|
||||
Delegate=yes
|
||||
CPUAccounting=yes
|
||||
MemoryAccounting=yes
|
||||
IOAccounting=yes
|
||||
SLICE
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable jabali.slice 2>/dev/null || true
|
||||
systemctl start jabali.slice 2>/dev/null || true
|
||||
|
||||
if [[ -w /sys/fs/cgroup/jabali.slice/cgroup.subtree_control ]]; then
|
||||
echo "+cpu +memory +io" > /sys/fs/cgroup/jabali.slice/cgroup.subtree_control || true
|
||||
fi
|
||||
|
||||
log "cgroup v2 slice configured"
|
||||
}
|
||||
|
||||
setup_queue_service() {
|
||||
header "Setting Up Jabali Queue Worker"
|
||||
|
||||
@@ -2918,11 +2891,6 @@ uninstall() {
|
||||
rm -f /etc/systemd/system/jabali-queue.service
|
||||
rm -rf /etc/systemd/system/jabali-queue.service.d
|
||||
|
||||
systemctl stop jabali.slice 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/jabali.slice
|
||||
rm -rf /sys/fs/cgroup/jabali.slice/user-* 2>/dev/null || true
|
||||
rmdir /sys/fs/cgroup/jabali.slice 2>/dev/null || true
|
||||
|
||||
local services=(
|
||||
nginx
|
||||
php-fpm
|
||||
@@ -3253,7 +3221,6 @@ main() {
|
||||
configure_redis
|
||||
setup_jabali
|
||||
setup_agent_service
|
||||
setup_cgroup_limits
|
||||
setup_queue_service
|
||||
setup_scheduler_cron
|
||||
setup_logrotate
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -5,7 +5,6 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"driver.js": "^1.4.0",
|
||||
"echarts": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1715,12 +1714,6 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/driver.js": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz",
|
||||
"integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"vite": "^7.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"driver.js": "^1.4.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 = [
|
||||
'logs' => ['label' => __('Logs'), 'icon' => 'heroicon-o-document-text'],
|
||||
'stats' => ['label' => __('Statistics'), 'icon' => 'heroicon-o-chart-bar'],
|
||||
'usage' => ['label' => __('Resource Usage'), 'icon' => 'heroicon-o-chart-pie'],
|
||||
'activity' => ['label' => __('Activity Log'), 'icon' => 'heroicon-o-clipboard-document-list'],
|
||||
];
|
||||
@endphp
|
||||
@@ -164,78 +163,6 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'usage')
|
||||
@php($usageData = $this->getUsageChartData())
|
||||
<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')
|
||||
<x-filament::section class="mt-4" icon="heroicon-o-clipboard-document-list">
|
||||
<x-slot name="heading">{{ __('Activity Log') }}</x-slot>
|
||||
|
||||
@@ -66,20 +66,6 @@ Schedule::command('jabali:sync-mailbox-quotas')
|
||||
->runInBackground()
|
||||
->appendOutputTo(storage_path('logs/mailbox-quota-sync.log'));
|
||||
|
||||
// User Resource Usage - runs hourly to capture per-user usage history
|
||||
Schedule::command('jabali:collect-user-usage')
|
||||
->hourly()
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->appendOutputTo(storage_path('logs/user-usage.log'));
|
||||
|
||||
// Cgroup Sync - runs every minute to keep user processes assigned to cgroups
|
||||
Schedule::command('jabali:sync-cgroups')
|
||||
->everyMinute()
|
||||
->withoutOverlapping()
|
||||
->runInBackground()
|
||||
->appendOutputTo(storage_path('logs/cgroup-sync.log'));
|
||||
|
||||
// Audit Log Rotation - runs daily to prune old audit logs (default: 90 days retention)
|
||||
Schedule::call(function () {
|
||||
$deleted = AuditLog::prune();
|
||||
|
||||
@@ -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));
|
||||
|
||||
if (file_exists('/etc/jabali/roundcube-sso.conf')) {
|
||||
$response->assertStatus(302);
|
||||
$location = $response->headers->get('Location');
|
||||
$this->assertNotFalse($location);
|
||||
$this->assertStringContainsString('/webmail/jabali-sso.php?token=', $location);
|
||||
} else {
|
||||
$response->assertStatus(200);
|
||||
$response->assertSee('Webmail Login Required');
|
||||
$response->assertSee('Open Webmail Login');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_webmail_sso_shows_reset_required_when_password_missing(): void
|
||||
{
|
||||
|
||||
@@ -5,14 +5,19 @@ declare(strict_types=1);
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Jobs\RunCpanelRestore;
|
||||
use App\Models\User;
|
||||
use App\Services\Agent\AgentClient;
|
||||
use App\Services\Migration\MigrationDnsSyncService;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RunCpanelRestoreTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_marks_restore_completed_and_logs_success(): void
|
||||
{
|
||||
$jobId = 'cpanel-restore-success';
|
||||
@@ -21,6 +26,10 @@ class RunCpanelRestoreTest extends TestCase
|
||||
File::ensureDirectoryExists(dirname($logPath));
|
||||
File::put($logPath, '');
|
||||
|
||||
User::factory()->create([
|
||||
'username' => 'example',
|
||||
]);
|
||||
|
||||
$this->app->instance(AgentClient::class, new class extends AgentClient
|
||||
{
|
||||
public function send(string $action, array $params = []): array
|
||||
@@ -41,7 +50,10 @@ class RunCpanelRestoreTest extends TestCase
|
||||
discoveredData: null,
|
||||
);
|
||||
|
||||
$job->handle($this->app->make(AgentClient::class));
|
||||
$job->handle(
|
||||
$this->app->make(AgentClient::class),
|
||||
$this->app->make(MigrationDnsSyncService::class),
|
||||
);
|
||||
|
||||
$status = Cache::get('cpanel_restore_status_'.$jobId);
|
||||
$this->assertSame('completed', $status['status'] ?? null);
|
||||
@@ -61,6 +73,10 @@ class RunCpanelRestoreTest extends TestCase
|
||||
File::ensureDirectoryExists(dirname($logPath));
|
||||
File::put($logPath, '');
|
||||
|
||||
User::factory()->create([
|
||||
'username' => 'example',
|
||||
]);
|
||||
|
||||
$this->app->instance(AgentClient::class, new class extends AgentClient
|
||||
{
|
||||
public function send(string $action, array $params = []): array
|
||||
@@ -81,7 +97,10 @@ class RunCpanelRestoreTest extends TestCase
|
||||
discoveredData: null,
|
||||
);
|
||||
|
||||
$job->handle($this->app->make(AgentClient::class));
|
||||
$job->handle(
|
||||
$this->app->make(AgentClient::class),
|
||||
$this->app->make(MigrationDnsSyncService::class),
|
||||
);
|
||||
|
||||
$status = Cache::get('cpanel_restore_status_'.$jobId);
|
||||
$this->assertSame('failed', $status['status'] ?? null);
|
||||
|
||||
@@ -14,7 +14,7 @@ class VersionFileTest extends TestCase
|
||||
$content = file_get_contents($versionPath);
|
||||
|
||||
$this->assertNotFalse($content);
|
||||
$this->assertStringContainsString('VERSION=0.9-rc2', $content);
|
||||
$this->assertMatchesRegularExpression('/^VERSION=0\\.9-rc\\d*$/m', trim($content));
|
||||
$this->assertStringNotContainsString('BUILD=', $content);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user