Merge branch 'main' into vehikl/singleton

This commit is contained in:
Vehikl 2025-06-05 16:04:07 -04:00
commit 2ba50e616c
29 changed files with 527 additions and 485 deletions

View File

@ -24,6 +24,7 @@ class MakeNodeCommand extends Command
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).} {--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.} {--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.} {--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.} {--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.} {--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}'; {--daemonBase= : Enter the base folder.}';
@ -57,6 +58,7 @@ class MakeNodeCommand extends Command
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1'); $data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256'); $data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080'); $data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022'); $data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), ''); $data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes'); $data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');

View File

@ -7,7 +7,6 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Webhook; use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
@ -44,8 +43,6 @@ class Kernel extends ConsoleKernel
$schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly(); $schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
if (config('backups.prune_age')) { if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes(); $schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();

View File

@ -635,7 +635,6 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state)) ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))), ->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
@ -646,7 +645,6 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state)) ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))), ->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),

View File

@ -124,15 +124,10 @@ class CreateNode extends CreateRecord
'lg' => 1, 'lg' => 1,
]), ]),
TextInput::make('daemon_listen') TextInput::make('daemon_connect')
->columnSpan([ ->columnSpan(1)
'default' => 1, ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
'sm' => 1, ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
'md' => 1,
'lg' => 1,
])
->label(trans('admin/node.port'))
->helperText(trans('admin/node.port_help'))
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(8080) ->default(8080)
@ -193,7 +188,21 @@ class CreateNode extends CreateRecord
->afterStateUpdated(function ($state, Set $set) { ->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https'); $set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy'); $set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}), }),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Step::make('advanced') Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))
@ -409,4 +418,13 @@ class CreateNode extends CreateRecord
{ {
return []; return [];
} }
protected function mutateFormDataBeforeCreate(array $data): array
{
if (!$data['behind_proxy']) {
$data['daemon_listen'] = $data['daemon_connect'];
}
return $data;
}
} }

View File

@ -181,10 +181,10 @@ class EditNode extends EditRecord
false => 'danger', false => 'danger',
]) ])
->columnSpan(1), ->columnSpan(1),
TextInput::make('daemon_listen') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(trans('admin/node.port_help')) ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(8080) ->default(8080)
@ -239,7 +239,20 @@ class EditNode extends EditRecord
->afterStateUpdated(function ($state, Set $set) { ->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https'); $set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy'); $set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}), }),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Tab::make('adv') Tab::make('adv')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))
@ -627,6 +640,15 @@ class EditNode extends EditRecord
]; ];
} }
protected function mutateFormDataBeforeSave(array $data): array
{
if (!$data['behind_proxy']) {
$data['daemon_listen'] = $data['daemon_connect'];
}
return $data;
}
protected function afterSave(): void protected function afterSave(): void
{ {
$this->fillForm(); $this->fillForm();

View File

@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\NodeResource\Widgets; namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node; use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,22 +15,34 @@ class NodeCpuChart extends ChartWidget
public Node $node; public Node $node;
/**
* @var array<int, array{cpu: string, timestamp: string}>
*/
protected array $cpuHistory = [];
protected int $threads = 0;
protected function getData(): array protected function getData(): array
{ {
$threads = $this->node->systemInformation()['cpu_count'] ?? 0; $sessionKey = "node_stats.{$this->node->id}";
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent")) $data = $this->node->statistics();
->slice(-10)
->map(fn ($value, $key) => [ $this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0);
'cpu' => round($value * $threads, 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), $this->cpuHistory = session("{$sessionKey}.cpu_history", []);
]) $this->cpuHistory[] = [
->all(); 'cpu' => round($data['cpu_percent'] * $this->threads, 2),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->cpuHistory = array_slice($this->cpuHistory, -60);
session()->put("{$sessionKey}.cpu_history", $this->cpuHistory);
return [ return [
'datasets' => [ 'datasets' => [
[ [
'data' => array_column($cpu, 'cpu'), 'data' => array_column($this->cpuHistory, 'cpu'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -39,7 +50,7 @@ class NodeCpuChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($cpu, 'timestamp'), 'labels' => array_column($this->cpuHistory, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -69,10 +80,10 @@ class NodeCpuChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$threads = $this->node->systemInformation()['cpu_count'] ?? 0; $data = array_slice(end($this->cpuHistory), -60);
$cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language); $cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
$max = Number::format($threads * 100, locale: auth()->user()->language); $max = Number::format($this->threads * 100, locale: auth()->user()->language);
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]); return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
} }

View File

@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\NodeResource\Widgets; namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node; use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,19 +15,36 @@ class NodeMemoryChart extends ChartWidget
public Node $node; public Node $node;
/**
* @var array<int, array{memory: string, timestamp: string}>
*/
protected array $memoryHistory = [];
protected int $totalMemory = 0;
protected function getData(): array protected function getData(): array
{ {
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10) $sessionKey = "node_stats.{$this->node->id}";
->map(fn ($value, $key) => [
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2), $data = $this->node->statistics();
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
]) $this->totalMemory = session("{$sessionKey}.total_memory", $data['memory_total']);
->all();
$this->memoryHistory = session("{$sessionKey}.memory_history", []);
$this->memoryHistory[] = [
'memory' => round(config('panel.use_binary_prefix')
? $data['memory_used'] / 1024 / 1024 / 1024
: $data['memory_used'] / 1000 / 1000 / 1000, 2),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->memoryHistory = array_slice($this->memoryHistory, -60);
session()->put("{$sessionKey}.memory_history", $this->memoryHistory);
return [ return [
'datasets' => [ 'datasets' => [
[ [
'data' => array_column($memUsed, 'memory'), 'data' => array_column($this->memoryHistory, 'memory'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -36,7 +52,7 @@ class NodeMemoryChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($memUsed, 'timestamp'), 'labels' => array_column($this->memoryHistory, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -66,16 +82,15 @@ class NodeMemoryChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last(); $latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
$totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last();
$used = config('panel.use_binary_prefix') $used = config('panel.use_binary_prefix')
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
$total = config('panel.use_binary_prefix') $total = config('panel.use_binary_prefix')
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]); return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
} }

View File

@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column;
class ServerEntryColumn extends Column class ServerEntryColumn extends Column
{ {
protected string $view = 'tables.columns.server-entry-column'; protected string $view = 'livewire.columns.server-entry-column';
} }

View File

@ -77,7 +77,7 @@ class AllocationResource extends Resource
Activity::event('server:allocation.delete') Activity::event('server:allocation.delete')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
}), }),
]); ]);

View File

@ -32,7 +32,7 @@ class ListAllocations extends ListRecords
Activity::event('server:allocation.create') Activity::event('server:allocation.create')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
}), }),
]; ];

View File

@ -314,9 +314,9 @@ class ListFiles extends ListRecords
->label('') ->label('')
->icon('tabler-trash') ->icon('tabler-trash')
->requiresConfirmation() ->requiresConfirmation()
->modalDescription(fn (File $file) => $file->name) ->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
->modalHeading('Delete file?')
->action(function (File $file) { ->action(function (File $file) {
$this->deselectAllTableRecords();
$this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]); $this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]);
Activity::event('server:file.delete') Activity::event('server:file.delete')

View File

@ -62,7 +62,7 @@ class NetworkAllocationController extends ClientApiController
if ($original !== $allocation->notes) { if ($original !== $allocation->notes) {
Activity::event('server:allocation.notes') Activity::event('server:allocation.notes')
->subject($allocation) ->subject($allocation)
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes]) ->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes])
->log(); ->log();
} }
@ -87,7 +87,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.primary') Activity::event('server:allocation.primary')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
return $this->fractal->item($allocation) return $this->fractal->item($allocation)
@ -114,7 +114,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.create') Activity::event('server:allocation.create')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
return $this->fractal->item($allocation) return $this->fractal->item($allocation)
@ -148,7 +148,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.delete') Activity::event('server:allocation.delete')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);

View File

@ -1,35 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Node;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NodeStatistics implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
foreach (Node::all() as $node) {
$stats = $node->statistics();
$timestamp = now()->getTimestamp();
foreach ($stats as $key => $value) {
$cacheKey = "nodes.{$node->id}.$key";
$data = cache()->get($cacheKey, []);
// Add current timestamp and value to the data array
$data[$timestamp] = $value;
// Update the cache with the new data, expires in 1 minute
cache()->put($cacheKey, $data, now()->addMinute());
}
}
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Livewire;
use App\Models\Server;
use Illuminate\View\View;
use Livewire\Component;
class ServerEntry extends Component
{
public Server $server;
public function render(): View
{
return view('livewire.server-entry');
}
public function placeholder(): string
{
return <<<'HTML'
<div class="relative">
<div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: #D97706;">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex items-center mb-5 gap-2">
<x-filament::loading-indicator class="h-5 w-5" />
<h2 class="text-xl font-bold">
{{ $server->name }}
</h2>
</div>
<div class="flex justify-between text-center">
<div>
<p class="text-sm dark:text-gray-400">CPU</p>
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Memory</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Disk</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
</div>
</div>
</div>
</div>
HTML;
}
}

View File

@ -121,11 +121,6 @@ class Allocation extends Model
); );
} }
public function toString(): string
{
return $this->address;
}
/** /**
* Gets information for the server associated with this allocation. * Gets information for the server associated with this allocation.
*/ */

View File

@ -38,6 +38,7 @@ use Symfony\Component\Yaml\Yaml;
* @property string $daemon_token_id * @property string $daemon_token_id
* @property string $daemon_token * @property string $daemon_token
* @property int $daemon_listen * @property int $daemon_listen
* @property int $daemon_connect
* @property int $daemon_sftp * @property int $daemon_sftp
* @property string|null $daemon_sftp_alias * @property string|null $daemon_sftp_alias
* @property string $daemon_base * @property string $daemon_base
@ -83,7 +84,7 @@ class Node extends Model implements Validatable
'memory', 'memory_overallocate', 'disk', 'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'cpu', 'cpu_overallocate', 'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base', 'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen', 'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen', 'daemon_connect',
'description', 'maintenance_mode', 'tags', 'description', 'maintenance_mode', 'tags',
]; ];
@ -105,6 +106,7 @@ class Node extends Model implements Validatable
'daemon_sftp' => ['required', 'numeric', 'between:1,65535'], 'daemon_sftp' => ['required', 'numeric', 'between:1,65535'],
'daemon_sftp_alias' => ['nullable', 'string'], 'daemon_sftp_alias' => ['nullable', 'string'],
'daemon_listen' => ['required', 'numeric', 'between:1,65535'], 'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
'maintenance_mode' => ['boolean'], 'maintenance_mode' => ['boolean'],
'upload_size' => ['int', 'between:1,1024'], 'upload_size' => ['int', 'between:1,1024'],
'tags' => ['array'], 'tags' => ['array'],
@ -125,6 +127,7 @@ class Node extends Model implements Validatable
'daemon_base' => '/var/lib/pelican/volumes', 'daemon_base' => '/var/lib/pelican/volumes',
'daemon_sftp' => 2022, 'daemon_sftp' => 2022,
'daemon_listen' => 8080, 'daemon_listen' => 8080,
'daemon_connect' => 8080,
'maintenance_mode' => false, 'maintenance_mode' => false,
'tags' => '[]', 'tags' => '[]',
]; ];
@ -136,6 +139,7 @@ class Node extends Model implements Validatable
'disk' => 'integer', 'disk' => 'integer',
'cpu' => 'integer', 'cpu' => 'integer',
'daemon_listen' => 'integer', 'daemon_listen' => 'integer',
'daemon_connect' => 'integer',
'daemon_sftp' => 'integer', 'daemon_sftp' => 'integer',
'daemon_token' => 'encrypted', 'daemon_token' => 'encrypted',
'behind_proxy' => 'boolean', 'behind_proxy' => 'boolean',
@ -171,7 +175,7 @@ class Node extends Model implements Validatable
*/ */
public function getConnectionAddress(): string public function getConnectionAddress(): string
{ {
return "$this->scheme://$this->fqdn:$this->daemon_listen"; return "$this->scheme://$this->fqdn:$this->daemon_connect";
} }
/** /**

View File

@ -19,7 +19,7 @@ class DaemonServerRepository extends DaemonRepository
public function getDetails(): array public function getDetails(): array
{ {
try { try {
return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}")->throw()->json(); return $this->getHttpClient()->connectTimeout(1)->timeout(1)->get("/api/servers/{$this->server->uuid}")->throw()->json();
} catch (RequestException $exception) { } catch (RequestException $exception) {
$cfId = $exception->response->header('Cf-Ray'); $cfId = $exception->response->header('Cf-Ray');
$cfCache = $exception->response->header('Cf-Cache-Status'); $cfCache = $exception->response->header('Cf-Cache-Status');

View File

@ -47,7 +47,7 @@ class TransferServerService
// Check if the node is viable for the transfer. // Check if the node is viable for the transfer.
$node = Node::query() $node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate']) ->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_connect', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
->withSum('servers', 'disk') ->withSum('servers', 'disk')
->withSum('servers', 'memory') ->withSum('servers', 'memory')
->withSum('servers', 'cpu') ->withSum('servers', 'cpu')

View File

@ -2,54 +2,20 @@
namespace App\Traits; namespace App\Traits;
use Exception; use Illuminate\Support\Env;
use RuntimeException;
trait EnvironmentWriterTrait trait EnvironmentWriterTrait
{ {
/**
* Escapes an environment value by looking for any characters that could
* reasonably cause environment parsing issues. Those values are then wrapped
* in quotes before being returned.
*/
public function escapeEnvironmentValue(string $value): string
{
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
return sprintf('"%s"', addcslashes($value, '\\"'));
}
return $value;
}
/** /**
* Update the .env file for the application using the passed in values. * Update the .env file for the application using the passed in values.
* *
* @param array<string, mixed> $values * @param array<string, mixed> $values
* *
* @throws Exception * @throws RuntimeException
*/ */
public function writeToEnvironment(array $values = []): void public function writeToEnvironment(array $values = []): void
{ {
$path = base_path('.env'); Env::writeVariables($values, base_path('.env'), true);
if (!file_exists($path)) {
throw new Exception('Cannot locate .env file, was this software installed correctly?');
}
$saveContents = file_get_contents($path);
if ($saveContents === false) {
$saveContents = '';
}
collect($values)->each(function ($value, $key) use (&$saveContents) {
$key = strtoupper($key);
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value ?? ''));
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
$saveContents = $saveContents . PHP_EOL . $saveValue;
} else {
$saveContents = preg_replace('/^' . $key . '=(.*)$/m', $saveValue, $saveContents);
}
});
file_put_contents($path, $saveContents);
} }
} }

View File

@ -18,10 +18,10 @@
"doctrine/dbal": "~3.6.0", "doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3", "filament/filament": "^3.3",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.16", "laravel/framework": "^12.17",
"laravel/helpers": "^1.7", "laravel/helpers": "^1.7",
"laravel/sanctum": "^4.1", "laravel/sanctum": "^4.1",
"laravel/socialite": "^5.20", "laravel/socialite": "^5.21",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6", "laravel/ui": "^4.6",
"lcobucci/jwt": "~4.3.0", "lcobucci/jwt": "~4.3.0",
@ -38,7 +38,7 @@
"spatie/laravel-data": "^4.15", "spatie/laravel-data": "^4.15",
"spatie/laravel-fractal": "^6.3", "spatie/laravel-fractal": "^6.3",
"spatie/laravel-health": "^1.34", "spatie/laravel-health": "^1.34",
"spatie/laravel-permission": "^6.18", "spatie/laravel-permission": "^6.19",
"spatie/laravel-query-builder": "^6.3", "spatie/laravel-query-builder": "^6.3",
"spatie/temporary-directory": "^2.3", "spatie/temporary-directory": "^2.3",
"symfony/http-client": "^7.2", "symfony/http-client": "^7.2",

480
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,7 @@ class NodeFactory extends Factory
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH), 'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH), 'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
'daemon_listen' => 8080, 'daemon_listen' => 8080,
'daemon_connect' => 8080,
'daemon_sftp' => 2022, 'daemon_sftp' => 2022,
'daemon_base' => '/var/lib/panel/volumes', 'daemon_base' => '/var/lib/panel/volumes',
'maintenance_mode' => false, 'maintenance_mode' => false,

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->smallInteger('daemon_connect')->unsigned()->default(8080);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('daemon_connect');
});
}
};

View File

@ -45,6 +45,10 @@ return [
'port' => 'Port', 'port' => 'Port',
'ports' => 'Ports', 'ports' => 'Ports',
'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.', 'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.',
'connect_port' => 'Connection Port',
'connect_port_help' => 'Connections to wings will use this port. If you are using a reverse proxy this can differ from the listen port. When using Cloudflare proxy you should use 8443.',
'listen_port' => 'Listening Port',
'listen_port_help' => 'Wings will listen on this port.',
'display_name' => 'Display Name', 'display_name' => 'Display Name',
'ssl' => 'Communicate over SSL', 'ssl' => 'Communicate over SSL',
'panel_on_ssl' => 'Your Panel is using a secure SSL connection,<br>so your Daemon must too.', 'panel_on_ssl' => 'Your Panel is using a secure SSL connection,<br>so your Daemon must too.',

View File

@ -36,6 +36,7 @@ return [
'cpu_overallocate' => 'Enter the amount of cpu to over allocate by, -1 will disable checking and 0 will prevent creating new server', 'cpu_overallocate' => 'Enter the amount of cpu to over allocate by, -1 will disable checking and 0 will prevent creating new server',
'upload_size' => "'Enter the maximum filesize upload", 'upload_size' => "'Enter the maximum filesize upload",
'daemonListen' => 'Enter the daemon listening port', 'daemonListen' => 'Enter the daemon listening port',
'daemonConnect' => 'Enter the daemon connecting port (can be same as listen port)',
'daemonSFTP' => 'Enter the daemon SFTP listening port', 'daemonSFTP' => 'Enter the daemon SFTP listening port',
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)', 'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
'daemonBase' => 'Enter the base folder', 'daemonBase' => 'Enter the base folder',

View File

@ -0,0 +1,8 @@
@php
/** @var \App\Models\Server $server */
$server = $getRecord();
@endphp
<div class="w-full">
@livewire('server-entry', ['server' => $server, 'lazy' => true], key($server->id))
</div>

View File

@ -0,0 +1,47 @@
<div class="relative">
<div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: {{ $server->condition->getColor(true) }};">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex items-center mb-5 gap-2">
<x-filament::icon-button
:icon="$server->condition->getIcon()"
:color="$server->condition->getColor()"
:tooltip="$server->condition->getLabel()"
size="xl"
/>
<h2 class="text-xl font-bold">
{{ $server->name }}
<span class="dark:text-gray-400">({{ $server->formatResource('uptime', type: \App\Enums\ServerResourceType::Time) }})</span>
</h2>
</div>
<div class="flex justify-between text-center">
<div>
<p class="text-sm dark:text-gray-400">CPU</p>
<p class="text-md font-semibold">{{ $server->formatResource('cpu_absolute', type: \App\Enums\ServerResourceType::Percentage) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Memory</p>
<p class="text-md font-semibold">{{ $server->formatResource('memory_bytes') }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Disk</p>
<p class="text-md font-semibold">{{ $server->formatResource('disk_bytes') }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
</div>
</div>
</div>
</div>

View File

@ -1,63 +0,0 @@
@php
use App\Enums\ServerResourceType;
/** @var \App\Models\Server $server */
$server = $getRecord();
@endphp
<head>
<style>
hr {
border-color: #9ca3af;
}
</style>
</head>
<div class="w-full">
<div class="relative">
<div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: {{ $server->condition->getColor(true) }};">
</div>
<div class="flex-1 bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex items-center mb-5 gap-2">
<x-filament::icon-button
:icon="$server->condition->getIcon()"
:color="$server->condition->getColor()"
:tooltip="$server->condition->getLabel()"
size="xl"
/>
<h2 class="text-xl font-bold">
{{ $server->name }}
<span class="dark:text-gray-400">({{ $server->formatResource('uptime', type: ServerResourceType::Time) }})</span>
</h2>
</div>
<div class="flex justify-between text-center">
<div>
<p class="text-sm dark:text-gray-400">CPU</p>
<p class="text-md font-semibold">{{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: ServerResourceType::Percentage, limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Memory</p>
<p class="text-md font-semibold">{{ $server->formatResource('memory_bytes') }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Disk</p>
<p class="text-md font-semibold">{{ $server->formatResource('disk_bytes') }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,43 +0,0 @@
<?php
namespace App\Tests\Unit\Helpers;
use App\Tests\TestCase;
use App\Traits\EnvironmentWriterTrait;
use PHPUnit\Framework\Attributes\DataProvider;
class EnvironmentWriterTraitTest extends TestCase
{
#[DataProvider('variableDataProvider')]
public function test_variable_is_escaped_properly($input, $expected): void
{
$output = (new FooClass())->escapeEnvironmentValue($input);
$this->assertSame($expected, $output);
}
public static function variableDataProvider(): array
{
return [
['foo', 'foo'],
['abc123', 'abc123'],
['val"ue', '"val\"ue"'],
['val\'ue', '"val\'ue"'],
['my test value', '"my test value"'],
['mysql_p@assword', '"mysql_p@assword"'],
['mysql_p#assword', '"mysql_p#assword"'],
['mysql p@$$word', '"mysql p@$$word"'],
['mysql p%word', '"mysql p%word"'],
['mysql p#word', '"mysql p#word"'],
['abc_@#test', '"abc_@#test"'],
['test 123 $$$', '"test 123 $$$"'],
['#password%', '"#password%"'],
['$pass ', '"$pass "'],
];
}
}
class FooClass
{
use EnvironmentWriterTrait;
}