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).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--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['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_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_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');

View File

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

View File

@ -635,7 +635,6 @@ class Settings extends Page implements HasForms
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (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'))),
@ -646,7 +645,6 @@ class Settings extends Page implements HasForms
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (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'))),

View File

@ -124,15 +124,10 @@ class CreateNode extends CreateRecord
'lg' => 1,
]),
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label(trans('admin/node.port'))
->helperText(trans('admin/node.port_help'))
TextInput::make('daemon_connect')
->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
@ -193,7 +188,21 @@ class CreateNode extends CreateRecord
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$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')
->label(trans('admin/node.tabs.advanced_settings'))
@ -409,4 +418,13 @@ class CreateNode extends CreateRecord
{
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',
])
->columnSpan(1),
TextInput::make('daemon_listen')
TextInput::make('daemon_connect')
->columnSpan(1)
->label(trans('admin/node.port'))
->helperText(trans('admin/node.port_help'))
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
@ -239,7 +239,20 @@ class EditNode extends EditRecord
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$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')
->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
{
$this->fillForm();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,6 +38,7 @@ use Symfony\Component\Yaml\Yaml;
* @property string $daemon_token_id
* @property string $daemon_token
* @property int $daemon_listen
* @property int $daemon_connect
* @property int $daemon_sftp
* @property string|null $daemon_sftp_alias
* @property string $daemon_base
@ -83,7 +84,7 @@ class Node extends Model implements Validatable
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen', 'daemon_connect',
'description', 'maintenance_mode', 'tags',
];
@ -105,6 +106,7 @@ class Node extends Model implements Validatable
'daemon_sftp' => ['required', 'numeric', 'between:1,65535'],
'daemon_sftp_alias' => ['nullable', 'string'],
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
'maintenance_mode' => ['boolean'],
'upload_size' => ['int', 'between:1,1024'],
'tags' => ['array'],
@ -125,6 +127,7 @@ class Node extends Model implements Validatable
'daemon_base' => '/var/lib/pelican/volumes',
'daemon_sftp' => 2022,
'daemon_listen' => 8080,
'daemon_connect' => 8080,
'maintenance_mode' => false,
'tags' => '[]',
];
@ -136,6 +139,7 @@ class Node extends Model implements Validatable
'disk' => 'integer',
'cpu' => 'integer',
'daemon_listen' => 'integer',
'daemon_connect' => 'integer',
'daemon_sftp' => 'integer',
'daemon_token' => 'encrypted',
'behind_proxy' => 'boolean',
@ -171,7 +175,7 @@ class Node extends Model implements Validatable
*/
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
{
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) {
$cfId = $exception->response->header('Cf-Ray');
$cfCache = $exception->response->header('Cf-Cache-Status');

View File

@ -47,7 +47,7 @@ class TransferServerService
// Check if the node is viable for the transfer.
$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', 'memory')
->withSum('servers', 'cpu')

View File

@ -2,54 +2,20 @@
namespace App\Traits;
use Exception;
use Illuminate\Support\Env;
use RuntimeException;
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.
*
* @param array<string, mixed> $values
*
* @throws Exception
* @throws RuntimeException
*/
public function writeToEnvironment(array $values = []): void
{
$path = base_path('.env');
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);
Env::writeVariables($values, base_path('.env'), true);
}
}

View File

@ -18,10 +18,10 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3",
"guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.16",
"laravel/framework": "^12.17",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.1",
"laravel/socialite": "^5.20",
"laravel/socialite": "^5.21",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"lcobucci/jwt": "~4.3.0",
@ -38,7 +38,7 @@
"spatie/laravel-data": "^4.15",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-health": "^1.34",
"spatie/laravel-permission": "^6.18",
"spatie/laravel-permission": "^6.19",
"spatie/laravel-query-builder": "^6.3",
"spatie/temporary-directory": "^2.3",
"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' => Str::random(Node::DAEMON_TOKEN_LENGTH),
'daemon_listen' => 8080,
'daemon_connect' => 8080,
'daemon_sftp' => 2022,
'daemon_base' => '/var/lib/panel/volumes',
'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',
'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.',
'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',
'ssl' => 'Communicate over SSL',
'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',
'upload_size' => "'Enter the maximum filesize upload",
'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',
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
'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;
}