Merge remote-tracking branch 'JoanFo1456/main' into charles/drag&drop

This commit is contained in:
notCharles 2025-11-08 17:53:54 -05:00
commit 993f3004f2
23 changed files with 517 additions and 58 deletions

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ yarn-error.log
/.idea
/.nova
/.vscode
/.ddev
public/assets/manifest.json
/database/*.sqlite*

View File

@ -4,7 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
@ -14,6 +14,8 @@ use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
@ -25,6 +27,7 @@ use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
@ -33,7 +36,10 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Filament\Support\RawJs;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
use Phiki\Grammar\Grammar;
use Throwable;
@ -45,13 +51,13 @@ class EditNode extends EditRecord
protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository;
private DaemonSystemRepository $daemonSystemRepository;
private NodeUpdateService $nodeUpdateService;
public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void
{
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
$this->daemonSystemRepository = $daemonSystemRepository;
$this->nodeUpdateService = $nodeUpdateService;
}
@ -624,6 +630,154 @@ class EditNode extends EditRecord
])->fullWidth(),
]),
]),
Tab::make('diagnostics')
->label(trans('admin/node.tabs.diagnostics'))
->icon('tabler-heart-search')
->schema([
Section::make('diag')
->heading(trans('admin/node.tabs.diagnostics'))
->columnSpanFull()
->columns(4)
->disabled(fn (Get $get) => $get('pulled'))
->headerActions([
Action::make('pull')
->label(trans('admin/node.diagnostics.pull'))
->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge)
->hidden(fn (Get $get) => $get('pulled'))
->action(function (Get $get, Set $set, Node $node) {
$includeEndpoints = $get('include_endpoints') ?? true;
$includeLogs = $get('include_logs') ?? true;
$logLines = $get('log_lines') ?? 200;
try {
$response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs);
if ($response->status() === 404) {
Notification::make()
->title(trans('admin/node.diagnostics.404'))
->warning()
->send();
return;
}
$set('pulled', true);
$set('uploaded', false);
$set('log', $response->body());
Notification::make()
->title(trans('admin/node.diagnostics.logs_pulled'))
->success()
->send();
} catch (ConnectionException $e) {
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('upload')
->label(trans('admin/node.diagnostics.upload'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->action(function (Get $get, Set $set) {
try {
$response = Http::asMultipart()->post('https://logs.pelican.dev', [
[
'name' => 'c',
'contents' => $get('log'),
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body(fn () => $response->status() . ' - ' . $response->body())
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/node.diagnostics.logs_uploaded'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/node.diagnostics.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
$set('log', $url);
$set('pulled', false);
$set('uploaded', true);
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('clear')
->label(trans('admin/node.diagnostics.clear'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger')
->action(function (Get $get, Set $set) {
$set('pulled', false);
$set('uploaded', false);
$set('log', null);
$this->refresh();
}
),
])
->schema([
ToggleButtons::make('include_endpoints')
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
ToggleButtons::make('include_logs')
->live()
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
Slider::make('log_lines')
->columnSpan(2)
->hiddenLabel()
->live()
->tooltips(RawJs::make(<<<'JS'
`${$value} lines`
JS))
->visible(fn (Get $get) => $get('include_logs'))
->range(minValue: 100, maxValue: 500)
->pips(PipsMode::Steps, density: 10)
->step(50)
->formatStateUsing(fn () => 200)
->fillTrack(),
Hidden::make('pulled'),
Hidden::make('uploaded'),
]),
Textarea::make('log')
->hiddenLabel()
->columnSpanFull()
->rows(35)
->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)),
]),
]),
]);
}
@ -681,7 +835,7 @@ class EditNode extends EditRecord
try {
if ($changed) {
$this->daemonConfigurationRepository->setNode($node)->update($node);
$this->daemonSystemRepository->setNode($node)->update($node);
}
parent::getSavedNotification()?->send();
} catch (ConnectionException) {

View File

@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Components\Actions\UpdateNodeAllocations;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
@ -80,7 +81,9 @@ class AllocationsRelationManager extends RelationManager
->searchable()
->label(trans('admin/node.table.ip')),
])
->headerActions([
->toolbarActions([
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
Action::make('create new allocation')
->label(trans('admin/node.create_allocation'))
->schema(fn () => [
@ -118,9 +121,8 @@ class AllocationsRelationManager extends RelationManager
->required(),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
])
->groupedBulkActions([
DeleteBulkAction::make()
UpdateNodeAllocations::make()
->nodeRecord($this->getOwnerRecord())
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
]);
}

View File

@ -116,6 +116,14 @@ class CreateServer extends CreateRecord
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(function () {
$lastUsedNode = session()->get('last_utilized_node');
if ($lastUsedNode && user()?->accessibleNodes()->where('id', $lastUsedNode)->exists()) {
$this->node = Node::find($lastUsedNode);
return $this->node?->id;
}
/** @var ?Node $latestNode */
$latestNode = user()?->accessibleNodes()->latest()->first();
$this->node = $latestNode;
@ -829,6 +837,8 @@ class CreateServer extends CreateRecord
$data['allocation_additional'] = collect($allocation_additional)->filter()->all();
}
session()->put('last_utilized_node', $data['node_id']);
try {
return $this->serverCreationService->handle($data);
} catch (Exception $exception) {

View File

@ -60,16 +60,35 @@ class AllocationsRelationManager extends RelationManager
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
->label(trans('admin/server.primary')),
IconColumn::make('is_locked')
->label(trans('admin/server.locked'))
->tooltip(trans('admin/server.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
Action::make('make-primary')
->label(trans('admin/server.make_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
Action::make('lock')
->label(trans('admin/server.lock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->is_locked),
Action::make('unlock')
->label(trans('admin/server.unlock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords())
->visible(fn (Allocation $allocation) => $allocation->is_locked),
DissociateAction::make()
->after(function (Allocation $allocation) {
$allocation->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
$allocation->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
])
->headerActions([
@ -116,13 +135,25 @@ class AllocationsRelationManager extends RelationManager
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->recordSelectSearchColumns(['ip', 'port'])
->label(trans('admin/server.add_allocation'))
->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])),
->after(function (array $data) {
Allocation::whereIn('id', array_values(array_unique($data['recordId'])))->update(['is_locked' => true]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]]);
}
}),
])
->groupedBulkActions([
DissociateBulkAction::make()
->after(function () {
Allocation::whereNull('server_id')->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
Allocation::whereNull('server_id')->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
]);
}

View File

@ -10,10 +10,17 @@ class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker';
protected static ?string $slug = '/';
protected static bool $shouldRegisterNavigation = false;
public static function getNavigationBadge(): ?string
{
return (string) user()?->directAccessibleServers()->where('owner_id', user()?->id)->count();
}
public static function canAccess(): bool
{
return true;
@ -25,4 +32,10 @@ class ServerResource extends Resource
'index' => ListServers::route('/'),
];
}
public static function embedServerList(bool $condition = true): void
{
static::$slug = $condition ? null : '/';
static::$shouldRegisterNavigation = $condition;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Allocation;
use App\Models\Node;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
class UpdateNodeAllocations extends Action
{
public static function getDefaultName(): ?string
{
return 'bulk_update_ip';
}
protected function setUp(): void
{
parent::setUp();
$this->label(trans('admin/node.bulk_update_ip'));
$this->icon('tabler-replace');
$this->color('warning');
$this->requiresConfirmation();
$this->modalHeading(trans('admin/node.bulk_update_ip'));
$this->modalDescription(trans('admin/node.bulk_update_ip_description'));
$this->modalIconColor('warning');
$this->modalSubmitActionLabel(trans('admin/node.update_ip'));
$this->schema(function () {
/** @var Node $node */
$node = $this->record;
$currentIps = Allocation::where('node_id', $node->id)
->pluck('ip')
->unique()
->values()
->all();
return [
Select::make('old_ip')
->label(trans('admin/node.old_ip'))
->options(array_combine($currentIps, $currentIps))
->selectablePlaceholder(false)
->required()
->live(),
Select::make('new_ip')
->label(trans('admin/node.new_ip'))
->options(fn () => array_combine($node->ipAddresses(), $node->ipAddresses()) ?: [])
->required()
->different('old_ip'),
];
});
$this->action(function (array $data) {
/** @var Node $node */
$node = $this->record;
$allocations = Allocation::where('node_id', $node->id)->where('ip', $data['old_ip'])->get();
if ($allocations->count() === 0) {
Notification::make()
->title(trans('admin/node.no_allocations_to_update'))
->warning()
->send();
return;
}
$updated = 0;
$failed = 0;
foreach ($allocations as $allocation) {
try {
$allocation->update(['ip' => $data['new_ip']]);
$updated++;
} catch (Exception $exception) {
$failed++;
report($exception);
}
}
Notification::make()
->title(trans('admin/node.ip_updated', ['count' => $updated, 'total' => $allocations->count()]))
->body($failed > 0 ? trans('admin/node.ip_update_failed', ['count' => $failed]) : null)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();
});
}
public function nodeRecord(Node $node): static
{
$this->record = $node;
return $this;
}
}

View File

@ -73,15 +73,22 @@ class AllocationResource extends Resource
->action(fn (Allocation $allocation) => user()?->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label(trans('server/network.primary')),
IconColumn::make('is_locked')
->label(trans('server/network.locked'))
->tooltip(trans('server/network.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
DetachAction::make()
->visible(fn (Allocation $allocation) => !$allocation->is_locked || user()?->can('update', $allocation->node))
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label(trans('server/network.delete'))
->icon('tabler-trash')
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
Allocation::where('id', $allocation->id)->update([
'notes' => null,
'is_locked' => false,
'server_id' => null,
]);
@ -93,7 +100,7 @@ class AllocationResource extends Resource
->after(fn (Allocation $allocation) => $allocation->id === $server->allocation_id && $server->update(['allocation_id' => $server->allocations()->first()?->id])),
])
->toolbarActions([
Action::make('addAllocation')
Action::make('add_allocation')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network')
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_CREATE, $server))

View File

@ -7,11 +7,13 @@ use App\Exceptions\Repository\FileNotEditableException;
use App\Facades\Activity;
use App\Filament\Server\Resources\Files\FileResource;
use App\Livewire\AlertBanner;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\CodeEditor;
@ -215,15 +217,17 @@ class EditFiles extends Page
$this->previousUrl = url()->previous();
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
->title(trans('server/file.alerts.pelicanignore.title'))
->body(trans('server/file.alerts.pelicanignore.body'))
foreach (File::getSpecialFiles() as $fileName => $data) {
if ($data['check'] instanceof Closure && $data['check']($path)) {
AlertBanner::make($fileName . '_info')
->title($data['title'])
->body($data['body'])
->info()
->closable()
->send();
}
}
}
protected function authorizeAccess(): void
{

View File

@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $address
* @property Server|null $server
* @property Node $node
* @property bool $is_locked
*
* @method static AllocationFactory factory(...$parameters)
* @method static Builder|Allocation newModelQuery()
@ -55,6 +56,10 @@ class Allocation extends Model
*/
public const RESOURCE_NAME = 'allocation';
protected $attributes = [
'is_locked' => false,
];
/**
* Fields that are not mass assignable.
*/
@ -68,10 +73,17 @@ class Allocation extends Model
'ip_alias' => ['nullable', 'string'],
'server_id' => ['nullable', 'exists:servers,id'],
'notes' => ['nullable', 'string', 'max:256'],
'is_locked' => ['boolean'],
];
protected static function booted(): void
{
static::updating(function (self $allocation) {
if (is_null($allocation->server_id)) {
$allocation->is_locked = false;
}
});
static::deleting(function (self $allocation) {
throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using')));
});
@ -83,6 +95,7 @@ class Allocation extends Model
'node_id' => 'integer',
'port' => 'integer',
'server_id' => 'integer',
'is_locked' => 'bool',
];
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Livewire\AlertBanner;
use App\Repositories\Daemon\DaemonFileRepository;
use Carbon\Carbon;
use Closure;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
@ -54,6 +55,32 @@ class File extends Model
protected static ?string $searchTerm;
/** @var array<string, array<string, string|Closure|null>> */
protected static array $customSpecialFiles = [];
public static function registerSpecialFile(string $fileName, string|Closure $bannerTitle, string|Closure|null $bannerBody = null, ?Closure $nameCheck = null): void
{
static::$customSpecialFiles[$fileName] = [
'title' => $bannerTitle,
'body' => $bannerBody,
'check' => $nameCheck ?? fn (string $path) => str($path)->endsWith($fileName),
];
}
/** @return array<string, array<string, string|Closure|null>> */
public static function getSpecialFiles(): array
{
$specialFiles = [
'.pelicanignore' => [
'title' => fn () => trans('server/file.alerts.pelicanignore.title'),
'body' => fn () => trans('server/file.alerts.pelicanignore.body'),
'check' => fn (string $path) => str($path)->endsWith('.pelicanignore'),
],
];
return array_merge($specialFiles, static::$customSpecialFiles);
}
public static function get(Server $server, string $path = '/', ?string $searchTerm = null): Builder
{
self::$server = $server;

View File

@ -4,7 +4,7 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\Service\HasActiveServersException;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use App\Traits\HasValidation;
use Carbon\Carbon;
use Exception;
@ -316,7 +316,7 @@ class Node extends Model implements Validatable
{
return once(function () {
try {
return (new DaemonConfigurationRepository())
return (new DaemonSystemRepository())
->setNode($this)
->getSystemInformation();
} catch (Exception $exception) {

View File

@ -287,7 +287,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
->where(function (Builder $builder) {
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
});
})
->distinct('servers.id');
}
public function accessibleNodes(): Builder

View File

@ -6,7 +6,7 @@ use App\Models\Node;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
class DaemonConfigurationRepository extends DaemonRepository
class DaemonSystemRepository extends DaemonRepository
{
/**
* Returns system information from the daemon instance.
@ -30,6 +30,23 @@ class DaemonConfigurationRepository extends DaemonRepository
})->json();
}
/**
* Retrieve diagnostics from the daemon for the current node.
*
*
* @throws ConnectionException
*/
public function getDiagnostics(int $lines, bool $includeEndpoints, bool $includeLogs): Response
{
return $this->getHttpClient()
->timeout(5)
->get('/api/diagnostics', [
'log_lines' => $lines,
'include_endpoints' => $includeEndpoints ? 'true' : 'false',
'include_logs' => $includeLogs ? 'true' : 'false',
]);
}
/**
* Updates the configuration information for a daemon. Updates the information for
* this instance using a passed-in model. This allows us to change plenty of information

View File

@ -4,7 +4,7 @@ namespace App\Services\Nodes;
use App\Exceptions\Service\Node\ConfigurationNotPersistedException;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Str;
@ -17,7 +17,7 @@ class NodeUpdateService
*/
public function __construct(
private ConnectionInterface $connection,
private DaemonConfigurationRepository $configurationRepository,
private DaemonSystemRepository $configurationRepository,
) {}
/**

View File

@ -191,6 +191,7 @@ class ServerCreationService
->get()
->each(function (Allocation $allocation) use ($server) {
$allocation->server_id = $server->id;
$allocation->is_locked = true;
$allocation->save();
});
}

View File

@ -15,7 +15,7 @@
"dedoc/scramble": "^0.12.10",
"filament/filament": "~4.0",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.31",
"laravel/framework": "^12.37",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.2",
"laravel/socialite": "^5.23",

53
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "101c2afb1f31acb872b4bed541397cd2",
"content-hash": "c8143eccd2736bd88b35d8fe6c8de289",
"packages": [
{
"name": "achyutn/filament-log-viewer",
@ -209,16 +209,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.359.3",
"version": "3.359.4",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "a32e4c9522f0b61c947fafa1713d3a24b397a757"
"reference": "510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a32e4c9522f0b61c947fafa1713d3a24b397a757",
"reference": "a32e4c9522f0b61c947fafa1713d3a24b397a757",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30",
"reference": "510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30",
"shasum": ""
},
"require": {
@ -300,9 +300,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.359.3"
"source": "https://github.com/aws/aws-sdk-php/tree/3.359.4"
},
"time": "2025-10-31T18:15:22+00:00"
"time": "2025-11-03T19:18:23+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@ -2561,16 +2561,16 @@
},
{
"name": "laravel/framework",
"version": "v12.36.1",
"version": "v12.37.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "cad110d7685fbab990a6bb8184d0cfd847d7c4d8"
"reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/cad110d7685fbab990a6bb8184d0cfd847d7c4d8",
"reference": "cad110d7685fbab990a6bb8184d0cfd847d7c4d8",
"url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
"reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125",
"shasum": ""
},
"require": {
@ -2776,7 +2776,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-10-29T14:20:57+00:00"
"time": "2025-11-04T15:39:33+00:00"
},
{
"name": "laravel/helpers",
@ -7757,16 +7757,16 @@
},
{
"name": "spatie/laravel-permission",
"version": "6.22.0",
"version": "6.23.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "8c87966ddc21893bfda54b792047473703992625"
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/8c87966ddc21893bfda54b792047473703992625",
"reference": "8c87966ddc21893bfda54b792047473703992625",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"shasum": ""
},
"require": {
@ -7828,7 +7828,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.22.0"
"source": "https://github.com/spatie/laravel-permission/tree/6.23.0"
},
"funding": [
{
@ -7836,7 +7836,7 @@
"type": "github"
}
],
"time": "2025-10-27T21:58:45+00:00"
"time": "2025-11-03T20:16:13+00:00"
},
{
"name": "spatie/laravel-query-builder",
@ -12203,16 +12203,16 @@
},
{
"name": "larastan/larastan",
"version": "v3.7.2",
"version": "v3.8.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae"
"reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
"reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
"url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
"reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
"shasum": ""
},
"require": {
@ -12226,7 +12226,7 @@
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
"phpstan/phpstan": "^2.1.28"
"phpstan/phpstan": "^2.1.29"
},
"require-dev": {
"doctrine/coding-standard": "^13",
@ -12239,7 +12239,8 @@
"phpunit/phpunit": "^10.5.35 || ^11.5.15"
},
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench"
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench",
"phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically"
},
"type": "phpstan-extension",
"extra": {
@ -12280,7 +12281,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.7.2"
"source": "https://github.com/larastan/larastan/tree/v3.8.0"
},
"funding": [
{
@ -12288,7 +12289,7 @@
"type": "github"
}
],
"time": "2025-09-19T09:03:05+00:00"
"time": "2025-10-27T23:09:14+00:00"
},
{
"name": "laravel/pail",

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('allocations', function (Blueprint $table) {
$table->boolean('is_locked')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropColumn('is_locked');
});
}
};

View File

@ -10,6 +10,7 @@ return [
'basic_settings' => 'Basic Settings',
'advanced_settings' => 'Advanced Settings',
'config_file' => 'Configuration File',
'diagnostics' => 'Diagnostics',
],
'table' => [
'health' => 'Health',
@ -43,7 +44,7 @@ return [
'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!',
'fqdn_help' => 'Your panel is currently secured via an SSL certificate and that means your nodes require one too. You must use a domain name, because you cannot get SSL certificates for IP Addresses.',
'dns' => 'DNS Record Check',
'dns_help' => 'This lets you know if you DNS record is pointing to the correct IP address.',
'dns_help' => 'This lets you know if your DNS record is pointing to the correct IP address.',
'valid' => 'Valid',
'invalid' => 'Invalid',
'port' => 'Port',
@ -117,8 +118,35 @@ return [
'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.',
'allocation' => 'Allocation',
'diagnostics' => [
'header' => 'Node Diagnostics',
'include_endpoints' => 'Include Endpoints',
'include_endpoints_hint' => 'Including endpoints will show panel urls within the logs and NOT obscure them.',
'include_logs' => 'Include Logs',
'include_logs_hint' => 'Including logs will show recent logs and help track down possible issues.',
'run_diagnostics' => 'Run Diagnostics',
'upload_to_pelican' => 'Upload Logs',
'logs_pulled' => 'Logs Pulled!',
'logs_uploaded' => 'Logs Uploaded',
'upload_failed' => 'Logs Upload Failed',
'view_logs' => 'View Logs',
'pull' => 'Pull',
'upload' => 'Upload',
'clear' => 'Clear',
'404' => 'The requested diagnostic report could not be found. Make sure wings is up to date and try again.',
],
'cloudflare_issue' => [
'title' => 'Cloudflare Issue',
'body' => 'Your Node is not accessible by Cloudflare',
],
'bulk_update_ip' => 'Update IPs',
'bulk_update_ip_description' => 'Replace an old IP address with a new one for allocations. This is useful when a node\'s IP address changes',
'update_ip' => 'Update IP',
'old_ip' => 'Old IP Address',
'new_ip' => 'New IP Address',
'no_allocations_to_update' => 'No allocations with the selected old IP address were found',
'ip_updated' => 'Successfully updated :count of :total allocation(s)',
'ip_update_failed' => ':count allocation(s) failed to update',
];

View File

@ -13,6 +13,10 @@ return [
'ports' => 'Ports',
'alias' => 'Alias',
'alias_helper' => 'Optional display name to help you remember what these are.',
'locked' => 'Locked?',
'locked_helper' => 'Users won\'t be able to delete locked allocations',
'lock' => 'Lock',
'unlock' => 'Unlock',
'name' => 'Name',
'external_id' => 'External ID',
'owner' => 'Owner',

View File

@ -12,4 +12,6 @@ return [
'primary' => 'Primary',
'make' => 'Make',
'delete' => 'Delete',
'locked' => 'Locked?',
'locked_helper' => 'Locked allocations can only be deleted by admins',
];

View File

@ -22,7 +22,10 @@
"></div>
@endif
<div class="flex items-center mb-5 gap-2">
<div @class([
'flex items-center gap-2',
'mb-5' => !$server->description,
])>
<x-filament::icon-button
:icon="$server->condition->getIcon()"
:color="$server->condition->getColor()"
@ -45,6 +48,12 @@
@endif
</div>
@if ($server->description)
<div class="text-left mb-1 ml-4 pl-4">
<p class="text-base text-gray-400">{{ Str::limit($server->description, 40, preserveWords: true) }}</p>
</div>
@endif
<div class="flex justify-between text-center items-center gap-4">
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>