diff --git a/Dockerfile b/Dockerfile index ccc06a934..418aa44cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin WORKDIR /var/www/html # Install additional required libraries -RUN apk update && apk add --no-cache \ - caddy ca-certificates supervisor supercronic +RUN apk add --no-cache \ + caddy ca-certificates supervisor supercronic fcgi COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public @@ -85,7 +85,8 @@ RUN chown root:www-data ./ \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ # Allow www-data write permissions where necessary && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ - && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord + && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \ + && chown -R www-data: /usr/local/etc/php/ # Configure Supervisor COPY docker/supervisord.conf /etc/supervisord.conf @@ -93,10 +94,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile # Add Laravel scheduler to crontab COPY docker/crontab /etc/supercronic/crontab -COPY docker/entrypoint.sh ./docker/entrypoint.sh +COPY docker/entrypoint.sh /entrypoint.sh +COPY docker/healthcheck.sh /healthcheck.sh HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost/up || exit 1 + CMD /bin/ash /healthcheck.sh EXPOSE 80 443 @@ -104,5 +106,5 @@ VOLUME /pelican-data USER www-data -ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ] +ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] diff --git a/Dockerfile.dev b/Dockerfile.dev index 797576e25..fc9ecb0fd 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -67,8 +67,8 @@ FROM --platform=$TARGETOS/$TARGETARCH base AS final WORKDIR /var/www/html # Install additional required libraries -RUN apk update && apk add --no-cache \ - caddy ca-certificates supervisor supercronic +RUN apk add --no-cache \ + caddy ca-certificates supervisor supercronic fcgi coreutils COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public @@ -89,7 +89,8 @@ RUN chown root:www-data ./ \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ # Allow www-data write permissions where necessary && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ - && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord + && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \ + && chown -R www-data: /usr/local/etc/php/ # Configure Supervisor COPY docker/supervisord.conf /etc/supervisord.conf @@ -97,10 +98,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile # Add Laravel scheduler to crontab COPY docker/crontab /etc/supercronic/crontab -COPY docker/entrypoint.sh ./docker/entrypoint.sh +COPY docker/entrypoint.sh /entrypoint.sh +COPY docker/healthcheck.sh /healthcheck.sh HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost/up || exit 1 + CMD /bin/ash /healthcheck.sh EXPOSE 80 443 @@ -108,5 +110,5 @@ VOLUME /pelican-data USER www-data -ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ] +ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] diff --git a/app/Console/Commands/Egg/CheckEggUpdatesCommand.php b/app/Console/Commands/Egg/CheckEggUpdatesCommand.php index b398f7ca9..e469e28f1 100644 --- a/app/Console/Commands/Egg/CheckEggUpdatesCommand.php +++ b/app/Console/Commands/Egg/CheckEggUpdatesCommand.php @@ -2,10 +2,13 @@ namespace App\Console\Commands\Egg; +use App\Enums\EggFormat; use App\Models\Egg; use App\Services\Eggs\Sharing\EggExporterService; use Exception; use Illuminate\Console\Command; +use JsonException; +use Symfony\Component\Yaml\Yaml; class CheckEggUpdatesCommand extends Command { @@ -23,6 +26,9 @@ class CheckEggUpdatesCommand extends Command } } + /** + * @throws JsonException + */ private function check(Egg $egg, EggExporterService $exporterService): void { if (is_null($egg->update_url)) { @@ -31,22 +37,26 @@ class CheckEggUpdatesCommand extends Command return; } - $currentJson = json_decode($exporterService->handle($egg->id)); - unset($currentJson->exported_at); + $ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION)); + $isYaml = in_array($ext, ['yaml', 'yml']); - $updatedEgg = file_get_contents($egg->update_url); - assert($updatedEgg !== false); - $updatedJson = json_decode($updatedEgg); - unset($updatedJson->exported_at); + $local = $isYaml + ? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML)) + : json_decode($exporterService->handle($egg->id, EggFormat::JSON), true); - if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) { - $this->info("$egg->name: Up-to-date"); - cache()->put("eggs.$egg->uuid.update", false, now()->addHour()); + $remote = file_get_contents($egg->update_url); + assert($remote !== false); - return; - } + $remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true); - $this->warn("$egg->name: Found update"); - cache()->put("eggs.$egg->uuid.update", true, now()->addHour()); + unset($local['exported_at'], $remote['exported_at']); + + $localHash = md5(json_encode($local, JSON_THROW_ON_ERROR)); + $remoteHash = md5(json_encode($remote, JSON_THROW_ON_ERROR)); + + $status = $localHash === $remoteHash ? 'Up-to-date' : 'Found update'; + $this->{($localHash === $remoteHash) ? 'info' : 'warn'}("$egg->name: $status"); + + cache()->put("eggs.$egg->uuid.update", $localHash !== $remoteHash, now()->addHour()); } } diff --git a/app/Enums/BackupStatus.php b/app/Enums/BackupStatus.php index 077fca54f..78f9781fa 100644 --- a/app/Enums/BackupStatus.php +++ b/app/Enums/BackupStatus.php @@ -32,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel public function getLabel(): string { - return str($this->value)->headline(); + return trans('server/backup.backup_status.' . strtolower($this->value)); } } diff --git a/app/Enums/ContainerStatus.php b/app/Enums/ContainerStatus.php index b83a61021..f7ec159c1 100644 --- a/app/Enums/ContainerStatus.php +++ b/app/Enums/ContainerStatus.php @@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel public function getLabel(): string { - return str($this->value)->title(); + return trans('server/console.status.' . strtolower($this->value)); } public function isOffline(): bool diff --git a/app/Enums/EggFormat.php b/app/Enums/EggFormat.php new file mode 100644 index 000000000..3100390d8 --- /dev/null +++ b/app/Enums/EggFormat.php @@ -0,0 +1,9 @@ +isLimit()) { + $resourceAmount = $server->{$this->value} ?? 0; + + if (!$this->isPercentage()) { + // Our limits are entered as MiB/ MB so we need to convert them to bytes + $resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000; + } + + return $resourceAmount; + } + + return $server->retrieveResources()[$this->value] ?? 0; + } + + public function isLimit(): bool + { + return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit; + } + + public function isTime(): bool + { + return $this === ServerResourceType::Uptime; + } + + public function isPercentage(): bool + { + return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit; + } } diff --git a/app/Enums/ServerState.php b/app/Enums/ServerState.php index 9ada541ad..e036bb922 100644 --- a/app/Enums/ServerState.php +++ b/app/Enums/ServerState.php @@ -8,7 +8,6 @@ use Filament\Support\Contracts\HasLabel; enum ServerState: string implements HasColor, HasIcon, HasLabel { - case Normal = 'normal'; case Installing = 'installing'; case InstallFailed = 'install_failed'; case ReinstallFailed = 'reinstall_failed'; @@ -18,7 +17,6 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel public function getIcon(): string { return match ($this) { - self::Normal => 'tabler-heart', self::Installing => 'tabler-heart-bolt', self::InstallFailed => 'tabler-heart-x', self::ReinstallFailed => 'tabler-heart-x', @@ -31,14 +29,13 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel { if ($hex) { return match ($this) { - self::Normal, self::Installing, self::RestoringBackup => '#2563EB', + self::Installing, self::RestoringBackup => '#2563EB', self::Suspended => '#D97706', self::InstallFailed, self::ReinstallFailed => '#EF4444', }; } return match ($this) { - self::Normal => 'primary', self::Installing => 'primary', self::InstallFailed => 'danger', self::ReinstallFailed => 'danger', diff --git a/app/Extensions/OAuth/OAuthSchemaInterface.php b/app/Extensions/OAuth/OAuthSchemaInterface.php index 837705888..46c82a630 100644 --- a/app/Extensions/OAuth/OAuthSchemaInterface.php +++ b/app/Extensions/OAuth/OAuthSchemaInterface.php @@ -32,4 +32,8 @@ interface OAuthSchemaInterface public function getHexColor(): ?string; public function isEnabled(): bool; + + public function shouldCreateMissingUsers(): bool; + + public function shouldLinkMissingUsers(): bool; } diff --git a/app/Extensions/OAuth/Schemas/OAuthSchema.php b/app/Extensions/OAuth/Schemas/OAuthSchema.php index 0f5873d41..94b6f91a4 100644 --- a/app/Extensions/OAuth/Schemas/OAuthSchema.php +++ b/app/Extensions/OAuth/Schemas/OAuthSchema.php @@ -5,7 +5,9 @@ namespace App\Extensions\OAuth\Schemas; use App\Extensions\OAuth\OAuthSchemaInterface; use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Wizard\Step; +use Filament\Forms\Set; use Illuminate\Support\Str; abstract class OAuthSchema implements OAuthSchemaInterface @@ -53,6 +55,28 @@ abstract class OAuthSchema implements OAuthSchemaInterface ->revealable() ->autocomplete(false) ->default(env("OAUTH_{$id}_CLIENT_SECRET")), + Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS") + ->label(trans('admin/setting.oauth.create_missing_users')) + ->columnSpanFull() + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->formatStateUsing(fn ($state) => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state)) + ->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")), + Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS") + ->label(trans('admin/setting.oauth.link_missing_users')) + ->columnSpanFull() + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->formatStateUsing(fn ($state) => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state)) + ->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")), ]; } @@ -96,4 +120,18 @@ abstract class OAuthSchema implements OAuthSchemaInterface return env("OAUTH_{$id}_ENABLED", false); } + + public function shouldCreateMissingUsers(): bool + { + $id = Str::upper($this->getId()); + + return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false); + } + + public function shouldLinkMissingUsers(): bool + { + $id = Str::upper($this->getId()); + + return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false); + } } diff --git a/app/Filament/Admin/Pages/Settings.php b/app/Filament/Admin/Pages/Settings.php index 8ddc63204..9a7ca051d 100644 --- a/app/Filament/Admin/Pages/Settings.php +++ b/app/Filament/Admin/Pages/Settings.php @@ -169,16 +169,6 @@ class Settings extends Page implements HasForms ->formatStateUsing(fn ($state): bool => (bool) $state) ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) ->default(env('APP_DEBUG', config('app.debug'))), - ToggleButtons::make('FILAMENT_TOP_NAVIGATION') - ->label(trans('admin/setting.general.navigation')) - ->inline() - ->options([ - false => trans('admin/setting.general.sidebar'), - true => trans('admin/setting.general.topbar'), - ]) - ->formatStateUsing(fn ($state): bool => (bool) $state) - ->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state)) - ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), ]), Group::make() ->columns(2) diff --git a/app/Filament/Admin/Resources/EggResource.php b/app/Filament/Admin/Resources/EggResource.php index b7743ba2e..6f5eafa76 100644 --- a/app/Filament/Admin/Resources/EggResource.php +++ b/app/Filament/Admin/Resources/EggResource.php @@ -29,7 +29,7 @@ class EggResource extends Resource public static function getNavigationGroup(): ?string { - return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server'); + return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server'); } public static function getNavigationLabel(): string diff --git a/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php index 675a22aa4..2efa6bd95 100644 --- a/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Admin/Resources/EggResource/Pages/CreateEgg.php @@ -207,7 +207,7 @@ class CreateEgg extends CreateRecord '*' => trans('admin/egg.error_reserved'), ]) ->required(), - TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255), + TextInput::make('default_value')->label(trans('admin/egg.default_value')), Fieldset::make(trans('admin/egg.user_permissions')) ->schema([ Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), diff --git a/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php index eec39859b..ec4fcb161 100644 --- a/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Admin/Resources/EggResource/Pages/EditEgg.php @@ -196,7 +196,7 @@ class EditEgg extends EditRecord '*' => trans('admin/egg.error_reserved'), ]) ->required(), - TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255), + TextInput::make('default_value')->label(trans('admin/egg.default_value')), Fieldset::make(trans('admin/egg.user_permissions')) ->schema([ Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), diff --git a/app/Filament/Admin/Resources/NodeResource.php b/app/Filament/Admin/Resources/NodeResource.php index 1b47ccdfe..757bde676 100644 --- a/app/Filament/Admin/Resources/NodeResource.php +++ b/app/Filament/Admin/Resources/NodeResource.php @@ -40,7 +40,8 @@ class NodeResource extends Resource public static function getNavigationGroup(): ?string { - return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server'); + return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server'); + } public static function getNavigationBadge(): ?string diff --git a/app/Filament/Admin/Resources/RoleResource.php b/app/Filament/Admin/Resources/RoleResource.php index 6f213f86b..c63ddfecf 100644 --- a/app/Filament/Admin/Resources/RoleResource.php +++ b/app/Filament/Admin/Resources/RoleResource.php @@ -59,7 +59,7 @@ class RoleResource extends Resource public static function getNavigationGroup(): ?string { - return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user'); + return !empty(auth()->user()->getCustomization()['top_navigation']) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user'); } public static function getNavigationBadge(): ?string diff --git a/app/Filament/Admin/Resources/ServerResource.php b/app/Filament/Admin/Resources/ServerResource.php index 3be48e7b7..5c4235f29 100644 --- a/app/Filament/Admin/Resources/ServerResource.php +++ b/app/Filament/Admin/Resources/ServerResource.php @@ -43,7 +43,7 @@ class ServerResource extends Resource public static function getNavigationGroup(): ?string { - return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server'); + return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server'); } public static function getNavigationBadge(): ?string diff --git a/app/Filament/Admin/Resources/UserResource.php b/app/Filament/Admin/Resources/UserResource.php index f811749f8..52087987f 100644 --- a/app/Filament/Admin/Resources/UserResource.php +++ b/app/Filament/Admin/Resources/UserResource.php @@ -56,7 +56,7 @@ class UserResource extends Resource public static function getNavigationGroup(): ?string { - return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user'); + return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.user'); } public static function getNavigationBadge(): ?string diff --git a/app/Filament/Admin/Resources/WebhookResource.php b/app/Filament/Admin/Resources/WebhookResource.php index 85f0f26b2..f11a854ba 100644 --- a/app/Filament/Admin/Resources/WebhookResource.php +++ b/app/Filament/Admin/Resources/WebhookResource.php @@ -19,6 +19,7 @@ use Filament\Forms\Components\Section; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Form; use Filament\Resources\Pages\PageRegistration; use Filament\Forms\Get; @@ -129,23 +130,12 @@ class WebhookResource extends Resource ->live() ->inline() ->options(WebhookType::class) - ->default(WebhookType::Regular->value) - ->afterStateHydrated(function (string $state) { - if ($state === WebhookType::Discord->value) { - self::sendHelpBanner(); - } - }) - ->afterStateUpdated(function (string $state) { - if ($state === WebhookType::Discord->value) { - self::sendHelpBanner(); - } - }), + ->default(WebhookType::Regular->value), TextInput::make('description') ->label(trans('admin/webhook.description')) ->required(), TextInput::make('endpoint') ->label(trans('admin/webhook.endpoint')) - ->activeUrl() ->required() ->columnSpanFull() ->afterStateUpdated(fn (string $state, Set $set) => $set('type', str($state)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value)), @@ -153,6 +143,15 @@ class WebhookResource extends Resource ->hidden(fn (Get $get) => $get('type') === WebhookType::Discord->value) ->dehydratedWhenHidden() ->schema(fn () => self::getRegularFields()) + ->headerActions([ + Action::make('reset_headers') + ->label(trans('admin/webhook.reset_headers')) + ->color('danger') + ->icon('heroicon-o-trash') + ->action(fn (Get $get, Set $set) => $set('headers', [ + 'X-Webhook-Event' => '{{event}}', + ])), + ]) ->formBefore(), Section::make(trans('admin/webhook.discord')) ->hidden(fn (Get $get) => $get('type') === WebhookType::Regular->value) @@ -163,8 +162,6 @@ class WebhookResource extends Resource ->aside() ->formBefore(), Section::make(trans('admin/webhook.events')) - ->collapsible() - ->collapsed(fn (Get $get) => count($get('events') ?? [])) ->schema([ CheckboxList::make('events') ->live() @@ -183,7 +180,10 @@ class WebhookResource extends Resource { return [ KeyValue::make('headers') - ->label(trans('admin/webhook.headers')), + ->label(trans('admin/webhook.headers')) + ->default(fn () => [ + 'X-Webhook-Event' => '{{event}}', + ]), ]; } diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php b/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php index 68ae9a22f..03996df8c 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/CreateWebhookConfiguration.php @@ -63,4 +63,15 @@ class CreateWebhookConfiguration extends CreateRecord return $data; } + + protected function getRedirectUrl(): string + { + return EditWebhookConfiguration::getUrl(['record' => $this->getRecord()]); + } + + public function mount(): void + { + parent::mount(); + WebhookResource::sendHelpBanner(); + } } diff --git a/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php b/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php index 6cc49df23..1da3baa0a 100644 --- a/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php +++ b/app/Filament/Admin/Resources/WebhookResource/Pages/EditWebhookConfiguration.php @@ -123,4 +123,10 @@ class EditWebhookConfiguration extends EditRecord { $this->dispatch('refresh-widget'); } + + public function mount(int|string $record): void + { + parent::mount($record); + WebhookResource::sendHelpBanner(); + } } diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index ce512f94c..e5c736c77 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -63,7 +63,7 @@ class ListServers extends ListRecords TextColumn::make('condition') ->label('Status') ->badge() - ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) + ->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime)) ->icon(fn (Server $server) => $server->condition->getIcon()) ->color(fn (Server $server) => $server->condition->getColor()), TextColumn::make('name') @@ -78,22 +78,22 @@ class ListServers extends ListRecords ->copyable(request()->isSecure()) ->state(fn (Server $server) => $server->allocation->address ?? 'None'), TextColumn::make('cpuUsage') - ->label('Resources') + ->label(trans('server/dashboard.resources')) ->icon('tabler-cpu') - ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0)) - ->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage)) + ->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::CPULimit)])) + ->state(fn (Server $server) => $server->formatResource(ServerResourceType::CPU)) ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')), TextColumn::make('memoryUsage') ->label('') ->icon('tabler-device-desktop-analytics') - ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true)) - ->state(fn (Server $server) => $server->formatResource('memory_bytes')) + ->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::MemoryLimit)])) + ->state(fn (Server $server) => $server->formatResource(ServerResourceType::Memory)) ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')), TextColumn::make('diskUsage') ->label('') ->icon('tabler-device-sd-card') - ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true)) - ->state(fn (Server $server) => $server->formatResource('disk_bytes')) + ->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::DiskLimit)])) + ->state(fn (Server $server) => $server->formatResource(ServerResourceType::Disk)) ->color(fn (Server $server) => $this->getResourceColor($server, 'disk')), ]; } diff --git a/app/Filament/Components/Actions/ExportEggAction.php b/app/Filament/Components/Actions/ExportEggAction.php index 1f3e4705d..acdf20624 100644 --- a/app/Filament/Components/Actions/ExportEggAction.php +++ b/app/Filament/Components/Actions/ExportEggAction.php @@ -2,9 +2,12 @@ namespace App\Filament\Components\Actions; +use App\Enums\EggFormat; use App\Models\Egg; use App\Services\Eggs\Sharing\EggExporterService; use Filament\Actions\Action; +use Filament\Forms\Components\Placeholder; +use Filament\Support\Enums\Alignment; class ExportEggAction extends Action { @@ -21,8 +24,30 @@ class ExportEggAction extends Action $this->authorize(fn () => auth()->user()->can('export egg')); - $this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { - echo $service->handle($egg->id); - }, 'egg-' . $egg->getKebabName() . '.json')); + $this->modalHeading(fn (Egg $egg) => trans('filament-actions::export.modal.actions.export.label') . ' ' . $egg->name); + + $this->modalIcon($this->icon); + + $this->form([ + Placeholder::make('') + ->label(fn (Egg $egg) => trans('admin/egg.export.modal', ['egg' => $egg->name])), + ]); + + $this->modalFooterActionsAlignment(Alignment::Center); + + $this->modalFooterActions([ + Action::make('json') + ->label(trans('admin/egg.export.as') . ' .json') + ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { + echo $service->handle($egg->id, EggFormat::JSON); + }, 'egg-' . $egg->getKebabName() . '.json')) + ->close(), + Action::make('yaml') + ->label(trans('admin/egg.export.as') . ' .yaml') + ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { + echo $service->handle($egg->id, EggFormat::YAML); + }, 'egg-' . $egg->getKebabName() . '.yaml')) + ->close(), + ]); } } diff --git a/app/Filament/Components/Actions/ImportEggAction.php b/app/Filament/Components/Actions/ImportEggAction.php index 615cbe0ba..58448fab2 100644 --- a/app/Filament/Components/Actions/ImportEggAction.php +++ b/app/Filament/Components/Actions/ImportEggAction.php @@ -47,13 +47,31 @@ class ImportEggAction extends Action foreach ($eggs as $egg) { if ($egg instanceof TemporaryUploadedFile) { - $name = str($egg->getClientOriginalName())->afterLast('egg-')->before('.json')->headline(); + $originalName = $egg->getClientOriginalName(); + $filename = str($originalName)->afterLast('egg-'); + $ext = str($originalName)->afterLast('.')->lower()->toString(); + + $name = match ($ext) { + 'json' => $filename->before('.json')->headline(), + 'yaml' => $filename->before('.yaml')->headline(), + 'yml' => $filename->before('.yml')->headline(), + default => $filename->headline(), + }; $method = 'fromFile'; } else { $egg = str($egg); $egg = $egg->contains('github.com') ? $egg->replaceFirst('blob', 'raw') : $egg; - $name = $egg->afterLast('/egg-')->before('.json')->headline(); $method = 'fromUrl'; + + $filename = $egg->afterLast('/egg-'); + $ext = $filename->afterLast('.')->lower()->toString(); + + $name = match ($ext) { + 'json' => $filename->before('.json')->headline(), + 'yaml' => $filename->before('.yaml')->headline(), + 'yml' => $filename->before('.yml')->headline(), + default => $filename->headline(), + }; } try { $eggImportService->$method($egg); @@ -94,7 +112,7 @@ class ImportEggAction extends Action FileUpload::make('files') ->label(trans('admin/egg.model_label')) ->hint(trans('admin/egg.import.egg_help')) - ->acceptedFileTypes(['application/json']) + ->acceptedFileTypes(['application/json', 'application/yaml', 'application/x-yaml', 'text/yaml']) ->preserveFilenames() ->previewable(false) ->storeFiles(false) @@ -125,7 +143,7 @@ class ImportEggAction extends Action }), Repeater::make('urls') ->label('') - ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline()) + ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline()) ->hint(trans('admin/egg.import.url_help')) ->addActionLabel(trans('admin/egg.import.add_url')) ->grid($isMultiple ? 2 : null) @@ -139,7 +157,7 @@ class ImportEggAction extends Action ->label(trans('admin/egg.import.url')) ->placeholder('https://github.com/pelican-eggs/generic/blob/main/nodejs/egg-node-js-generic.json') ->url() - ->endsWith('.json') + ->endsWith(['.json', '.yaml', '.yml']) ->validationAttribute(trans('admin/egg.import.url')), ]), ]), diff --git a/app/Filament/Components/Actions/ImportScheduleAction.php b/app/Filament/Components/Actions/ImportScheduleAction.php index 37e934c0e..27824c90b 100644 --- a/app/Filament/Components/Actions/ImportScheduleAction.php +++ b/app/Filament/Components/Actions/ImportScheduleAction.php @@ -39,26 +39,26 @@ class ImportScheduleAction extends Action Tabs::make('Tabs') ->contained(false) ->tabs([ - Tab::make(trans('admin/schedule.import.file')) + Tab::make(trans('server/schedule.import_action.file')) ->icon('tabler-file-upload') ->schema([ FileUpload::make('files') - ->label(trans('admin/schedule.model_label')) - ->hint(trans('admin/schedule.import.schedule_help')) + ->hiddenLabel() + ->hint(trans('server/schedule.import_action.schedule_help')) ->acceptedFileTypes(['application/json']) ->preserveFilenames() ->previewable(false) ->storeFiles(false) ->multiple(true), ]), - Tab::make(trans('admin/schedule.import.url')) + Tab::make(trans('server/schedule.import_action.url')) ->icon('tabler-world-upload') ->schema([ Repeater::make('urls') - ->label('') + ->hiddenLabel() ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline()) - ->hint(trans('admin/schedule.import.url_help')) - ->addActionLabel(trans('admin/schedule.import.add_url')) + ->hint(trans('server/schedule.import_action.url_help')) + ->addActionLabel(trans('server/schedule.import_action.add_url')) ->grid(2) ->reorderable(false) ->addable(true) @@ -66,10 +66,10 @@ class ImportScheduleAction extends Action ->schema([ TextInput::make('url') ->live() - ->label(trans('admin/schedule.import.url')) + ->label(trans('server/schedule.import_action.url')) ->url() ->endsWith('.json') - ->validationAttribute(trans('admin/schedule.import.url')), + ->validationAttribute(trans('server/schedule.import_action.url')), ]), ]), ]), @@ -104,14 +104,14 @@ class ImportScheduleAction extends Action if ($failed->count() > 0) { Notification::make() - ->title(trans('admin/schedule.import.import_failed')) + ->title(trans('server/schedule.import_action.import_failed')) ->body($failed->join(', ')) ->danger() ->send(); } if ($success->count() > 0) { Notification::make() - ->title(trans('admin/schedule.import.import_success')) + ->title(trans('server/schedule.import_action.import_success')) ->body($success->join(', ')) ->success() ->send(); diff --git a/app/Filament/Components/Forms/Actions/PreviewStartupAction.php b/app/Filament/Components/Forms/Actions/PreviewStartupAction.php index 699580b9d..65a6f80bb 100644 --- a/app/Filament/Components/Forms/Actions/PreviewStartupAction.php +++ b/app/Filament/Components/Forms/Actions/PreviewStartupAction.php @@ -15,6 +15,11 @@ class PreviewStartupAction extends Action return 'preview'; } + public function getLabel(): string + { + return trans('server/startup.preview'); + } + protected function setUp(): void { parent::setUp(); diff --git a/app/Filament/Components/Tables/Actions/ExportEggAction.php b/app/Filament/Components/Tables/Actions/ExportEggAction.php index 67e51de36..978bb0f86 100644 --- a/app/Filament/Components/Tables/Actions/ExportEggAction.php +++ b/app/Filament/Components/Tables/Actions/ExportEggAction.php @@ -2,8 +2,11 @@ namespace App\Filament\Components\Tables\Actions; +use App\Enums\EggFormat; use App\Models\Egg; use App\Services\Eggs\Sharing\EggExporterService; +use Filament\Forms\Components\Placeholder; +use Filament\Support\Enums\Alignment; use Filament\Tables\Actions\Action; class ExportEggAction extends Action @@ -23,8 +26,30 @@ class ExportEggAction extends Action $this->authorize(fn () => auth()->user()->can('export egg')); - $this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { - echo $service->handle($egg->id); - }, 'egg-' . $egg->getKebabName() . '.json')); + $this->modalHeading(fn (Egg $egg) => trans('filament-actions::export.modal.actions.export.label') . ' ' . $egg->name); + + $this->modalIcon($this->icon); + + $this->form([ + Placeholder::make('') + ->label(fn (Egg $egg) => trans('admin/egg.export.modal', ['egg' => $egg->name])), + ]); + + $this->modalFooterActionsAlignment(Alignment::Center); + + $this->modalFooterActions([ + Action::make('json') + ->label(trans('admin/egg.export.as') . ' .json') + ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { + echo $service->handle($egg->id, EggFormat::JSON); + }, 'egg-' . $egg->getKebabName() . '.json')) + ->close(), + Action::make('yaml') + ->label(trans('admin/egg.export.as') . ' .yaml') + ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { + echo $service->handle($egg->id, EggFormat::YAML); + }, 'egg-' . $egg->getKebabName() . '.yaml')) + ->close(), + ]); } } diff --git a/app/Filament/Components/Tables/Actions/ImportEggAction.php b/app/Filament/Components/Tables/Actions/ImportEggAction.php index b51576535..848d41f82 100644 --- a/app/Filament/Components/Tables/Actions/ImportEggAction.php +++ b/app/Filament/Components/Tables/Actions/ImportEggAction.php @@ -117,7 +117,7 @@ class ImportEggAction extends Action } }), Repeater::make('urls') - ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline()) + ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline()) ->hint(trans('admin/egg.import.url_help')) ->addActionLabel(trans('admin/egg.import.add_url')) ->grid($isMultiple ? 2 : null) @@ -131,7 +131,7 @@ class ImportEggAction extends Action ->label(trans('admin/egg.import.url')) ->placeholder('https://github.com/pelican-eggs/generic/blob/main/nodejs/egg-node-js-generic.json') ->url() - ->endsWith('.json') + ->endsWith(['.json', '.yaml', '.yml']) ->validationAttribute(trans('admin/egg.import.url')), ]), ]), diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index d38d0e1da..0ad423ad9 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -174,7 +174,7 @@ class EditProfile extends BaseEditProfile $unlink = array_key_exists($id, $this->getUser()->oauth ?? []); $actions[] = Action::make("oauth_$id") - ->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name) + ->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name])) ->icon($unlink ? 'tabler-unlink' : 'tabler-link') ->color(Color::hex($schema->getHexColor())) ->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) { @@ -322,6 +322,7 @@ class EditProfile extends BaseEditProfile Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([ Repeater::make('api_keys') ->hiddenLabel() + ->inlineLabel(false) ->relationship('apiKeys') ->addable(false) ->itemLabel(fn ($state) => $state['identifier']) @@ -406,6 +407,7 @@ class EditProfile extends BaseEditProfile Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([ Repeater::make('ssh_keys') ->hiddenLabel() + ->inlineLabel(false) ->relationship('sshKeys') ->addable(false) ->itemLabel(fn ($state) => $state['name']) @@ -445,14 +447,17 @@ class EditProfile extends BaseEditProfile ->icon('tabler-history') ->schema([ Repeater::make('activity') - ->label('') + ->hiddenLabel() + ->inlineLabel(false) ->deletable(false) ->addable(false) ->relationship(null, function (Builder $query) { $query->orderBy('timestamp', 'desc'); }) ->schema([ - Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), + Placeholder::make('log') + ->hiddenLabel() + ->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), ]), ]), @@ -471,6 +476,14 @@ class EditProfile extends BaseEditProfile 'grid' => trans('profile.grid'), 'table' => trans('profile.table'), ]), + ToggleButtons::make('top_navigation') + ->label(trans('profile.navigation')) + ->inline() + ->required() + ->options([ + true => trans('profile.top'), + false => trans('profile.side'), + ]), ]), Section::make(trans('profile.console')) ->collapsible() @@ -628,9 +641,10 @@ class EditProfile extends BaseEditProfile 'console_rows' => $data['console_rows'], 'console_graph_period' => $data['console_graph_period'], 'dashboard_layout' => $data['dashboard_layout'], + 'top_navigation' => $data['top_navigation'], ]; - unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']); + unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout'], $data['top_navigation']); $data['customization'] = json_encode($moarbetterdata); return $data; @@ -645,6 +659,7 @@ class EditProfile extends BaseEditProfile $data['console_rows'] = $moarbetterdata['console_rows'] ?? 30; $data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30; $data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid'; + $data['top_navigation'] = $moarbetterdata['top_navigation'] ?? false; return $data; } diff --git a/app/Filament/Server/Pages/Console.php b/app/Filament/Server/Pages/Console.php index 97cb6c9d1..91ded5968 100644 --- a/app/Filament/Server/Pages/Console.php +++ b/app/Filament/Server/Pages/Console.php @@ -162,6 +162,7 @@ class Console extends Page return [ Action::make('start') + ->label(trans('server/console.power_actions.start')) ->color('primary') ->size(ActionSize::ExtraLarge) ->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid]) @@ -169,6 +170,7 @@ class Console extends Page ->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()) ->icon('tabler-player-play-filled'), Action::make('restart') + ->label(trans('server/console.power_actions.restart')) ->color('gray') ->size(ActionSize::ExtraLarge) ->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid]) @@ -176,6 +178,7 @@ class Console extends Page ->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()) ->icon('tabler-reload'), Action::make('stop') + ->label(trans('server/console.power_actions.stop')) ->color('danger') ->size(ActionSize::ExtraLarge) ->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid]) @@ -184,8 +187,9 @@ class Console extends Page ->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()) ->icon('tabler-player-stop-filled'), Action::make('kill') + ->label(trans('server/console.power_actions.kill')) ->color('danger') - ->tooltip('This can result in data corruption and/or data loss!') + ->tooltip(trans('server/console.power_actions.kill_tooltip')) ->size(ActionSize::ExtraLarge) ->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid]) ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) @@ -193,4 +197,14 @@ class Console extends Page ->icon('tabler-alert-square'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/console.title'); + } + + public function getTitle(): string + { + return trans('server/console.title'); + } } diff --git a/app/Filament/Server/Pages/Settings.php b/app/Filament/Server/Pages/Settings.php index 55c352943..4180abc5f 100644 --- a/app/Filament/Server/Pages/Settings.php +++ b/app/Filament/Server/Pages/Settings.php @@ -39,7 +39,7 @@ class Settings extends ServerFormPage 'lg' => 6, ]) ->schema([ - Section::make('Server Information') + Section::make(trans('server/setting.server_info.title')) ->columns([ 'default' => 1, 'sm' => 2, @@ -47,11 +47,11 @@ class Settings extends ServerFormPage 'lg' => 6, ]) ->schema([ - Fieldset::make('Server') - ->label('Information') + Fieldset::make() + ->label(trans('server/setting.server_info.information')) ->schema([ TextInput::make('name') - ->label('Server Name') + ->label(trans('server/setting.server_info.name')) ->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server)) ->required() ->columnSpan([ @@ -63,7 +63,7 @@ class Settings extends ServerFormPage ->live(onBlur: true) ->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)), Textarea::make('description') - ->label('Server Description') + ->label(trans('server/setting.server_info.description')) ->hidden(!config('panel.editable_server_descriptions')) ->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server)) ->columnSpan([ @@ -76,7 +76,7 @@ class Settings extends ServerFormPage ->live(onBlur: true) ->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)), TextInput::make('uuid') - ->label('Server UUID') + ->label(trans('server/setting.server_info.uuid')) ->columnSpan([ 'default' => 1, 'sm' => 1, @@ -85,12 +85,12 @@ class Settings extends ServerFormPage ]) ->disabled(), TextInput::make('id') - ->label('Server ID') + ->label(trans('server/setting.server_info.id')) ->disabled() ->columnSpan(1), ]), - Fieldset::make('Limits') - ->label('Limits') + Fieldset::make() + ->label(trans('server/setting.server_info.limits.title')) ->columns([ 'default' => 1, 'sm' => 1, @@ -100,57 +100,56 @@ class Settings extends ServerFormPage ->schema([ TextInput::make('cpu') ->label('') - ->prefix('CPU') + ->prefix(trans('server/setting.server_info.limits.cpu')) ->prefixIcon('tabler-cpu') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : Number::format($server->cpu, locale: auth()->user()->language) . '%'), + ->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : Number::format($server->cpu, locale: auth()->user()->language) . '%'), TextInput::make('memory') ->label('') - ->prefix('Memory') + ->prefix(trans('server/setting.server_info.limits.memory')) ->prefixIcon('tabler-device-desktop-analytics') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->memory * 2 ** 20)), + ->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->memory * 2 ** 20)), TextInput::make('disk') ->label('') - ->prefix('Disk Space') + ->prefix(trans('server/setting.server_info.limits.disk')) ->prefixIcon('tabler-device-sd-card') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->disk * 2 ** 20)), + ->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->disk * 2 ** 20)), TextInput::make('backup_limit') ->label('') - ->prefix('Backups') + ->prefix(trans('server/setting.server_info.limits.backups')) ->prefixIcon('tabler-file-zip') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' of ' . $state), + ->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state), TextInput::make('database_limit') ->label('') - ->prefix('Databases') + ->prefix(trans('server/setting.server_info.limits.databases')) ->prefixIcon('tabler-database') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' of ' . $state), + ->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of') . ' ' .$state), TextInput::make('allocation_limit') ->label('') - ->prefix('Allocations') + ->prefix(trans('server/setting.server_info.limits.allocations')) ->prefixIcon('tabler-network') ->columnSpan(1) ->disabled() - ->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Additional Allocations' : $server->allocations->count() . ' of ' . $state), + ->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.no_allocations') : $server->allocations->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state), ]), ]), - Section::make('Node Information') + Section::make(trans('server/setting.node_info.title')) ->schema([ TextInput::make('node.name') - ->label('Node Name') + ->label(trans('server/setting.node_info.name')) ->formatStateUsing(fn (Server $server) => $server->node->name) ->disabled(), - Fieldset::make('SFTP Information') + Fieldset::make(trans('server/setting.node_info.sftp.title')) ->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server)) - ->label('SFTP Information') ->columns([ 'default' => 1, 'sm' => 1, @@ -159,13 +158,13 @@ class Settings extends ServerFormPage ]) ->schema([ TextInput::make('connection') - ->label('Connection') + ->label(trans('server/setting.node_info.sftp.connection')) ->columnSpan(1) ->disabled() ->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null) ->hintAction( Action::make('connect_sftp') - ->label('Connect to SFTP') + ->label(trans('server/setting.node_info.sftp.action')) ->color('success') ->icon('tabler-plug') ->url(function (Server $server) { @@ -180,28 +179,29 @@ class Settings extends ServerFormPage return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp; }), TextInput::make('username') - ->label('Username') + ->label(trans('server/setting.node_info.sftp.username')) ->columnSpan(1) ->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null) ->disabled() ->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short), Placeholder::make('password') + ->label(trans('server/setting.node_info.sftp.password')) ->columnSpan(1) - ->content('Your SFTP password is the same as the password you use to access this panel.'), + ->content(trans('server/setting.node_info.sftp.password_body')), ]), ]), - Section::make('Reinstall Server') + Section::make(trans('server/setting.reinstall.title')) ->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server)) ->collapsible() ->footerActions([ Action::make('reinstall') + ->label(trans('server/setting.reinstall.action')) ->color('danger') ->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server)) - ->label('Reinstall') ->requiresConfirmation() - ->modalHeading('Are you sure you want to reinstall the server?') - ->modalDescription('Some files may be deleted or modified during this process, please back up your data before continuing.') - ->modalSubmitActionLabel('Yes, Reinstall') + ->modalHeading(trans('server/setting.reinstall.modal')) + ->modalDescription(trans('server/setting.reinstall.modal_description')) + ->modalSubmitActionLabel(trans('server/setting.reinstall.yes')) ->action(function (Server $server, ReinstallServerService $reinstallService) { abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403); @@ -211,9 +211,9 @@ class Settings extends ServerFormPage report($exception); Notification::make() - ->danger() - ->title('Server Reinstall failed') + ->title(trans('server/setting.reinstall.notification_fail')) ->body($exception->getMessage()) + ->danger() ->send(); return; @@ -223,8 +223,8 @@ class Settings extends ServerFormPage ->log(); Notification::make() + ->title(trans('server/setting.reinstall.notification_start')) ->success() - ->title('Server Reinstall started') ->send(); redirect(Console::getUrl()); @@ -233,9 +233,9 @@ class Settings extends ServerFormPage ->footerActionsAlignment(Alignment::Right) ->schema([ Placeholder::make('') - ->label('Reinstalling your server will stop it, and then re-run the installation script that initially set it up.'), + ->label(trans('server/setting.reinstall.body')), Placeholder::make('') - ->label('Some files may be deleted or modified during this process, please back up your data before continuing.'), + ->label(trans('server/setting.reinstall.body2')), ]), ]); } @@ -258,15 +258,15 @@ class Settings extends ServerFormPage } Notification::make() - ->success() - ->title('Updated Server Name') + ->title(trans('server/setting.notification_name')) ->body(fn () => $original . ' -> ' . $name) + ->success() ->send(); } catch (Exception $exception) { Notification::make() - ->danger() - ->title('Failed') + ->title(trans('server/setting.failed')) ->body($exception->getMessage()) + ->danger() ->send(); } } @@ -289,16 +289,26 @@ class Settings extends ServerFormPage } Notification::make() - ->success() - ->title('Updated Server Description') + ->title(trans('server/setting.notification_description')) ->body(fn () => $original . ' -> ' . $description) + ->success() ->send(); } catch (Exception $exception) { Notification::make() - ->danger() - ->title('Failed') + ->title(trans('server/setting.failed')) ->body($exception->getMessage()) + ->danger() ->send(); } } + + public function getTitle(): string + { + return trans('server/setting.title'); + } + + public static function getNavigationLabel(): string + { + return trans('server/setting.title'); + } } diff --git a/app/Filament/Server/Pages/Startup.php b/app/Filament/Server/Pages/Startup.php index 457b9765e..89a66d65d 100644 --- a/app/Filament/Server/Pages/Startup.php +++ b/app/Filament/Server/Pages/Startup.php @@ -18,6 +18,7 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Notifications\Notification; +use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Validator; @@ -43,7 +44,7 @@ class Startup extends ServerFormPage Hidden::make('previewing') ->default(false), Textarea::make('startup') - ->label('Startup Command') + ->label(trans('server/startup.command')) ->columnSpan([ 'default' => 1, 'sm' => 1, @@ -51,10 +52,10 @@ class Startup extends ServerFormPage 'lg' => 4, ]) ->autosize() - ->hintAction(PreviewStartupAction::make('preview')) + ->hintAction(PreviewStartupAction::make()) ->readOnly(), TextInput::make('custom_image') - ->label('Docker Image') + ->label(trans('server/startup.docker_image')) ->readOnly() ->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images)) ->formatStateUsing(fn (Server $server) => $server->image) @@ -65,7 +66,7 @@ class Startup extends ServerFormPage 'lg' => 2, ]), Select::make('image') - ->label('Docker Image') + ->label(trans('server/startup.docker_image')) ->live() ->visible(fn (Server $server) => in_array($server->image, $server->egg->docker_images)) ->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server)) @@ -80,8 +81,8 @@ class Startup extends ServerFormPage } Notification::make() - ->title('Docker image updated') - ->body('Restart the server to use the new image.') + ->title(trans('server/startup.notification_docker')) + ->body(trans('server/startup.notification_docker_body')) ->success() ->send(); }) @@ -97,10 +98,10 @@ class Startup extends ServerFormPage 'md' => 2, 'lg' => 2, ]), - Section::make('Server Variables') + Section::make(trans('server/startup.variables')) ->schema([ Repeater::make('server_variables') - ->label('') + ->hiddenLabel() ->relationship('serverVariables', fn (Builder $query) => $query->where('egg_variables.user_viewable', true)->orderByPowerJoins('variable.sort')) ->grid() ->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) @@ -207,9 +208,9 @@ class Startup extends ServerFormPage if ($validator->fails()) { Notification::make() - ->danger() - ->title('Validation Failed: ' . $serverVariable->variable->name) + ->title(trans('server/startup.validation_fail', ['variable' => $serverVariable->variable->name])) ->body(implode(', ', $validator->errors()->all())) + ->danger() ->send(); return null; @@ -232,18 +233,28 @@ class Startup extends ServerFormPage ->log(); } Notification::make() - ->success() - ->title('Updated: ' . $serverVariable->variable->name) + ->title(trans('server/startup.update', ['variable' => $serverVariable->variable->name])) ->body(fn () => $original . ' -> ' . $state) + ->success() ->send(); } catch (\Exception $e) { Notification::make() - ->danger() - ->title('Failed: ' . $serverVariable->variable->name) + ->title(trans('server/startup.fail', ['variable' => $serverVariable->variable->name])) ->body($e->getMessage()) + ->danger() ->send(); } return null; } + + public function getTitle(): string|Htmlable + { + return trans('server/startup.title'); + } + + public static function getNavigationLabel(): string + { + return trans('server/startup.title'); + } } diff --git a/app/Filament/Server/Resources/ActivityResource.php b/app/Filament/Server/Resources/ActivityResource.php index 17d353867..cfa8e33aa 100644 --- a/app/Filament/Server/Resources/ActivityResource.php +++ b/app/Filament/Server/Resources/ActivityResource.php @@ -38,10 +38,6 @@ class ActivityResource extends Resource protected static ?string $model = ActivityLog::class; - protected static ?string $modelLabel = 'Activity'; - - protected static ?string $pluralModelLabel = 'Activity'; - protected static ?int $navigationSort = 8; protected static ?string $navigationIcon = 'tabler-stack'; @@ -56,14 +52,16 @@ class ActivityResource extends Resource ->defaultPaginationPageOption(25) ->columns([ TextColumn::make('event') + ->label(trans('server/activity.event')) ->html() ->description(fn ($state) => $state) ->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon()) ->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()), TextColumn::make('user') + ->label(trans('server/activity.user')) ->state(function (ActivityLog $activityLog) use ($server) { if (!$activityLog->actor instanceof User) { - return $activityLog->actor_id === null ? 'System' : 'Deleted user'; + return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user'); } $user = $activityLog->actor->username; @@ -79,6 +77,7 @@ class ActivityResource extends Resource ->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '') ->grow(false), DateTimeColumn::make('timestamp') + ->label(trans('server/activity.timestamp')) ->since() ->sortable() ->grow(false), @@ -89,11 +88,13 @@ class ActivityResource extends Resource //->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata()) ->form([ Placeholder::make('event') + ->label(trans('server/activity.event')) ->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())), TextInput::make('user') + ->label(trans('server/activity.user')) ->formatStateUsing(function (ActivityLog $activityLog) use ($server) { if (!$activityLog->actor instanceof User) { - return $activityLog->actor_id === null ? 'System' : 'Deleted user'; + return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user'); } $user = $activityLog->actor->username; @@ -116,9 +117,10 @@ class ActivityResource extends Resource ->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor)) ->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin')) ), - DateTimePicker::make('timestamp'), + DateTimePicker::make('timestamp') + ->label(trans('server/activity.timestamp')), KeyValue::make('properties') - ->label('Metadata') + ->label(trans('server/activity.metadata')) ->formatStateUsing(fn ($state) => Arr::dot($state)), ]), ]) @@ -168,4 +170,9 @@ class ActivityResource extends Resource 'index' => Pages\ListActivities::route('/'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/activity.title'); + } } diff --git a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php index e60a2127f..3ca559b0a 100644 --- a/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/Server/Resources/ActivityResource/Pages/ListActivities.php @@ -18,4 +18,9 @@ class ListActivities extends ListRecords { return []; } + + public function getTitle(): string + { + return trans('server/activity.title'); + } } diff --git a/app/Filament/Server/Resources/AllocationResource.php b/app/Filament/Server/Resources/AllocationResource.php index 15fd24527..333b98080 100644 --- a/app/Filament/Server/Resources/AllocationResource.php +++ b/app/Filament/Server/Resources/AllocationResource.php @@ -30,10 +30,6 @@ class AllocationResource extends Resource protected static ?string $model = Allocation::class; - protected static ?string $modelLabel = 'Network'; - - protected static ?string $pluralModelLabel = 'Network'; - protected static ?int $navigationSort = 7; protected static ?string $navigationIcon = 'tabler-network'; @@ -46,16 +42,17 @@ class AllocationResource extends Resource return $table ->columns([ TextColumn::make('ip') - ->label('Address') + ->label(trans('server/network.address')) ->formatStateUsing(fn (Allocation $allocation) => $allocation->alias), TextColumn::make('alias') ->hidden(), - TextColumn::make('port'), + TextColumn::make('port') + ->label(trans('server/network.port')), TextInputColumn::make('notes') + ->label(trans('server/network.notes')) ->visibleFrom('sm') ->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server)) - ->label('Notes') - ->placeholder('No Notes'), + ->placeholder(trans('server/network.no_notes')), IconColumn::make('primary') ->icon(fn ($state) => match ($state) { true => 'tabler-star-filled', @@ -65,15 +62,15 @@ class AllocationResource extends Resource true => 'warning', default => 'gray', }) - ->tooltip(fn (Allocation $allocation) => ($allocation->id === $server->allocation_id ? 'Already' : 'Make') . ' Primary') + ->tooltip(fn (Allocation $allocation) => $allocation->id === $server->allocation_id ? trans('server/network.primary') : trans('server/network.make_primary')) ->action(fn (Allocation $allocation) => auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id])) ->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) - ->label('Primary'), + ->label(trans('server/network.primary')), ]) ->actions([ DetachAction::make() ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server)) - ->label('Delete') + ->label(trans('server/network.delete')) ->icon('tabler-trash') ->action(function (Allocation $allocation) { Allocation::query()->where('id', $allocation->id)->update([ @@ -117,4 +114,9 @@ class AllocationResource extends Resource 'index' => Pages\ListAllocations::route('/'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/network.title'); + } } diff --git a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php index 266c6bb7b..6ae33475a 100644 --- a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php +++ b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php @@ -33,7 +33,7 @@ class ListAllocations extends ListRecords ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network') ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server)) - ->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation') + ->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? trans('server/network.limit') : trans('server/network.add')) ->hidden(fn () => !config('panel.client_features.allocations.enabled')) ->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit) ->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary') @@ -56,4 +56,14 @@ class ListAllocations extends ListRecords { return []; } + + public function getTitle(): string + { + return trans('server/network.title'); + } + + public static function getNavigationLabel(): string + { + return trans('server/network.title'); + } } diff --git a/app/Filament/Server/Resources/BackupResource.php b/app/Filament/Server/Resources/BackupResource.php index 211cb0154..acca80701 100644 --- a/app/Filament/Server/Resources/BackupResource.php +++ b/app/Filament/Server/Resources/BackupResource.php @@ -79,14 +79,15 @@ class BackupResource extends Resource return $form ->schema([ TextInput::make('name') - ->label('Name') + ->label(trans('server/backup.actions.create.name')) ->columnSpanFull(), TextArea::make('ignored') - ->columnSpanFull() - ->label('Ignored Files & Directories'), + ->label(trans('server/backup.actions.create.ignored')) + ->columnSpanFull(), Toggle::make('is_locked') - ->label('Lock?') - ->helperText('Prevents this backup from being deleted until explicitly unlocked.'), + ->label(trans('server/backup.actions.create.locked')) + ->helperText(trans('server/backup.actions.create.lock_helper')) + ->columnSpanFull(), ]); } @@ -98,60 +99,94 @@ class BackupResource extends Resource return $table ->columns([ TextColumn::make('name') + ->label(trans('server/backup.actions.create.name')) ->searchable(), BytesColumn::make('bytes') - ->label('Size'), + ->label(trans('server/backup.size')), DateTimeColumn::make('created_at') - ->label('Created') + ->label(trans('server/backup.created_at')) ->since() ->sortable(), TextColumn::make('status') - ->label('Status') + ->label(trans('server/backup.status')) ->badge(), IconColumn::make('is_locked') + ->label(trans('server/backup.is_locked')) ->visibleFrom('md') - ->label('Lock Status') ->trueIcon('tabler-lock') ->falseIcon('tabler-lock-open'), ]) ->actions([ ActionGroup::make([ + Action::make('rename') + ->icon('tabler-pencil') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) + ->label('Rename') + ->form([ + TextInput::make('name') + ->label('Backup Name') + ->required() + ->maxLength(255) + ->default(fn (Backup $backup) => $backup->name), + ]) + ->action(function (Backup $backup, $data) { + $oldName = $backup->name; + $newName = $data['name']; + + $backup->update(['name' => $newName]); + + if ($oldName !== $newName) { + Activity::event('server:backup.rename') + ->subject($backup) + ->property(['old_name' => $oldName, 'new_name' => $newName]) + ->log(); + } + + Notification::make() + ->title('Backup Renamed') + ->body('The backup has been successfully renamed.') + ->success() + ->send(); + }) + ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), Action::make('lock') ->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open') ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) - ->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock') + ->label(fn (Backup $backup) => !$backup->is_locked ? trans('server/backup.actions.lock.lock') : trans('server/backup.actions.lock.unlock')) ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)) ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), Action::make('download') + ->label(trans('server/backup.actions.download')) ->color('primary') ->icon('tabler-download') ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server)) ->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true) ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), Action::make('restore') + ->label(trans('server/backup.actions.restore.title')) ->color('success') ->icon('tabler-folder-up') ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server)) ->form([ Placeholder::make('') - ->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'), + ->helperText(trans('server/backup.actions.restore.helper')), Checkbox::make('truncate') - ->label('Delete all files before restoring backup?'), + ->label(trans('server/backup.actions.restore.delete_all')), ]) ->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) { if (!is_null($server->status)) { return Notification::make() + ->title(trans('server/backup.actions.restore.notification_fail')) + ->body(trans('server/backup.actions.restore.notification_fail_body_1')) ->danger() - ->title('Backup Restore Failed') - ->body('This server is not currently in a state that allows for a backup to be restored.') ->send(); } if (!$backup->is_successful && is_null($backup->completed_at)) { return Notification::make() + ->title(trans('server/backup.actions.restore.notification_fail')) + ->body(trans('server/backup.actions.restore.notification_fail_body_2')) ->danger() - ->title('Backup Restore Failed') - ->body('This backup cannot be restored at this time: not completed or failed.') ->send(); } @@ -174,21 +209,26 @@ class BackupResource extends Resource }); return Notification::make() - ->title('Restoring Backup') + ->title(trans('server/backup.actions.restore.notification_started')) ->send(); }) ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful), DeleteAction::make('delete') ->disabled(fn (Backup $backup) => $backup->is_locked) - ->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?') - ->modalSubmitActionLabel('Delete Backup') + ->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name])) + ->modalSubmitActionLabel(trans('server/backup.actions.delete.title')) ->action(function (Backup $backup, DeleteBackupService $deleteBackupService) { try { $deleteBackupService->handle($backup); + + Notification::make() + ->title(trans('server/backup.actions.delete.notification_success')) + ->success() + ->send(); } catch (ConnectionException) { Notification::make() - ->title('Could not delete backup') - ->body('Connection to node failed') + ->title(trans('server/backup.actions.delete.notification_fail')) + ->body(trans('server/backup.actions.delete.notification_fail_body')) ->danger() ->send(); @@ -227,4 +267,9 @@ class BackupResource extends Resource 'index' => Pages\ListBackups::route('/'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/backup.title'); + } } diff --git a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php index 4d73e6b3d..3ab6aac8b 100644 --- a/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php +++ b/app/Filament/Server/Resources/BackupResource/Pages/ListBackups.php @@ -36,7 +36,7 @@ class ListBackups extends ListRecords ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server)) ->icon('tabler-file-zip')->iconButton()->iconSize(IconSize::Large) ->disabled(fn () => $server->backups()->count() >= $server->backup_limit) - ->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup Limit Reached' : 'Create Backup') // Disabled Buttons have no tooltips in v3 :/ + ->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? trans('server/backup.actions.create.limit') : trans('server/backup.actions.create.title')) ->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary') ->createAnother(false) ->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) { @@ -55,15 +55,15 @@ class ListBackups extends ListRecords ->log(); return Notification::make() - ->title('Backup Created') - ->body($backup->name . ' created.') + ->title(trans('server/backup.actions.create.notification_success')) + ->body(trans('server/backup.actions.create.created', ['name' => $backup->name])) ->success() ->send(); } catch (HttpException $e) { return Notification::make() - ->danger() - ->title('Backup Failed') + ->title(trans('server/backup.actions.create.notification_fail')) ->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : '')) + ->danger() ->send(); } }), @@ -74,4 +74,9 @@ class ListBackups extends ListRecords { return []; } + + public function getTitle(): string + { + return trans('server/backup.title'); + } } diff --git a/app/Filament/Server/Resources/DatabaseResource.php b/app/Filament/Server/Resources/DatabaseResource.php index 9effd8c2c..4025179f2 100644 --- a/app/Filament/Server/Resources/DatabaseResource.php +++ b/app/Filament/Server/Resources/DatabaseResource.php @@ -66,13 +66,17 @@ class DatabaseResource extends Resource return $form ->schema([ TextInput::make('host') + ->label(trans('server/database.host')) ->formatStateUsing(fn (Database $database) => $database->address()) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), TextInput::make('database') + ->label(trans('server/database.database')) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), TextInput::make('username') + ->label(trans('server/database.username')) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null), TextInput::make('password') + ->label(trans('server/database.password')) ->password()->revealable() ->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) ->hintAction( @@ -82,11 +86,12 @@ class DatabaseResource extends Resource ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->formatStateUsing(fn (Database $database) => $database->password), TextInput::make('remote') - ->label('Connections From'), + ->label(trans('server/database.remote')), TextInput::make('max_connections') + ->label(trans('server/database.max_connections')) ->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'), TextInput::make('jdbc') - ->label('JDBC Connection String') + ->label(trans('server/database.jdbc')) ->password()->revealable() ->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server)) ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) @@ -100,12 +105,17 @@ class DatabaseResource extends Resource return $table ->columns([ TextColumn::make('host') + ->label(trans('server/database.host')) ->state(fn (Database $database) => $database->address()) ->badge(), - TextColumn::make('database'), - TextColumn::make('username'), - TextColumn::make('remote'), + TextColumn::make('database') + ->label(trans('server/database.database')), + TextColumn::make('username') + ->label(trans('server/database.username')), + TextColumn::make('remote') + ->label(trans('server/database.remote')), DateTimeColumn::make('created_at') + ->label(trans('server/database.created_at')) ->sortable(), ]) ->actions([ @@ -148,4 +158,9 @@ class DatabaseResource extends Resource 'index' => Pages\ListDatabases::route('/'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/database.title'); + } } diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 167e745ef..5440a71fd 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -35,7 +35,7 @@ class ListDatabases extends ListRecords CreateAction::make('new') ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon(fn () => $server->databases()->count() >= $server->database_limit ? 'tabler-database-x' : 'tabler-database-plus') - ->tooltip(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database') + ->tooltip(fn () => $server->databases()->count() >= $server->database_limit ? trans('server/database.limit') : trans('server/database.create_database')) ->disabled(fn () => $server->databases()->count() >= $server->database_limit) ->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary') ->createAnother(false) @@ -44,20 +44,20 @@ class ListDatabases extends ListRecords ->columns(2) ->schema([ Select::make('database_host_id') - ->label('Database Host') + ->label(trans('server/database.database_host')) ->columnSpan(2) ->required() - ->placeholder('Select Database Host') + ->placeholder(trans('server/database.database_host_select')) ->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])), TextInput::make('database') + ->label(trans('server/database.name')) ->columnSpan(1) - ->label('Database Name') ->prefix('s'. $server->id . '_') ->hintIcon('tabler-question-mark') - ->hintIconTooltip('Leaving this blank will auto generate a random name'), + ->hintIconTooltip(trans('server/database.name_hint')), TextInput::make('remote') + ->label(trans('server/database.connections_from')) ->columnSpan(1) - ->label('Connections From') ->default('%'), ]), ]) @@ -76,4 +76,9 @@ class ListDatabases extends ListRecords { return []; } + + public function getTitle(): string + { + return trans('server/database.title'); + } } diff --git a/app/Filament/Server/Resources/FileResource.php b/app/Filament/Server/Resources/FileResource.php index 13f9d5d40..844fd4f09 100644 --- a/app/Filament/Server/Resources/FileResource.php +++ b/app/Filament/Server/Resources/FileResource.php @@ -55,4 +55,9 @@ class FileResource extends Resource 'index' => Pages\ListFiles::route('/{path?}'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/file.title'); + } } diff --git a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php index faec69c34..0989876a6 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php @@ -69,10 +69,10 @@ class EditFiles extends Page return $form ->schema([ - Section::make('Editing: ' . $this->path) + Section::make(trans('server/file.actions.edit.title', ['file' => $this->path])) ->footerActions([ Action::make('save_and_close') - ->label('Save & Close') + ->label(trans('server/file.actions.edit.save_close')) ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) ->icon('tabler-device-floppy') ->keyBindings('mod+shift+s') @@ -85,14 +85,14 @@ class EditFiles extends Page Notification::make() ->success() - ->title('File saved') + ->title(trans('server/file.actions.edit.notification')) ->body(fn () => $this->path) ->send(); $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); }), Action::make('save') - ->label('Save') + ->label(trans('server/file.actions.edit.save')) ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) ->icon('tabler-device-floppy') ->keyBindings('mod+s') @@ -105,12 +105,12 @@ class EditFiles extends Page Notification::make() ->success() - ->title('File saved') + ->title(trans('server/file.actions.edit.notification')) ->body(fn () => $this->path) ->send(); }), Action::make('cancel') - ->label('Cancel') + ->label(trans('server/file.actions.edit.cancel')) ->color('danger') ->icon('tabler-x') ->url(fn () => ListFiles::getUrl(['path' => dirname($this->path)])), @@ -118,7 +118,7 @@ class EditFiles extends Page ->footerActionsAlignment(Alignment::End) ->schema([ Select::make('lang') - ->label('Syntax Highlighting') + ->label(trans('server/file.actions.new_file.syntax')) ->searchable() ->native(false) ->live() @@ -133,7 +133,7 @@ class EditFiles extends Page try { return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size')); } catch (FileSizeTooLargeException) { - AlertBanner::make() + AlertBanner::make('file_too_large') ->title('' . basename($this->path) . ' is too large!') ->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size'))) ->danger() @@ -142,7 +142,7 @@ class EditFiles extends Page $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); } catch (FileNotFoundException) { - AlertBanner::make() + AlertBanner::make('file_not_found') ->title('' . basename($this->path) . ' not found!') ->danger() ->closable() @@ -150,7 +150,7 @@ class EditFiles extends Page $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); } catch (FileNotEditableException) { - AlertBanner::make() + AlertBanner::make('file_is_directory') ->title('' . basename($this->path) . ' is a directory') ->danger() ->closable() @@ -184,15 +184,6 @@ class EditFiles extends Page ->info() ->closable() ->send(); - - try { - $this->getDaemonFileRepository()->getDirectory('/'); - } catch (ConnectionException) { - AlertBanner::make('node_connection_error') - ->title('Could not connect to the node!') - ->danger() - ->send(); - } } } diff --git a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index d2473b235..419112f6e 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -90,14 +90,17 @@ class ListFiles extends ListRecords ->defaultSort('name') ->columns([ TextColumn::make('name') + ->label(trans('server/file.name')) ->searchable() ->sortable() ->icon(fn (File $file) => $file->getIcon()), BytesColumn::make('size') + ->label(trans('server/file.size')) ->visibleFrom('md') ->state(fn (File $file) => $file->is_directory ? null : $file->size) ->sortable(), DateTimeColumn::make('modified_at') + ->label(trans('server/file.modified_at')) ->visibleFrom('md') ->since() ->sortable(), @@ -116,7 +119,7 @@ class ListFiles extends ListRecords ->actions([ Action::make('view') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server)) - ->label('Open') + ->label(trans('server/file.actions.open')) ->icon('tabler-eye') ->visible(fn (File $file) => $file->is_directory) ->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])), @@ -128,11 +131,11 @@ class ListFiles extends ListRecords ActionGroup::make([ Action::make('rename') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->label('Rename') + ->label(trans('server/file.actions.rename.title')) ->icon('tabler-forms') ->form([ TextInput::make('name') - ->label('File name') + ->label(trans('server/file.actions.rename.name')) ->default(fn (File $file) => $file->name) ->required(), ]) @@ -149,14 +152,14 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('File Renamed') + ->title(trans('server/file.actions.rename.notification')) ->body(fn () => $file->name . ' -> ' . $data['name']) ->success() ->send(); }), Action::make('copy') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->label('Copy') + ->label(trans('server/file.actions.copy.title')) ->icon('tabler-copy') ->visible(fn (File $file) => $file->is_file) ->action(function (File $file) { @@ -167,7 +170,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('File copied') + ->title(trans('server/file.actions.copy.notification')) ->success() ->send(); @@ -175,18 +178,18 @@ class ListFiles extends ListRecords }), Action::make('download') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) - ->label('Download') + ->label(trans('server/file.actions.download')) ->icon('tabler-download') ->visible(fn (File $file) => $file->is_file) ->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true), Action::make('move') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->label('Move') + ->label(trans('server/file.actions.move.title')) ->icon('tabler-replace') ->form([ TextInput::make('location') - ->label('New location') - ->hint('Enter the location of this file or folder, relative to the current directory.') + ->label(trans('server/file.actions.move.new_location')) + ->hint(trans('server/file.actions.move.new_location_hint')) ->required() ->live(), Placeholder::make('new_location') @@ -209,22 +212,24 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('File Moved') + ->title(trans('server/file.actions.move.notification')) ->body($oldLocation . ' -> ' . $newLocation) ->success() ->send(); }), Action::make('permissions') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->label('Permissions') + ->label(trans('server/file.actions.permissions.title')) ->icon('tabler-license') ->form([ CheckboxList::make('owner') + ->label(trans('server/file.actions.permissions.owner')) ->bulkToggleable() + ->columns(3) ->options([ - 'read' => 'Read', - 'write' => 'Write', - 'execute' => 'Execute', + 'read' => trans('server/file.actions.permissions.read'), + 'write' => trans('server/file.actions.permissions.write'), + 'execute' => trans('server/file.actions.permissions.execute'), ]) ->formatStateUsing(function ($state, File $file) { $mode = (int) substr((string) $file->mode_bits, 0, 1); @@ -232,11 +237,13 @@ class ListFiles extends ListRecords return $this->getPermissionsFromModeBit($mode); }), CheckboxList::make('group') + ->label(trans('server/file.actions.permissions.group')) ->bulkToggleable() + ->columns(3) ->options([ - 'read' => 'Read', - 'write' => 'Write', - 'execute' => 'Execute', + 'read' => trans('server/file.actions.permissions.read'), + 'write' => trans('server/file.actions.permissions.write'), + 'execute' => trans('server/file.actions.permissions.execute'), ]) ->formatStateUsing(function ($state, File $file) { $mode = (int) substr((string) $file->mode_bits, 1, 1); @@ -244,11 +251,13 @@ class ListFiles extends ListRecords return $this->getPermissionsFromModeBit($mode); }), CheckboxList::make('public') + ->label(trans('server/file.actions.permissions.public')) ->bulkToggleable() + ->columns(3) ->options([ - 'read' => 'Read', - 'write' => 'Write', - 'execute' => 'Execute', + 'read' => trans('server/file.actions.permissions.read'), + 'write' => trans('server/file.actions.permissions.write'), + 'execute' => trans('server/file.actions.permissions.execute'), ]) ->formatStateUsing(function ($state, File $file) { $mode = (int) substr((string) $file->mode_bits, 2, 1); @@ -266,17 +275,17 @@ class ListFiles extends ListRecords $this->getDaemonFileRepository()->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]); Notification::make() - ->title('Permissions changed to ' . $mode) + ->title(trans('server/file.actions.permissions.notification', ['mode' => $mode])) ->success() ->send(); }), Action::make('archive') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) - ->label('Archive') + ->label(trans('server/file.actions.archive.title')) ->icon('tabler-archive') ->form([ TextInput::make('name') - ->label('Archive name') + ->label(trans('server/file.actions.archive.archive_name')) ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z') ->suffix('.tar.gz'), ]) @@ -290,7 +299,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('Archive created') + ->title(trans('server/file.actions.archive.notification')) ->body($archive['name']) ->success() ->send(); @@ -299,7 +308,7 @@ class ListFiles extends ListRecords }), Action::make('unarchive') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) - ->label('Unarchive') + ->label(trans('server/file.actions.unarchive.title')) ->icon('tabler-archive') ->visible(fn (File $file) => $file->isArchive()) ->action(function (File $file) { @@ -311,7 +320,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('Unarchive completed') + ->title(trans('server/file.actions.unarchive.notification')) ->success() ->send(); @@ -339,8 +348,8 @@ class ListFiles extends ListRecords ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) ->form([ TextInput::make('location') - ->label('Directory') - ->hint('Enter the new directory, relative to the current directory.') + ->label(trans('server/file.actions.move.directory')) + ->hint(trans('server/file.actions.move.directory_hint')) ->required() ->live(), Placeholder::make('new_location') @@ -358,7 +367,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location))) + ->title(trans('server/file.actions.move.bulk_notification', ['count' => count($files), 'directory' => resolve_path(join_paths($this->path, $location))])) ->success() ->send(); }), @@ -366,7 +375,7 @@ class ListFiles extends ListRecords ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) ->form([ TextInput::make('name') - ->label('Archive name') + ->label(trans('server/file.actions.archive.archive_name')) ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z') ->suffix('.tar.gz'), ]) @@ -382,7 +391,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title('Archive created') + ->title(trans('server/file.actions.archive.notification')) ->body($archive['name']) ->success() ->send(); @@ -401,7 +410,7 @@ class ListFiles extends ListRecords ->log(); Notification::make() - ->title(count($files) . ' Files deleted.') + ->title(trans('server/file.actions.delete.bulk_notification', ['count' => count($files)])) ->success() ->send(); }), @@ -417,10 +426,10 @@ class ListFiles extends ListRecords return [ HeaderAction::make('new_file') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->tooltip('New File') + ->tooltip(trans('server/file.actions.new_file.title')) ->hiddenLabel()->icon('tabler-file-plus')->iconButton()->iconSize(IconSize::Large) ->color('primary') - ->modalSubmitActionLabel('Create') + ->modalSubmitActionLabel(trans('server/file.actions.new_file.create')) ->action(function ($data) { $path = join_paths($this->path, $data['name']); try { @@ -430,7 +439,7 @@ class ListFiles extends ListRecords ->property('file', join_paths($path, $data['name'])) ->log(); } catch (FileExistsException) { - AlertBanner::make() + AlertBanner::make('file_already_exists') ->title('' . $path . ' already exists!') ->danger() ->closable() @@ -441,10 +450,10 @@ class ListFiles extends ListRecords }) ->form([ TextInput::make('name') - ->label('File Name') + ->label(trans('server/file.actions.new_file.file_name')) ->required(), Select::make('lang') - ->label('Syntax Highlighting') + ->label(trans('server/file.actions.new_file.syntax')) ->searchable() ->native(false) ->live() @@ -460,7 +469,7 @@ class ListFiles extends ListRecords HeaderAction::make('new_folder') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) ->hiddenLabel()->icon('tabler-folder-plus')->iconButton()->iconSize(IconSize::Large) - ->tooltip('New Folder') + ->tooltip(trans('server/file.actions.new_folder.title')) ->color('primary') ->action(function ($data) { try { @@ -471,7 +480,7 @@ class ListFiles extends ListRecords ->log(); } catch (FileExistsException) { $path = join_paths($this->path, $data['name']); - AlertBanner::make() + AlertBanner::make('folder_already_exists') ->title('' . $path . ' already exists!') ->danger() ->closable() @@ -482,13 +491,13 @@ class ListFiles extends ListRecords }) ->form([ TextInput::make('name') - ->label('Folder Name') + ->label(trans('server/file.actions.new_folder.folder_name')) ->required(), ]), HeaderAction::make('upload') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) ->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::Large) - ->tooltip('Upload') + ->tooltip(trans('server/file.actions.upload.title')) ->color('success') ->action(function ($data) { if (count($data['files']) > 0 && !isset($data['url'])) { @@ -516,7 +525,7 @@ class ListFiles extends ListRecords Tabs::make() ->contained(false) ->schema([ - Tab::make('Upload Files') + Tab::make(trans('server/file.actions.upload.from_files')) ->live() ->schema([ FileUpload::make('files') @@ -526,12 +535,12 @@ class ListFiles extends ListRecords ->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000))) ->multiple(), ]), - Tab::make('Upload From URL') + Tab::make(trans('server/file.actions.upload.url')) ->live() ->disabled(fn (Get $get) => count($get('files')) > 0) ->schema([ TextInput::make('url') - ->label('URL') + ->label(trans('server/file.actions.upload.url')) ->url(), ]), ]), @@ -539,14 +548,15 @@ class ListFiles extends ListRecords HeaderAction::make('search') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server)) ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) - ->tooltip('Global Search') + ->tooltip(trans('server/file.actions.global_search.title')) ->color('primary') ->icon('tabler-world-search') - ->modalHeading('Global Search') - ->modalSubmitActionLabel('Search') + ->modalHeading(trans('server/file.actions.global_search.title')) + ->modalSubmitActionLabel(trans('server/file.actions.global_search.search')) ->form([ TextInput::make('searchTerm') - ->placeholder('Enter a search term, e.g. *.txt') + ->label(trans('server/file.actions.global_search.search_term')) + ->placeholder(trans('server/file.actions.global_search.search_term_placeholder')) ->required() ->regex('/^[^*]*\*?[^*]*$/') ->minValue(3), @@ -594,4 +604,9 @@ class ListFiles extends ListRecords ->where('path', '.*'), ); } + + public function getTitle(): string + { + return trans('server/file.title'); + } } diff --git a/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php b/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php index 6b0d9aadf..2bb97c1b6 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/SearchFiles.php @@ -13,6 +13,7 @@ use Filament\Facades\Filament; use Filament\Resources\Pages\ListRecords; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; +use Illuminate\Contracts\Support\Htmlable; use Livewire\Attributes\Locked; use Livewire\Attributes\Url; @@ -23,8 +24,6 @@ class SearchFiles extends ListRecords protected static string $resource = FileResource::class; - protected static ?string $title = 'Global Search'; - #[Locked] public string $searchTerm; @@ -37,7 +36,7 @@ class SearchFiles extends ListRecords return [ $resource::getUrl() => $resource::getBreadcrumb(), - self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"', + self::getUrl(['searchTerm' => $this->searchTerm]) => trans('server/file.actions.global_search.search') . ' "' . $this->searchTerm . '"', ]; } @@ -51,10 +50,18 @@ class SearchFiles extends ListRecords ->query(fn () => File::get($server, $this->path, $this->searchTerm)->orderByDesc('is_directory')->orderBy('name')) ->columns([ TextColumn::make('name') + ->label(trans('server/file.name')) ->searchable() + ->sortable() ->icon(fn (File $file) => $file->getIcon()), - BytesColumn::make('size'), + BytesColumn::make('size') + ->label(trans('server/file.size')) + ->visibleFrom('md') + ->state(fn (File $file) => $file->size) + ->sortable(), DateTimeColumn::make('modified_at') + ->label(trans('server/file.modified_at')) + ->visibleFrom('md') ->since() ->sortable(), ]) @@ -66,4 +73,9 @@ class SearchFiles extends ListRecords return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null; }); } + + public function getTitle(): string|Htmlable + { + return trans('server/file.actions.global_search.title'); + } } diff --git a/app/Filament/Server/Resources/ScheduleResource.php b/app/Filament/Server/Resources/ScheduleResource.php index 8f641cac3..cd186e96c 100644 --- a/app/Filament/Server/Resources/ScheduleResource.php +++ b/app/Filament/Server/Resources/ScheduleResource.php @@ -85,21 +85,20 @@ class ScheduleResource extends Resource ]) ->schema([ TextInput::make('name') + ->label(trans('server/schedule.name')) ->columnSpanFull() - ->label('Schedule Name') - ->placeholder('A human readable identifier for this schedule.') ->autocomplete(false) ->required(), Toggle::make('only_when_online') - ->label('Only when Server is Online?') - ->hintIconTooltip('Only execute this schedule when the server is in a running state.') + ->label(trans('server/schedule.only_online')) + ->hintIconTooltip(trans('server/schedule.only_online_hint')) ->hintIcon('tabler-question-mark') ->inline(false) ->required() ->default(1), Toggle::make('is_active') - ->label('Enable Schedule?') - ->hintIconTooltip('This schedule will be executed automatically if enabled.') + ->label(trans('server/schedule.enabled')) + ->hintIconTooltip(trans('server/schedule.enabled_hint')) ->hintIcon('tabler-question-mark') ->inline(false) ->hiddenOn('view') @@ -107,7 +106,7 @@ class ScheduleResource extends Resource ->default(1), ToggleButtons::make('Status') ->formatStateUsing(fn (Schedule $schedule) => !$schedule->is_active ? 'inactive' : ($schedule->is_processing ? 'processing' : 'active')) - ->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => 'Inactive'] : ($schedule->is_processing ? ['processing' => 'Processing'] : ['active' => 'Active'])) + ->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => trans('server/schedule.inactive')] : ($schedule->is_processing ? ['processing' => trans('server/schedule.processing')] : ['active' => trans('server/schedule.active')])) ->colors([ 'inactive' => 'danger', 'processing' => 'warning', @@ -115,22 +114,35 @@ class ScheduleResource extends Resource ]) ->visibleOn('view'), Section::make('Cron') - ->description(fn (Get $get) => new HtmlString('Please keep in mind that the cron inputs below always assume UTC.
Next run in your timezone (' . auth()->user()->timezone . '): '. Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone) . '')) + ->label(trans('server/schedule.cron')) + ->description(function (Get $get) { + try { + $nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone); + } catch (Exception) { + $nextRun = trans('server/schedule.invalid'); + } + + return new HtmlString(trans('server/schedule.cron_body') . '
' . trans('server/schedule.cron_timezone', ['timezone' => auth()->user()->timezone, 'next_run' => $nextRun])); + }) ->schema([ Actions::make([ CronPresetAction::make('hourly') + ->label(trans('server/schedule.time.hourly')) ->cron('0', '*', '*', '*', '*'), CronPresetAction::make('daily') + ->label(trans('server/schedule.time.daily')) ->cron('0', '0', '*', '*', '*'), CronPresetAction::make('weekly_monday') - ->label('Weekly (Monday)') + ->label(trans('server/schedule.time.weekly_mon')) ->cron('0', '0', '*', '*', '1'), CronPresetAction::make('weekly_sunday') - ->label('Weekly (Sunday)') + ->label(trans('server/schedule.time.weekly_sun')) ->cron('0', '0', '*', '*', '0'), CronPresetAction::make('monthly') + ->label(trans('server/schedule.time.monthly')) ->cron('0', '0', '1', '*', '*'), CronPresetAction::make('every_x_minutes') + ->label(trans('server/schedule.time.every_min')) ->color(fn (Get $get) => str($get('cron_minute'))->startsWith('*/') && $get('cron_hour') == '*' && $get('cron_day_of_month') == '*' @@ -142,8 +154,8 @@ class ScheduleResource extends Resource ->numeric() ->minValue(1) ->maxValue(60) - ->prefix('Every') - ->suffix('Minutes'), + ->prefix(trans('server/schedule.time.every')) + ->suffix(trans('server/schedule.time.minutes')), ]) ->action(function (Set $set, $data) { $set('cron_minute', '*/' . $data['x']); @@ -164,8 +176,8 @@ class ScheduleResource extends Resource ->numeric() ->minValue(1) ->maxValue(24) - ->prefix('Every') - ->suffix('Hours'), + ->prefix(trans('server/schedule.time.every')) + ->suffix(trans('server/schedule.time.hours')), ]) ->action(function (Set $set, $data) { $set('cron_minute', '0'); @@ -186,8 +198,8 @@ class ScheduleResource extends Resource ->numeric() ->minValue(1) ->maxValue(24) - ->prefix('Every') - ->suffix('Days'), + ->prefix(trans('server/schedule.time.every')) + ->suffix(trans('server/schedule.time.days')), ]) ->action(function (Set $set, $data) { $set('cron_minute', '0'); @@ -208,8 +220,8 @@ class ScheduleResource extends Resource ->numeric() ->minValue(1) ->maxValue(24) - ->prefix('Every') - ->suffix('Months'), + ->prefix(trans('server/schedule.time.every')) + ->suffix(trans('server/schedule.time.months')), ]) ->action(function (Set $set, $data) { $set('cron_minute', '0'); @@ -227,15 +239,15 @@ class ScheduleResource extends Resource ->form([ Select::make('x') ->label('') - ->prefix('Every') + ->prefix(trans('server/schedule.time.every')) ->options([ - '1' => 'Monday', - '2' => 'Tuesday', - '3' => 'Wednesday', - '4' => 'Thursday', - '5' => 'Friday', - '6' => 'Saturday', - '0' => 'Sunday', + '1' => trans('server/schedule.time.monday'), + '2' => trans('server/schedule.time.tuesday'), + '3' => trans('server/schedule.time.wednesday'), + '4' => trans('server/schedule.time.thursday'), + '5' => trans('server/schedule.time.friday'), + '6' => trans('server/schedule.time.saturday'), + '0' => trans('server/schedule.time.sunday'), ]) ->selectablePlaceholder(false) ->native(false), @@ -251,47 +263,47 @@ class ScheduleResource extends Resource ->hiddenOn('view'), Group::make([ TextInput::make('cron_minute') + ->label(trans('server/schedule.time.minute')) ->columnSpan([ 'default' => 2, 'lg' => 1, ]) - ->label('Minute') ->default('*/5') ->required() ->live(), TextInput::make('cron_hour') + ->label(trans('server/schedule.time.hour')) ->columnSpan([ 'default' => 2, 'lg' => 1, ]) - ->label('Hour') ->default('*') ->required() ->live(), TextInput::make('cron_day_of_month') + ->label(trans('server/schedule.time.day_of_month')) ->columnSpan([ 'default' => 2, 'lg' => 1, ]) - ->label('Day of Month') ->default('*') ->required() ->live(), TextInput::make('cron_month') + ->label(trans('server/schedule.time.month')) ->columnSpan([ 'default' => 2, 'lg' => 1, ]) - ->label('Month') ->default('*') ->required() ->live(), TextInput::make('cron_day_of_week') + ->label(trans('server/schedule.time.day_of_week')) ->columnSpan([ 'default' => 2, 'lg' => 1, ]) - ->label('Day of Week') ->default('*') ->required() ->live(), @@ -309,22 +321,26 @@ class ScheduleResource extends Resource return $table ->columns([ TextColumn::make('name') + ->label(trans('server/schedule.name')) ->searchable(), TextColumn::make('cron') + ->label(trans('server/schedule.cron')) ->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week), TextColumn::make('status') - ->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')), + ->label(trans('server/schedule.status')) + ->state(fn (Schedule $schedule) => !$schedule->is_active ? trans('server/schedule.inactive') : ($schedule->is_processing ? trans('server/schedule.processing') : trans('server/schedule.active'))), IconColumn::make('only_when_online') + ->label(trans('server/schedule.online_only')) ->boolean() ->sortable(), DateTimeColumn::make('last_run_at') - ->label('Last run') - ->placeholder('Never') + ->label(trans('server/schedule.last_run')) + ->placeholder(trans('server/schedule.never')) ->since() ->sortable(), DateTimeColumn::make('next_run_at') - ->label('Next run') - ->placeholder('Never') + ->label(trans('server/schedule.next_run')) + ->placeholder(trans('server/schedule.never')) ->since() ->sortable() ->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null), @@ -367,11 +383,16 @@ class ScheduleResource extends Resource return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek); } catch (Exception) { Notification::make() - ->title('The cron data provided does not evaluate to a valid expression') + ->title(trans('server/schedule.notification_invalid_cron')) ->danger() ->send(); throw new Halt(); } } + + public static function getNavigationLabel(): string + { + return trans('server/schedule.title'); + } } diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php b/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php index 91e4b651e..e86124bd3 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/EditSchedule.php @@ -49,7 +49,7 @@ class EditSchedule extends EditRecord Actions\DeleteAction::make() ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-trash') - ->tooltip('Delete Schedule') + ->tooltip(trans('server/schedule.delete')) ->after(function ($record) { Activity::event('server:schedule.delete') ->property('name', $record->name) @@ -58,15 +58,15 @@ class EditSchedule extends EditRecord ExportScheduleAction::make() ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-download') - ->tooltip('Export Schedule'), + ->tooltip(trans('server/schedule.export')), $this->getSaveFormAction()->formId('form') ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-device-floppy') - ->tooltip('Save Schedule'), + ->tooltip(trans('server/schedule.save')), $this->getCancelFormAction()->formId('form') ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-cancel') - ->tooltip('Cancel'), + ->tooltip(trans('server/schedule.cancel')), ]; } diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php index eb83ca79a..62adebe39 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ListSchedules.php @@ -26,11 +26,11 @@ class ListSchedules extends ListRecords CreateAction::make() ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-calendar-plus') - ->tooltip('New Schedule'), + ->tooltip(trans('server/schedule.new')), ImportScheduleAction::make() ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-download') - ->tooltip('Import Schedule'), + ->tooltip(trans('server/schedule.import')), ]; } @@ -38,4 +38,9 @@ class ListSchedules extends ListRecords { return []; } + + public function getTitle(): string + { + return trans('server/schedule.title'); + } } diff --git a/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php b/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php index d89d2af02..2c55ef019 100644 --- a/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php +++ b/app/Filament/Server/Resources/ScheduleResource/Pages/ViewSchedule.php @@ -29,7 +29,7 @@ class ViewSchedule extends ViewRecord return [ Action::make('runNow') ->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant())) - ->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now')) + ->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? trans('server/schedule.no_tasks') : ($schedule->is_processing ? trans('server/schedule.processing') : trans('server/schedule.run_now'))) ->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary') ->disabled(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing) ->action(function (ProcessScheduleService $service, Schedule $schedule) { @@ -45,7 +45,7 @@ class ViewSchedule extends ViewRecord EditAction::make() ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-calendar-code') - ->tooltip('Edit Schedule'), + ->tooltip(trans('server/schedule.edit')), ]; } diff --git a/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php b/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php index 141818a54..29c430116 100644 --- a/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php +++ b/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php @@ -30,10 +30,10 @@ class TasksRelationManager extends RelationManager private function getActionOptions(bool $full = true): array { return [ - Task::ACTION_POWER => $full ? 'Send power action' : 'Power action', - Task::ACTION_COMMAND => $full ? 'Send command' : 'Command', - Task::ACTION_BACKUP => $full ? 'Create backup' : 'Files to ignore', - Task::ACTION_DELETE_FILES => $full ? 'Delete files' : 'Files to delete', + Task::ACTION_POWER => $full ? trans('server/schedule.tasks.actions.power.title') : trans('server/schedule.tasks.actions.power.action'), + Task::ACTION_COMMAND => $full ? trans('server/schedule.tasks.actions.command.title') : trans('server/schedule.tasks.actions.command.command'), + Task::ACTION_BACKUP => $full ? trans('server/schedule.tasks.actions.backup.title') : trans('server/schedule.tasks.actions.backup.files_to_ignore'), + Task::ACTION_DELETE_FILES => $full ? trans('server/schedule.tasks.actions.delete.title') : trans('server/schedule.tasks.actions.delete.files_to_delete'), ]; } @@ -44,6 +44,7 @@ class TasksRelationManager extends RelationManager { return [ Select::make('action') + ->label(trans('server/schedule.tasks.actions.title')) ->required() ->live() ->disableOptionWhen(fn (string $value) => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0) @@ -53,27 +54,29 @@ class TasksRelationManager extends RelationManager ->afterStateUpdated(fn ($state, Set $set) => $set('payload', $state === Task::ACTION_POWER ? 'restart' : null)), Textarea::make('payload') ->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER) - ->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? 'Payload'), + ->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? trans('server/schedule.tasks.payload')), Select::make('payload') ->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER) - ->label('Power Action') + ->label(trans('server/schedule.tasks.actions.power.action')) ->required() ->options([ - 'start' => 'Start', - 'restart' => 'Restart', - 'stop' => 'Stop', - 'kill' => 'Kill', + 'start' => trans('server/schedule.tasks.actions.power.start'), + 'restart' => trans('server/schedule.tasks.actions.power.restart'), + 'stop' => trans('server/schedule.tasks.actions.power.stop'), + 'kill' => trans('server/schedule.tasks.actions.power.kill'), ]) ->selectablePlaceholder(false) ->default('restart'), TextInput::make('time_offset') + ->label(trans('server/schedule.tasks.time_offset')) ->hidden(fn (Get $get) => config('queue.default') === 'sync' || $get('sequence_id') === 1) ->default(0) ->numeric() ->minValue(0) ->maxValue(900) - ->suffix('Seconds'), - Toggle::make('continue_on_failure'), + ->suffix(trans('server/schedule.tasks.seconds')), + Toggle::make('continue_on_failure') + ->label(trans('server/schedule.tasks.continue_on_failure')), ]; } @@ -87,17 +90,21 @@ class TasksRelationManager extends RelationManager ->defaultSort('sequence_id') ->columns([ TextColumn::make('action') + ->label(trans('server/schedule.tasks.actions.title')) ->state(fn (Task $task) => $this->getActionOptions()[$task->action] ?? $task->action), TextColumn::make('payload') + ->label(trans('server/schedule.tasks.payload')) ->state(fn (Task $task) => match ($task->payload) { 'start', 'restart', 'stop', 'kill' => mb_ucfirst($task->payload), default => explode(PHP_EOL, $task->payload) }) ->badge(), TextColumn::make('time_offset') + ->label(trans('server/schedule.tasks.time_offset')) ->hidden(fn () => config('queue.default') === 'sync') - ->suffix(' Seconds'), + ->suffix(' '. trans('server/schedule.tasks.seconds')), IconColumn::make('continue_on_failure') + ->label(trans('server/schedule.tasks.continue_on_failure')) ->boolean(), ]) ->actions([ @@ -133,7 +140,7 @@ class TasksRelationManager extends RelationManager ->headerActions([ CreateAction::make() ->createAnother(false) - ->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? 'Task Limit Reached' : 'Create Task') + ->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? trans('server/schedule.tasks.limit') : trans('server/schedule.tasks.create')) ->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10)) ->form($this->getTaskForm($schedule)) ->action(function ($data) use ($schedule) { diff --git a/app/Filament/Server/Resources/UserResource.php b/app/Filament/Server/Resources/UserResource.php index eb0e1a69d..1aceb74a4 100644 --- a/app/Filament/Server/Resources/UserResource.php +++ b/app/Filament/Server/Resources/UserResource.php @@ -91,14 +91,14 @@ class UserResource extends Resource foreach ($data['permissions'] as $permission) { $options[$permission] = str($permission)->headline(); - $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); + $descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); $permissionsArray[$data['name']][] = $permission; } $tabs[] = Tab::make(str($data['name'])->headline()) ->schema([ Section::make() - ->description(trans('server/users.permissions.' . $data['name'] . '_desc')) + ->description(trans('server/user.permissions.' . $data['name'] . '_desc')) ->icon($data['icon']) ->schema([ CheckboxList::make($data['name']) @@ -121,30 +121,33 @@ class UserResource extends Resource ->alignCenter()->circular() ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)), TextColumn::make('username') + ->label(trans('server/user.username')) ->searchable(), TextColumn::make('email') + ->label(trans('server/user.email')) ->searchable(), TextColumn::make('permissions') + ->label(trans('server/user.permissions.title')) ->state(fn (User $user) => count($server->subusers->where('user_id', $user->id)->first()->permissions)), ]) ->actions([ DeleteAction::make() - ->label('Remove User') + ->label(trans('server/user.delete')) ->hidden(fn (User $user) => auth()->user()->id === $user->id) ->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) { $subuser = $server->subusers->where('user_id', $user->id)->first(); $subuserDeletionService->handle($subuser, $server); Notification::make() - ->title('User Deleted!') + ->title(trans('server/user.notification_delete')) ->success() ->send(); }), EditAction::make() - ->label('Edit User') + ->label(trans('server/user.edit')) ->hidden(fn (User $user) => auth()->user()->id === $user->id) ->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server)) - ->modalHeading(fn (User $user) => 'Editing ' . $user->email) + ->modalHeading(fn (User $user) => trans('server/user.editing', ['user' => $user->email])) ->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) { $subuser = $server->subusers->where('user_id', $user->id)->first(); @@ -158,7 +161,7 @@ class UserResource extends Resource $subuserUpdateService->handle($subuser, $server, $permissions); Notification::make() - ->title('User Updated!') + ->title(trans('server/user.notification_edit')) ->success() ->send(); @@ -185,7 +188,7 @@ class UserResource extends Resource ]), Actions::make([ Action::make('assignAll') - ->label('Assign All') + ->label(trans('server/user.assign_all')) ->action(function (Set $set) use ($permissionsArray) { $permissions = $permissionsArray; foreach ($permissions as $key => $value) { @@ -231,4 +234,9 @@ class UserResource extends Resource 'index' => Pages\ListUsers::route('/'), ]; } + + public static function getNavigationLabel(): string + { + return trans('server/user.title'); + } } diff --git a/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php index 10243d3f5..313afb41b 100644 --- a/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Server/Resources/UserResource/Pages/ListUsers.php @@ -25,6 +25,7 @@ use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; use Filament\Support\Enums\IconSize; +use Illuminate\Contracts\Support\Htmlable; class ListUsers extends ListRecords { @@ -48,14 +49,14 @@ class ListUsers extends ListRecords foreach ($data['permissions'] as $permission) { $options[$permission] = str($permission)->headline(); - $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); + $descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); $permissionsArray[$data['name']][] = $permission; } $tabs[] = Tab::make(str($data['name'])->headline()) ->schema([ Section::make() - ->description(trans('server/users.permissions.' . $data['name'] . '_desc')) + ->description(trans('server/user.permissions.' . $data['name'] . '_desc')) ->icon($data['icon']) ->schema([ CheckboxList::make($data['name']) @@ -72,7 +73,7 @@ class ListUsers extends ListRecords Actions\CreateAction::make('invite') ->hiddenLabel()->iconButton()->iconSize(IconSize::Large) ->icon('tabler-user-plus') - ->tooltip('Invite User') + ->tooltip(trans('server/user.invite_user')) ->createAnother(false) ->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server)) ->form([ @@ -86,6 +87,7 @@ class ListUsers extends ListRecords ]) ->schema([ TextInput::make('email') + ->label(trans('server/user.email')) ->email() ->inlineLabel() ->columnSpan([ @@ -97,7 +99,7 @@ class ListUsers extends ListRecords ->required(), assignAll::make([ Action::make('assignAll') - ->label('Assign All') + ->label(trans('server/user.assign_all')) ->action(function (Set $set, Get $get) use ($permissionsArray) { $permissions = $permissionsArray; foreach ($permissions as $key => $value) { @@ -117,8 +119,8 @@ class ListUsers extends ListRecords ->schema($tabs), ]), ]) - ->modalHeading('Invite User') - ->modalSubmitActionLabel('Invite') + ->modalHeading(trans('server/user.invite_user')) + ->modalSubmitActionLabel(trans('server/user.action')) ->action(function (array $data, SubuserCreationService $service) use ($server) { $email = strtolower($data['email']); @@ -140,12 +142,12 @@ class ListUsers extends ListRecords ]); Notification::make() - ->title('User Invited!') + ->title(trans('server/user.notification_add')) ->success() ->send(); } catch (Exception $exception) { Notification::make() - ->title('Failed') + ->title(trans('server/user.notification_failed')) ->body($exception->getMessage()) ->danger() ->send(); @@ -160,4 +162,9 @@ class ListUsers extends ListRecords { return []; } + + public function getTitle(): string|Htmlable + { + return trans('server/user.title'); + } } diff --git a/app/Filament/Server/Widgets/ServerCpuChart.php b/app/Filament/Server/Widgets/ServerCpuChart.php index a9fd434f7..7a6268502 100644 --- a/app/Filament/Server/Widgets/ServerCpuChart.php +++ b/app/Filament/Server/Widgets/ServerCpuChart.php @@ -80,6 +80,6 @@ class ServerCpuChart extends ChartWidget public function getHeading(): string { - return 'CPU'; + return trans('server/console.labels.cpu'); } } diff --git a/app/Filament/Server/Widgets/ServerMemoryChart.php b/app/Filament/Server/Widgets/ServerMemoryChart.php index 704e61581..e5ef76bb2 100644 --- a/app/Filament/Server/Widgets/ServerMemoryChart.php +++ b/app/Filament/Server/Widgets/ServerMemoryChart.php @@ -80,6 +80,6 @@ class ServerMemoryChart extends ChartWidget public function getHeading(): string { - return 'Memory'; + return trans('server/console.labels.memory'); } } diff --git a/app/Filament/Server/Widgets/ServerNetworkChart.php b/app/Filament/Server/Widgets/ServerNetworkChart.php index 79c9a2d15..9cbb00f84 100644 --- a/app/Filament/Server/Widgets/ServerNetworkChart.php +++ b/app/Filament/Server/Widgets/ServerNetworkChart.php @@ -112,6 +112,6 @@ class ServerNetworkChart extends ChartWidget { $lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last(); - return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0); + return trans('server/console.labels.network') . ' - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0); } } diff --git a/app/Filament/Server/Widgets/ServerOverview.php b/app/Filament/Server/Widgets/ServerOverview.php index 510e42e5b..9259c9efa 100644 --- a/app/Filament/Server/Widgets/ServerOverview.php +++ b/app/Filament/Server/Widgets/ServerOverview.php @@ -20,14 +20,14 @@ class ServerOverview extends StatsOverviewWidget protected function getStats(): array { return [ - SmallStatBlock::make('Name', $this->server->name) + SmallStatBlock::make(trans('server/console.labels.name'), $this->server->name) ->copyOnClick(fn () => request()->isSecure()), - SmallStatBlock::make('Status', $this->status()), - SmallStatBlock::make('Address', $this->server?->allocation->address ?? 'None') + SmallStatBlock::make(trans('server/console.labels.status'), $this->status()), + SmallStatBlock::make(trans('server/console.labels.address'), $this->server?->allocation->address ?? 'None') ->copyOnClick(fn () => request()->isSecure()), - SmallStatBlock::make('CPU', $this->cpuUsage()), - SmallStatBlock::make('Memory', $this->memoryUsage()), - SmallStatBlock::make('Disk', $this->diskUsage()), + SmallStatBlock::make(trans('server/console.labels.cpu'), $this->cpuUsage()), + SmallStatBlock::make(trans('server/console.labels.memory'), $this->memoryUsage()), + SmallStatBlock::make(trans('server/console.labels.disk'), $this->diskUsage()), ]; } diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index df908c74f..ab706bf45 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -19,6 +19,7 @@ use App\Http\Controllers\Api\Client\ClientApiController; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use App\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest; use App\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest; +use App\Http\Requests\Api\Client\Servers\Backups\RenameBackupRequest; use Dedoc\Scramble\Attributes\Group; #[Group('Server - Backup')] @@ -195,6 +196,35 @@ class BackupController extends ClientApiController ]); } + /** + * Rename backup + * + * Updates the name of a backup for a server instance. + * + * @return array + * + * @throws \Throwable + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function rename(RenameBackupRequest $request, Server $server, Backup $backup): array + { + $oldName = $backup->name; + $newName = $request->input('name'); + + $backup->update(['name' => $newName]); + + if ($oldName !== $newName) { + Activity::event('server:backup.rename') + ->subject($backup) + ->property(['old_name' => $oldName, 'new_name' => $newName]) + ->log(); + } + + return $this->fractal->item($backup) + ->transformWith($this->getTransformer(BackupTransformer::class)) + ->toArray(); + } + /** * Restore backup * diff --git a/app/Http/Controllers/Api/Remote/ActivityProcessingController.php b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php index ab84eb626..1dae9407a 100644 --- a/app/Http/Controllers/Api/Remote/ActivityProcessingController.php +++ b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Api\Remote; +use App\Models\Node; use Carbon\Carbon; use Illuminate\Support\Str; use App\Models\User; @@ -14,7 +15,7 @@ class ActivityProcessingController extends Controller { public function __invoke(ActivityEventRequest $request): void { - /** @var \App\Models\Node $node */ + /** @var Node $node */ $node = $request->attributes->get('node'); $servers = $node->servers()->whereIn('uuid', $request->servers())->get()->keyBy('uuid'); @@ -22,7 +23,7 @@ class ActivityProcessingController extends Controller $logs = []; foreach ($request->input('data') as $datum) { - /** @var \App\Models\Server|null $server */ + /** @var Server|null $server */ $server = $servers->get($datum['server']); if (is_null($server) || !Str::startsWith($datum['event'], 'server:')) { continue; diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php index 84b6b5473..196e18394 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php @@ -2,7 +2,8 @@ namespace App\Http\Controllers\Api\Remote\Servers; -use Illuminate\Http\Request; +use App\Enums\ContainerStatus; +use App\Http\Requests\Api\Remote\ServerRequest; use App\Models\Server; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; @@ -12,11 +13,11 @@ class ServerContainersController extends Controller /** * Updates the server container's status on the Panel */ - public function status(Server $server, Request $request): JsonResponse + public function status(ServerRequest $request, Server $server): JsonResponse { - $status = fluent($request->json()->all())->get('data.new_state'); + $status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing; - cache()->put("servers.$server->uuid.container.status", $status, now()->addHour()); + cache()->put("servers.$server->uuid.status", $status, now()->addHour()); return new JsonResponse([]); } diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php index ba32d5553..52828487c 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php @@ -3,7 +3,10 @@ namespace App\Http\Controllers\Api\Remote\Servers; use App\Enums\ServerState; +use App\Http\Requests\Api\Remote\ServerRequest; +use App\Models\ActivityLog; use App\Models\Backup; +use App\Models\Node; use Illuminate\Http\Request; use App\Models\Server; use Illuminate\Http\JsonResponse; @@ -29,7 +32,7 @@ class ServerDetailsController extends Controller * Returns details about the server that allows daemon to self-recover and ensure * that the state of the server matches the Panel at all times. */ - public function __invoke(Server $server): JsonResponse + public function __invoke(ServerRequest $request, Server $server): JsonResponse { return new JsonResponse([ 'settings' => $this->configurationStructureService->handle($server), @@ -42,7 +45,7 @@ class ServerDetailsController extends Controller */ public function list(Request $request): ServerConfigurationCollection { - /** @var \App\Models\Node $node */ + /** @var Node $node */ $node = $request->attributes->get('node'); // Avoid run-away N+1 SQL queries by preloading the relationships that are used @@ -85,9 +88,9 @@ class ServerDetailsController extends Controller ->get(); $this->connection->transaction(function () use ($node, $servers) { - /** @var \App\Models\Server $server */ + /** @var Server $server */ foreach ($servers as $server) { - /** @var \App\Models\ActivityLog|null $activity */ + /** @var ActivityLog|null $activity */ $activity = $server->activity->first(); if (!$activity) { continue; diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php b/app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php index 513d26ef3..eec5744ea 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerInstallController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api\Remote\Servers; use App\Enums\ServerState; +use App\Http\Requests\Api\Remote\ServerRequest; use Illuminate\Http\Response; use App\Models\Server; use Illuminate\Http\JsonResponse; @@ -15,14 +16,12 @@ class ServerInstallController extends Controller /** * Returns installation information for a server. */ - public function index(Server $server): JsonResponse + public function index(ServerRequest $request, Server $server): JsonResponse { - $egg = $server->egg; - return new JsonResponse([ - 'container_image' => $egg->copy_script_container, - 'entrypoint' => $egg->copy_script_entry, - 'script' => $egg->copy_script_install, + 'container_image' => $server->egg->copy_script_container, + 'entrypoint' => $server->egg->copy_script_entry, + 'script' => $server->egg->copy_script_install, ]); } diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 514f7f60f..c49b9e534 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -2,12 +2,12 @@ namespace App\Http\Controllers\Api\Remote\Servers; +use App\Http\Requests\Api\Remote\ServerRequest; use App\Models\Server; use App\Repositories\Daemon\DaemonServerRepository; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; use App\Models\Allocation; -use App\Models\ServerTransfer; use Illuminate\Database\ConnectionInterface; use App\Http\Controllers\Controller; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -28,14 +28,23 @@ class ServerTransferController extends Controller * * @throws \Throwable */ - public function failure(Server $server): JsonResponse + public function failure(ServerRequest $request, Server $server): JsonResponse { $transfer = $server->transfer; if (is_null($transfer)) { throw new ConflictHttpException('Server is not being transferred.'); } - return $this->processFailedTransfer($transfer); + $this->connection->transaction(function () use ($transfer) { + $transfer->forceFill(['successful' => false])->saveOrFail(); + + if ($transfer->new_allocation || $transfer->new_additional_allocations) { + $allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations); + Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); + } + }); + + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** @@ -43,16 +52,17 @@ class ServerTransferController extends Controller * * @throws \Throwable */ - public function success(Server $server): JsonResponse + public function success(ServerRequest $request, Server $server): JsonResponse { $transfer = $server->transfer; if (is_null($transfer)) { throw new ConflictHttpException('Server is not being transferred.'); } - $data = []; - /** @var \App\Models\Server $server */ - $server = $this->connection->transaction(function () use ($server, $transfer, $data) { + /** @var Server $server */ + $server = $this->connection->transaction(function () use ($server, $transfer) { + $data = []; + if ($transfer->old_allocation || $transfer->old_additional_allocations) { $allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations); // Remove the old allocations for the server and re-assign the server to the new @@ -60,6 +70,7 @@ class ServerTransferController extends Controller Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); $data['allocation_id'] = $transfer->new_allocation; } + $data['node_id'] = $transfer->new_node; $server->update($data); @@ -82,24 +93,4 @@ class ServerTransferController extends Controller return new JsonResponse([], Response::HTTP_NO_CONTENT); } - - /** - * Release all the reserved allocations for this transfer and mark it as failed in - * the database. - * - * @throws \Throwable - */ - protected function processFailedTransfer(ServerTransfer $transfer): JsonResponse - { - $this->connection->transaction(function () use (&$transfer) { - $transfer->forceFill(['successful' => false])->saveOrFail(); - - if ($transfer->new_allocation || $transfer->new_additional_allocations) { - $allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations); - Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); - } - }); - - return new JsonResponse([], Response::HTTP_NO_CONTENT); - } } diff --git a/app/Http/Controllers/Auth/OAuthController.php b/app/Http/Controllers/Auth/OAuthController.php index 97e6a4f15..12c820706 100644 --- a/app/Http/Controllers/Auth/OAuthController.php +++ b/app/Http/Controllers/Auth/OAuthController.php @@ -2,37 +2,37 @@ namespace App\Http\Controllers\Auth; +use App\Extensions\OAuth\OAuthSchemaInterface; use App\Extensions\OAuth\OAuthService; use App\Filament\Pages\Auth\EditProfile; use App\Http\Controllers\Controller; use App\Models\User; -use App\Services\Users\UserUpdateService; +use App\Services\Users\UserCreationService; use Exception; use Filament\Notifications\Notification; -use Illuminate\Auth\AuthManager; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Laravel\Socialite\Contracts\User as OAuthUser; use Laravel\Socialite\Facades\Socialite; +use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse; class OAuthController extends Controller { public function __construct( - private readonly AuthManager $auth, - private readonly UserUpdateService $updateService, - private readonly OAuthService $oauthService + private readonly UserCreationService $userCreation, + private readonly OAuthService $oauthService, ) {} /** * Redirect user to the OAuth provider */ - public function redirect(string $driver): RedirectResponse + public function redirect(string $driver): SymfonyRedirectResponse|RedirectResponse { - // Driver is disabled - redirect to normal login if (!$this->oauthService->get($driver)->isEnabled()) { return redirect()->route('auth.login'); } - return Socialite::with($driver)->redirect(); + return Socialite::driver($driver)->redirect(); } /** @@ -40,8 +40,9 @@ class OAuthController extends Controller */ public function callback(Request $request, string $driver): RedirectResponse { - // Driver is disabled - redirect to normal login - if (!$this->oauthService->get($driver)?->isEnabled()) { + $driver = $this->oauthService->get($driver); + + if (!$driver || !$driver->isEnabled()) { return redirect()->route('auth.login'); } @@ -49,43 +50,89 @@ class OAuthController extends Controller if ($request->get('error')) { report($request->get('error_description') ?? $request->get('error')); - Notification::make() - ->title('Something went wrong') - ->body($request->get('error')) - ->danger() - ->persistent() - ->send(); - - return redirect()->route('auth.login'); + return $this->errorRedirect($request->get('error')); } - $oauthUser = Socialite::driver($driver)->user(); + $oauthUser = Socialite::driver($driver->getId())->user(); - // User is already logged in and wants to link a new OAuth Provider if ($request->user()) { - $oauth = $request->user()->oauth; - $oauth[$driver] = $oauthUser->getId(); - - $this->updateService->handle($request->user(), ['oauth' => $oauth]); + $this->linkUser($request->user(), $driver, $oauthUser); return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'], panel: 'app')); } - try { - $user = User::query()->whereJsonContains('oauth->'. $driver, $oauthUser->getId())->firstOrFail(); - - $this->auth->guard()->login($user, true); - } catch (Exception) { - // No user found - redirect to normal login - Notification::make() - ->title('No linked User found') - ->danger() - ->persistent() - ->send(); - - return redirect()->route('auth.login'); + $user = User::whereJsonContains('oauth->'. $driver->getId(), $oauthUser->getId())->first(); + if ($user) { + return $this->loginUser($user); } + return $this->handleMissingUser($driver, $oauthUser); + } + + private function linkUser(User $user, OAuthSchemaInterface $driver, OAuthUser $oauthUser): User + { + $oauth = $user->oauth; + $oauth[$driver->getId()] = $oauthUser->getId(); + + $user->update(['oauth' => $oauth]); + + return $user->refresh(); + } + + private function handleMissingUser(OAuthSchemaInterface $driver, OAuthUser $oauthUser): RedirectResponse + { + $email = $oauthUser->getEmail(); + + if (!$email) { + return $this->errorRedirect(); + } + + $user = User::whereEmail($email)->first(); + if ($user) { + if (!$driver->shouldLinkMissingUsers()) { + return $this->errorRedirect(); + } + + $user = $this->linkUser($user, $driver, $oauthUser); + } else { + if (!$driver->shouldCreateMissingUsers()) { + return $this->errorRedirect(); + } + + try { + $user = $this->userCreation->handle([ + 'username' => $oauthUser->getNickname(), + 'email' => $email, + 'oauth' => [ + $driver->getId() => $oauthUser->getId(), + ], + ]); + } catch (Exception $exception) { + report($exception); + + return $this->errorRedirect(); + } + } + + return $this->loginUser($user); + } + + private function loginUser(User $user): RedirectResponse + { + auth()->guard()->login($user, true); + return redirect('/'); } + + private function errorRedirect(?string $error = null): RedirectResponse + { + Notification::make() + ->title($error ? 'Something went wrong' : 'No linked User found') + ->body($error) + ->danger() + ->persistent() + ->send(); + + return redirect()->route('auth.login'); + } } diff --git a/app/Http/Requests/Api/Client/Servers/Backups/RenameBackupRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/RenameBackupRequest.php new file mode 100644 index 000000000..5978c5ed2 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Backups/RenameBackupRequest.php @@ -0,0 +1,21 @@ + 'required|string|max:255', + ]; + } +} diff --git a/app/Http/Requests/Api/Remote/InstallationDataRequest.php b/app/Http/Requests/Api/Remote/InstallationDataRequest.php index 99adcef5f..59d52c978 100644 --- a/app/Http/Requests/Api/Remote/InstallationDataRequest.php +++ b/app/Http/Requests/Api/Remote/InstallationDataRequest.php @@ -2,15 +2,8 @@ namespace App\Http\Requests\Api\Remote; -use Illuminate\Foundation\Http\FormRequest; - -class InstallationDataRequest extends FormRequest +class InstallationDataRequest extends ServerRequest { - public function authorize(): bool - { - return true; - } - /** * @return array */ diff --git a/app/Http/Requests/Api/Remote/ServerRequest.php b/app/Http/Requests/Api/Remote/ServerRequest.php new file mode 100644 index 000000000..32e1d2b1e --- /dev/null +++ b/app/Http/Requests/Api/Remote/ServerRequest.php @@ -0,0 +1,21 @@ +attributes->get('node'); + + /** @var ?Server $server */ + $server = $this->route()->parameter('server'); + + return $server && $server->node_id === $node->id; + } +} diff --git a/app/Jobs/ProcessWebhook.php b/app/Jobs/ProcessWebhook.php index f499518a8..5efac6c84 100644 --- a/app/Jobs/ProcessWebhook.php +++ b/app/Jobs/ProcessWebhook.php @@ -28,14 +28,14 @@ class ProcessWebhook implements ShouldQueue public function handle(): void { - $data = $this->data[0]; + $data = $this->data[0] ?? []; + if (count($data) === 1) { + $data = reset($data); + } + $data = is_array($data) ? $data : (json_decode($data, true) ?? []); + $data['event'] = $this->webhookConfiguration->transformClassName($this->eventName); if ($this->webhookConfiguration->type === WebhookType::Discord) { - $data = array_merge( - is_array($data) ? $data : json_decode($data, true), - ['event' => $this->webhookConfiguration->transformClassName($this->eventName)] - ); - $payload = json_encode($this->webhookConfiguration->payload); $tmp = $this->webhookConfiguration->replaceVars($data, $payload); $data = json_decode($tmp, true); @@ -53,9 +53,10 @@ class ProcessWebhook implements ShouldQueue } try { + $customHeaders = $this->webhookConfiguration->headers; $headers = []; - if ($this->webhookConfiguration->type === WebhookType::Regular && $customHeaders = $this->webhookConfiguration->headers) { - $headers = array_merge(['X-Webhook-Event', $this->eventName], $customHeaders); + foreach ($customHeaders as $key => $value) { + $headers[$key] = $this->webhookConfiguration->replaceVars($data, $value); } Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw(); diff --git a/app/Livewire/AlertBanner.php b/app/Livewire/AlertBanner.php index 773046081..813012945 100644 --- a/app/Livewire/AlertBanner.php +++ b/app/Livewire/AlertBanner.php @@ -4,25 +4,32 @@ namespace App\Livewire; use Closure; use Filament\Notifications\Concerns; -use Filament\Support\Concerns\EvaluatesClosures; -use Illuminate\Support\Str; -use Livewire\Wireable; +use Filament\Support\Components\ViewComponent; +use Illuminate\Contracts\Support\Arrayable; -final class AlertBanner implements Wireable +final class AlertBanner extends ViewComponent implements Arrayable { use Concerns\HasBody; use Concerns\HasIcon; use Concerns\HasId; use Concerns\HasStatus; use Concerns\HasTitle; - use EvaluatesClosures; protected bool|Closure $closable = false; - public static function make(?string $id = null): AlertBanner + protected string $view = 'livewire.alerts.alert-banner'; + + protected string $viewIdentifier = 'alert-banner'; + + public function __construct(string $id) { - $static = new self(); - $static->id($id ?? Str::orderedUuid()); + $this->id($id); + } + + public static function make(string $id): AlertBanner + { + $static = new self($id); + $static->configure(); return $static; } @@ -30,7 +37,7 @@ final class AlertBanner implements Wireable /** * @return array{id: string, title: ?string, body: ?string, status: ?string, icon: ?string, closeable: bool} */ - public function toLivewire(): array + public function toArray(): array { return [ 'id' => $this->getId(), @@ -42,15 +49,18 @@ final class AlertBanner implements Wireable ]; } - public static function fromLivewire(mixed $value): AlertBanner + /** + * @param array{id: string, title: ?string, body: ?string, status: ?string, icon: ?string, closeable: bool} $data + */ + public static function fromArray(array $data): AlertBanner { - $static = AlertBanner::make($value['id']); + $static = AlertBanner::make($data['id']); - $static->title($value['title']); - $static->body($value['body']); - $static->status($value['status']); - $static->icon($value['icon']); - $static->closable($value['closeable']); + $static->title($data['title']); + $static->body($data['body']); + $static->status($data['status']); + $static->icon($data['icon']); + $static->closable($data['closeable']); return $static; } @@ -69,7 +79,7 @@ final class AlertBanner implements Wireable public function send(): AlertBanner { - session()->push('alert-banners', $this->toLivewire()); + session()->push('alert-banners', $this->toArray()); return $this; } diff --git a/app/Livewire/AlertBannerContainer.php b/app/Livewire/AlertBannerContainer.php index d5d7bf60c..66dba002d 100644 --- a/app/Livewire/AlertBannerContainer.php +++ b/app/Livewire/AlertBannerContainer.php @@ -2,18 +2,18 @@ namespace App\Livewire; +use Filament\Notifications\Collection; use Illuminate\Contracts\View\View; use Livewire\Attributes\On; use Livewire\Component; class AlertBannerContainer extends Component { - /** @var array */ - public array $alertBanners; + public Collection $alertBanners; public function mount(): void { - $this->alertBanners = []; + $this->alertBanners = new Collection(); $this->pullFromSession(); } @@ -21,15 +21,16 @@ class AlertBannerContainer extends Component public function pullFromSession(): void { foreach (session()->pull('alert-banners', []) as $alertBanner) { - $alertBanner = AlertBanner::fromLivewire($alertBanner); - $this->alertBanners[$alertBanner->getId()] = $alertBanner; + $alertBanner = AlertBanner::fromArray($alertBanner); + $this->alertBanners->put($alertBanner->getId(), $alertBanner); } } public function remove(string $id): void { - $alertBanners = &$this->alertBanners; - unset($alertBanners[$id]); + if ($this->alertBanners->has($id)) { + $this->alertBanners->forget($id); + } } public function render(): View diff --git a/app/Livewire/ServerEntry.php b/app/Livewire/ServerEntry.php index 958c6bbbc..42d44e13d 100644 --- a/app/Livewire/ServerEntry.php +++ b/app/Livewire/ServerEntry.php @@ -24,37 +24,40 @@ class ServerEntry extends Component style="background-color: #D97706;"> -
+
- +

{{ $server->name }} + + ({{ trans('server/dashboard.loading') }}) +

-
+
-

CPU

+

{{ trans('server/dashboard.cpu') }}

{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}


-

{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}

-

Memory

+

{{ trans('server/dashboard.memory') }}

{{ convert_bytes_to_readable(0, decimals: 2) }}


-

{{ $server->formatResource('memory', limit: true) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}

-

Disk

+

{{ trans('server/dashboard.disk') }}

{{ convert_bytes_to_readable(0, decimals: 2) }}


-

{{ $server->formatResource('disk', limit: true) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}

diff --git a/app/Models/Egg.php b/app/Models/Egg.php index a68d21311..dfa42e068 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -69,7 +69,7 @@ class Egg extends Model implements Validatable /** * Defines the current egg export version. */ - public const EXPORT_VERSION = 'PLCN_v1'; + public const EXPORT_VERSION = 'PLCN_v2'; /** * Fields that are not mass assignable. diff --git a/app/Models/File.php b/app/Models/File.php index b8d9cfef2..759f5e15b 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -194,7 +194,7 @@ class File extends Model $message = str('Node connection failed'); } - AlertBanner::make() + AlertBanner::make('files_node_error') ->title('Could not load files!') ->body($message->toString()) ->danger() diff --git a/app/Models/Server.php b/app/Models/Server.php index 58ad0e089..7c1c2205a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -462,17 +462,15 @@ class Server extends Model implements Validatable }); } - public function formatResource(string $resourceKey, bool $limit = false, ServerResourceType $type = ServerResourceType::Unit, int $precision = 2): string + public function formatResource(ServerResourceType $resourceType): string { - $resourceAmount = $this->{$resourceKey} ?? 0; - if (!$limit) { - $resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0; - } + $resourceAmount = $resourceType->getResourceAmount($this); - if ($type === ServerResourceType::Time) { - if ($this->isSuspended()) { - return 'Suspended'; + if ($resourceType->isTime()) { + if (!is_null($this->status)) { + return $this->status->getLabel(); } + if ($resourceAmount === 0) { return ContainerStatus::Offline->getLabel(); } @@ -480,20 +478,16 @@ class Server extends Model implements Validatable return now()->subMillis($resourceAmount)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 4); } - if ($resourceAmount === 0 & $limit) { + if ($resourceAmount === 0 & $resourceType->isLimit()) { + // Unlimited symbol return "\u{221E}"; } - if ($type === ServerResourceType::Percentage) { - return Number::format($resourceAmount, precision: $precision, locale: auth()->user()->language ?? 'en') . '%'; + if ($resourceType->isPercentage()) { + return Number::format($resourceAmount, precision: 2, locale: auth()->user()->language ?? 'en') . '%'; } - // Our current limits are set in MB - if ($limit) { - $resourceAmount *= 2 ** 20; - } - - return convert_bytes_to_readable($resourceAmount, decimals: $precision, base: 3); + return convert_bytes_to_readable($resourceAmount, base: 3); } public function condition(): Attribute diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a36ab4247..ec8e4f4fb 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -101,24 +101,24 @@ class AppServiceProvider extends ServiceProvider 'blurple' => Color::hex('#5865F2'), ]); - FilamentView::registerRenderHook( - PanelsRenderHook::HEAD_START, - fn () => Blade::render('filament.layouts.header') - ); - FilamentView::registerRenderHook( PanelsRenderHook::PAGE_START, fn () => Blade::render('@livewire(\App\Livewire\AlertBannerContainer::class)'), ); FilamentView::registerRenderHook( - PanelsRenderHook::BODY_END, - fn () => Blade::render('filament.layouts.body-end'), + PanelsRenderHook::FOOTER, + fn () => Blade::render('filament.layouts.footer'), ); FilamentView::registerRenderHook( - PanelsRenderHook::FOOTER, - fn () => Blade::render('filament.layouts.footer'), + PanelsRenderHook::STYLES_BEFORE, + fn () => Blade::render("@vite(['resources/css/app.css'])") + ); + + FilamentView::registerRenderHook( + PanelsRenderHook::SCRIPTS_AFTER, + fn () => Blade::render("@vite(['resources/js/app.js'])"), ); on('dehydrate', function (Component $component) { diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 5233d0a5e..a35e6ce84 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,45 +2,21 @@ namespace App\Providers\Filament; -use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\EditProfile; -use App\Http\Middleware\LanguageMiddleware; -use App\Http\Middleware\RequireTwoFactorAuthentication; -use Filament\Http\Middleware\Authenticate; -use Filament\Http\Middleware\DisableBladeIconComponents; -use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\MenuItem; use Filament\Navigation\NavigationGroup; use Filament\Panel; -use Filament\PanelProvider; -use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; -use Illuminate\Routing\Middleware\SubstituteBindings; -use Illuminate\Session\Middleware\AuthenticateSession; -use Illuminate\Session\Middleware\StartSession; -use Illuminate\View\Middleware\ShareErrorsFromSession; class AdminPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { - return $panel - ->default() + return parent::panel($panel) ->id('admin') ->path('admin') ->homeUrl('/') - ->spa() - ->databaseNotifications() ->breadcrumbs(false) - ->brandName(config('app.name', 'Pelican')) - ->brandLogo(config('app.logo')) - ->brandLogoHeight('2rem') - ->favicon(config('app.favicon', '/pelican.ico')) - ->topNavigation(config('panel.filament.top-navigation', false)) - ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) - ->login(Login::class) - ->passwordReset() + ->sidebarCollapsibleOnDesktop() ->userMenuItems([ 'profile' => MenuItem::make() ->label(fn () => trans('filament-panels::pages/auth/edit-profile.label')) @@ -58,25 +34,8 @@ class AdminPanelProvider extends PanelProvider ->collapsible(false), NavigationGroup::make(fn () => trans('admin/dashboard.advanced')), ]) - ->sidebarCollapsibleOnDesktop() ->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources') ->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages') - ->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets') - ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - LanguageMiddleware::class, - RequireTwoFactorAuthentication::class, - ]) - ->authMiddleware([ - Authenticate::class, - ]); + ->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets'); } } diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index 44ac35ec9..8d7b2de6c 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -2,68 +2,27 @@ namespace App\Providers\Filament; -use App\Filament\Pages\Auth\Login; -use App\Filament\Pages\Auth\EditProfile; -use App\Http\Middleware\LanguageMiddleware; -use App\Http\Middleware\RequireTwoFactorAuthentication; use Filament\Facades\Filament; -use Filament\Http\Middleware\Authenticate; -use Filament\Http\Middleware\DisableBladeIconComponents; -use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\MenuItem; use Filament\Panel; -use Filament\PanelProvider; -use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; -use Illuminate\Routing\Middleware\SubstituteBindings; -use Illuminate\Session\Middleware\AuthenticateSession; -use Illuminate\Session\Middleware\StartSession; -use Illuminate\View\Middleware\ShareErrorsFromSession; class AppPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { - return $panel + return parent::panel($panel) ->id('app') - ->spa() - ->databaseNotifications() + ->default() ->breadcrumbs(false) - ->brandName(config('app.name', 'Pelican')) - ->brandLogo(config('app.logo')) - ->brandLogoHeight('2rem') - ->favicon(config('app.favicon', '/pelican.ico')) - ->topNavigation(config('panel.filament.top-navigation', false)) - ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) ->navigation(false) - ->profile(EditProfile::class, false) - ->login(Login::class) - ->passwordReset() ->userMenuItems([ MenuItem::make() - ->label('Admin') + ->label(trans('profile.admin')) ->url('/admin') ->icon('tabler-arrow-forward') ->sort(5) - ->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))), + ->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin'))), ]) - ->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources') - ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - LanguageMiddleware::class, - RequireTwoFactorAuthentication::class, - ]) - ->authMiddleware([ - Authenticate::class, - ]); + ->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources'); } } diff --git a/app/Providers/Filament/PanelProvider.php b/app/Providers/Filament/PanelProvider.php new file mode 100644 index 000000000..8edf38a76 --- /dev/null +++ b/app/Providers/Filament/PanelProvider.php @@ -0,0 +1,55 @@ +spa() + ->databaseNotifications() + ->brandName(config('app.name', 'Pelican')) + ->brandLogo(config('app.logo')) + ->brandLogoHeight('2rem') + ->favicon(config('app.favicon', '/pelican.ico')) + ->topNavigation(fn () => auth()->user()->getCustomization()['top_navigation'] ?? false) + ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) + ->profile(EditProfile::class, false) + ->login(Login::class) + ->passwordReset() + ->middleware([ + EncryptCookies::class, + AddQueuedCookiesToResponse::class, + StartSession::class, + AuthenticateSession::class, + ShareErrorsFromSession::class, + VerifyCsrfToken::class, + SubstituteBindings::class, + DisableBladeIconComponents::class, + DispatchServingFilamentEvent::class, + LanguageMiddleware::class, + RequireTwoFactorAuthentication::class, + ]) + ->authMiddleware([ + Authenticate::class, + ]); + } +} diff --git a/app/Providers/Filament/ServerPanelProvider.php b/app/Providers/Filament/ServerPanelProvider.php index 035faaa15..9c2901ad6 100644 --- a/app/Providers/Filament/ServerPanelProvider.php +++ b/app/Providers/Filament/ServerPanelProvider.php @@ -3,48 +3,24 @@ namespace App\Providers\Filament; use App\Filament\App\Resources\ServerResource\Pages\ListServers; -use App\Filament\Pages\Auth\Login; use App\Filament\Admin\Resources\ServerResource\Pages\EditServer; use App\Filament\Pages\Auth\EditProfile; use App\Http\Middleware\Activity\ServerSubject; -use App\Http\Middleware\LanguageMiddleware; -use App\Http\Middleware\RequireTwoFactorAuthentication; use App\Models\Server; use Filament\Facades\Filament; -use Filament\Http\Middleware\Authenticate; -use Filament\Http\Middleware\DisableBladeIconComponents; -use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\MenuItem; use Filament\Navigation\NavigationItem; use Filament\Panel; -use Filament\PanelProvider; -use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; -use Illuminate\Routing\Middleware\SubstituteBindings; -use Illuminate\Session\Middleware\AuthenticateSession; -use Illuminate\Session\Middleware\StartSession; -use Illuminate\View\Middleware\ShareErrorsFromSession; class ServerPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { - return $panel + return parent::panel($panel) ->id('server') ->path('server') ->homeUrl('/') - ->spa() - ->databaseNotifications() ->tenant(Server::class) - ->brandName(config('app.name', 'Pelican')) - ->brandLogo(config('app.logo')) - ->brandLogoHeight('2rem') - ->favicon(config('app.favicon', '/pelican.ico')) - ->topNavigation(config('panel.filament.top-navigation', false)) - ->maxContentWidth(config('panel.filament.display-width', 'screen-2xl')) - ->login(Login::class) - ->passwordReset() ->userMenuItems([ 'profile' => MenuItem::make() ->label(fn () => trans('filament-panels::pages/auth/edit-profile.label')) @@ -55,14 +31,14 @@ class ServerPanelProvider extends PanelProvider ->url(fn () => ListServers::getUrl(panel: 'app')) ->sort(6), MenuItem::make() - ->label('Admin') + ->label(trans('profile.admin')) ->icon('tabler-arrow-forward') ->url(fn () => Filament::getPanel('admin')->getUrl()) ->sort(5) - ->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))), + ->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin'))), ]) ->navigationItems([ - NavigationItem::make('Open in Admin') + NavigationItem::make(trans('server/console.open_in_admin')) ->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin')) ->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) && auth()->user()->can('view server', Filament::getTenant())) ->icon('tabler-arrow-back') @@ -72,21 +48,7 @@ class ServerPanelProvider extends PanelProvider ->discoverPages(in: app_path('Filament/Server/Pages'), for: 'App\\Filament\\Server\\Pages') ->discoverWidgets(in: app_path('Filament/Server/Widgets'), for: 'App\\Filament\\Server\\Widgets') ->middleware([ - EncryptCookies::class, - AddQueuedCookiesToResponse::class, - StartSession::class, - AuthenticateSession::class, - ShareErrorsFromSession::class, - VerifyCsrfToken::class, - SubstituteBindings::class, - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - LanguageMiddleware::class, - RequireTwoFactorAuthentication::class, ServerSubject::class, - ]) - ->authMiddleware([ - Authenticate::class, ]); } } diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index c72d6e7c5..52e0068df 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -20,7 +20,14 @@ class EggConfigurationService * @return array{ * startup: array{done: string[], user_interaction: string[], strip_ansi: bool}, * stop: array{type: string, value: string}, - * configs: array + * configs: list + * }> * } */ public function handle(Server $server): array @@ -81,9 +88,10 @@ class EggConfigurationService } /** - * @return array + * @param array $configs + * @return array> */ - protected function replacePlaceholders(Server $server, object $configs): array + protected function replacePlaceholders(Server $server, object|array $configs): array { // Get the legacy configuration structure for the server so that we // can property map the egg placeholders to values. diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index d41591f0b..553e8befe 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -2,17 +2,19 @@ namespace App\Services\Eggs\Sharing; +use App\Enums\EggFormat; use Carbon\Carbon; use App\Models\Egg; use Illuminate\Support\Collection; use App\Models\EggVariable; +use Symfony\Component\Yaml\Yaml; class EggExporterService { /** - * Return a JSON representation of an egg and its variables. + * Return a JSON or YAML representation of an egg and its variables. */ - public function handle(int $egg): string + public function handle(int $egg, EggFormat $format): string { $egg = Egg::with(['scriptFrom', 'configFrom', 'variables'])->findOrFail($egg); @@ -30,9 +32,7 @@ class EggExporterService 'tags' => $egg->tags, 'features' => $egg->features, 'docker_images' => $egg->docker_images, - 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(function ($value) { - return !empty($value); - }), + 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(), 'startup' => $egg->startup, 'config' => [ 'files' => $egg->inherit_config_files, @@ -50,9 +50,50 @@ class EggExporterService 'variables' => $egg->variables->map(function (EggVariable $eggVariable) { return Collection::make($eggVariable->toArray()) ->except(['id', 'egg_id', 'created_at', 'updated_at']); - }), + })->values()->toArray(), ]; - return json_encode($struct, JSON_PRETTY_PRINT); + return match ($format) { + EggFormat::JSON => json_encode($struct, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), + EggFormat::YAML => Yaml::dump($this->yamlExport($struct), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | Yaml::DUMP_OBJECT_AS_MAP), + }; + } + + protected function yamlExport(mixed $data): mixed + { + if ($data instanceof Collection) { + $data = $data->all(); + } + + if (is_string($data)) { + $decoded = json_decode($data, true); + if (json_last_error() === JSON_ERROR_NONE) { + return $this->yamlExport($decoded); + } + + return str_replace(["\r\n", '\\r\\n', '\\n'], "\n", $data); + } + + if (is_array($data)) { + $result = []; + + foreach ($data as $key => $value) { + if ( + is_string($value) && + strtolower($key) === 'description' && + (str_contains($value, "\n") || strlen($value) > 80) + ) { + $value = wordwrap($value, 100, "\n"); + } else { + $value = $this->yamlExport($value); + } + + $result[$key] = $value; + } + + return $result; + } + + return $data; } } diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 31fb7f44f..e5d8f5eea 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -3,6 +3,7 @@ namespace App\Services\Eggs\Sharing; use App\Exceptions\Service\InvalidFileUploadException; +use JsonException; use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; use App\Models\Egg; @@ -11,6 +12,9 @@ use App\Models\EggVariable; use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Collection; use Spatie\TemporaryDirectory\TemporaryDirectory; +use stdClass; +use Symfony\Component\Yaml\Yaml; +use Throwable; class EggImporterService { @@ -28,9 +32,9 @@ class EggImporterService public function __construct(protected ConnectionInterface $connection) {} /** - * Take an uploaded JSON file and parse it into a new egg. + * Take an uploaded JSON or YAML file and parse it into a new egg. * - * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable + * @throws InvalidFileUploadException|Throwable */ public function fromFile(UploadedFile $file, ?Egg $egg = null): Egg { @@ -46,7 +50,6 @@ class EggImporterService 'copy_script_from' => null, ]); - // Don't check for this anymore for ($i = 0; $i < count($parsed['variables']); $i++) { unset($parsed['variables'][$i]['field_type']); } @@ -54,7 +57,6 @@ class EggImporterService $egg = $this->fillFromParsed($egg, $parsed); $egg->save(); - // Update existing variables or create new ones. foreach ($parsed['variables'] ?? [] as $variable) { EggVariable::unguarded(function () use ($egg, $variable) { $variable['rules'] = is_array($variable['rules']) ? $variable['rules'] : explode('|', $variable['rules']); @@ -66,7 +68,6 @@ class EggImporterService } $imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []); - $egg->variables()->whereNotIn('env_variable', $imported)->delete(); return $egg->refresh(); @@ -74,31 +75,39 @@ class EggImporterService } /** - * Take an url and parse it into a new egg or update an existing one. + * Take a URL (YAML or JSON) and parse it into a new egg or update an existing one. * - * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable + * @throws InvalidFileUploadException|Throwable */ public function fromUrl(string $url, ?Egg $egg = null): Egg { $info = pathinfo($url); + $extension = strtolower($info['extension']); + $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); $tmpPath = $tmpDir->path($info['basename']); - if (!file_put_contents($tmpPath, file_get_contents($url))) { - throw new InvalidFileUploadException('Could not write temporary file.'); + $fileContents = @file_get_contents($url); + + if (!$fileContents || !file_put_contents($tmpPath, $fileContents)) { + throw new InvalidFileUploadException('Could not download or write temporary file.'); } - return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'), $egg); + $mime = match ($extension) { + 'yaml', 'yml' => 'application/yaml', + 'json' => 'application/json', + default => throw new InvalidFileUploadException('Unsupported file format.'), + }; + + return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], $mime), $egg); } /** * Takes an uploaded file and parses out the egg configuration from within. * - * @todo replace with DTO - * * @return array * - * @throws \App\Exceptions\Service\InvalidFileUploadException + * @throws InvalidFileUploadException|JsonException */ protected function parseFile(UploadedFile $file): array { @@ -106,30 +115,56 @@ class EggImporterService throw new InvalidFileUploadException('The selected file was not uploaded successfully'); } + $extension = strtolower($file->getClientOriginalExtension()); + $mime = $file->getMimeType(); + try { - $parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR); - } catch (\JsonException $exception) { - throw new InvalidFileUploadException('Could not read JSON file: ' . $exception->getMessage()); + $content = $file->getContent(); + + $parsed = match (true) { + in_array($extension, ['yaml', 'yml']), + str_contains($mime, 'yaml') => Yaml::parse($content), + default => json_decode($content, true, 512, JSON_THROW_ON_ERROR), + }; + } catch (Throwable $e) { + throw new InvalidFileUploadException('File parse failed: ' . $e->getMessage()); } $version = $parsed['meta']['version'] ?? ''; $parsed = match ($version) { 'PTDL_v1' => $this->convertToV2($parsed), - 'PTDL_v2' => $parsed, - 'PLCN_v1' => $parsed, - default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.') + 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $parsed, + default => throw new InvalidFileUploadException('The file format is not recognized.'), }; - // Make sure we only use recent variable format from now on - if (array_get($parsed['config'], 'files')) { - $parsed['config']['files'] = str_replace( - array_keys(self::UPGRADE_VARIABLES), - array_values(self::UPGRADE_VARIABLES), - $parsed['config']['files'], - ); + if (isset($parsed['config']) && (is_array($parsed['config']) || $parsed['config'] instanceof stdClass)) { + $parsed['config'] = (array) $parsed['config']; + foreach ($parsed['config'] as $key => $value) { + if (is_array($value) || $value instanceof stdClass) { + $parsed['config'][$key] = json_encode((array) $value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + } + + if ($key === 'files' && is_string($parsed['config'][$key])) { + $parsed['config'][$key] = str_replace( + array_keys(self::UPGRADE_VARIABLES), + array_values(self::UPGRADE_VARIABLES), + $parsed['config'][$key] + ); + } + } } + if (isset($parsed['scripts']['installation']) && (is_array($parsed['scripts']['installation']) || $parsed['scripts']['installation'] instanceof stdClass)) { + $parsed['scripts']['installation'] = (array) $parsed['scripts']['installation']; + foreach ($parsed['scripts']['installation'] as $key => $value) { + if (is_array($value) || $value instanceof stdClass) { + $parsed['scripts']['installation'][$key] = json_encode((array) $value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + } + } + } + + // Reserved env var name handling [$forbidden, $allowed] = collect($parsed['variables']) ->map(fn ($variable) => array_merge( $variable, @@ -155,22 +190,7 @@ class EggImporterService } /** - * Fills the provided model with the parsed JSON data. - * - * @param array{ - * name: string, - * description: string, - * tags: string[], - * features: string[], - * docker_images: string[], - * file_denylist: string[], - * meta: array{update_url: string}, - * config: array{files: string, startup: string, logs: string, stop: string}, - * startup: string, - * scripts: array{ - * installation: array{script: string, entrypoint: string, container: string}, - * }, - * } $parsed + * @param array $parsed */ protected function fillFromParsed(Egg $model, array $parsed): Egg { @@ -182,9 +202,9 @@ class EggImporterService 'docker_images' => Arr::get($parsed, 'docker_images'), 'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))->filter(fn ($value) => !empty($value)), 'update_url' => Arr::get($parsed, 'meta.update_url'), - 'config_files' => Arr::get($parsed, 'config.files'), - 'config_startup' => Arr::get($parsed, 'config.startup'), - 'config_logs' => Arr::get($parsed, 'config.logs'), + 'config_files' => json_encode(json_decode(Arr::get($parsed, 'config.files')), JSON_PRETTY_PRINT), + 'config_startup' => json_encode(json_decode(Arr::get($parsed, 'config.startup')), JSON_PRETTY_PRINT), + 'config_logs' => json_encode(json_decode(Arr::get($parsed, 'config.logs')), JSON_PRETTY_PRINT), 'config_stop' => Arr::get($parsed, 'config.stop'), 'startup' => Arr::get($parsed, 'startup'), 'script_install' => Arr::get($parsed, 'scripts.installation.script'), @@ -194,17 +214,11 @@ class EggImporterService } /** - * Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles - * the "docker_images" field potentially not being present, and not being in the - * expected "key => value" format. - * - * @param array{images?: string[], image?: string, field_type?: string, docker_images?: array} $parsed - * @return array + * @param array $parsed + * @return array */ protected function convertToV2(array $parsed): array { - // Maintain backwards compatability for eggs that are still using the old single image - // string format. New eggs can provide an array of Docker images that can be used. if (!isset($parsed['images'])) { $images = [Arr::get($parsed, 'image') ?? 'nil']; } else { diff --git a/app/Services/Nodes/NodeJWTService.php b/app/Services/Nodes/NodeJWTService.php index 7d9765fa9..53d355a18 100644 --- a/app/Services/Nodes/NodeJWTService.php +++ b/app/Services/Nodes/NodeJWTService.php @@ -2,16 +2,16 @@ namespace App\Services\Nodes; +use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates; use Carbon\CarbonImmutable; use DateTimeImmutable; use Illuminate\Support\Str; use App\Models\Node; use App\Models\User; -use Lcobucci\JWT\Token\Plain; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; -use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates; +use Lcobucci\JWT\UnencryptedToken; class NodeJWTService { @@ -64,7 +64,7 @@ class NodeJWTService /** * Generate a new JWT for a given node. */ - public function handle(Node $node, ?string $identifiedBy, string $algo = 'md5'): Plain + public function handle(Node $node, ?string $identifiedBy, string $algo = 'sha256'): UnencryptedToken { $identifier = hash($algo, $identifiedBy); $config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->daemon_token)); @@ -80,7 +80,9 @@ class NodeJWTService $builder = $builder->expiresAt($this->expiresAt); if (!empty($this->subject)) { - $builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject); + $builder = $builder + ->relatedTo($this->subject) + ->withHeader('sub', $this->subject); } foreach ($this->claims as $key => $value) { @@ -88,14 +90,7 @@ class NodeJWTService } if (!is_null($this->user)) { - $builder = $builder - ->withClaim('user_uuid', $this->user->uuid) - // The "user_id" claim is deprecated and should not be referenced — it remains - // here solely to ensure older versions of daemon are unaffected when the Panel - // is updated. - // - // This claim will be removed in Panel@1.11 or later. - ->withClaim('user_id', $this->user->id); + $builder = $builder->withClaim('user_uuid', $this->user->uuid); } return $builder diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 6fd76e55c..887c01e47 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -39,15 +39,7 @@ class ServerCreationService * as possible given the input data. For example, if an allocation_id is passed with * no node_id the node_is will be picked from the allocation. * - * @param array{ - * node_id?: int, - * oom_killer?: bool, - * oom_disabled?: bool, - * egg_id?: int, - * image?: ?string, - * startup?: ?string, - * start_on_completion?: ?bool, - * } $data + * @param array $data * * @throws \Throwable * @throws \App\Exceptions\DisplayException @@ -64,8 +56,8 @@ class ServerCreationService $egg = Egg::query()->findOrFail($data['egg_id']); // Fill missing fields from egg - $data['image'] = $data['image'] ?? collect($egg->docker_images)->first(); - $data['startup'] = $data['startup'] ?? $egg->startup; + $data['image'] ??= collect($egg->docker_images)->first(); + $data['startup'] ??= $egg->startup; // If a deployment object has been passed we need to get the allocation and node that the server should use. if ($deployment) { @@ -94,6 +86,8 @@ class ServerCreationService if (empty($data['node_id'])) { $data['node_id'] = $nodes->first(); } + } else { + $data['node_id'] ??= Allocation::find($data['allocation_id'])?->node_id; } Assert::false(empty($data['node_id']), 'Expected a non-empty node_id in server creation data.'); diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index d1aabf86d..d89d3f800 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -10,7 +10,7 @@ use App\Services\Nodes\NodeJWTService; use Carbon\CarbonImmutable; use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Facades\Http; -use Lcobucci\JWT\Token\Plain; +use Lcobucci\JWT\UnencryptedToken; class TransferServerService { @@ -22,7 +22,7 @@ class TransferServerService private NodeJWTService $nodeJWTService, ) {} - private function notify(ServerTransfer $transfer, Plain $token): void + private function notify(ServerTransfer $transfer, UnencryptedToken $token): void { Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [ 'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers', diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 4fe6e5093..cafb22873 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -4,7 +4,6 @@ namespace App\Services\Subusers; use App\Events\Server\SubUserAdded; use App\Models\User; -use Illuminate\Support\Str; use App\Models\Server; use App\Models\Subuser; use Illuminate\Database\ConnectionInterface; @@ -40,14 +39,8 @@ class SubuserCreationService return $this->connection->transaction(function () use ($server, $email, $permissions) { $user = User::query()->where('email', $email)->first(); if (!$user) { - // Just cap the username generated at 64 characters at most and then append a random string - // to the end to make it "unique"... - [$beforeDomain] = explode('@', $email, 1); - $username = substr(preg_replace('/([^\w.-]+)/', '', $beforeDomain), 0, 64) . Str::random(3); - $user = $this->userCreationService->handle([ 'email' => $email, - 'username' => $username, 'root_admin' => false, ]); } diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index 589b7e45e..644dac7f5 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -3,6 +3,7 @@ namespace App\Services\Users; use App\Models\Role; +use Illuminate\Support\Str; use Ramsey\Uuid\Uuid; use App\Models\User; use Illuminate\Contracts\Hashing\Hasher; @@ -42,6 +43,16 @@ class UserCreationService $isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin']; unset($data['root_admin']); + if (empty($data['username'])) { + $data['username'] = str($data['email'])->before('@')->toString() . Str::random(3); + } + + $data['username'] = str($data['username']) + ->replace(['.', '-'], '') + ->ascii() + ->substr(0, 64) + ->toString(); + /** @var User $user */ $user = User::query()->forceCreate(array_merge($data, [ 'uuid' => Uuid::uuid4()->toString(), diff --git a/compose.yml b/compose.yml index becbab503..24aea6832 100644 --- a/compose.yml +++ b/compose.yml @@ -45,6 +45,7 @@ services: <<: [*panel-environment, *mail-environment] XDG_DATA_HOME: /pelican-data # SKIP_CADDY: true # enable when not using caddy. + TRUSTED_PROXIES: volumes: pelican-data: diff --git a/composer.json b/composer.json index e4d53449e..6c5691065 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,13 @@ "doctrine/dbal": "~3.6.0", "filament/filament": "^3.3", "guzzlehttp/guzzle": "^7.9", - "laravel/framework": "^12.21", + "laravel/framework": "^12.23", "laravel/helpers": "^1.7", "laravel/sanctum": "^4.1", "laravel/socialite": "^5.21", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6", - "lcobucci/jwt": "~4.3.0", + "lcobucci/jwt": "^5.5", "league/flysystem-aws-s3-v3": "^3.29", "league/flysystem-memory": "^3.29", "phpseclib/phpseclib": "~3.0.18", @@ -77,18 +77,13 @@ } }, "scripts": { - "cs:fix": "php-cs-fixer fix", - "cs:check": "php-cs-fixer fix --dry-run --diff --verbose", + "pint": "pint", "phpstan": "phpstan --memory-limit=-1", "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump" ], "post-install-cmd": [ "php -r \"file_exists('.env') || copy('.env.example', '.env');\"" - ], - "dev": [ - "Composer\\Config::disableProcessTimeout", - "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" ] }, "config": { diff --git a/composer.lock b/composer.lock index 2f1dc9f2c..b32d3ae6e 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "c1c3e31cb57271e9ce2edcc715d16d4c", + "content-hash": "9fe021f7367f1d07f7740ba2f5e02683", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -900,16 +900,16 @@ }, { "name": "anourvalar/eloquent-serialize", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/AnourValar/eloquent-serialize.git", - "reference": "2f05023f1e465a91dc4f08483e6710325641a444" + "reference": "0934a98866e02b73e38696961a9d7984b834c9d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/2f05023f1e465a91dc4f08483e6710325641a444", - "reference": "2f05023f1e465a91dc4f08483e6710325641a444", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/0934a98866e02b73e38696961a9d7984b834c9d9", + "reference": "0934a98866e02b73e38696961a9d7984b834c9d9", "shasum": "" }, "require": { @@ -960,9 +960,9 @@ ], "support": { "issues": "https://github.com/AnourValar/eloquent-serialize/issues", - "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.3" + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.4" }, - "time": "2025-05-28T17:07:28+00:00" + "time": "2025-07-30T15:45:57+00:00" }, { "name": "aws/aws-crt-php", @@ -1020,16 +1020,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.351.3", + "version": "3.352.7", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7c58f4a8acd2230daad1ef23bceb9972e62bdf94" + "reference": "26d8988376984e4684c497e71722a97b79aeef4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7c58f4a8acd2230daad1ef23bceb9972e62bdf94", - "reference": "7c58f4a8acd2230daad1ef23bceb9972e62bdf94", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/26d8988376984e4684c497e71722a97b79aeef4e", + "reference": "26d8988376984e4684c497e71722a97b79aeef4e", "shasum": "" }, "require": { @@ -1111,9 +1111,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.351.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.352.7" }, - "time": "2025-07-21T18:04:02+00:00" + "time": "2025-08-12T18:29:26+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -1758,16 +1758,16 @@ }, { "name": "dedoc/scramble", - "version": "v0.12.23", + "version": "v0.12.29", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0" + "reference": "b1fecfa9f6dc0094cf716d609975b933c5d522f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/5b650167c81c59138e844c2ae550c14dc1a249d0", - "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/b1fecfa9f6dc0094cf716d609975b933c5d522f0", + "reference": "b1fecfa9f6dc0094cf716d609975b933c5d522f0", "shasum": "" }, "require": { @@ -1826,7 +1826,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.12.23" + "source": "https://github.com/dedoc/scramble/tree/v0.12.29" }, "funding": [ { @@ -1834,7 +1834,7 @@ "type": "github" } ], - "time": "2025-06-15T09:04:49+00:00" + "time": "2025-08-12T12:17:56+00:00" }, { "name": "dflydev/dot-access-data", @@ -2258,33 +2258,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2329,7 +2328,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -2345,7 +2344,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -2558,7 +2557,7 @@ }, { "name": "filament/actions", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", @@ -2611,16 +2610,16 @@ }, { "name": "filament/filament", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "8e6618036c9235d968740d43bb8afb58fe705e5b" + "reference": "6f460f7f5146217b71fc242b288f908fa58c9131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/8e6618036c9235d968740d43bb8afb58fe705e5b", - "reference": "8e6618036c9235d968740d43bb8afb58fe705e5b", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/6f460f7f5146217b71fc242b288f908fa58c9131", + "reference": "6f460f7f5146217b71fc242b288f908fa58c9131", "shasum": "" }, "require": { @@ -2672,20 +2671,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-21T10:08:08+00:00" + "time": "2025-08-12T13:15:51+00:00" }, { "name": "filament/forms", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "72ec2ede65d8e9fa979a066bce78812458793dde" + "reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/72ec2ede65d8e9fa979a066bce78812458793dde", - "reference": "72ec2ede65d8e9fa979a066bce78812458793dde", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/6d2eddf754f30dee8730535dfcbcefb4cd5e1136", + "reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136", "shasum": "" }, "require": { @@ -2728,20 +2727,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-21T10:07:59+00:00" + "time": "2025-08-12T13:15:47+00:00" }, { "name": "filament/infolists", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa" + "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/89a3f1f236863e2035be3d7b0c68987508dd06fa", - "reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de", + "reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de", "shasum": "" }, "require": { @@ -2779,11 +2778,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-23T10:46:53+00:00" + "time": "2025-08-12T13:15:27+00:00" }, { "name": "filament/notifications", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -2835,16 +2834,16 @@ }, { "name": "filament/support", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "0bf4856840e160624ba79f43aaaa3ec396140f1d" + "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/0bf4856840e160624ba79f43aaaa3ec396140f1d", - "reference": "0bf4856840e160624ba79f43aaaa3ec396140f1d", + "url": "https://api.github.com/repos/filamentphp/support/zipball/afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", + "reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78", "shasum": "" }, "require": { @@ -2890,20 +2889,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-16T08:51:22+00:00" + "time": "2025-08-12T13:15:44+00:00" }, { "name": "filament/tables", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4" + "reference": "20ce6217382785df7b39b8473644c1bfe967963c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/3f0d827c960f1ee4a67ab71c416ad67e24747dc4", - "reference": "3f0d827c960f1ee4a67ab71c416ad67e24747dc4", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/20ce6217382785df7b39b8473644c1bfe967963c", + "reference": "20ce6217382785df7b39b8473644c1bfe967963c", "shasum": "" }, "require": { @@ -2942,11 +2941,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-07-08T20:42:18+00:00" + "time": "2025-08-12T13:15:31+00:00" }, { "name": "filament/widgets", - "version": "v3.3.33", + "version": "v3.3.36", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -3655,16 +3654,16 @@ }, { "name": "kirschbaum-development/eloquent-power-joins", - "version": "4.2.6", + "version": "4.2.7", "source": { "type": "git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "72cff1e838bb3f826dc09a5566219ad7fa56237f" + "reference": "f2f8d3575a54d91b3e5058d65ac1fccb3ea7dd94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/72cff1e838bb3f826dc09a5566219ad7fa56237f", - "reference": "72cff1e838bb3f826dc09a5566219ad7fa56237f", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/f2f8d3575a54d91b3e5058d65ac1fccb3ea7dd94", + "reference": "f2f8d3575a54d91b3e5058d65ac1fccb3ea7dd94", "shasum": "" }, "require": { @@ -3712,22 +3711,22 @@ ], "support": { "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.6" + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.7" }, - "time": "2025-07-10T16:55:34+00:00" + "time": "2025-08-06T10:46:13+00:00" }, { "name": "laravel/framework", - "version": "v12.21.0", + "version": "v12.23.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b" + "reference": "2a0e9331a0db904236143fe915c281ff4be274a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ac8c4e73bf1b5387b709f7736d41427e6af1c93b", - "reference": "ac8c4e73bf1b5387b709f7736d41427e6af1c93b", + "url": "https://api.github.com/repos/laravel/framework/zipball/2a0e9331a0db904236143fe915c281ff4be274a3", + "reference": "2a0e9331a0db904236143fe915c281ff4be274a3", "shasum": "" }, "require": { @@ -3768,6 +3767,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.31", + "symfony/polyfill-php84": "^1.31", + "symfony/polyfill-php85": "^1.31", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -3929,7 +3930,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-07-22T15:41:55+00:00" + "time": "2025-08-12T17:35:05+00:00" }, { "name": "laravel/helpers", @@ -4174,16 +4175,16 @@ }, { "name": "laravel/socialite", - "version": "v5.22.0", + "version": "v5.23.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "99d0fe750a7c68e5b60d8b1850de2554f3ea4072" + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/99d0fe750a7c68e5b60d8b1850de2554f3ea4072", - "reference": "99d0fe750a7c68e5b60d8b1850de2554f3ea4072", + "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", "shasum": "" }, "require": { @@ -4242,7 +4243,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-07-08T22:07:57+00:00" + "time": "2025-07-23T14:16:08+00:00" }, { "name": "laravel/tinker", @@ -4373,105 +4374,40 @@ }, "time": "2025-01-28T15:15:29+00:00" }, - { - "name": "lcobucci/clock", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "shasum": "" - }, - "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\Clock\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com" - } - ], - "description": "Yet another clock abstraction", - "support": { - "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2024-09-24T20:45:14+00:00" - }, { "name": "lcobucci/jwt", - "version": "4.3.0", + "version": "5.5.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4" + "reference": "a835af59b030d3f2967725697cf88300f579088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/4d7de2fe0d51a96418c0d04004986e410e87f6b4", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", + "reference": "a835af59b030d3f2967725697cf88300f579088e", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", "ext-sodium": "*", - "lcobucci/clock": "^2.0 || ^3.0", - "php": "^7.4 || ^8.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.21", - "lcobucci/coding-standard": "^6.0", - "mikey179/vfsstream": "^1.6.7", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/php-invoker": "^3.1", - "phpunit/phpunit": "^9.5" + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -4497,7 +4433,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/4.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.5.0" }, "funding": [ { @@ -4509,7 +4445,7 @@ "type": "patreon" } ], - "time": "2023-01-02T13:28:00+00:00" + "time": "2025-01-26T21:29:45+00:00" }, { "name": "league/commonmark", @@ -5480,16 +5416,16 @@ }, { "name": "masterminds/html5", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "reference": "fcf91eb64359852f00d921887b219479b4f21251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", "shasum": "" }, "require": { @@ -5541,9 +5477,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" }, - "time": "2024-03-31T07:05:07+00:00" + "time": "2025-07-25T09:04:22+00:00" }, { "name": "monolog/monolog", @@ -5716,16 +5652,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -5764,7 +5700,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -5772,20 +5708,20 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nesbot/carbon", - "version": "3.10.1", + "version": "3.10.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", - "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", "shasum": "" }, "require": { @@ -5877,7 +5813,7 @@ "type": "tidelift" } ], - "time": "2025-06-21T15:19:35+00:00" + "time": "2025-08-02T09:36:06+00:00" }, { "name": "nette/schema", @@ -5943,29 +5879,29 @@ }, { "name": "nette/utils", - "version": "v4.0.7", + "version": "v4.0.8", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", - "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", "shasum": "" }, "require": { - "php": "8.0 - 8.4" + "php": "8.0 - 8.5" }, "conflict": { "nette/finder": "<3", "nette/schema": "<1.2.2" }, "require-dev": { - "jetbrains/phpstorm-attributes": "dev-master", + "jetbrains/phpstorm-attributes": "^1.2", "nette/tester": "^2.5", - "phpstan/phpstan": "^1.0", + "phpstan/phpstan-nette": "^2.0@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -5983,6 +5919,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -6023,22 +5962,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.7" + "source": "https://github.com/nette/utils/tree/v4.0.8" }, - "time": "2025-06-03T04:55:08+00:00" + "time": "2025-08-06T21:43:34+00:00" }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", "shasum": "" }, "require": { @@ -6081,9 +6020,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-07-27T20:03:57+00:00" }, { "name": "nunomaduro/termwind", @@ -7438,16 +7377,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.9", + "version": "v0.12.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1b801844becfe648985372cb4b12ad6840245ace" + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", - "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", "shasum": "" }, "require": { @@ -7497,12 +7436,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -7511,9 +7449,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" }, - "time": "2025-06-23T02:35:06+00:00" + "time": "2025-08-04T12:39:37+00:00" }, { "name": "ralouphie/getallheaders", @@ -8623,16 +8561,16 @@ }, { "name": "spatie/laravel-health", - "version": "1.34.4", + "version": "1.34.5", "source": { "type": "git", "url": "https://github.com/spatie/laravel-health.git", - "reference": "ac04fb0b82b4c89ab88c18897f9eda4e559d624b" + "reference": "8487a3a43551f3d24e73546362f93da2684a0a3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-health/zipball/ac04fb0b82b4c89ab88c18897f9eda4e559d624b", - "reference": "ac04fb0b82b4c89ab88c18897f9eda4e559d624b", + "url": "https://api.github.com/repos/spatie/laravel-health/zipball/8487a3a43551f3d24e73546362f93da2684a0a3a", + "reference": "8487a3a43551f3d24e73546362f93da2684a0a3a", "shasum": "" }, "require": { @@ -8704,7 +8642,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/laravel-health/tree/1.34.4" + "source": "https://github.com/spatie/laravel-health/tree/1.34.5" }, "funding": [ { @@ -8712,7 +8650,7 @@ "type": "github" } ], - "time": "2025-07-22T08:06:42+00:00" + "time": "2025-07-25T07:00:21+00:00" }, { "name": "spatie/laravel-package-tools", @@ -8777,16 +8715,16 @@ }, { "name": "spatie/laravel-permission", - "version": "6.20.0", + "version": "6.21.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "31c05679102c73f3b0d05790d2400182745a5615" + "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/31c05679102c73f3b0d05790d2400182745a5615", - "reference": "31c05679102c73f3b0d05790d2400182745a5615", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", + "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", "shasum": "" }, "require": { @@ -8848,7 +8786,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.20.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.21.0" }, "funding": [ { @@ -8856,20 +8794,20 @@ "type": "github" } ], - "time": "2025-06-05T07:33:07+00:00" + "time": "2025-07-23T16:08:05+00:00" }, { "name": "spatie/laravel-query-builder", - "version": "6.3.3", + "version": "6.3.5", "source": { "type": "git", "url": "https://github.com/spatie/laravel-query-builder.git", - "reference": "0d80323d2b2ffc410f06bf73c2e3a6ad763e4b8d" + "reference": "ee3c98235616f88c11e75d3df5ea48dc7b20dd93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/0d80323d2b2ffc410f06bf73c2e3a6ad763e4b8d", - "reference": "0d80323d2b2ffc410f06bf73c2e3a6ad763e4b8d", + "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/ee3c98235616f88c11e75d3df5ea48dc7b20dd93", + "reference": "ee3c98235616f88c11e75d3df5ea48dc7b20dd93", "shasum": "" }, "require": { @@ -8930,7 +8868,7 @@ "type": "custom" } ], - "time": "2025-07-14T08:31:42+00:00" + "time": "2025-08-04T07:36:33+00:00" }, { "name": "spatie/php-structure-discoverer", @@ -9211,16 +9149,16 @@ }, { "name": "symfony/console", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", + "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", "shasum": "" }, "require": { @@ -9285,7 +9223,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.1" + "source": "https://github.com/symfony/console/tree/v7.3.2" }, "funding": [ { @@ -9296,12 +9234,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-30T17:13:41+00:00" }, { "name": "symfony/css-selector", @@ -9437,16 +9379,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", - "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", "shasum": "" }, "require": { @@ -9494,7 +9436,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.1" + "source": "https://github.com/symfony/error-handler/tree/v7.3.2" }, "funding": [ { @@ -9505,12 +9447,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-13T07:48:40+00:00" + "time": "2025-07-07T08:17:57+00:00" }, { "name": "symfony/event-dispatcher", @@ -9670,16 +9616,16 @@ }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", "shasum": "" }, "require": { @@ -9714,7 +9660,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.2" }, "funding": [ { @@ -9725,25 +9671,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-30T19:00:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "cf21254e982b12276329940ca4af5e623ee06c58" + "reference": "3388e208450fcac57d24aef4d5ae41037b663630" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/cf21254e982b12276329940ca4af5e623ee06c58", - "reference": "cf21254e982b12276329940ca4af5e623ee06c58", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/3388e208450fcac57d24aef4d5ae41037b663630", + "reference": "3388e208450fcac57d24aef4d5ae41037b663630", "shasum": "" }, "require": { @@ -9783,7 +9733,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.0" + "source": "https://github.com/symfony/html-sanitizer/tree/v7.3.2" }, "funding": [ { @@ -9794,25 +9744,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-03-31T08:49:55+00:00" + "time": "2025-07-10T08:29:33+00:00" }, { "name": "symfony/http-client", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" + "reference": "1c064a0c67749923483216b081066642751cc2c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", - "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7", + "reference": "1c064a0c67749923483216b081066642751cc2c7", "shasum": "" }, "require": { @@ -9878,7 +9832,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.1" + "source": "https://github.com/symfony/http-client/tree/v7.3.2" }, "funding": [ { @@ -9889,12 +9843,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T07:58:39+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/http-client-contracts", @@ -9976,16 +9934,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", - "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", + "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", "shasum": "" }, "require": { @@ -10035,7 +9993,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" }, "funding": [ { @@ -10046,25 +10004,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-23T15:07:14+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", - "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", "shasum": "" }, "require": { @@ -10149,7 +10111,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" }, "funding": [ { @@ -10160,25 +10122,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-28T08:24:55+00:00" + "time": "2025-07-31T10:45:04+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", - "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", "shasum": "" }, "require": { @@ -10229,7 +10195,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.1" + "source": "https://github.com/symfony/mailer/tree/v7.3.2" }, "funding": [ { @@ -10240,12 +10206,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/mailgun-mailer", @@ -10318,16 +10288,16 @@ }, { "name": "symfony/mime", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9" + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", - "reference": "0e7b19b2f399c31df0cdbe5d8cbf53f02f6cfcd9", + "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", "shasum": "" }, "require": { @@ -10382,7 +10352,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.0" + "source": "https://github.com/symfony/mime/tree/v7.3.2" }, "funding": [ { @@ -10393,12 +10363,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-19T08:51:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/polyfill-ctype", @@ -10958,6 +10932,158 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-02T08:40:52+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.32.0", @@ -11170,16 +11296,16 @@ }, { "name": "symfony/routing", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8e213820c5fea844ecea29203d2a308019007c15" + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", - "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", "shasum": "" }, "require": { @@ -11231,7 +11357,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.0" + "source": "https://github.com/symfony/routing/tree/v7.3.2" }, "funding": [ { @@ -11242,12 +11368,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-24T20:43:28+00:00" + "time": "2025-07-15T11:36:08+00:00" }, { "name": "symfony/service-contracts", @@ -11334,16 +11464,16 @@ }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { @@ -11401,7 +11531,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -11412,25 +11542,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "symfony/translation", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", - "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", "shasum": "" }, "require": { @@ -11497,7 +11631,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.1" + "source": "https://github.com/symfony/translation/tree/v7.3.2" }, "funding": [ { @@ -11508,12 +11642,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-30T17:31:46+00:00" }, { "name": "symfony/translation-contracts", @@ -11669,16 +11807,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" + "reference": "53205bea27450dc5c65377518b3275e126d45e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", - "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", + "reference": "53205bea27450dc5c65377518b3275e126d45e75", "shasum": "" }, "require": { @@ -11690,7 +11828,6 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", @@ -11733,7 +11870,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" }, "funding": [ { @@ -11744,25 +11881,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-07-29T20:02:46+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.1", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", - "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30", + "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30", "shasum": "" }, "require": { @@ -11805,7 +11946,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.1" + "source": "https://github.com/symfony/yaml/tree/v7.3.2" }, "funding": [ { @@ -11816,12 +11957,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-03T06:57:57+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -12667,16 +12812,16 @@ }, { "name": "filp/whoops", - "version": "2.18.3", + "version": "2.18.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "59a123a3d459c5a23055802237cb317f609867e5" + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", - "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { @@ -12726,7 +12871,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.3" + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { @@ -12734,7 +12879,7 @@ "type": "github" } ], - "time": "2025-06-16T00:02:10+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -13945,16 +14090,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.19", + "version": "2.1.22", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "473a8c30e450d87099f76313edcbb90852f9afdf" + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/473a8c30e450d87099f76313edcbb90852f9afdf", - "reference": "473a8c30e450d87099f76313edcbb90852f9afdf", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", "shasum": "" }, "require": { @@ -13999,7 +14144,7 @@ "type": "github" } ], - "time": "2025-07-21T19:58:24+00:00" + "time": "2025-08-04T19:17:37+00:00" }, { "name": "phpunit/php-code-coverage", @@ -14609,16 +14754,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.1", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -14677,15 +14822,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", @@ -15266,16 +15423,16 @@ }, { "name": "sebastian/type", - "version": "5.1.2", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -15311,15 +15468,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2025-03-18T13:35:50+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", diff --git a/config/panel.php b/config/panel.php index 504092db4..ddb6aed2c 100644 --- a/config/panel.php +++ b/config/panel.php @@ -50,7 +50,6 @@ return [ ], 'filament' => [ - 'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false), 'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'), 'avatar-provider' => env('FILAMENT_AVATAR_PROVIDER', 'gravatar'), 'uploadable-avatars' => env('FILAMENT_UPLOADABLE_AVATARS', false), diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index e3f410348..44d739e20 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -3,10 +3,12 @@ namespace Database\Seeders; use App\Models\Egg; -use Exception; +use DirectoryIterator; use Illuminate\Database\Seeder; use Illuminate\Http\UploadedFile; use App\Services\Eggs\Sharing\EggImporterService; +use Symfony\Component\Yaml\Yaml; +use Throwable; class EggSeeder extends Seeder { @@ -46,22 +48,39 @@ class EggSeeder extends Seeder */ protected function parseEggFiles($name): void { - $files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($name))); + $path = database_path('Seeders/eggs/' . kebab_case($name)); + $files = new DirectoryIterator($path); $this->command->alert('Updating Eggs for: ' . $name); - /** @var \DirectoryIterator $file */ + + /** @var DirectoryIterator $file */ foreach ($files as $file) { if (!$file->isFile() || !$file->isReadable()) { continue; } + $extension = strtolower($file->getExtension()); + $filePath = $file->getRealPath(); + try { - $decoded = json_decode(file_get_contents($file->getRealPath()), true, 512, JSON_THROW_ON_ERROR); - } catch (Exception) { + $decoded = match ($extension) { + 'json' => json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR), + 'yaml', 'yml' => Yaml::parseFile($filePath), + default => null, + }; + } catch (Throwable) { + $this->command->warn("Failed to parse {$file->getFilename()}, skipping."); + continue; } - $file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json'); + if (!is_array($decoded) || !isset($decoded['name'], $decoded['author'])) { + $this->command->warn("Invalid structure in {$file->getFilename()}, skipping."); + + continue; + } + + $uploaded = new UploadedFile($filePath, $file->getFilename()); $egg = Egg::query() ->where('author', $decoded['author']) @@ -69,10 +88,10 @@ class EggSeeder extends Seeder ->first(); if ($egg instanceof Egg) { - $this->importerService->fromFile($file, $egg); + $this->importerService->fromFile($uploaded, $egg); $this->command->info('Updated ' . $decoded['name']); } else { - $this->importerService->fromFile($file); + $this->importerService->fromFile($uploaded); $this->command->comment('Created ' . $decoded['name']); } } diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json deleted file mode 100644 index a4e328978..000000000 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json" - }, - "exported_at": "2025-03-18T12:35:34+00:00", - "name": "Bungeecord", - "author": "panel@example.com", - "uuid": "9e6b409e-4028-4947-aea8-50a2c404c271", - "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", - "tags": [ - "minecraft", - "proxy" - ], - "features": [ - "eula", - "java_version", - "pid_limit" - ], - "docker_images": { - "Java 21": "ghcr.io\/parkervcp\/yolks:java_21", - "Java 17": "ghcr.io\/parkervcp\/yolks:java_17", - "Java 16": "ghcr.io\/parkervcp\/yolks:java_16", - "Java 11": "ghcr.io\/parkervcp\/yolks:java_11", - "Java 8": "ghcr.io\/parkervcp\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", - "config": { - "files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.allocations.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.allocations.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \"Listening on \"\r\n}", - "logs": "{}", - "stop": "end" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# Bungeecord Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\r\n BUNGEE_VERSION=\"lastStableBuild\"\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Bungeecord Version", - "description": "The version of Bungeecord to download and use.", - "env_variable": "BUNGEE_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "alpha_num", - "between:1,6" - ], - "sort": 1 - }, - { - "name": "Bungeecord Jar File", - "description": "The name of the Jarfile to use when running Bungeecord.", - "env_variable": "SERVER_JARFILE", - "default_value": "bungeecord.jar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ], - "sort": 2 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.yaml b/database/Seeders/eggs/minecraft/egg-bungeecord.yaml new file mode 100644 index 000000000..d079a6bf5 --- /dev/null +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.yaml @@ -0,0 +1,83 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-bungeecord.yaml' +exported_at: '2025-07-25T13:32:34+00:00' +name: Bungeecord +author: panel@example.com +uuid: 9e6b409e-4028-4947-aea8-50a2c404c271 +description: |- + For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and + reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. + Whether you are a small server wishing to string multiple game-modes together, or the owner of the + ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be + able to unlock your community's full potential. +tags: + - minecraft + - proxy +features: + - eula + - java_version + - pid_limit +docker_images: + 'Java 21': 'ghcr.io/parkervcp/yolks:java_21' + 'Java 17': 'ghcr.io/parkervcp/yolks:java_17' + 'Java 16': 'ghcr.io/parkervcp/yolks:java_16' + 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' + 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' +file_denylist: { } +startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +config: + files: + config.yml: + parser: yaml + find: + 'listeners[0].query_port': '{{server.allocations.default.port}}' + 'listeners[0].host': '0.0.0.0:{{server.allocations.default.port}}' + 'servers.*.address': + 'regex:^(127\.0\.0\.1|localhost)(:\d{1,5})?$': '{{config.docker.interface}}$2' + startup: + done: 'Listening on ' + logs: { } + stop: end +scripts: + installation: + script: |- + #!/bin/ash + # Bungeecord Installation Script + # + # Server Files: /mnt/server + + cd /mnt/server + + if [ -z "${BUNGEE_VERSION}" ] || [ "${BUNGEE_VERSION}" == "latest" ]; then + BUNGEE_VERSION="lastStableBuild" + fi + + curl -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'Bungeecord Jar File' + description: 'The name of the Jarfile to use when running Bungeecord.' + env_variable: SERVER_JARFILE + default_value: bungeecord.jar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^([\w\d._-]+)(\.jar)$/' + sort: 2 + - + name: 'Bungeecord Version' + description: 'The version of Bungeecord to download and use.' + env_variable: BUNGEE_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - alpha_num + - 'between:1,6' + sort: 1 diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json deleted file mode 100644 index 8ac58cc3e..000000000 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-forge-minecraft.json" - }, - "exported_at": "2025-03-18T12:35:39+00:00", - "name": "Forge Minecraft", - "author": "panel@example.com", - "uuid": "ed072427-f209-4603-875c-f540c6dd5a65", - "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", - "tags": [ - "minecraft" - ], - "features": [ - "eula", - "java_version", - "pid_limit" - ], - "docker_images": { - "Java 21": "ghcr.io\/parkervcp\/yolks:java_21", - "Java 17": "ghcr.io\/parkervcp\/yolks:java_17", - "Java 16": "ghcr.io\/parkervcp\/yolks:java_16", - "Java 11": "ghcr.io\/parkervcp\/yolks:java_11", - "Java 8": "ghcr.io\/parkervcp\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt update\r\napt install -y curl jq\r\n\r\nif [[ ! -d \/mnt\/server ]]; then\r\n mkdir \/mnt\/server\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\n# Remove spaces from the version number to avoid issues with curl\r\nFORGE_VERSION=\"$(echo \"$FORGE_VERSION\" | tr -d ' ')\"\r\nMC_VERSION=\"$(echo \"$MC_VERSION\" | tr -d ' ')\"\r\n\r\nif [[ ! -z ${FORGE_VERSION} ]]; then\r\n DOWNLOAD_LINK=https:\/\/maven.minecraftforge.net\/net\/minecraftforge\/forge\/${FORGE_VERSION}\/forge-${FORGE_VERSION}\r\n FORGE_JAR=forge-${FORGE_VERSION}*.jar\r\nelse\r\n JSON_DATA=$(curl -sSL https:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/promotions_slim.json)\r\n\r\n if [[ \"${MC_VERSION}\" == \"latest\" ]] || [[ \"${MC_VERSION}\" == \"\" ]]; then\r\n echo -e \"getting latest version of forge.\"\r\n MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains(\"latest\")) | split(\"-\")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)\r\n BUILD_TYPE=latest\r\n fi\r\n\r\n if [[ \"${BUILD_TYPE}\" != \"recommended\" ]] && [[ \"${BUILD_TYPE}\" != \"latest\" ]]; then\r\n BUILD_TYPE=recommended\r\n fi\r\n\r\n echo -e \"minecraft version: ${MC_VERSION}\"\r\n echo -e \"build type: ${BUILD_TYPE}\"\r\n\r\n ## some variables for getting versions and things\r\n FILE_SITE=https:\/\/maven.minecraftforge.net\/net\/minecraftforge\/forge\/\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" --arg BUILD_TYPE \"${BUILD_TYPE}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')\r\n\r\n ## locating the forge version\r\n if [[ \"${VERSION_KEY}\" == \"\" ]] && [[ \"${BUILD_TYPE}\" == \"recommended\" ]]; then\r\n echo -e \"dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested.\"\r\n VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION \"${MC_VERSION}\" '.promos | del(.\"latest-1.7.10\") | del(.\"1.7.10-latest-1.7.10\") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains(\"latest\"))')\r\n fi\r\n\r\n ## Error if the mc version set wasn't valid.\r\n if [ \"${VERSION_KEY}\" == \"\" ] || [ \"${VERSION_KEY}\" == \"null\" ]; then\r\n echo -e \"The install failed because there is no valid version of forge for the version of minecraft selected.\"\r\n exit 1\r\n fi\r\n\r\n FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY \"$VERSION_KEY\" '.promos | .[$VERSION_KEY]')\r\n\r\n if [[ \"${MC_VERSION}\" == \"1.7.10\" ]] || [[ \"${MC_VERSION}\" == \"1.8.9\" ]]; then\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar\r\n if [[ \"${MC_VERSION}\" == \"1.7.10\" ]]; then\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar\r\n fi\r\n else\r\n DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}\/forge-${MC_VERSION}-${FORGE_VERSION}\r\n FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar\r\n fi\r\nfi\r\n\r\n#Adding .jar when not eding by SERVER_JARFILE\r\nif [[ ! $SERVER_JARFILE = *\\.jar ]]; then\r\n SERVER_JARFILE=\"$SERVER_JARFILE.jar\"\r\nfi\r\n\r\n#Downloading jars\r\necho -e \"Downloading forge version ${FORGE_VERSION}\"\r\necho -e \"Download link is ${DOWNLOAD_LINK}\"\r\n\r\nif [[ ! -z \"${DOWNLOAD_LINK}\" ]]; then\r\n if curl --output \/dev\/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then\r\n echo -e \"installer jar download link is valid.\"\r\n else\r\n echo -e \"link is invalid. Exiting now\"\r\n exit 2\r\n fi\r\nelse\r\n echo -e \"no download link provided. Exiting now\"\r\n exit 3\r\nfi\r\n\r\ncurl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar\r\n\r\n#Checking if downloaded jars exist\r\nif [[ ! -f .\/installer.jar ]]; then\r\n echo \"!!! Error downloading forge version ${FORGE_VERSION} !!!\"\r\n exit\r\nfi\r\n\r\nfunction unix_args {\r\n echo -e \"Detected Forge 1.17 or newer version. Setting up forge unix args.\"\r\n ln -sf libraries\/net\/minecraftforge\/forge\/*\/unix_args.txt unix_args.txt\r\n}\r\n\r\n# Delete args to support downgrading\/upgrading\r\nrm -rf libraries\/net\/minecraftforge\/forge\r\nrm unix_args.txt\r\n\r\n#Installing server\r\necho -e \"Installing forge server.\\n\"\r\njava -jar installer.jar --installServer || { echo -e \"\\nInstall failed using Forge version ${FORGE_VERSION} and Minecraft version ${MINECRAFT_VERSION}.\\nShould you be using unlimited memory value of 0, make sure to increase the default install resource limits in the Daemon config or specify exact allocated memory in the server Build Configuration instead of 0! \\nOtherwise, the Forge installer will not have enough memory.\"; exit 4; }\r\n\r\n# Check if we need a symlink for 1.17+ Forge JPMS args\r\nif [[ $MC_VERSION =~ ^1\\.(17|18|19|20|21|22|23) || $FORGE_VERSION =~ ^1\\.(17|18|19|20|21|22|23) ]]; then\r\n unix_args\r\n\r\n# Check if someone has set MC to latest but overwrote it with older Forge version, otherwise we would have false positives\r\nelif [[ $MC_VERSION == \"latest\" && $FORGE_VERSION =~ ^1\\.(17|18|19|20|21|22|23) ]]; then\r\n unix_args\r\nelse\r\n # For versions below 1.17 that ship with jar\r\n mv $FORGE_JAR $SERVER_JARFILE\r\nfi\r\n\r\necho -e \"Deleting installer.jar file.\\n\"\r\nrm -rf installer.jar\r\necho -e \"Installation process is completed\"", - "container": "openjdk:8-jdk-slim", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Server Jar File", - "description": "The name of the Jarfile to use when running Forge version below 1.17.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ], - "sort": 1 - }, - { - "name": "Minecraft Version", - "description": "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version.", - "env_variable": "MC_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:9" - ], - "sort": 2 - }, - { - "name": "Build Type", - "description": "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\".", - "env_variable": "BUILD_TYPE", - "default_value": "recommended", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "in:recommended,latest" - ], - "sort": 3 - }, - { - "name": "Forge Version", - "description": "The full exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to install.", - "env_variable": "FORGE_VERSION", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "regex:\/^[0-9\\.\\-]+$\/" - ], - "sort": 4 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml b/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml new file mode 100644 index 000000000..ed8b3a5ff --- /dev/null +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml @@ -0,0 +1,217 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml' +exported_at: '2025-08-05T21:00:17+00:00' +name: 'Forge Minecraft' +author: panel@example.com +uuid: ed072427-f209-4603-875c-f540c6dd5a65 +description: |- + Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which + makes it easier to create mods, and also make sure mods are compatible with each other. +tags: + - minecraft +features: + - eula + - java_version + - pid_limit +docker_images: + 'Java 21': 'ghcr.io/parkervcp/yolks:java_21' + 'Java 17': 'ghcr.io/parkervcp/yolks:java_17' + 'Java 16': 'ghcr.io/parkervcp/yolks:java_16' + 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' + 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' +file_denylist: { } +startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s "-jar {{SERVER_JARFILE}}" || printf %s "@unix_args.txt" )' +config: + files: + server.properties: + parser: properties + find: + server-ip: '' + server-port: '{{server.allocations.default.port}}' + query.port: '{{server.allocations.default.port}}' + startup: + done: ')! For help, type ' + logs: { } + stop: stop +scripts: + installation: + script: |- + #!/bin/bash + # Forge Installation Script + # + # Server Files: /mnt/server + apt update + apt install -y curl jq + + if [[ ! -d /mnt/server ]]; then + mkdir /mnt/server + fi + + cd /mnt/server + + # Remove spaces from the version number to avoid issues with curl + FORGE_VERSION="$(echo "$FORGE_VERSION" | tr -d ' ')" + MC_VERSION="$(echo "$MC_VERSION" | tr -d ' ')" + + if [[ ! -z ${FORGE_VERSION} ]]; then + DOWNLOAD_LINK=https://maven.minecraftforge.net/net/minecraftforge/forge/${FORGE_VERSION}/forge-${FORGE_VERSION} + FORGE_JAR=forge-${FORGE_VERSION}*.jar + else + JSON_DATA=$(curl -sSL https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json) + + if [[ "${MC_VERSION}" == "latest" ]] || [[ "${MC_VERSION}" == "" ]]; then + echo -e "getting latest version of forge." + MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains("latest")) | split("-")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1) + BUILD_TYPE=latest + fi + + if [[ "${BUILD_TYPE}" != "recommended" ]] && [[ "${BUILD_TYPE}" != "latest" ]]; then + BUILD_TYPE=recommended + fi + + echo -e "minecraft version: ${MC_VERSION}" + echo -e "build type: ${BUILD_TYPE}" + + ## some variables for getting versions and things + FILE_SITE=https://maven.minecraftforge.net/net/minecraftforge/forge/ + VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION "${MC_VERSION}" --arg BUILD_TYPE "${BUILD_TYPE}" '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))') + + ## locating the forge version + if [[ "${VERSION_KEY}" == "" ]] && [[ "${BUILD_TYPE}" == "recommended" ]]; then + echo -e "dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested." + VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION "${MC_VERSION}" '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains("latest"))') + fi + + ## Error if the mc version set wasn't valid. + if [ "${VERSION_KEY}" == "" ] || [ "${VERSION_KEY}" == "null" ]; then + echo -e "The install failed because there is no valid version of forge for the version of minecraft selected." + exit 1 + fi + + FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY "$VERSION_KEY" '.promos | .[$VERSION_KEY]') + + if [[ "${MC_VERSION}" == "1.7.10" ]] || [[ "${MC_VERSION}" == "1.8.9" ]]; then + DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION} + FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar + if [[ "${MC_VERSION}" == "1.7.10" ]]; then + FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar + fi + else + DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}/forge-${MC_VERSION}-${FORGE_VERSION} + FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar + fi + fi + + #Adding .jar when not eding by SERVER_JARFILE + if [[ ! $SERVER_JARFILE = *\.jar ]]; then + SERVER_JARFILE="$SERVER_JARFILE.jar" + fi + + #Downloading jars + echo -e "Downloading forge version ${FORGE_VERSION}" + echo -e "Download link is ${DOWNLOAD_LINK}" + + if [[ ! -z "${DOWNLOAD_LINK}" ]]; then + if curl --output /dev/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then + echo -e "installer jar download link is valid." + else + echo -e "link is invalid. Exiting now" + exit 2 + fi + else + echo -e "no download link provided. Exiting now" + exit 3 + fi + + curl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar + + #Checking if downloaded jars exist + if [[ ! -f ./installer.jar ]]; then + echo "!!! Error downloading forge version ${FORGE_VERSION} !!!" + exit + fi + + function unix_args { + echo -e "Detected Forge 1.17 or newer version. Setting up forge unix args." + ln -sf libraries/net/minecraftforge/forge/*/unix_args.txt unix_args.txt + } + + # Delete args to support downgrading/upgrading + rm -rf libraries/net/minecraftforge/forge + rm unix_args.txt + + #Installing server + echo -e "Installing forge server. + " + java -jar installer.jar --installServer || { echo -e " + Install failed using Forge version ${FORGE_VERSION} and Minecraft version ${MINECRAFT_VERSION}. + Should you be using unlimited memory value of 0, make sure to increase the default install resource limits in the Daemon config or specify exact allocated memory in the server Build Configuration instead of 0! + Otherwise, the Forge installer will not have enough memory."; exit 4; } + + # Check if we need a symlink for 1.17+ Forge JPMS args + if [[ $MC_VERSION =~ ^1\.(17|18|19|20|21|22|23) || $FORGE_VERSION =~ ^1\.(17|18|19|20|21|22|23) ]]; then + unix_args + + # Check if someone has set MC to latest but overwrote it with older Forge version, otherwise we would have false positives + elif [[ $MC_VERSION == "latest" && $FORGE_VERSION =~ ^1\.(17|18|19|20|21|22|23) ]]; then + unix_args + else + # For versions below 1.17 that ship with jar + mv $FORGE_JAR $SERVER_JARFILE + fi + + echo -e "Deleting installer.jar file. + " + rm -rf installer.jar + echo -e "Installation process is completed" + container: 'openjdk:8-jdk-slim' + entrypoint: bash +variables: + - + name: 'Build Type' + description: "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\"." + env_variable: BUILD_TYPE + default_value: recommended + user_viewable: true + user_editable: true + rules: + - required + - string + - 'in:recommended,latest' + sort: 3 + - + name: 'Forge Version' + description: "The full exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to\ninstall." + env_variable: FORGE_VERSION + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - 'regex:/^[0-9\.\-]+$/' + sort: 4 + - + name: 'Minecraft Version' + description: "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version." + env_variable: MC_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:9' + sort: 2 + - + name: 'Server Jar File' + description: 'The name of the Jarfile to use when running Forge version below 1.17.' + env_variable: SERVER_JARFILE + default_value: server.jar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^([\w\d._-]+)(\.jar)$/' + sort: 1 diff --git a/database/Seeders/eggs/minecraft/egg-paper.json b/database/Seeders/eggs/minecraft/egg-paper.json deleted file mode 100644 index f4a428293..000000000 --- a/database/Seeders/eggs/minecraft/egg-paper.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json" - }, - "exported_at": "2025-03-18T12:35:44+00:00", - "name": "Paper", - "author": "parker@example.com", - "uuid": "5da37ef6-58da-4169-90a6-e683e1721247", - "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", - "tags": [ - "minecraft" - ], - "features": [ - "eula", - "java_version", - "pid_limit" - ], - "docker_images": { - "Java 21": "ghcr.io\/parkervcp\/yolks:java_21", - "Java 17": "ghcr.io\/parkervcp\/yolks:java_17", - "Java 16": "ghcr.io\/parkervcp\/yolks:java_16", - "Java 11": "ghcr.io\/parkervcp\/yolks:java_11", - "Java 8": "ghcr.io\/parkervcp\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nPROJECT=paper\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n\techo -e \"Using supplied download url: ${DL_PATH}\"\r\n\tDOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n\tVER_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`\r\n\tLATEST_VERSION=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n\telse\r\n\t\techo -e \"Specified version not found. Defaulting to the latest ${PROJECT} version\"\r\n\t\tMINECRAFT_VERSION=${LATEST_VERSION}\r\n\tfi\r\n\r\n\tBUILD_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`\r\n\tLATEST_BUILD=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${BUILD_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}\"\r\n\telse\r\n\t\techo -e \"Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}\"\r\n\t\tBUILD_NUMBER=${LATEST_BUILD}\r\n\tfi\r\n\r\n\tJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\r\n\r\n\techo \"Version being downloaded\"\r\n\techo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n\techo -e \"Build: ${BUILD_NUMBER}\"\r\n\techo -e \"JAR Name of Build: ${JAR_NAME}\"\r\n\tDOWNLOAD_URL=https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION}\/builds\/${BUILD_NUMBER}\/downloads\/${JAR_NAME}\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n\tmv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft\/java\/server.properties\r\nfi", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Minecraft Version", - "description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", - "env_variable": "MINECRAFT_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string", - "max:20" - ], - "sort": 1 - }, - { - "name": "Server Jar File", - "description": "The name of the server jarfile to run the server with.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ], - "sort": 2 - }, - { - "name": "Download Path", - "description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.", - "env_variable": "DL_PATH", - "default_value": "", - "user_viewable": false, - "user_editable": false, - "rules": [ - "nullable", - "string" - ], - "sort": 3 - }, - { - "name": "Build Number", - "description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", - "env_variable": "BUILD_NUMBER", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:20" - ], - "sort": 4 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-paper.yaml b/database/Seeders/eggs/minecraft/egg-paper.yaml new file mode 100644 index 000000000..0431c3373 --- /dev/null +++ b/database/Seeders/eggs/minecraft/egg-paper.yaml @@ -0,0 +1,142 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-paper.yaml' +exported_at: '2025-08-05T21:00:17+00:00' +name: Paper +author: parker@example.com +uuid: 5da37ef6-58da-4169-90a6-e683e1721247 +description: 'High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.' +tags: + - minecraft +features: + - eula + - java_version + - pid_limit +docker_images: + 'Java 21': 'ghcr.io/parkervcp/yolks:java_21' + 'Java 17': 'ghcr.io/parkervcp/yolks:java_17' + 'Java 16': 'ghcr.io/parkervcp/yolks:java_16' + 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' + 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' +file_denylist: { } +startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}' +config: + files: + server.properties: + parser: properties + find: + server-ip: '' + server-port: '{{server.allocations.default.port}}' + query.port: '{{server.allocations.default.port}}' + startup: + done: ')! For help, type ' + logs: { } + stop: stop +scripts: + installation: + script: |- + #!/bin/ash + # Paper Installation Script + # + # Server Files: /mnt/server + PROJECT=paper + + if [ -n "${DL_PATH}" ]; then + echo -e "Using supplied download url: ${DL_PATH}" + DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's/{{/${/g' -e 's/}}/}/g')` + else + VER_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true` + LATEST_VERSION=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'` + + if [ "${VER_EXISTS}" == "true" ]; then + echo -e "Version is valid. Using version ${MINECRAFT_VERSION}" + else + echo -e "Specified version not found. Defaulting to the latest ${PROJECT} version" + MINECRAFT_VERSION=${LATEST_VERSION} + fi + + BUILD_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true` + LATEST_BUILD=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'` + + if [ "${BUILD_EXISTS}" == "true" ]; then + echo -e "Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}" + else + echo -e "Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}" + BUILD_NUMBER=${LATEST_BUILD} + fi + + JAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar + + echo "Version being downloaded" + echo -e "MC Version: ${MINECRAFT_VERSION}" + echo -e "Build: ${BUILD_NUMBER}" + echo -e "JAR Name of Build: ${JAR_NAME}" + DOWNLOAD_URL=https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION}/builds/${BUILD_NUMBER}/downloads/${JAR_NAME} + fi + + cd /mnt/server + + echo -e "Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}" + + if [ -f ${SERVER_JARFILE} ]; then + mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old + fi + + curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL} + + if [ ! -f server.properties ]; then + echo -e "Downloading MC server.properties" + curl -o server.properties https://raw.githubusercontent.com/parkervcp/eggs/master/minecraft/java/server.properties + fi + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'Build Number' + description: "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest." + env_variable: BUILD_NUMBER + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:20' + sort: 4 + - + name: 'Download Path' + description: |- + A URL to use to download a server.jar rather than the ones in the install script. This is not user + viewable. + env_variable: DL_PATH + default_value: '' + user_viewable: false + user_editable: false + rules: + - nullable + - string + sort: 3 + - + name: 'Minecraft Version' + description: "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest." + env_variable: MINECRAFT_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - nullable + - string + - 'max:20' + sort: 1 + - + name: 'Server Jar File' + description: 'The name of the server jarfile to run the server with.' + env_variable: SERVER_JARFILE + default_value: server.jar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^([\w\d._-]+)(\.jar)$/' + sort: 2 diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json deleted file mode 100644 index 6f87376da..000000000 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json" - }, - "exported_at": "2025-04-25T06:05:10+00:00", - "name": "Sponge", - "author": "panel@example.com", - "uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d", - "description": "A community-driven open source Minecraft: Java Edition modding platform.", - "tags": [ - "minecraft" - ], - "features": [ - "eula", - "java_version", - "pid_limit" - ], - "docker_images": { - "Java 21": "ghcr.io\/parkervcp\/yolks:java_21", - "Java 17": "ghcr.io\/parkervcp\/yolks:java_17", - "Java 16": "ghcr.io\/parkervcp\/yolks:java_16", - "Java 11": "ghcr.io\/parkervcp\/yolks:java_11", - "Java 8": "ghcr.io\/parkervcp\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/latest?recommended=true)\r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find latest recommended version!\"\r\n exit 1\r\n fi\r\n echo -e \"Found latest version for ${SPONGE_TYPE}\"\r\nelse\r\n if [ $SPONGE_TYPE = 'spongevanilla' ]; then \r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)\r\n else\r\n FORGETAG='forge'\r\n if [ $SPONGE_TYPE = 'spongeneo' ]; then\r\n FORGETAG='neoforge'\r\n fi\r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)\r\n fi\r\n \r\n if [ -z \"${VERSIONS_JSON}\" ]; then\r\n echo -e \"Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n \r\n VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions\/${VERSION_KEY})\r\n \r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n\r\n echo -e \"Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}\"\r\nfi\r\n\r\nTARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"universal\")'`\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"\" and .extension == \"jar\")'`\r\nfi\r\n\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n echo -e \"Failed to get download url data from the selected version\"\r\n exit 1\r\nfi\r\n\r\nSPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')\r\nCHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')\r\necho -e \"Found file at ${SPONGE_URL} with checksum ${CHECKSUM}\"\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}\"\r\ncurl -o ${SERVER_JARFILE} ${SPONGE_URL}\r\n\r\nif [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then\r\n echo \"Checksum passed\"\r\nelse\r\n echo \"Checksum failed\"\r\nfi\r\n\r\necho -e \"Install Complete\"", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "sort": 3, - "name": "Forge\/Neoforge Version", - "description": "The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using `spongevanilla`", - "env_variable": "FORGE_VERSION", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "string" - ] - }, - { - "sort": 1, - "name": "Minecraft Version", - "description": "The version of Minecraft to target. Use \"latest\" to install the latest version. Go to Settings > Reinstall Server to apply.", - "env_variable": "MINECRAFT_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "between:3,15" - ] - }, - { - "sort": 4, - "name": "Server Jar File", - "description": "The name of the Jarfile to use when running Sponge.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ] - }, - { - "sort": 2, - "name": "Sponge Type", - "description": "SpongeVanilla if you are only using Sponge plugins.\nSpongeForge when using Forge mods and Sponge plugins.\nSpongeNeo when using NeoForge mods and Sponge plugins.", - "env_variable": "SPONGE_TYPE", - "default_value": "spongevanilla", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "in:spongevanilla,spongeforge,spongeneo" - ] - } - ] -} diff --git a/database/Seeders/eggs/minecraft/egg-sponge.yaml b/database/Seeders/eggs/minecraft/egg-sponge.yaml new file mode 100644 index 000000000..2fc5630e7 --- /dev/null +++ b/database/Seeders/eggs/minecraft/egg-sponge.yaml @@ -0,0 +1,157 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.yaml' +exported_at: '2025-08-05T21:00:17+00:00' +name: Sponge +author: panel@example.com +uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d +description: 'A community-driven open source Minecraft: Java Edition modding platform.' +tags: + - minecraft +features: + - eula + - java_version + - pid_limit +docker_images: + 'Java 21': 'ghcr.io/parkervcp/yolks:java_21' + 'Java 17': 'ghcr.io/parkervcp/yolks:java_17' + 'Java 16': 'ghcr.io/parkervcp/yolks:java_16' + 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' + 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' +file_denylist: { } +startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +config: + files: + server.properties: + parser: properties + find: + server-ip: '' + server-port: '{{server.allocations.default.port}}' + query.port: '{{server.allocations.default.port}}' + startup: + done: ')! For help, type ' + logs: { } + stop: stop +scripts: + installation: + script: |- + #!/bin/ash + # Sponge Installation Script + # + # Server Files: /mnt/server + + cd /mnt/server + + if [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then + TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/latest?recommended=true) + if [ -z "${TARGET_VERSION_JSON}" ]; then + echo -e "Failed to find latest recommended version!" + exit 1 + fi + echo -e "Found latest version for ${SPONGE_TYPE}" + else + if [ $SPONGE_TYPE = 'spongevanilla' ]; then + VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1) + else + FORGETAG='forge' + if [ $SPONGE_TYPE = 'spongeneo' ]; then + FORGETAG='neoforge' + fi + VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1) + fi + + if [ -z "${VERSIONS_JSON}" ]; then + echo -e "Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!" + exit 1 + fi + + VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key') + TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions/${VERSION_KEY}) + + if [ -z "${TARGET_VERSION_JSON}" ]; then + echo -e "Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!" + exit 1 + fi + + echo -e "Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}" + fi + + TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "universal")'` + if [ -z "${TARGET_VERSION}" ]; then + TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "" and .extension == "jar")'` + fi + + if [ -z "${TARGET_VERSION}" ]; then + echo -e "Failed to get download url data from the selected version" + exit 1 + fi + + SPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl') + CHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5') + echo -e "Found file at ${SPONGE_URL} with checksum ${CHECKSUM}" + + echo -e "running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}" + curl -o ${SERVER_JARFILE} ${SPONGE_URL} + + if [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then + echo "Checksum passed" + else + echo "Checksum failed" + fi + + echo -e "Install Complete" + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'Forge/Neoforge Version' + description: |- + The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using + `spongevanilla` + env_variable: FORGE_VERSION + default_value: '' + user_viewable: true + user_editable: true + rules: + - string + sort: 3 + - + name: 'Minecraft Version' + description: |- + The version of Minecraft to target. Use "latest" to install the latest version. Go to Settings > + Reinstall Server to apply. + env_variable: MINECRAFT_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - string + - 'between:3,15' + sort: 1 + - + name: 'Server Jar File' + description: 'The name of the Jarfile to use when running Sponge.' + env_variable: SERVER_JARFILE + default_value: server.jar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^([\w\d._-]+)(\.jar)$/' + sort: 4 + - + name: 'Sponge Type' + description: |- + SpongeVanilla if you are only using Sponge plugins. + SpongeForge when using Forge mods and Sponge plugins. + SpongeNeo when using NeoForge mods and Sponge plugins. + env_variable: SPONGE_TYPE + default_value: spongevanilla + user_viewable: true + user_editable: true + rules: + - required + - 'in:spongevanilla,spongeforge,spongeneo' + sort: 2 diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json deleted file mode 100644 index d5ca65719..000000000 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json" - }, - "exported_at": "2025-03-18T12:35:55+00:00", - "name": "Vanilla Minecraft", - "author": "panel@example.com", - "uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b", - "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", - "tags": [ - "minecraft" - ], - "features": [ - "eula", - "java_version", - "pid_limit" - ], - "docker_images": { - "Java 21": "ghcr.io\/parkervcp\/yolks:java_21", - "Java 17": "ghcr.io\/parkervcp\/yolks:java_17", - "Java 16": "ghcr.io\/parkervcp\/yolks:java_16", - "Java 11": "ghcr.io\/parkervcp\/yolks:java_11", - "Java 8": "ghcr.io\/parkervcp\/yolks:java_8" - }, - "file_denylist": [], - "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \"\r\n}", - "logs": "{}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\nLATEST_SNAPSHOT_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.snapshot'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\necho -e \"latest snapshot is $LATEST_SNAPSHOT_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelif [ \"$VANILLA_VERSION\" == \"snapshot\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_SNAPSHOT_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Server Jar File", - "description": "The name of the server jarfile to run the server with.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ], - "sort": 1 - }, - { - "name": "Server Version", - "description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot. Go to Settings > Reinstall Server to apply.", - "env_variable": "VANILLA_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "between:3,15" - ], - "sort": 2 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml new file mode 100644 index 000000000..7328ca196 --- /dev/null +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml @@ -0,0 +1,97 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml' +exported_at: '2025-08-05T20:59:51+00:00' +name: 'Vanilla Minecraft' +author: panel@example.com +uuid: 9ac39f3d-0c34-4d93-8174-c52ab9e6c57b +description: |- + Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds + and build amazing things from the simplest of homes to the grandest of castles. Play in Creative + Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off + dangerous mobs. Do all this alone or with friends. +tags: + - minecraft +features: + - eula + - java_version + - pid_limit +docker_images: + 'Java 21': 'ghcr.io/parkervcp/yolks:java_21' + 'Java 17': 'ghcr.io/parkervcp/yolks:java_17' + 'Java 16': 'ghcr.io/parkervcp/yolks:java_16' + 'Java 11': 'ghcr.io/parkervcp/yolks:java_11' + 'Java 8': 'ghcr.io/parkervcp/yolks:java_8' +file_denylist: { } +startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}' +config: + files: + server.properties: + parser: properties + find: + server-ip: '' + server-port: '{{server.allocations.default.port}}' + query.port: '{{server.allocations.default.port}}' + startup: + done: ')! For help, type ' + logs: { } + stop: stop +scripts: + installation: + script: |- + #!/bin/ash + # Vanilla MC Installation Script + # + # Server Files: /mnt/server + mkdir -p /mnt/server + cd /mnt/server + + LATEST_VERSION=`curl https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r '.latest.release'` + LATEST_SNAPSHOT_VERSION=`curl https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r '.latest.snapshot'` + + echo -e "latest version is $LATEST_VERSION" + echo -e "latest snapshot is $LATEST_SNAPSHOT_VERSION" + + if [ -z "$VANILLA_VERSION" ] || [ "$VANILLA_VERSION" == "latest" ]; then + MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url') + elif [ "$VANILLA_VERSION" == "snapshot" ]; then + MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $LATEST_SNAPSHOT_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url') + else + MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url') + fi + + DOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url') + + echo -e "running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL" + curl -o ${SERVER_JARFILE} $DOWNLOAD_URL + + echo -e "Install Complete" + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'Server Jar File' + description: 'The name of the server jarfile to run the server with.' + env_variable: SERVER_JARFILE + default_value: server.jar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^([\w\d._-]+)(\.jar)$/' + sort: 1 + - + name: 'Server Version' + description: |- + The version of Minecraft Vanilla to install. Use "latest" to install the latest version, or use + "snapshot" to install the latest snapshot. Go to Settings > Reinstall Server to apply. + env_variable: VANILLA_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - string + - 'between:3,15' + sort: 2 diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json deleted file mode 100644 index ebe84d132..000000000 --- a/database/Seeders/eggs/rust/egg-rust.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json" - }, - "exported_at": "2025-06-06T11:57:17+00:00", - "name": "Rust", - "author": "panel@example.com", - "uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae", - "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:rust": "ghcr.io\/parkervcp\/games:rust" - }, - "file_denylist": [], - "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{SERVER_HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"Server startup complete\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Server Name", - "description": "The name of your server in the public server list.", - "env_variable": "SERVER_HOSTNAME", - "default_value": "A Rust Server", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:60" - ], - "sort": 1 - }, - { - "name": "Modding Framework", - "description": "The modding framework to be used: carbon, oxide, vanilla.\r\nDefaults to \"vanilla\" for a non-modded server installation.", - "env_variable": "FRAMEWORK", - "default_value": "vanilla", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "in:vanilla,oxide,carbon" - ], - "sort": 2 - }, - { - "name": "Level", - "description": "The world file for Rust to use.", - "env_variable": "LEVEL", - "default_value": "Procedural Map", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:20" - ], - "sort": 3 - }, - { - "name": "Description", - "description": "The description under your server title. Commonly used for rules & info. Use \\n for newlines.", - "env_variable": "DESCRIPTION", - "default_value": "Powered by Panel", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string" - ], - "sort": 4 - }, - { - "name": "URL", - "description": "The URL for your server. This is what comes up when clicking the \"Visit Website\" button.", - "env_variable": "SERVER_URL", - "default_value": "http:\/\/example.com", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "url" - ], - "sort": 5 - }, - { - "name": "World Size", - "description": "The world size for a procedural map.", - "env_variable": "WORLD_SIZE", - "default_value": "3000", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "integer" - ], - "sort": 6 - }, - { - "name": "World Seed", - "description": "The seed for a procedural map.", - "env_variable": "WORLD_SEED", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string" - ], - "sort": 7 - }, - { - "name": "Max Players", - "description": "The maximum amount of players allowed in the server at once.", - "env_variable": "MAX_PLAYERS", - "default_value": "40", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "integer" - ], - "sort": 8 - }, - { - "name": "Server Image", - "description": "The header image for the top of your server listing.", - "env_variable": "SERVER_IMG", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "url" - ], - "sort": 9 - }, - { - "name": "Query Port", - "description": "Server Query Port. Can't be the same as Game's primary port.", - "env_variable": "QUERY_PORT", - "default_value": "27017", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer" - ], - "sort": 10 - }, - { - "name": "RCON Port", - "description": "Port for RCON connections.", - "env_variable": "RCON_PORT", - "default_value": "28016", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer" - ], - "sort": 11 - }, - { - "name": "RCON Password", - "description": "RCON access password.", - "env_variable": "RCON_PASS", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^[\\w.-]*$\/", - "max:64" - ], - "sort": 12 - }, - { - "name": "Save Interval", - "description": "Sets the server\u2019s auto-save interval in seconds.", - "env_variable": "SAVEINTERVAL", - "default_value": "60", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "integer" - ], - "sort": 13 - }, - { - "name": "Additional Arguments", - "description": "Add additional startup parameters to the server.", - "env_variable": "ADDITIONAL_ARGS", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string" - ], - "sort": 14 - }, - { - "name": "App Port", - "description": "Port for the Rust+ App. -1 to disable.", - "env_variable": "APP_PORT", - "default_value": "28082", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer" - ], - "sort": 15 - }, - { - "name": "Server Logo", - "description": "The circular server logo for the Rust+ app.", - "env_variable": "SERVER_LOGO", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "url" - ], - "sort": 16 - }, - { - "name": "Custom Map URL", - "description": "Overwrites the map with the one from the direct download URL. Invalid URLs will cause the server to crash.", - "env_variable": "MAP_URL", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "url" - ], - "sort": 17 - }, - { - "name": "App ID", - "description": "", - "env_variable": "SRCDS_APPID", - "default_value": "258550", - "user_viewable": false, - "user_editable": false, - "rules": [ - "required", - "string", - "in:258550" - ], - "sort": 18 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/rust/egg-rust.yaml b/database/Seeders/eggs/rust/egg-rust.yaml new file mode 100644 index 000000000..f65fcb2ba --- /dev/null +++ b/database/Seeders/eggs/rust/egg-rust.yaml @@ -0,0 +1,280 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/rust/egg-rust.yaml' +exported_at: '2025-07-25T13:30:56+00:00' +name: Rust +author: panel@example.com +uuid: bace2dfb-209c-452a-9459-7d6f340b07ae +description: |- + The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, + thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other + players, and kill them for meat. Create alliances with other players and form a town. Do whatever it + takes to survive. +tags: + - source + - steamcmd +features: + - steam_disk_space +docker_images: + 'ghcr.io/parkervcp/games:rust': 'ghcr.io/parkervcp/games:rust' +file_denylist: { } +startup: './RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity "rust" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \"{{SERVER_HOSTNAME}}\" +server.level \"{{LEVEL}}\" +server.description \"{{DESCRIPTION}}\" +server.url \"{{SERVER_URL}}\" +server.headerimage \"{{SERVER_IMG}}\" +server.logoimage \"{{SERVER_LOGO}}\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \"{{RCON_PASS}}\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s "+server.worldsize \"{{WORLD_SIZE}}\" +server.seed \"{{WORLD_SEED}}\"" || printf %s "+server.levelurl {{MAP_URL}}" ) {{ADDITIONAL_ARGS}}' +config: + files: { } + startup: + done: 'Server startup complete' + logs: { } + stop: quit +scripts: + installation: + script: |- + #!/bin/bash + # steamcmd Base Installation Script + # + # Server Files: /mnt/server + + + ## just in case someone removed the defaults. + if [ "${STEAM_USER}" == "" ]; then + echo -e "steam user is not set. + " + echo -e "Using anonymous user. + " + STEAM_USER=anonymous + STEAM_PASS="" + STEAM_AUTH="" + else + echo -e "user set to ${STEAM_USER}" + fi + + ## download and install steamcmd + cd /tmp + mkdir -p /mnt/server/steamcmd + curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz + tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd + mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing + cd /mnt/server/steamcmd + + # SteamCMD fails otherwise for some reason, even running as root. + # This is changed at the end of the install process anyways. + chown -R root:root /mnt + export HOME=/mnt/server + + ## install game using steamcmd + ./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6 + + ## set up 32 bit libraries + mkdir -p /mnt/server/.steam/sdk32 + cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so + + ## set up 64 bit libraries + mkdir -p /mnt/server/.steam/sdk64 + cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so + container: 'ghcr.io/parkervcp/installers:debian' + entrypoint: bash +variables: + - + name: 'Additional Arguments' + description: 'Add additional startup parameters to the server.' + env_variable: ADDITIONAL_ARGS + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + sort: 14 + - + name: 'App ID' + description: '' + env_variable: SRCDS_APPID + default_value: 258550 + user_viewable: false + user_editable: false + rules: + - required + - string + - 'in:258550' + sort: 18 + - + name: 'App Port' + description: 'Port for the Rust+ App. -1 to disable.' + env_variable: APP_PORT + default_value: 28082 + user_viewable: true + user_editable: false + rules: + - required + - integer + sort: 15 + - + name: 'Custom Map URL' + description: |- + Overwrites the map with the one from the direct download URL. Invalid URLs will cause the server to + crash. + env_variable: MAP_URL + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - url + sort: 17 + - + name: Description + description: 'The description under your server title. Commonly used for rules & info. Use \n for newlines.' + env_variable: DESCRIPTION + default_value: 'Powered by Panel' + user_viewable: true + user_editable: true + rules: + - required + - string + sort: 4 + - + name: Level + description: 'The world file for Rust to use.' + env_variable: LEVEL + default_value: 'Procedural Map' + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:20' + sort: 3 + - + name: 'Max Players' + description: 'The maximum amount of players allowed in the server at once.' + env_variable: MAX_PLAYERS + default_value: 40 + user_viewable: true + user_editable: true + rules: + - required + - integer + sort: 8 + - + name: 'Modding Framework' + description: "The modding framework to be used: carbon, oxide, vanilla.\r\nDefaults to \"vanilla\" for a non-modded server installation." + env_variable: FRAMEWORK + default_value: vanilla + user_viewable: true + user_editable: true + rules: + - required + - 'in:vanilla,oxide,carbon' + sort: 2 + - + name: 'Query Port' + description: "Server Query Port. Can't be the same as Game's primary port." + env_variable: QUERY_PORT + default_value: 27017 + user_viewable: true + user_editable: false + rules: + - required + - integer + sort: 10 + - + name: 'RCON Password' + description: 'RCON access password.' + env_variable: RCON_PASS + default_value: '' + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^[\w.-]*$/' + - 'max:64' + sort: 12 + - + name: 'RCON Port' + description: 'Port for RCON connections.' + env_variable: RCON_PORT + default_value: 28016 + user_viewable: true + user_editable: false + rules: + - required + - integer + sort: 11 + - + name: 'Save Interval' + description: 'Sets the server’s auto-save interval in seconds.' + env_variable: SAVEINTERVAL + default_value: 60 + user_viewable: true + user_editable: true + rules: + - required + - integer + sort: 13 + - + name: 'Server Image' + description: 'The header image for the top of your server listing.' + env_variable: SERVER_IMG + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - url + sort: 9 + - + name: 'Server Logo' + description: 'The circular server logo for the Rust+ app.' + env_variable: SERVER_LOGO + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - url + sort: 16 + - + name: 'Server Name' + description: 'The name of your server in the public server list.' + env_variable: SERVER_HOSTNAME + default_value: 'A Rust Server' + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:60' + sort: 1 + - + name: URL + description: 'The URL for your server. This is what comes up when clicking the "Visit Website" button.' + env_variable: SERVER_URL + default_value: 'http://example.com' + user_viewable: true + user_editable: true + rules: + - nullable + - url + sort: 5 + - + name: 'World Seed' + description: 'The seed for a procedural map.' + env_variable: WORLD_SEED + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + sort: 7 + - + name: 'World Size' + description: 'The world size for a procedural map.' + env_variable: WORLD_SIZE + default_value: 3000 + user_viewable: true + user_editable: true + rules: + - required + - integer + sort: 6 diff --git a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json deleted file mode 100644 index 00c9de02e..000000000 --- a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-custom-source-engine-game.json" - }, - "exported_at": "2025-03-18T12:36:07+00:00", - "name": "Custom Source Engine Game", - "author": "panel@example.com", - "uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490", - "description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:source": "ghcr.io\/parkervcp\/games:source" - }, - "file_denylist": [], - "startup": ".\/srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Game ID", - "description": "The ID corresponding to the game to download and run using SRCDS.", - "env_variable": "SRCDS_APPID", - "default_value": "", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "numeric", - "digits_between:1,6" - ], - "sort": 1 - }, - { - "name": "Game Name", - "description": "The name corresponding to the game to download and run using SRCDS.", - "env_variable": "SRCDS_GAME", - "default_value": "", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "alpha_dash", - "between:1,100" - ], - "sort": 2 - }, - { - "name": "Map", - "description": "The default map for the server.", - "env_variable": "SRCDS_MAP", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "alpha_dash" - ], - "sort": 3 - }, - { - "name": "Steam Username", - "description": "", - "env_variable": "STEAM_USER", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string" - ], - "sort": 4 - }, - { - "name": "Steam Password", - "description": "", - "env_variable": "STEAM_PASS", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string" - ], - "sort": 5 - }, - { - "name": "Steam Auth", - "description": "", - "env_variable": "STEAM_AUTH", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string" - ], - "sort": 6 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml new file mode 100644 index 000000000..ee871c043 --- /dev/null +++ b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml @@ -0,0 +1,153 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml' +exported_at: '2025-07-25T13:30:23+00:00' +name: 'Custom Source Engine Game' +author: panel@example.com +uuid: 2a42d0c2-c0ba-4067-9a0a-9b95d77a3490 +description: |- + This option allows modifying the startup arguments and other details to run a custom SRCDS based + game on the panel. +tags: + - source + - steamcmd +features: + - steam_disk_space +docker_images: + 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' +file_denylist: { } +startup: './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' +config: + files: { } + startup: + done: 'gameserver Steam ID' + logs: { } + stop: quit +scripts: + installation: + script: |- + #!/bin/bash + # steamcmd Base Installation Script + # + # Server Files: /mnt/server + + ## + # + # Variables + # STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install. + # WINDOWS_INSTALL - if it's a windows server you want to install set to 1 + # SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List + # EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates. + # + ## + + + ## just in case someone removed the defaults. + if [ "${STEAM_USER}" == "" ]; then + echo -e "steam user is not set. + " + echo -e "Using anonymous user. + " + STEAM_USER=anonymous + STEAM_PASS="" + STEAM_AUTH="" + else + echo -e "user set to ${STEAM_USER}" + fi + + ## download and install steamcmd + cd /tmp + mkdir -p /mnt/server/steamcmd + curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz + tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd + mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing + cd /mnt/server/steamcmd + + # SteamCMD fails otherwise for some reason, even running as root. + # This is changed at the end of the install process anyways. + chown -R root:root /mnt + export HOME=/mnt/server + + ## install game using steamcmd + ./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6 + + ## set up 32 bit libraries + mkdir -p /mnt/server/.steam/sdk32 + cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so + + ## set up 64 bit libraries + mkdir -p /mnt/server/.steam/sdk64 + cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so + container: 'ghcr.io/parkervcp/installers:debian' + entrypoint: bash +variables: + - + name: 'Game ID' + description: 'The ID corresponding to the game to download and run using SRCDS.' + env_variable: SRCDS_APPID + default_value: '' + user_viewable: true + user_editable: false + rules: + - required + - numeric + - 'digits_between:1,6' + sort: 1 + - + name: 'Game Name' + description: 'The name corresponding to the game to download and run using SRCDS.' + env_variable: SRCDS_GAME + default_value: '' + user_viewable: true + user_editable: false + rules: + - required + - alpha_dash + - 'between:1,100' + sort: 2 + - + name: Map + description: 'The default map for the server.' + env_variable: SRCDS_MAP + default_value: '' + user_viewable: true + user_editable: true + rules: + - required + - string + - alpha_dash + sort: 3 + - + name: 'Steam Auth' + description: '' + env_variable: STEAM_AUTH + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + sort: 6 + - + name: 'Steam Password' + description: '' + env_variable: STEAM_PASS + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + sort: 5 + - + name: 'Steam Username' + description: '' + env_variable: STEAM_USER + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + sort: 4 diff --git a/database/Seeders/eggs/source-engine/egg-garrys-mod.json b/database/Seeders/eggs/source-engine/egg-garrys-mod.json deleted file mode 100644 index 96a332d86..000000000 --- a/database/Seeders/eggs/source-engine/egg-garrys-mod.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-garrys-mod.json" - }, - "exported_at": "2025-03-18T12:36:13+00:00", - "name": "Garrys Mod", - "author": "panel@example.com", - "uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915", - "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "gsl_token", - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:source": "ghcr.io\/parkervcp\/games:source" - }, - "file_denylist": [], - "startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ \"$LUA_REFRESH\" == \"1\" ] || printf %s '-disableluarefresh' )", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\nsv_downloadurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\n\/\/ sv_location \"eu\"\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Map", - "description": "The default map for the server.", - "env_variable": "SRCDS_MAP", - "default_value": "gm_flatgrass", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "alpha_dash" - ], - "sort": 1 - }, - { - "name": "Steam Account Token", - "description": "The Steam Account Token required for the server to be displayed publicly.", - "env_variable": "STEAM_ACC", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "string", - "alpha_num", - "size:32" - ], - "sort": 2 - }, - { - "name": "Source AppID", - "description": "Required for game to update on server restart. Do not modify this.", - "env_variable": "SRCDS_APPID", - "default_value": "4020", - "user_viewable": false, - "user_editable": false, - "rules": [ - "required", - "string", - "max:20" - ], - "sort": 3 - }, - { - "name": "Workshop ID", - "description": "The ID of your workshop collection (the numbers at the end of the URL)", - "env_variable": "WORKSHOP_ID", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "nullable", - "integer" - ], - "sort": 4 - }, - { - "name": "Gamemode", - "description": "The gamemode of your server.", - "env_variable": "GAMEMODE", - "default_value": "sandbox", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string" - ], - "sort": 5 - }, - { - "name": "Max Players", - "description": "The maximum amount of players allowed on your game server.", - "env_variable": "MAX_PLAYERS", - "default_value": "32", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "integer", - "max:128" - ], - "sort": 6 - }, - { - "name": "Tickrate", - "description": "The tickrate defines how fast the server will update each entity's location.", - "env_variable": "TICKRATE", - "default_value": "22", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "integer", - "max:100" - ], - "sort": 7 - }, - { - "name": "Lua Refresh", - "description": "0 = disable Lua refresh,\r\n1 = enable Lua refresh", - "env_variable": "LUA_REFRESH", - "default_value": "0", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "boolean" - ], - "sort": 8 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml b/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml new file mode 100644 index 000000000..7c8761599 --- /dev/null +++ b/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml @@ -0,0 +1,228 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml' +exported_at: '2025-07-25T13:30:30+00:00' +name: 'Garrys Mod' +author: panel@example.com +uuid: 60ef81d4-30a2-4d98-ab64-f59c69e2f915 +description: |- + Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, + Facepunch Studios. +tags: + - source + - steamcmd +features: + - gsl_token + - steam_disk_space +docker_images: + 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' +file_denylist: { } +startup: './srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ "$LUA_REFRESH" == "1" ] || printf %s ''-disableluarefresh'' )' +config: + files: { } + startup: + done: 'gameserver Steam ID' + logs: { } + stop: quit +scripts: + installation: + script: |- + #!/bin/bash + # steamcmd Base Installation Script + # + # Server Files: /mnt/server + + ## just in case someone removed the defaults. + if [ "${STEAM_USER}" == "" ]; then + echo -e "steam user is not set. + " + echo -e "Using anonymous user. + " + STEAM_USER=anonymous + STEAM_PASS="" + STEAM_AUTH="" + else + echo -e "user set to ${STEAM_USER}" + fi + + ## download and install steamcmd + cd /tmp + mkdir -p /mnt/server/steamcmd + curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz + tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd + mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing + cd /mnt/server/steamcmd + + # SteamCMD fails otherwise for some reason, even running as root. + # This is changed at the end of the install process anyways. + chown -R root:root /mnt + export HOME=/mnt/server + + ## install game using steamcmd + ./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6 + + ## set up 32 bit libraries + mkdir -p /mnt/server/.steam/sdk32 + cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so + + ## set up 64 bit libraries + mkdir -p /mnt/server/.steam/sdk64 + cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so + + # Creating needed default files for the game + cd /mnt/server/garrysmod/lua/autorun/server + echo ' + -- Docs: https://wiki.garrysmod.com/page/resource/AddWorkshop + -- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID + -- Use https://beta.configcreator.com/create/gmod/resources.lua to easily create a list based on your collection ID + + resource.AddWorkshop( "" ) + ' > workshop.lua + + cd /mnt/server/garrysmod/cfg + echo ' + // Please do not set RCon in here, use the startup parameters. + + hostname "New Gmod Server" + sv_password "" + sv_loadingurl "" + sv_downloadurl "" + + // Steam Server List Settings + // sv_location "eu" + sv_region "255" + sv_lan "0" + sv_max_queries_sec_global "30000" + sv_max_queries_window "45" + sv_max_queries_sec "5" + + // Server Limits + sbox_maxprops 100 + sbox_maxragdolls 5 + sbox_maxnpcs 10 + sbox_maxballoons 10 + sbox_maxeffects 10 + sbox_maxdynamite 10 + sbox_maxlamps 10 + sbox_maxthrusters 10 + sbox_maxwheels 10 + sbox_maxhoverballs 10 + sbox_maxvehicles 20 + sbox_maxbuttons 10 + sbox_maxsents 20 + sbox_maxemitters 5 + sbox_godmode 0 + sbox_noclip 0 + + // Network Settings - Please keep these set to default. + + sv_minrate 75000 + sv_maxrate 0 + gmod_physiterations 2 + net_splitpacket_maxrate 45000 + decalfrequency 12 + + // Execute Ban Files - Please do not edit + exec banned_ip.cfg + exec banned_user.cfg + + // Add custom lines under here + ' > server.cfg + container: 'ghcr.io/parkervcp/installers:debian' + entrypoint: bash +variables: + - + name: Gamemode + description: 'The gamemode of your server.' + env_variable: GAMEMODE + default_value: sandbox + user_viewable: true + user_editable: true + rules: + - required + - string + sort: 5 + - + name: 'Lua Refresh' + description: "0 = disable Lua refresh,\r\n1 = enable Lua refresh" + env_variable: LUA_REFRESH + default_value: 0 + user_viewable: true + user_editable: true + rules: + - required + - boolean + sort: 8 + - + name: Map + description: 'The default map for the server.' + env_variable: SRCDS_MAP + default_value: gm_flatgrass + user_viewable: true + user_editable: true + rules: + - required + - string + - alpha_dash + sort: 1 + - + name: 'Max Players' + description: 'The maximum amount of players allowed on your game server.' + env_variable: MAX_PLAYERS + default_value: 32 + user_viewable: true + user_editable: true + rules: + - required + - integer + - 'max:128' + sort: 6 + - + name: 'Source AppID' + description: 'Required for game to update on server restart. Do not modify this.' + env_variable: SRCDS_APPID + default_value: 4020 + user_viewable: false + user_editable: false + rules: + - required + - string + - 'max:20' + sort: 3 + - + name: 'Steam Account Token' + description: 'The Steam Account Token required for the server to be displayed publicly.' + env_variable: STEAM_ACC + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - string + - alpha_num + - 'size:32' + sort: 2 + - + name: Tickrate + description: "The tickrate defines how fast the server will update each entity's location." + env_variable: TICKRATE + default_value: 22 + user_viewable: true + user_editable: true + rules: + - required + - integer + - 'max:100' + sort: 7 + - + name: 'Workshop ID' + description: 'The ID of your workshop collection (the numbers at the end of the URL)' + env_variable: WORKSHOP_ID + default_value: '' + user_viewable: true + user_editable: true + rules: + - nullable + - integer + sort: 4 diff --git a/database/Seeders/eggs/source-engine/egg-insurgency.json b/database/Seeders/eggs/source-engine/egg-insurgency.json deleted file mode 100644 index 737056af9..000000000 --- a/database/Seeders/eggs/source-engine/egg-insurgency.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-insurgency.json" - }, - "exported_at": "2025-03-18T12:36:22+00:00", - "name": "Insurgency", - "author": "panel@example.com", - "uuid": "a5702286-655b-4069-bf1e-925c7300b61a", - "description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:source": "ghcr.io\/parkervcp\/games:source" - }, - "file_denylist": [], - "startup": ".\/srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login anonymous +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Game ID", - "description": "The ID corresponding to the game to download and run using SRCDS.", - "env_variable": "SRCDS_APPID", - "default_value": "237410", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "regex:\/^(237410)$\/" - ], - "sort": 1 - }, - { - "name": "Default Map", - "description": "The default map to use when starting the server.", - "env_variable": "SRCDS_MAP", - "default_value": "sinjar", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^(\\w{1,20})$\/" - ], - "sort": 2 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-insurgency.yaml b/database/Seeders/eggs/source-engine/egg-insurgency.yaml new file mode 100644 index 000000000..0c5b0515b --- /dev/null +++ b/database/Seeders/eggs/source-engine/egg-insurgency.yaml @@ -0,0 +1,82 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-insurgency.yaml' +exported_at: '2025-07-25T13:30:35+00:00' +name: Insurgency +author: panel@example.com +uuid: a5702286-655b-4069-bf1e-925c7300b61a +description: |- + Take to the streets for intense close quarters combat, where a team's survival depends upon securing + crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine + based experience. +tags: + - source + - steamcmd +features: + - steam_disk_space +docker_images: + 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' +file_denylist: { } +startup: './srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart' +config: + files: { } + startup: + done: 'gameserver Steam ID' + logs: { } + stop: quit +scripts: + installation: + script: |- + #!/bin/bash + # steamcmd Base Installation Script + # + # Server Files: /mnt/server + + ## download and install steamcmd + cd /tmp + mkdir -p /mnt/server/steamcmd + curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz + tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd + cd /mnt/server/steamcmd + + # SteamCMD fails otherwise for some reason, even running as root. + # This is changed at the end of the install process anyways. + chown -R root:root /mnt + export HOME=/mnt/server + + ## install game using steamcmd + ./steamcmd.sh +force_install_dir /mnt/server +login anonymous +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit + + ## set up 32 bit libraries + mkdir -p /mnt/server/.steam/sdk32 + cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so + + ## set up 64 bit libraries + mkdir -p /mnt/server/.steam/sdk64 + cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so + container: 'ghcr.io/parkervcp/installers:debian' + entrypoint: bash +variables: + - + name: 'Default Map' + description: 'The default map to use when starting the server.' + env_variable: SRCDS_MAP + default_value: sinjar + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^(\w{1,20})$/' + sort: 2 + - + name: 'Game ID' + description: 'The ID corresponding to the game to download and run using SRCDS.' + env_variable: SRCDS_APPID + default_value: 237410 + user_viewable: true + user_editable: false + rules: + - required + - 'regex:/^(237410)$/' + sort: 1 diff --git a/database/Seeders/eggs/source-engine/egg-team-fortress2.json b/database/Seeders/eggs/source-engine/egg-team-fortress2.json deleted file mode 100644 index 0002106d4..000000000 --- a/database/Seeders/eggs/source-engine/egg-team-fortress2.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-team-fortress2.json" - }, - "exported_at": "2025-03-18T12:36:28+00:00", - "name": "Team Fortress 2", - "author": "panel@example.com", - "uuid": "7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d", - "description": "Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.", - "tags": [ - "source", - "steamcmd" - ], - "features": [ - "gsl_token", - "steam_disk_space" - ], - "docker_images": { - "ghcr.io\/parkervcp\/games:source": "ghcr.io\/parkervcp\/games:source" - }, - "file_denylist": [], - "startup": ".\/srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"gameserver Steam ID\"\r\n}", - "logs": "{}", - "stop": "quit" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so", - "container": "ghcr.io\/parkervcp\/installers:debian", - "entrypoint": "bash" - } - }, - "variables": [ - { - "name": "Game ID", - "description": "The ID corresponding to the game to download and run using SRCDS.", - "env_variable": "SRCDS_APPID", - "default_value": "232250", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "in:232250" - ], - "sort": 1 - }, - { - "name": "Default Map", - "description": "The default map to use when starting the server.", - "env_variable": "SRCDS_MAP", - "default_value": "cp_dustbowl", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "regex:\/^(\\w{1,20})$\/" - ], - "sort": 2 - }, - { - "name": "Steam", - "description": "The Steam Game Server Login Token to display servers publicly. Generate one at https:\/\/steamcommunity.com\/dev\/managegameservers", - "env_variable": "STEAM_ACC", - "default_value": "", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "alpha_num", - "size:32" - ], - "sort": 3 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml b/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml new file mode 100644 index 000000000..dafcad0b3 --- /dev/null +++ b/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml @@ -0,0 +1,122 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml' +exported_at: '2025-07-25T13:30:44+00:00' +name: 'Team Fortress 2' +author: panel@example.com +uuid: 7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d +description: |- + Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published + by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake. +tags: + - source + - steamcmd +features: + - gsl_token + - steam_disk_space +docker_images: + 'ghcr.io/parkervcp/games:source': 'ghcr.io/parkervcp/games:source' +file_denylist: { } +startup: './srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}' +config: + files: { } + startup: + done: 'gameserver Steam ID' + logs: { } + stop: quit +scripts: + installation: + script: |- + #!/bin/bash + # steamcmd Base Installation Script + # + # Server Files: /mnt/server + # Image to install with is 'debian:buster-slim' + + ## + # + # Variables + # STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install. + # WINDOWS_INSTALL - if it's a windows server you want to install set to 1 + # SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List + # EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates. + # + ## + + ## just in case someone removed the defaults. + if [ "${STEAM_USER}" == "" ]; then + echo -e "steam user is not set. + " + echo -e "Using anonymous user. + " + STEAM_USER=anonymous + STEAM_PASS="" + STEAM_AUTH="" + else + echo -e "user set to ${STEAM_USER}" + fi + + ## download and install steamcmd + cd /tmp + mkdir -p /mnt/server/steamcmd + curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz + tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd + mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing + cd /mnt/server/steamcmd + + # SteamCMD fails otherwise for some reason, even running as root. + # This is changed at the end of the install process anyways. + chown -R root:root /mnt + export HOME=/mnt/server + + ## install game using steamcmd + ./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6 + + ## set up 32 bit libraries + mkdir -p /mnt/server/.steam/sdk32 + cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so + + ## set up 64 bit libraries + mkdir -p /mnt/server/.steam/sdk64 + cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so + container: 'ghcr.io/parkervcp/installers:debian' + entrypoint: bash +variables: + - + name: 'Default Map' + description: 'The default map to use when starting the server.' + env_variable: SRCDS_MAP + default_value: cp_dustbowl + user_viewable: true + user_editable: true + rules: + - required + - 'regex:/^(\w{1,20})$/' + sort: 2 + - + name: 'Game ID' + description: 'The ID corresponding to the game to download and run using SRCDS.' + env_variable: SRCDS_APPID + default_value: 232250 + user_viewable: true + user_editable: false + rules: + - required + - 'in:232250' + sort: 1 + - + name: Steam + description: |- + The Steam Game Server Login Token to display servers publicly. Generate one at + https://steamcommunity.com/dev/managegameservers + env_variable: STEAM_ACC + default_value: '' + user_viewable: true + user_editable: true + rules: + - required + - string + - alpha_num + - 'size:32' + sort: 3 diff --git a/database/Seeders/eggs/voice-servers/egg-mumble-server.json b/database/Seeders/eggs/voice-servers/egg-mumble-server.json deleted file mode 100644 index 9f34bdf80..000000000 --- a/database/Seeders/eggs/voice-servers/egg-mumble-server.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-mumble-server.json" - }, - "exported_at": "2025-03-18T12:36:35+00:00", - "name": "Mumble Server", - "author": "panel@example.com", - "uuid": "727ee758-7fb2-4979-972b-d3eba4e1e9f0", - "description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.", - "tags": [ - "voice" - ], - "features": [], - "docker_images": { - "Mumble": "ghcr.io\/parkervcp\/yolks:voice_mumble" - }, - "file_denylist": [], - "startup": "mumble-server -fg -ini murmur.ini", - "config": { - "files": "{\r\n \"murmur.ini\": {\r\n \"parser\": \"ini\",\r\n \"find\": {\r\n \"database\": \"\/home\/container\/murmur.sqlite\",\r\n \"logfile\": \"\/home\/container\/murmur.log\",\r\n \"port\": \"{{server.allocations.default.port}}\",\r\n \"host\": \"\",\r\n \"users\": \"{{server.environment.MAX_USERS}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \"Server listening on\"\r\n}", - "logs": "{}", - "stop": "^C" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n\r\nif [ ! -d \/mnt\/server\/ ]; then\r\n mkdir \/mnt\/server\/\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nFILE=\/mnt\/server\/murmur.ini\r\nif [ -f \"$FILE\" ]; then\r\n echo \"Config file already exists.\"\r\nelse \r\n echo \"Downloading the config file.\"\r\n apk add --no-cache murmur\r\n cp \/etc\/murmur.ini \/mnt\/server\/murmur.ini\r\n apk del murmur\r\nfi\r\necho \"done\"", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Maximum Users", - "description": "Maximum concurrent users on the mumble server.", - "env_variable": "MAX_USERS", - "default_value": "100", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "numeric", - "digits_between:1,5" - ], - "sort": 1 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml b/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml new file mode 100644 index 000000000..6efe64c4c --- /dev/null +++ b/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml @@ -0,0 +1,68 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/voice-servers/egg-mumble-server.yaml' +exported_at: '2025-07-25T13:30:48+00:00' +name: 'Mumble Server' +author: panel@example.com +uuid: 727ee758-7fb2-4979-972b-d3eba4e1e9f0 +description: |- + Mumble is an open source, low-latency, high quality voice chat software primarily intended for use + while gaming. +tags: + - voice +features: { } +docker_images: + Mumble: 'ghcr.io/parkervcp/yolks:voice_mumble' +file_denylist: { } +startup: 'mumble-server -fg -ini murmur.ini' +config: + files: + murmur.ini: + parser: ini + find: + database: /home/container/murmur.sqlite + logfile: /home/container/murmur.log + port: '{{server.allocations.default.port}}' + host: '' + users: '{{server.environment.MAX_USERS}}' + startup: + done: 'Server listening on' + logs: { } + stop: ^C +scripts: + installation: + script: |- + #!/bin/ash + + if [ ! -d /mnt/server/ ]; then + mkdir /mnt/server/ + fi + + cd /mnt/server + + FILE=/mnt/server/murmur.ini + if [ -f "$FILE" ]; then + echo "Config file already exists." + else + echo "Downloading the config file." + apk add --no-cache murmur + cp /etc/murmur.ini /mnt/server/murmur.ini + apk del murmur + fi + echo "done" + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'Maximum Users' + description: 'Maximum concurrent users on the mumble server.' + env_variable: MAX_USERS + default_value: 100 + user_viewable: true + user_editable: false + rules: + - required + - numeric + - 'digits_between:1,5' + sort: 1 diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json deleted file mode 100644 index cd98e2833..000000000 --- a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", - "meta": { - "version": "PLCN_v1", - "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-teamspeak3-server.json" - }, - "exported_at": "2025-03-18T12:36:41+00:00", - "name": "Teamspeak3 Server", - "author": "panel@example.com", - "uuid": "983b1fac-d322-4d5f-a636-436127326b37", - "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", - "tags": [ - "voice" - ], - "features": [], - "docker_images": { - "Debian": "ghcr.io\/parkervcp\/yolks:debian" - }, - "file_denylist": [], - "startup": ".\/ts3server default_voice_port={{SERVER_PORT}} query_port={{QUERY_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} query_http_port={{QUERY_HTTP}} query_ssh_port={{QUERY_SSH}} query_protocols={{QUERY_PROTOCOLS_VAR}} license_accepted=1", - "config": { - "files": "{}", - "startup": "{\r\n \"done\": \"listening on 0.0.0.0:\"\r\n}", - "logs": "{}", - "stop": "^C" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# TS3 Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\nif [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then\r\n TS_VERSION=$(curl -sSL https:\/\/teamspeak.com\/versions\/server.json | jq -r '.linux.x86_64.version')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"getting files from http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\" \r\ncurl -L http:\/\/files.teamspeak-services.com\/releases\/server\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar -xvj --strip-components=1\r\ncp .\/redist\/libmariadb.so.2 .\/", - "container": "ghcr.io\/parkervcp\/installers:alpine", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Server Version", - "description": "The version of Teamspeak 3 to use when running the server.", - "env_variable": "TS_VERSION", - "default_value": "latest", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:6" - ], - "sort": 1 - }, - { - "name": "File Transfer Port", - "description": "The Teamspeak file transfer port", - "env_variable": "FILE_TRANSFER", - "default_value": "30033", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer", - "between:1025,65535" - ], - "sort": 2 - }, - { - "name": "Query Port", - "description": "The Teamspeak Query Port", - "env_variable": "QUERY_PORT", - "default_value": "10011", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer", - "between:1025,65535" - ], - "sort": 3 - }, - { - "name": "Query Protocols", - "description": "Comma separated list of protocols that can be used to connect to the ServerQuery | \r\nPossible values are raw, ssh and http | \r\nE.g.: raw,ssh,http", - "env_variable": "QUERY_PROTOCOLS_VAR", - "default_value": "raw,http,ssh", - "user_viewable": true, - "user_editable": true, - "rules": [ - "required", - "string", - "max:12" - ], - "sort": 4 - }, - { - "name": "Query SSH Port", - "description": "TCP Port opened for ServerQuery connections using SSH", - "env_variable": "QUERY_SSH", - "default_value": "10022", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer", - "between:1025,65535" - ], - "sort": 5 - }, - { - "name": "Query HTTP Port", - "description": "TCP Port opened for ServerQuery connections using http", - "env_variable": "QUERY_HTTP", - "default_value": "10080", - "user_viewable": true, - "user_editable": false, - "rules": [ - "required", - "integer", - "between:1025,65535" - ], - "sort": 6 - } - ] -} \ No newline at end of file diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml new file mode 100644 index 000000000..15fe93290 --- /dev/null +++ b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml @@ -0,0 +1,116 @@ +_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL' +meta: + version: PLCN_v2 + update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml' +exported_at: '2025-07-25T13:30:53+00:00' +name: 'Teamspeak3 Server' +author: panel@example.com +uuid: 983b1fac-d322-4d5f-a636-436127326b37 +description: |- + VoIP software designed with security in mind, featuring crystal clear voice quality, endless + customization options, and scalabilty up to thousands of simultaneous users. +tags: + - voice +features: { } +docker_images: + Debian: 'ghcr.io/parkervcp/yolks:debian' +file_denylist: { } +startup: './ts3server default_voice_port={{SERVER_PORT}} query_port={{QUERY_PORT}} filetransfer_ip=0.0.0.0 filetransfer_port={{FILE_TRANSFER}} query_http_port={{QUERY_HTTP}} query_ssh_port={{QUERY_SSH}} query_protocols={{QUERY_PROTOCOLS_VAR}} license_accepted=1' +config: + files: { } + startup: + done: 'listening on 0.0.0.0:' + logs: { } + stop: ^C +scripts: + installation: + script: |- + #!/bin/ash + # TS3 Installation Script + # + # Server Files: /mnt/server + + if [ -z ${TS_VERSION} ] || [ ${TS_VERSION} == latest ]; then + TS_VERSION=$(curl -sSL https://teamspeak.com/versions/server.json | jq -r '.linux.x86_64.version') + fi + + cd /mnt/server + + echo -e "getting files from http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2" + curl -L http://files.teamspeak-services.com/releases/server/${TS_VERSION}/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar -xvj --strip-components=1 + cp ./redist/libmariadb.so.2 ./ + container: 'ghcr.io/parkervcp/installers:alpine' + entrypoint: ash +variables: + - + name: 'File Transfer Port' + description: 'The Teamspeak file transfer port' + env_variable: FILE_TRANSFER + default_value: 30033 + user_viewable: true + user_editable: false + rules: + - required + - integer + - 'between:1025,65535' + sort: 2 + - + name: 'Query HTTP Port' + description: 'TCP Port opened for ServerQuery connections using http' + env_variable: QUERY_HTTP + default_value: 10080 + user_viewable: true + user_editable: false + rules: + - required + - integer + - 'between:1025,65535' + sort: 6 + - + name: 'Query Port' + description: 'The Teamspeak Query Port' + env_variable: QUERY_PORT + default_value: 10011 + user_viewable: true + user_editable: false + rules: + - required + - integer + - 'between:1025,65535' + sort: 3 + - + name: 'Query Protocols' + description: "Comma separated list of protocols that can be used to connect to the ServerQuery | \r\nPossible values are raw, ssh and http | \r\nE.g.: raw,ssh,http" + env_variable: QUERY_PROTOCOLS_VAR + default_value: 'raw,http,ssh' + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:12' + sort: 4 + - + name: 'Query SSH Port' + description: 'TCP Port opened for ServerQuery connections using SSH' + env_variable: QUERY_SSH + default_value: 10022 + user_viewable: true + user_editable: false + rules: + - required + - integer + - 'between:1025,65535' + sort: 5 + - + name: 'Server Version' + description: 'The version of Teamspeak 3 to use when running the server.' + env_variable: TS_VERSION + default_value: latest + user_viewable: true + user_editable: true + rules: + - required + - string + - 'max:6' + sort: 1 diff --git a/docker/Caddyfile b/docker/Caddyfile index e30760959..92c2f8dae 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -1,12 +1,18 @@ { - admin off - email {$ADMIN_EMAIL} + servers { + ## docs https://caddyserver.com/docs/caddyfile/options#trusted-proxies + {$CADDY_TRUSTED_PROXIES} + {$CADDY_STRICT_PROXIES} + } + admin off + auto_https off + email {$ADMIN_EMAIL} } {$APP_URL} { - root * /var/www/html/public - encode gzip + root * /var/www/html/public + encode gzip - php_fastcgi 127.0.0.1:9000 - file_server + file_server + php_fastcgi 127.0.0.1:9000 } diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 1089ea539..9734223c4 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,5 +1,4 @@ #!/bin/ash -e - ## check for .env file or symlink and generate app keys if missing if [ -f /var/www/html/.env ]; then echo "external vars exist." @@ -23,6 +22,8 @@ else echo -e "APP_INSTALLED=false" >> /pelican-data/.env fi +sed -i "s/upload_max_filesize = 2M/upload_max_filesize = ${UPLOAD_LIMIT}M/" /usr/local/etc/php/php.ini-production + mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /var/www/html/storage/logs/supervisord 2>/dev/null if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then @@ -39,6 +40,7 @@ php artisan migrate --force echo -e "Optimizing Filament" php artisan filament:optimize +# default to caddy not starting export SUPERVISORD_CADDY=false ## disable caddy if SKIP_CADDY is set @@ -46,7 +48,14 @@ if [[ "${SKIP_CADDY:-}" == "true" ]]; then echo "Starting PHP-FPM only" else echo "Starting PHP-FPM and Caddy" + # enable caddy export SUPERVISORD_CADDY=true + + # handle trusted proxies for caddy + if [[ ! -z ${TRUSTED_PROXIES} ]]; then + export CADDY_TRUSTED_PROXIES=$(echo "trusted_proxies static ${TRUSTED_PROXIES}" | sed 's/,/ /g') + export CADDY_STRICT_PROXIES="trusted_proxies_strict" + fi fi echo "Starting Supervisord" diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 000000000..8eef81989 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/ash -e + +if [ ${SKIP_CADDY} ! "true" ]; then + curl -f http://localhost/up || exit 1 +fi + +cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 2 + +exit 0 \ No newline at end of file diff --git a/lang/en/activity.php b/lang/en/activity.php index 9518ac970..a6f743dce 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -58,6 +58,7 @@ return [ 'fail' => 'Marked the :name backup as failed', 'lock' => 'Locked the :name backup', 'unlock' => 'Unlocked the :name backup', + 'rename' => 'Renamed backup from ":old_name" to ":new_name"', ], 'database' => [ 'create' => 'Created new database :name', diff --git a/lang/en/admin/egg.php b/lang/en/admin/egg.php index 365fa1670..7a3c19a45 100644 --- a/lang/en/admin/egg.php +++ b/lang/en/admin/egg.php @@ -13,14 +13,18 @@ return [ 'import' => [ 'file' => 'File', 'url' => 'URL', - 'egg_help' => 'This should be the raw .json file ( egg-minecraft.json )', - 'url_help' => 'URLs must point directly to the raw .json file', + 'egg_help' => 'This should be the raw .json/.yaml file', + 'url_help' => 'URLs must point directly to the raw .json/.yaml file', 'add_url' => 'New URL', 'import_failed' => 'Import Failed', 'import_success' => 'Import Success', 'github' => 'Add from Github', 'refresh' => 'Refresh', ], + 'export' => [ + 'modal' => 'How would you like to export :egg ?', + 'as' => 'As', + ], 'in_use' => 'In Use', 'servers' => 'Servers', 'name' => 'Name', diff --git a/lang/en/admin/schedule.php b/lang/en/admin/schedule.php deleted file mode 100644 index 79e6b449b..000000000 --- a/lang/en/admin/schedule.php +++ /dev/null @@ -1,15 +0,0 @@ - 'Schedule', - 'model_label_plural' => 'Schedule', - 'import' => [ - 'file' => 'File', - 'url' => 'URL', - 'schedule_help' => 'This should be the raw .json file ( schedule-daily-restart.json )', - 'url_help' => 'URLs must point directly to the raw .json file', - 'add_url' => 'New URL', - 'import_failed' => 'Import Failed', - 'import_success' => 'Import Success', - ], -]; diff --git a/lang/en/admin/setting.php b/lang/en/admin/setting.php index 9f0ee01be..97bafa4ab 100644 --- a/lang/en/admin/setting.php +++ b/lang/en/admin/setting.php @@ -97,6 +97,8 @@ return [ 'base_url' => 'Base URL', 'display_name' => 'Display Name', 'auth_url' => 'Authorization callback URL', + 'create_missing_users' => 'Auto Create Missing Users?', + 'link_missing_users' => 'Auto Link Missing Users?', ], 'misc' => [ 'auto_allocation' => [ diff --git a/lang/en/admin/webhook.php b/lang/en/admin/webhook.php index 691ad8501..9af58d75c 100644 --- a/lang/en/admin/webhook.php +++ b/lang/en/admin/webhook.php @@ -19,6 +19,7 @@ return [ 'headers' => 'Headers', 'events' => 'Events', 'regular' => 'Regular', + 'reset_headers' => 'Reset Headers', 'discord' => 'Discord', 'discord_message' => [ 'profile' => 'Profile', diff --git a/lang/en/profile.php b/lang/en/profile.php index 08eec394f..662f84df1 100644 --- a/lang/en/profile.php +++ b/lang/en/profile.php @@ -12,6 +12,7 @@ return [ 'customization' => 'Customization', ], 'username' => 'Username', + 'admin' => 'Admin', 'exit_admin' => 'Exit Admin', 'email' => 'Email', 'password' => 'Password', @@ -20,8 +21,8 @@ return [ 'timezone' => 'Timezone', 'language' => 'Language', 'language_help' => 'Your language :state has not been translated yet!', - 'link' => 'Link ', - 'unlink' => 'Unlink ', + 'link' => 'Link :name', + 'unlink' => 'Unlink :name', 'unlinked' => ':name unlinked', 'scan_qr' => 'Scan QR Code', 'code' => 'Code', @@ -57,4 +58,7 @@ return [ 'seconds' => 'Seconds', 'graph_period' => 'Graph Period', 'graph_period_helper' => 'The amount of data points, seconds, shown on the console graphs.', + 'navigation' => 'Navigation Type', + 'top' => 'Topbar', + 'side' => 'Sidebar', ]; diff --git a/lang/en/server/activity.php b/lang/en/server/activity.php new file mode 100644 index 000000000..43db3d6cc --- /dev/null +++ b/lang/en/server/activity.php @@ -0,0 +1,11 @@ + 'Activity', + 'event' => 'Event', + 'user' => 'User', + 'deleted_user' => 'Deleted User', + 'system' => 'System', + 'timestamp' => 'Timestamp', + 'metadata' => 'Metadata', +]; diff --git a/lang/en/server/backup.php b/lang/en/server/backup.php new file mode 100644 index 000000000..71276b638 --- /dev/null +++ b/lang/en/server/backup.php @@ -0,0 +1,50 @@ + 'Backups', + 'empty' => 'No Backups', + 'size' => 'Size', + 'created_at' => 'Created at', + 'status' => 'Status', + 'is_locked' => 'Lock Status', + 'backup_status' => [ + 'in_progress' => 'In Progress', + 'successful' => 'Successful', + 'failed' => 'Failed', + ], + 'actions' => [ + 'create' => [ + 'title' => 'Create Backup', + 'limit' => 'Backup Limit Reached', + 'created' => ':name created', + 'notification_success' => 'Backup Created Successfully', + 'notification_fail' => 'Backup Creation Failed', + 'name' => 'Name', + 'ignored' => 'Ignored Files & Directories', + 'locked' => 'Locked?', + 'lock_helper' => 'Prevents this backup from being deleted until explicitly unlocked.', + ], + 'lock' => [ + 'lock' => 'Lock', + 'unlock' => 'Unlock', + ], + 'download' => 'Download', + 'restore' => [ + 'title' => 'Restore', + 'helper' => 'Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.', + 'delete_all' => 'Delete all files before restoring backup?', + 'notification_started' => 'Restoring Backup', + 'notification_success' => 'Backup Restored Successfully', + 'notification_fail' => 'Backup Restore Failed', + 'notification_fail_body_1' => 'This server is not currently in a state that allows for a backup to be restored.', + 'notification_fail_body_2' => 'This backup cannot be restored at this time: not completed or failed.', + ], + 'delete' => [ + 'title' => 'Delete Backup', + 'description' => 'Do you wish to delete :backup?', + 'notification_success' => 'Backup Deleted', + 'notification_fail' => 'Could not delete backup', + 'notification_fail_body' => 'Connection to node failed. Please try again.', + ], + ], +]; diff --git a/lang/en/server/console.php b/lang/en/server/console.php new file mode 100644 index 000000000..8a00cdd00 --- /dev/null +++ b/lang/en/server/console.php @@ -0,0 +1,39 @@ + 'Console', + 'command' => 'Type a command...', + 'command_blocked' => 'Server Offline...', + 'command_blocked_title' => 'Can\'t send command when the server is Offline', + 'open_in_admin' => 'Open in Admin', + 'power_actions' => [ + 'start' => 'Start', + 'stop' => 'Stop', + 'restart' => 'Restart', + 'kill' => 'Kill', + 'kill_tooltip' => 'This can result in data corruption and/or data loss!', + ], + 'labels' => [ + 'cpu' => 'CPU', + 'memory' => 'Memory', + 'network' => 'Network', + 'disk' => 'Disk', + 'name' => 'Name', + 'status' => 'Status', + 'address' => 'Address', + 'unavailable' => 'Unavailable', + ], + 'status' => [ + 'created' => 'Created', + 'starting' => 'Starting', + 'running' => 'Running', + 'restarting' => 'Restarting', + 'exited' => 'Exited', + 'paused' => 'Paused', + 'dead' => 'Dead', + 'removing' => 'Removing', + 'stopping' => 'Stopping', + 'offline' => 'Offline', + 'missing' => 'Missing', + ], +]; diff --git a/lang/en/server/dashboard.php b/lang/en/server/dashboard.php new file mode 100644 index 000000000..bf527ae09 --- /dev/null +++ b/lang/en/server/dashboard.php @@ -0,0 +1,25 @@ + 'Servers', + 'list' => 'Server List', + 'my_servers' => 'My Servers', + 'other_servers' => 'Others\' Servers', + 'all_servers' => 'All Servers', + 'empty_own' => 'You don\'t own any servers!', + 'empty_other' => 'You don\'t have access to any servers!', + + 'status' => 'Status', + 'server' => 'Server', + 'resources' => 'Resources', + 'usage_limit' => 'Usage Limit: :resource', + + 'cpu' => 'CPU', + 'memory' => 'Memory', + 'disk' => 'Disk', + 'network' => 'Network', + 'none' => 'None', + 'loading' => 'Loading...', + + 'power_actions' => 'Power Actions', +]; diff --git a/lang/en/server/database.php b/lang/en/server/database.php new file mode 100644 index 000000000..72a3ddd04 --- /dev/null +++ b/lang/en/server/database.php @@ -0,0 +1,21 @@ + 'Databases', + 'create_database' => 'Create Database', + 'limit' => 'Database limit reached', + 'viewing' => 'Viewing: :database', + 'host' => 'Host', + 'database' => 'Database', + 'username' => 'Username', + 'password' => 'Password', + 'remote' => 'Remote', + 'created_at' => 'Created at', + 'name' => 'Database Name', + 'name_hint' => 'Leaving this blank will auto generate a random name', + 'connections_from' => 'Connections From', + 'max_connections' => 'Max Connections', + 'database_host' => 'Database Host', + 'database_host_select' => 'Select Database Host', + 'jdbc' => 'JDBC Connection String', +]; diff --git a/lang/en/server/file.php b/lang/en/server/file.php new file mode 100644 index 000000000..8e4bee67b --- /dev/null +++ b/lang/en/server/file.php @@ -0,0 +1,82 @@ + 'Files', + 'name' => 'Name', + 'size' => 'Size', + 'modified_at' => 'Modified at', + 'actions' => [ + 'open' => 'Open', + 'download' => 'Download', + 'copy' => [ + 'title' => 'Copy', + 'notification' => 'File Copied', + ], + 'upload' => [ + 'title' => 'Upload', + 'from_files' => 'Upload Files', + 'from_url' => 'Upload from URL', + 'url' => 'URL', + ], + 'rename' => [ + 'title' => 'Rename', + 'file_name' => 'File Name', + 'notification' => 'File Renamed', + ], + 'move' => [ + 'title' => 'Move', + 'directory' => 'Directory', + 'directory_hint' => 'Enter the new directory, relative to the current directory.', + 'new_location' => 'New Location', + 'new_location_hint' => 'Enter the location of this file or folder, relative to the current directory.', + 'notification' => 'File Moved', + 'bulk_notification' => ':count Files were moved to :directory', + ], + 'permissions' => [ + 'title' => 'Permissions', + 'read' => 'Read', + 'write' => 'Write', + 'execute' => 'Execute', + 'owner' => 'Owner', + 'group' => 'Group', + 'public' => 'Public', + 'notification' => 'Permissions changed to :mode', + ], + 'archive' => [ + 'title' => 'Archive', + 'archive_name' => 'Archive Name', + 'notification' => 'Archive Created', + ], + 'unarchive' => [ + 'title' => 'Unarchive', + 'notification' => 'Unarchive Completed', + ], + 'new_file' => [ + 'title' => 'New file', + 'file_name' => 'New file name', + 'syntax' => 'Syntax Highlighting', + 'create' => 'Create', + ], + 'new_folder' => [ + 'title' => 'New folder', + 'folder_name' => 'New folder name', + ], + 'global_search' => [ + 'title' => 'Global Search', + 'search_term' => 'Search term', + 'search_term_placeholder' => 'Enter a search term, ex. *.txt', + 'search' => 'Search', + ], + 'delete' => [ + 'notification' => 'File Deleted', + 'bulk_notification' => ':count files were deleted', + ], + 'edit' => [ + 'title' => 'Editing: :file', + 'save_close' => 'Save & Close', + 'save' => 'Save', + 'cancel' => 'Cancel', + 'notification' => 'File Saved', + ], + ], +]; diff --git a/lang/en/server/network.php b/lang/en/server/network.php new file mode 100644 index 000000000..21ef09012 --- /dev/null +++ b/lang/en/server/network.php @@ -0,0 +1,15 @@ + 'Network', + 'add' => 'Add Allocation', + 'limit' => 'Allocation limit reached', + 'address' => 'Address', + 'port' => 'Port', + 'notes' => 'Notes', + 'no_notes' => 'No Notes', + 'make_primary' => 'Make Primary', + 'primary' => 'Primary', + 'make' => 'Make', + 'delete' => 'Delete', +]; diff --git a/lang/en/server/schedule.php b/lang/en/server/schedule.php new file mode 100644 index 000000000..fbb49637e --- /dev/null +++ b/lang/en/server/schedule.php @@ -0,0 +1,118 @@ + 'Schedules', + 'new' => 'New Schedule', + 'edit' => 'Edit Schedule', + 'save' => 'Save Schedule', + 'delete' => 'Delete Schedule', + 'import' => 'Import Schedule', + 'export' => 'Export Schedule', + 'name' => 'Name', + 'cron' => 'Cron', + 'status' => 'Status', + 'inactive' => 'Inactive', + 'processing' => 'Processing', + 'active' => 'Active', + 'no_tasks' => 'No Tasks', + 'run_now' => 'Run Now', + 'online_only' => 'Only When Online', + 'last_run' => 'Last Run', + 'next_run' => 'Next Run', + 'never' => 'Never', + 'cancel' => 'Cancel', + + 'only_online' => 'Only when Server is Online?', + 'only_online_hint' => 'Only execute this schedule when the server is in a running state.', + 'enabled' => 'Enable Schedule?', + 'enabled_hint' => 'This schedule will be executed automatically if enabled.', + + 'cron_body' => 'Please keep in mind that the cron inputs below always assume UTC.', + 'cron_timezone' => 'Next run in your timezone (:timezone): :next_run ', + + 'invalid' => 'Invalid', + + 'time' => [ + 'minute' => 'Minute', + 'hour' => 'Hour', + 'day' => 'Day', + 'week' => 'Week', + 'month' => 'Month', + 'day_of_month' => 'Day of Month', + 'day_of_week' => 'Day of Week', + + 'hourly' => 'Hourly', + 'daily' => 'Daily', + 'weekly_mon' => 'Weekly (Monday)', + 'weekly_sun' => 'Weekly (Sunday)', + 'monthly' => 'Monthly', + 'every_min' => 'Every x minutes', + 'every_hour' => 'Every x hours', + 'every_day' => 'Every x days', + 'every_week' => 'Every x weeks', + 'every_month' => 'Every x months', + 'every_day_of_week' => 'Every x day of week', + + 'every' => 'Every', + 'minutes' => 'Minutes', + 'hours' => 'Hours', + 'days' => 'Days', + 'months' => 'Months', + + 'monday' => 'Monday', + 'tuesday' => 'Tuesday', + 'wednesday' => 'Wednesday', + 'thursday' => 'Thursday', + 'friday' => 'Friday', + 'saturday' => 'Saturday', + 'sunday' => 'Sunday', + ], + + 'tasks' => [ + 'title' => 'Tasks', + 'create' => 'Create Task', + 'limit' => 'Task Limit Reached', + 'action' => 'Action', + 'payload' => 'Payload', + 'time_offset' => 'Time Offset', + 'seconds' => 'Seconds', + 'continue_on_failure' => 'Continue On Failure', + + 'actions' => [ + 'title' => 'Action', + 'power' => [ + 'title' => 'Send Power Action', + 'action' => 'Power action', + 'start' => 'Start', + 'stop' => 'Stop', + 'restart' => 'Restart', + 'kill' => 'Kill', + ], + 'command' => [ + 'title' => 'Send Command', + 'command' => 'Command', + ], + 'backup' => [ + 'title' => 'Create Backup', + 'files_to_ignore' => 'Files to Ignore', + ], + 'delete' => [ + 'title' => 'Delete Files', + 'files_to_delete' => 'Files to Delete', + + ], + ], + ], + + 'notification_invalid_cron' => 'The cron data provided does not evaluate to a valid expression', + + 'import_action' => [ + 'file' => 'File', + 'url' => 'URL', + 'schedule_help' => 'This should be the raw .json file ( schedule-daily-restart.json )', + 'url_help' => 'URLs must point directly to the raw .json file', + 'add_url' => 'New URL', + 'import_failed' => 'Import Failed', + 'import_success' => 'Import Success', + ], +]; diff --git a/lang/en/server/setting.php b/lang/en/server/setting.php new file mode 100644 index 000000000..677ab41c4 --- /dev/null +++ b/lang/en/server/setting.php @@ -0,0 +1,51 @@ + 'Settings', + 'server_info' => [ + 'title' => 'Server Information', + 'information' => 'Information', + 'name' => 'Server Name', + 'notification_name' => 'Updated Server Name', + 'description' => 'Server Description', + 'notification_description' => 'Updated Server Description', + 'failed' => 'Failed', + 'uuid' => 'Server UUID', + 'id' => 'Server ID', + 'limits' => [ + 'title' => 'Limits', + 'unlimited' => 'Unlimited', + 'of' => 'of', + 'cpu' => 'CPU', + 'memory' => 'Memory', + 'disk' => 'Disk Space', + 'backups' => 'Backups', + 'databases' => 'Databases', + 'allocations' => 'Allocations', + 'no_allocations' => 'No Additional Allocations', + ], + ], + 'node_info' => [ + 'title' => 'Node Information', + 'name' => 'Node Name', + 'sftp' => [ + 'title' => 'SFTP Information', + 'connection' => 'Connection', + 'action' => 'Connect to SFTP', + 'username' => 'Username', + 'password' => 'Password', + 'password_body' => 'Your SFTP password is the same as the password you use to access this panel.', + ], + ], + 'reinstall' => [ + 'title' => 'Reinstall Server', + 'body' => 'Reinstalling your server will stop it, and then re-run the installation script that initially set it up.', + 'body2' => 'Some files may be deleted or modified during this process, please back up your data before continuing.', + 'action' => 'Reinstall', + 'modal' => 'Are you sure you want to reinstall the server?', + 'modal_description' => 'Some files may be deleted or modified during this process, please back up your data before continuing.', + 'yes' => 'Yes, Reinstall', + 'notification_start' => 'Reinstall Started', + 'notification_fail' => 'Reinstall Failed', + ], +]; diff --git a/lang/en/server/startup.php b/lang/en/server/startup.php new file mode 100644 index 000000000..030ffc738 --- /dev/null +++ b/lang/en/server/startup.php @@ -0,0 +1,14 @@ + 'Startup', + 'command' => 'Startup Command', + 'preview' => 'Preview', + 'docker_image' => 'Docker Image', + 'notification_docker' => 'Docker Image Updated', + 'notification_docker_body' => 'Restart the server to use the new image.', + 'variables' => 'Server Variables', + 'update' => 'Updated: :variable', + 'fail' => 'Failed: :variable', + 'validation_fail' => 'Validation Failed: :variable', +]; diff --git a/lang/en/server/users.php b/lang/en/server/user.php similarity index 91% rename from lang/en/server/users.php rename to lang/en/server/user.php index 99486bd48..e8e21d12a 100644 --- a/lang/en/server/users.php +++ b/lang/en/server/user.php @@ -1,7 +1,22 @@ 'Users', + 'username' => 'Username', + 'email' => 'Email', + 'assign_all' => 'Assign all', + 'invite_user' => 'Invite User', + 'action' => 'Invite', + 'remove' => 'Remove User', + 'edit' => 'Edit User', + 'editing' => 'Editing :user', + 'delete' => 'Delete User', + 'notification_add' => 'User Invited!', + 'notification_edit' => 'User Updated!', + 'notification_delete' => 'User Deleted!', + 'notification_failed' => 'Failed to invite user!', 'permissions' => [ + 'title' => 'Permissions', 'activity_desc' => 'Permissions that control a user\'s access to the server activity logs.', 'startup_desc' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.', 'settings_desc' => 'Permissions that control a user\'s ability to modify this server\'s settings.', diff --git a/package.json b/package.json index b8e0dfd16..d91f4c6bc 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,13 @@ "dev": "vite" }, "devDependencies": { - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/typography": "^0.5.15", - "autoprefixer": "^10.4.20", - "axios": "^1.7.4", - "concurrently": "^9.0.1", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "autoprefixer": "^10.4.21", "laravel-vite-plugin": "^1.0", - "postcss": "^8.4.47", - "postcss-nesting": "^13.0.1", - "prettier": "^3.4.2", - "tailwindcss": "^3.4.13", + "postcss": "^8.5.6", + "postcss-nesting": "^13.0.2", + "tailwindcss": "^3.4.17", "vite": "^6.0" }, "dependencies": { diff --git a/public/js/filament-monaco-editor/filament-monaco-editor-scripts.js b/public/js/filament-monaco-editor/filament-monaco-editor-scripts.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0ad..e69de29bb 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +0,0 @@ -import './bootstrap'; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js deleted file mode 100644 index 5f1390b01..000000000 --- a/resources/js/bootstrap.js +++ /dev/null @@ -1,4 +0,0 @@ -import axios from 'axios'; -window.axios = axios; - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/views/filament/components/server-console.blade.php b/resources/views/filament/components/server-console.blade.php index 715ee218c..7aa8586c6 100644 --- a/resources/views/filament/components/server-console.blade.php +++ b/resources/views/filament/components/server-console.blade.php @@ -30,8 +30,8 @@ class="w-full focus:outline-none focus:ring-0 border-none dark:bg-gray-900" type="text" :readonly="{{ $this->canSendCommand() ? 'false' : 'true' }}" - title="{{ $this->canSendCommand() ? '' : 'Can\'t send command when the server is Offline' }}" - placeholder="{{ $this->canSendCommand() ? 'Type a command...' : 'Server Offline...' }}" + title="{{ $this->canSendCommand() ? '' : trans('server/console.command_blocked_title') }}" + placeholder="{{ $this->canSendCommand() ? trans('server/console.command') : trans('server/console.command_blocked') }}" wire:model="input" wire:keydown.enter="enter" wire:keydown.up.prevent="up" diff --git a/resources/views/filament/layouts/body-end.blade.php b/resources/views/filament/layouts/body-end.blade.php deleted file mode 100644 index e4611808f..000000000 --- a/resources/views/filament/layouts/body-end.blade.php +++ /dev/null @@ -1,2 +0,0 @@ -@livewireScripts -@vite(['resources/js/app.js']) \ No newline at end of file diff --git a/resources/views/filament/layouts/header.blade.php b/resources/views/filament/layouts/header.blade.php deleted file mode 100644 index b5f8cc151..000000000 --- a/resources/views/filament/layouts/header.blade.php +++ /dev/null @@ -1,2 +0,0 @@ -@livewireStyles -@vite(['resources/css/app.css']) \ No newline at end of file diff --git a/resources/views/livewire/alerts/alert-banner-container.blade.php b/resources/views/livewire/alerts/alert-banner-container.blade.php index 3d1f34d3f..6ebd6b62b 100644 --- a/resources/views/livewire/alerts/alert-banner-container.blade.php +++ b/resources/views/livewire/alerts/alert-banner-container.blade.php @@ -1,5 +1,5 @@
- @foreach (array_values($alertBanners) as $alertBanner) - @include('livewire.alerts.alert-banner', ['alertBanner' => $alertBanner]) + @foreach ($alertBanners as $alertBanner) + {{ $alertBanner }} @endforeach
diff --git a/resources/views/livewire/alerts/alert-banner.blade.php b/resources/views/livewire/alerts/alert-banner.blade.php index 68bae2706..5866a10c3 100644 --- a/resources/views/livewire/alerts/alert-banner.blade.php +++ b/resources/views/livewire/alerts/alert-banner.blade.php @@ -1,14 +1,12 @@ -@props(['alertBanner']) - @php - $icon = $alertBanner->getIcon(); - $title = $alertBanner->getTitle(); - $body = $alertBanner->getBody(); + $icon = $getIcon(); + $title = $getTitle(); + $body = $getBody(); @endphp -
+
@if (filled($icon)) - + @endif
@@ -21,7 +19,7 @@ @endif
- @if ($alertBanner->isCloseable()) - + @if ($isCloseable()) + @endif
diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index 2376a9e30..193db7039 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -17,7 +17,7 @@

{{ $server->name }} - ({{ $server->formatResource('uptime', type: \App\Enums\ServerResourceType::Time) }}) + ({{ $server->formatResource(\App\Enums\ServerResourceType::Uptime) }})

@@ -33,27 +33,27 @@
-

CPU

-

{{ $server->formatResource('cpu_absolute', type: \App\Enums\ServerResourceType::Percentage) }}

+

{{ trans('server/dashboard.cpu') }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::CPU) }}


-

{{ $server->formatResource('cpu', limit: true, type: \App\Enums\ServerResourceType::Percentage) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}

-

Memory

-

{{ $server->formatResource('memory_bytes') }}

+

{{ trans('server/dashboard.memory') }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::Memory) }}


-

{{ $server->formatResource('memory', limit: true) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}

-

Disk

-

{{ $server->formatResource('disk_bytes') }}

+

{{ trans('server/dashboard.disk') }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::Disk) }}


-

{{ $server->formatResource('disk', limit: true) }}

+

{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}

diff --git a/routes/api-client.php b/routes/api-client.php index c5ab1d3ba..f2e195ae1 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -117,6 +117,7 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe Route::post('/', [Client\Servers\BackupController::class, 'store']); Route::get('/{backup:uuid}', [Client\Servers\BackupController::class, 'view']); Route::get('/{backup:uuid}/download', [Client\Servers\BackupController::class, 'download']); + Route::put('/{backup:uuid}/rename', [Client\Servers\BackupController::class, 'rename']); Route::post('/{backup:uuid}/lock', [Client\Servers\BackupController::class, 'toggleLock']); Route::post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']); Route::delete('/{backup:uuid}', [Client\Servers\BackupController::class, 'delete']); diff --git a/tailwind.config.js b/tailwind.config.js index 2d550157e..eb3d9723a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,9 +5,11 @@ export default { presets: [preset], content: [ './app/Filament/**/*.php', - './resources/views/filament/**/*.blade.php', - './vendor/filament/**/*.blade.php', './app/Livewire/**/*.php', + + './resources/views/filament/**/*.blade.php', './resources/views/livewire/**/*.blade.php', + + './vendor/filament/**/*.blade.php', ], }; diff --git a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php index 7fdcf4374..689f50ceb 100644 --- a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php +++ b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php @@ -8,15 +8,12 @@ use Lcobucci\JWT\Configuration; use App\Models\Permission; use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\Constraint\SignedWith; use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase; class WebsocketControllerTest extends ClientApiIntegrationTestCase { - /** - * Test that a subuser attempting to connect to the websocket receives an error if they - * do not explicitly have the permission. - */ public function test_subuser_without_websocket_permission_receives_error(): void { [$user, $server] = $this->generateTestAccount([Permission::ACTION_CONTROL_RESTART]); @@ -59,41 +56,34 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase $response->assertJsonStructure(['data' => ['token', 'socket']]); $connection = $response->json('data.socket'); - $this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.'); - $this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection, 'Failed asserting that websocket connection address uses expected Daemon endpoint.'); + $this->assertStringStartsWith('wss://', $connection); + $this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection); + + $key = InMemory::plainText($server->node->daemon_token); + $config = Configuration::forSymmetricSigner(new Sha256(), $key); - $config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token)); - $config->setValidationConstraints(new SignedWith(new Sha256(), $key)); - /** @var \Lcobucci\JWT\Token\Plain $token */ $token = $config->parser()->parse($response->json('data.token')); + $this->assertInstanceOf(UnencryptedToken::class, $token); + $constraints = [new SignedWith(new Sha256(), $key)]; $this->assertTrue( - $config->validator()->validate($token, ...$config->validationConstraints()), + $config->validator()->validate($token, ...$constraints), 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' ); - // The way we generate times for the JWT will truncate the microseconds from the - // time, but CarbonImmutable::now() will include them, thus causing test failures. - // - // This little chunk of logic just strips those out by generating a new CarbonImmutable - // instance from the current timestamp, which is how the JWT works. We also need to - // switch to UTC here for consistency. - $expect = CarbonImmutable::createFromTimestamp(CarbonImmutable::now()->getTimestamp())->timezone('UTC'); + $expect = CarbonImmutable::createFromTimestamp(CarbonImmutable::now()->getTimestamp())->timezone('UTC')->setMicroseconds(0); - // Check that the claims are generated correctly. - $this->assertTrue($token->hasBeenIssuedBy(config('app.url'))); - $this->assertTrue($token->isPermittedFor($server->node->getConnectionAddress())); - $this->assertEquals($expect, $token->claims()->get('iat')); - $this->assertEquals($expect->subMinutes(5), $token->claims()->get('nbf')); - $this->assertEquals($expect->addMinutes(10), $token->claims()->get('exp')); - $this->assertSame($user->id, $token->claims()->get('user_id')); - $this->assertSame($server->uuid, $token->claims()->get('server_uuid')); - $this->assertSame(['*'], $token->claims()->get('permissions')); + $claims = $token->claims(); + $this->assertSame(config('app.url'), $claims->get('iss')); + $this->assertSame($server->node->getConnectionAddress(), $claims->get('aud')[0] ?? null); + $this->assertEquals($expect, CarbonImmutable::instance($claims->get('iat'))->setMicroseconds(0)); + $this->assertEquals($expect->subMinutes(5), CarbonImmutable::instance($claims->get('nbf'))->setMicroseconds(0)); + $this->assertEquals($expect->addMinutes(10), CarbonImmutable::instance($claims->get('exp'))->setMicroseconds(0)); + $this->assertSame($user->uuid, $claims->get('user_uuid')); + $this->assertSame($server->uuid, $claims->get('server_uuid')); + $this->assertSame(['*'], $claims->get('permissions')); } - /** - * Test that the subuser's permissions are passed along correctly in the generated JWT. - */ public function test_jwt_is_configured_correctly_for_server_subuser(): void { $permissions = [Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_CONTROL_CONSOLE]; @@ -107,17 +97,18 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase $response->assertOk(); $response->assertJsonStructure(['data' => ['token', 'socket']]); - $config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token)); - $config->setValidationConstraints(new SignedWith(new Sha256(), $key)); - /** @var \Lcobucci\JWT\Token\Plain $token */ - $token = $config->parser()->parse($response->json('data.token')); + $key = InMemory::plainText($server->node->daemon_token); + $config = Configuration::forSymmetricSigner(new Sha256(), $key); + $token = $config->parser()->parse($response->json('data.token')); + $this->assertInstanceOf(UnencryptedToken::class, $token); + + $constraints = [new SignedWith(new Sha256(), $key)]; $this->assertTrue( - $config->validator()->validate($token, ...$config->validationConstraints()), + $config->validator()->validate($token, ...$constraints), 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' ); - // Check that the claims are generated correctly. $this->assertSame($permissions, $token->claims()->get('permissions')); } } diff --git a/vite.config.js b/vite.config.js index 445a94453..53ec23244 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,14 +1,13 @@ import { defineConfig } from 'vite'; import laravel, { refreshPaths } from 'laravel-vite-plugin'; +import { globSync } from 'glob'; export default defineConfig({ plugins: [ laravel({ input: [ - 'resources/css/app.css', - 'resources/js/app.js', - 'resources/js/console.js', - 'resources/css/console.css', + ...globSync('resources/css/**/*.css'), + ...globSync('resources/js/**/*.js'), ], refresh: [...refreshPaths, 'app/Livewire/**'], }), diff --git a/yarn.lock b/yarn.lock index 344c9367f..0daff28bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@csstools/selector-resolve-nested@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" - integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== +"@csstools/selector-resolve-nested@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz#848c6f44cb65e3733e478319b9342b7aa436fac7" + integrity sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g== "@csstools/selector-specificity@^5.0.0": version "5.0.0" @@ -307,14 +307,14 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz#f3d03ce2d82723eb089188ea1494a719b09e1561" integrity sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w== -"@tailwindcss/forms@^0.5.9": +"@tailwindcss/forms@^0.5.10": version "0.5.10" resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.10.tgz#0a1cd67b6933402f1985a04595bd24f9785aa302" integrity sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw== dependencies: mini-svg-data-uri "^1.2.3" -"@tailwindcss/typography@^0.5.15": +"@tailwindcss/typography@^0.5.16": version "0.5.16" resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.16.tgz#a926c8f44d5c439b2915e231cad80058850047c6" integrity sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA== @@ -359,7 +359,7 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -389,32 +389,18 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -autoprefixer@^10.4.20: - version "10.4.20" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" - integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== +autoprefixer@^10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - browserslist "^4.23.3" - caniuse-lite "^1.0.30001646" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.1" + picocolors "^1.1.1" postcss-value-parser "^4.2.0" -axios@^1.7.4: - version "1.7.9" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" - integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" @@ -447,33 +433,25 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.23.3: - version "4.24.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" - integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== +browserslist@^4.24.4: + version "4.25.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.1.tgz#ba9e8e6f298a1d86f829c9b975e07948967bb111" + integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== dependencies: - caniuse-lite "^1.0.30001688" - electron-to-chromium "^1.5.73" + caniuse-lite "^1.0.30001726" + electron-to-chromium "^1.5.173" node-releases "^2.0.19" - update-browserslist-db "^1.1.1" + update-browserslist-db "^1.1.3" camelcase-css@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: - version "1.0.30001699" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz#a102cf330d153bf8c92bfb5be3cd44c0a89c8c12" - integrity sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w== - -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001726: + version "1.0.30001727" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85" + integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== chokidar@^3.6.0: version "3.6.0" @@ -490,15 +468,6 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -511,31 +480,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -concurrently@^9.0.1: - version "9.1.2" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c" - integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ== - dependencies: - chalk "^4.1.2" - lodash "^4.17.21" - rxjs "^7.8.1" - shell-quote "^1.8.1" - supports-color "^8.1.1" - tree-kill "^1.2.2" - yargs "^17.7.2" - core-js@^2.4.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -555,11 +504,6 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -575,10 +519,10 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -electron-to-chromium@^1.5.73: - version "1.5.97" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz#5c4a4744c79e7c85b187adf5160264ac130c776f" - integrity sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ== +electron-to-chromium@^1.5.173: + version "1.5.191" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz#8ae49a471447b1ceaf1d4d183a9000082f52363c" + integrity sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA== emoji-regex@^8.0.0: version "8.0.0" @@ -621,7 +565,7 @@ esbuild@^0.24.2: "@esbuild/win32-ia32" "0.24.2" "@esbuild/win32-x64" "0.24.2" -escalade@^3.1.1, escalade@^3.2.0: +escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -651,11 +595,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" @@ -664,15 +603,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -688,11 +618,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -719,11 +644,6 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -819,11 +739,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -842,18 +757,6 @@ micromatch@^4.0.8: braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mini-svg-data-uri@^1.2.3: version "1.4.4" resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" @@ -880,6 +783,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + nanoid@^3.3.8: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" @@ -933,7 +841,7 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -984,12 +892,12 @@ postcss-nested@^6.2.0: dependencies: postcss-selector-parser "^6.1.1" -postcss-nesting@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" - integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== +postcss-nesting@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.2.tgz#fde0d4df772b76d03b52eccc84372e8d1ca1402e" + integrity sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ== dependencies: - "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-resolve-nested" "^3.1.0" "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" @@ -1031,15 +939,14 @@ postcss@^8.4.47, postcss@^8.5.1: picocolors "^1.1.1" source-map-js "^1.2.1" -prettier@^3.4.2: - version "3.5.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.0.tgz#50325a28887c6dfdf2ca3f8eaba02b66a8429ca7" - integrity sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA== - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" queue-microtask@^1.2.2: version "1.2.3" @@ -1065,11 +972,6 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - resolve@^1.1.7, resolve@^1.22.8: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" @@ -1124,13 +1026,6 @@ rxjs-compat@^6.5.4: resolved "https://registry.yarnpkg.com/rxjs-compat/-/rxjs-compat-6.6.7.tgz#6eb4ef75c0a58ea672854a701ccc8d49f41e69cb" integrity sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw== -rxjs@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1143,11 +1038,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" - integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== - signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -1167,7 +1057,7 @@ source-map-js@^1.2.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -1219,26 +1109,12 @@ sucrase@^3.35.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tailwindcss@^3.4.13: +tailwindcss@^3.4.17: version "3.4.17" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== @@ -1287,25 +1163,15 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tree-kill@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== - ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -tslib@^2.1.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -update-browserslist-db@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580" - integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== dependencies: escalade "^3.2.0" picocolors "^1.1.1" @@ -1350,15 +1216,6 @@ which@^2.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -1376,30 +1233,7 @@ xterm-addon-search-bar@^0.2.0: babel-runtime "^6.26.0" rxjs-compat "^6.5.4" -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - yaml@^2.3.4: version "2.7.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.7.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1"