Compare commits

..

28 Commits

Author SHA1 Message Date
Charles
1bbbcd0e25 Update server/egg icon url supported file types (#2249) 2026-02-18 16:30:24 -05:00
Michael (Parker) Parker
677d2f742c docker env fixes (#2234)
Co-authored-by: Charles <charles@pelican.dev>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-02-18 14:40:46 -05:00
Charles
650fb16d2d exclude bulk actions in list eggs (#2239) 2026-02-17 06:17:28 -05:00
Boy132
58814ea782 Allow to delete errored plugins (#2246) 2026-02-17 11:52:25 +01:00
hallo123wert
0918ed308b exclude bulk actions (#2240) 2026-02-16 06:34:14 -05:00
Lance Pioch
85d5f2ec3f Fix alert banners (#1492) (#2177) 2026-02-15 15:34:09 -05:00
Lance Pioch
810f237547 Remove ignition, just use new default error handler (#2241) 2026-02-15 15:03:55 -05:00
Lance Pioch
8f191890a1 Fix null backup limit exception (#2242) 2026-02-15 15:03:37 -05:00
Boy132
160e0e54f5 More action fixes (#2237)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-15 17:31:50 +01:00
Charles
fdcfbb00ca Revert "Fix UTF double encoding in file editor" (#2236) 2026-02-15 11:08:01 -05:00
Lance Pioch
44f6cf8928 Fix UTF double encoding in file editor (#2199) 2026-02-14 10:33:35 -05:00
Boy132
cf2a26bbf0 Improve plugin loading (#2233) 2026-02-14 14:06:50 +01:00
gOOvER
adb6678eee Convert YAML boolean defaults to strings (#2232) 2026-02-13 18:12:44 -05:00
Lance Pioch
9539e21b39 Handle invalid svg icon in generic-oidc provider gracefully (#2216) 2026-02-13 10:06:07 -05:00
Charles
33660f635f Fix yet another action (#2230) 2026-02-12 21:00:26 -05:00
Lance Pioch
8c475ed95f Add “reachable” column for Client -> Wings connections for Nodes (#2200) 2026-02-12 17:06:38 -05:00
Lance Pioch
d43cb1d180 Fix empty egg config_files causing fatal 500 error (#2195) (#2197) 2026-02-12 17:06:19 -05:00
Lance Pioch
fe55dbd200 Ignore oauth redirects for spa (#2224) 2026-02-12 16:31:35 -05:00
Charles
0bb4503c2b fix unsuspend button (#2227) 2026-02-12 16:31:20 -05:00
Lance Pioch
3c1168beb5 Update job runners (#2225) 2026-02-12 10:29:33 -05:00
Quinten
1a092aedc8 Docker: chown composer.json and composer.lock for plugins (#2220) 2026-02-12 10:02:12 -05:00
Charles
8c99a8030f Fix more actions (#2208) 2026-02-12 09:56:56 -05:00
Lance Pioch
6e53b1cd7d Allow custom ips to still be entered (#2223) 2026-02-12 09:49:54 -05:00
Charles
4042e0416b Revert toolbar change (#2218) 2026-02-11 17:54:58 -05:00
Lance Pioch
cc8973cf00 Laravel 12.51.0 Shift (#2213)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-11 11:18:33 -05:00
Quinten
8ebe75b947 fix: composer not been installed in the docker image (#2211) 2026-02-10 16:00:18 -05:00
Hythera
f8144407d1 fix: composer content hash (#2209) 2026-02-09 22:26:01 -05:00
Charles
e431ccb66a add rounding to list-files header (#2207) 2026-02-09 22:19:10 -05:00
54 changed files with 695 additions and 717 deletions

View File

@@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.3, 8.4, 8.5]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
@@ -79,8 +79,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
database: ["mysql:8"]
php: [8.5]
database: ["mysql:8.4", "mysql:9.6"]
services:
database:
image: ${{ matrix.database }}
@@ -147,8 +147,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
php: [8.5]
database: ["mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
@@ -215,8 +215,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
database: ["postgres:14"]
php: [8.5]
database: ["postgres:17", "postgres:18"]
services:
database:
image: ${{ matrix.database }}

View File

@@ -68,6 +68,8 @@ RUN apk add --no-cache \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
@@ -83,8 +85,7 @@ RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile

View File

@@ -73,6 +73,8 @@ RUN apk add --no-cache \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
@@ -88,7 +90,7 @@ RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions;
use App\Exceptions\Solutions\ManifestDoesNotExistSolution;
use Exception;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new ManifestDoesNotExistSolution();
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Exceptions;
use Exception;
class PluginIdMismatchException extends Exception {}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Exceptions\Solutions;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistSolution implements Solution
{
public function getSolutionTitle(): string
{
return "The manifest.json file hasn't been generated yet";
}
public function getSolutionDescription(): string
{
return 'Run yarn run build:production to build the frontend first.';
}
public function getDocumentationLinks(): array
{
return [
'Docs' => 'https://github.com/pelican/panel/blob/master/package.json',
];
}
}

View File

@@ -13,6 +13,8 @@ use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use BackedEnum;
use BladeUI\Icons\Exceptions\SvgNotFound;
use BladeUI\Icons\Factory as IconFactory;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -68,6 +70,8 @@ class Settings extends Page implements HasSchemas
protected CaptchaService $captchaService;
protected IconFactory $iconFactory;
/** @var array<mixed>|null */
public ?array $data = [];
@@ -76,11 +80,12 @@ class Settings extends Page implements HasSchemas
$this->form->fill();
}
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService, IconFactory $iconFactory): void
{
$this->oauthService = $oauthService;
$this->avatarService = $avatarService;
$this->captchaService = $captchaService;
$this->iconFactory = $iconFactory;
}
public static function canAccess(): bool
@@ -565,9 +570,18 @@ class Settings extends Page implements HasSchemas
foreach ($oauthSchemas as $schema) {
$key = $schema->getConfigKey();
$icon = $schema->getIcon();
if (is_string($icon)) {
try {
$this->iconFactory->svg($icon);
} catch (SvgNotFound) {
$icon = null;
}
}
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($schema->getIcon() ?? TablerIcon::BrandOauth)
->icon($icon ?? TablerIcon::BrandOauth)
->collapsed(fn () => !$schema->isEnabled())
->collapsible()
->schema([

View File

@@ -101,7 +101,7 @@ class DatabaseHostResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::Database)

View File

@@ -8,6 +8,7 @@ use App\Services\Databases\Hosts\HostCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
@@ -45,6 +46,17 @@ class CreateDatabaseHost extends CreateRecord
$this->service = $service;
}
protected function getCreateFormAction(): Action
{
$hasFormWrapper = $this->hasFormWrapper();
return Action::make('exclude_create')
->label(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit($hasFormWrapper ? $this->getSubmitFormLivewireMethodName() : null)
->action($hasFormWrapper ? null : $this->getSubmitFormLivewireMethodName())
->keyBindings(['mod+s']);
}
/** @return Step[]
* @throws Exception
*/

View File

@@ -175,7 +175,8 @@ class CreateEgg extends CreateRecord
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)

View File

@@ -318,6 +318,7 @@ class EditEgg extends EditRecord
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->dehydrateStateUsing(fn ($state) => blank($state) ? '{}' : $state)
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
@@ -332,9 +333,9 @@ class EditEgg extends EditRecord
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
@@ -484,18 +485,20 @@ class EditEgg extends EditRecord
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
}

View File

@@ -86,7 +86,7 @@ class ListEggs extends ListRecords
->multiple(),
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make()
DeleteBulkAction::make('exclude_bulk_delete')
->before(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
@@ -106,7 +106,7 @@ class ListEggs extends ListRecords
$this->halt();
}
}),
UpdateEggBulkAction::make()
UpdateEggBulkAction::make('exclude_bulk_update')
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);

View File

@@ -104,7 +104,7 @@ class MountResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::LayersLinked)

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Filament\Components\Tables\Columns\NodeClientHealthColumn;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node;
@@ -34,6 +35,7 @@ class ListNodes extends ListRecords
->searchable()
->hidden(),
NodeHealthColumn::make('health'),
NodeClientHealthColumn::make('reachable'),
TextColumn::make('name')
->label(trans('admin/node.table.name'))
->sortable()

View File

@@ -91,7 +91,10 @@ class AllocationsRelationManager extends RelationManager
->icon(TablerIcon::WorldPlus)
->schema(fn () => [
Select::make('allocation_ip')
->options(fn () => collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->options(fn (Get $get) => collect($this->getOwnerRecord()->ipAddresses())
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/node.ip_address'))
->inlineLabel()
->ip()
@@ -100,12 +103,25 @@ class AllocationsRelationManager extends RelationManager
->live()
->hintAction(
Action::make('hint_refresh')
->hiddenLabel()
->icon(TablerIcon::Refresh)
->tooltip(trans('admin/node.refresh'))
->action(function () {
cache()->forget("nodes.{$this->getOwnerRecord()->id}.ips");
})
)
->suffixAction(
Action::make('custom_ip')
->icon(TablerIcon::Keyboard)
->tooltip(trans('admin/node.custom_ip'))
->schema([
TextInput::make('custom_ip')
->label(trans('admin/node.ip_address'))
->ip()
->required(),
])
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
)
->required(),
TextInput::make('allocation_alias')
->label(trans('admin/node.table.alias'))

View File

@@ -202,7 +202,7 @@ class PluginResource extends Resource
->icon(TablerIcon::Trash)
->color('danger')
->requiresConfirmation()
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->deletePlugin($plugin);

View File

@@ -106,7 +106,7 @@ class RoleResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0);

View File

@@ -251,7 +251,10 @@ class CreateServer extends CreateRecord
return [
Select::make('allocation_ip')
->options(fn () => collect(Node::find($get('node_id'))?->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->options(fn (Get $get) => collect(Node::find($getPage('node_id'))?->ipAddresses())
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/server.ip_address'))->inlineLabel()
->helperText(trans('admin/server.ip_address_helper'))
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
@@ -266,6 +269,18 @@ class CreateServer extends CreateRecord
cache()->forget("nodes.{$get('node_id')}.ips");
})
)
->suffixAction(
Action::make('custom_ip')
->icon(TablerIcon::Keyboard)
->tooltip(trans('admin/node.custom_ip'))
->schema([
TextInput::make('custom_ip')
->label(trans('admin/node.ip_address'))
->ip()
->required(),
])
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
)
->required(),
TextInput::make('allocation_alias')
->label(trans('admin/server.alias'))->inlineLabel()

View File

@@ -121,6 +121,7 @@ class EditServer extends EditRecord
->columnSpan(2)
->alignJustify(),
Action::make('uploadIcon')
->hiddenLabel()
->icon(TablerIcon::PhotoUp)
->tooltip(trans('admin/server.import_image'))
->modal()
@@ -935,7 +936,7 @@ class EditServer extends EditRecord
->send();
}
}),
Action::make('toggleUnsuspend')
Action::make('exclude_toggle_unsuspend')
->label(trans('admin/server.unsuspend'))
->color('success')
->hidden(fn (Server $server) => !$server->isSuspended())
@@ -1218,18 +1219,20 @@ class EditServer extends EditRecord
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}
}

View File

@@ -113,7 +113,10 @@ class AllocationsRelationManager extends RelationManager
->createAnother(false)
->schema(fn () => [
Select::make('allocation_ip')
->options(fn () => collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->options(fn (Get $get) => collect($this->getOwnerRecord()->node->ipAddresses())
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/server.ip_address'))
->inlineLabel()
->ip()
@@ -126,6 +129,18 @@ class AllocationsRelationManager extends RelationManager
cache()->forget("nodes.{$this->getOwnerRecord()->node->id}.ips");
})
)
->suffixAction(
Action::make('custom_ip')
->icon(TablerIcon::Keyboard)
->tooltip(trans('admin/node.custom_ip'))
->schema([
TextInput::make('custom_ip')
->label(trans('admin/node.ip_address'))
->ip()
->required(),
])
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
)
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->required(),
TextInput::make('allocation_alias')

View File

@@ -142,7 +142,7 @@ class UserResource extends Resource
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
CreateAction::make()
->hiddenLabel()

View File

@@ -114,7 +114,7 @@ class WebhookResource extends Resource
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon(TablerIcon::Webhook)

View File

@@ -16,13 +16,15 @@ class RotateDatabasePasswordAction extends Action
{
public static function getDefaultName(): ?string
{
return 'hint_rotate';
return 'exclude_hint_rotate';
}
protected function setUp(): void
{
parent::setUp();
$this->hiddenLabel();
$this->tooltip(trans('admin/databasehost.rotate'));
$this->icon(TablerIcon::Refresh);

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Filament\Components\Tables\Columns;
use Filament\Support\Enums\Alignment;
use Filament\Tables\Columns\IconColumn;
use Illuminate\Support\Facades\Blade;
class NodeClientHealthColumn extends IconColumn
{
protected function setUp(): void
{
parent::setUp();
$this->label(trans('admin/node.table.reachable'));
$this->alignCenter();
}
public function toEmbeddedHtml(): string
{
$alignment = $this->getAlignment();
$attributes = $this->getExtraAttributeBag()
->class([
'fi-ta-icon',
'fi-inline' => $this->isInline(),
'fi-ta-icon-has-line-breaks' => $this->isListWithLineBreaks(),
'fi-wrapped' => $this->canWrap(),
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
])
->toHtml();
return Blade::render(<<<'BLADE'
<div <?= $attributes ?>>
@livewire('node-client-connectivity', ['node' => $record, 'lazy' => true])
</div>
BLADE, [
'attributes' => $attributes,
'record' => $this->getRecord(),
]);
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Filament\Pages\Auth;
use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\OAuthService;
use BladeUI\Icons\Exceptions\SvgNotFound;
use BladeUI\Icons\Factory as IconFactory;
use Filament\Actions\Action;
use Filament\Auth\Pages\Login as BaseLogin;
use Filament\Forms\Components\TextInput;
@@ -19,10 +21,13 @@ class Login extends BaseLogin
protected CaptchaService $captchaService;
public function boot(OAuthService $oauthService, CaptchaService $captchaService): void
protected IconFactory $iconFactory;
public function boot(OAuthService $oauthService, CaptchaService $captchaService, IconFactory $iconFactory): void
{
$this->oauthService = $oauthService;
$this->captchaService = $captchaService;
$this->iconFactory = $iconFactory;
}
public function form(Schema $schema): Schema
@@ -87,9 +92,18 @@ class Login extends BaseLogin
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$icon = $schema->getIcon();
if (is_string($icon)) {
try {
$this->iconFactory->svg($icon);
} catch (SvgNotFound) {
$icon = null;
}
}
$actions[] = Action::make("oauth_$id")
->label($schema->getName())
->icon($schema->getIcon())
->icon($icon)
->color($color)
->url(route('auth.oauth.redirect', ['driver' => $id], false));
}

View File

@@ -462,18 +462,20 @@ class Settings extends ServerFormPage
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}

View File

@@ -27,7 +27,6 @@ use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Textarea;
@@ -77,7 +76,7 @@ class BackupResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
return $server->backup_limit;
return $server->backup_limit ?? 0;
}
public static function defaultForm(Schema $schema): Schema
@@ -128,7 +127,7 @@ class BackupResource extends Resource
])
->recordActions([
ActionGroup::make([
Action::make('rename')
Action::make('exclude_rename')
->icon(TablerIcon::Pencil)
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
->label(trans('server/backup.actions.rename.title'))
@@ -158,14 +157,14 @@ class BackupResource extends Resource
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('lock')
Action::make('exclude_lock')
->iconSize(IconSize::Large)
->icon(fn (Backup $backup) => !$backup->is_locked ? TablerIcon::Lock : TablerIcon::LockOpen)
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
->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')
Action::make('exclude_download')
->label(trans('server/backup.actions.download'))
->iconSize(IconSize::Large)
->color('primary')
@@ -173,7 +172,7 @@ class BackupResource extends Resource
->authorize(fn () => user()?->can(SubuserPermission::BackupDownload, $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')
Action::make('exclude_restore')
->label(trans('server/backup.actions.restore.title'))
->iconSize(IconSize::Large)
->color('success')
@@ -226,7 +225,12 @@ class BackupResource extends Resource
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
Action::make('exclude_delete')
->icon(TablerIcon::Trash)
->color('danger')
->requiresConfirmation()
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
->label(trans('filament-actions::delete.single.label'))
->iconSize(IconSize::Large)
->disabled(fn (Backup $backup) => $backup->is_locked && $backup->status !== BackupStatus::Failed)
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))

View File

@@ -62,7 +62,7 @@ class DatabaseResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
return $server->database_limit;
return $server->database_limit ?? 0;
}
/**

View File

@@ -8,6 +8,7 @@ use App\Models\Schedule;
use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Resources\Pages\CreateRecord;
@@ -20,6 +21,17 @@ class CreateSchedule extends CreateRecord
protected static bool $canCreateAnother = false;
protected function getCreateFormAction(): Action
{
$hasFormWrapper = $this->hasFormWrapper();
return Action::make('exclude_create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit($hasFormWrapper ? $this->getSubmitFormLivewireMethodName() : null)
->action($hasFormWrapper ? null : $this->getSubmitFormLivewireMethodName())
->keyBindings(['mod+s']);
}
protected function afterCreate(): void
{
/** @var Schedule $schedule */

View File

@@ -7,6 +7,7 @@ use App\Facades\Activity;
use App\Models\Schedule;
use App\Models\Task;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
@@ -73,6 +74,7 @@ class TasksRelationManager extends RelationManager
return $table
->reorderable('sequence_id')
->defaultSort('sequence_id')
->reorderRecordsTriggerAction(fn (Action $action, bool $isReordering) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->columns([
TextColumn::make('action')
->label(trans('server/schedule.tasks.actions.title'))

View File

@@ -97,8 +97,7 @@ class ScheduleResource extends Resource
->formatStateUsing(fn (?Schedule $schedule) => $schedule?->status->value ?? 'new')
->options(fn (?Schedule $schedule) => [$schedule?->status->value ?? 'new' => $schedule?->status->getLabel() ?? 'New'])
->visibleOn('view'),
Section::make('Cron')
->label(trans('server/schedule.cron'))
Section::make(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(user()->timezone ?? 'UTC');
@@ -110,22 +109,22 @@ class ScheduleResource extends Resource
})
->schema([
Actions::make([
CronPresetAction::make('hourly')
CronPresetAction::make('exclude_hourly')
->label(trans('server/schedule.time.hourly'))
->cron('0', '*', '*', '*', '*'),
CronPresetAction::make('daily')
CronPresetAction::make('exclude_daily')
->label(trans('server/schedule.time.daily'))
->cron('0', '0', '*', '*', '*'),
CronPresetAction::make('weekly_monday')
CronPresetAction::make('exclude_weekly_monday')
->label(trans('server/schedule.time.weekly_mon'))
->cron('0', '0', '*', '*', '1'),
CronPresetAction::make('weekly_sunday')
CronPresetAction::make('exclude_weekly_sunday')
->label(trans('server/schedule.time.weekly_sun'))
->cron('0', '0', '*', '*', '0'),
CronPresetAction::make('monthly')
CronPresetAction::make('exclude_monthly')
->label(trans('server/schedule.time.monthly'))
->cron('0', '0', '1', '*', '*'),
CronPresetAction::make('every_x_minutes')
CronPresetAction::make('exclude_every_x_minutes')
->label(trans('server/schedule.time.every_min'))
->color(fn (Get $get) => str($get('cron_minute'))->startsWith('*/')
&& $get('cron_hour') == '*'
@@ -148,7 +147,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_hours')
CronPresetAction::make('exclude_every_x_hours')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& str($get('cron_hour'))->startsWith('*/')
&& $get('cron_day_of_month') == '*'
@@ -170,7 +169,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_days')
CronPresetAction::make('exclude_every_x_days')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& str($get('cron_day_of_month'))->startsWith('*/')
@@ -192,7 +191,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_months')
CronPresetAction::make('exclude_every_x_months')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '1'
@@ -214,7 +213,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*/' . $data['x']);
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_day_of_week')
CronPresetAction::make('exclude_every_x_day_of_week')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '*'

View File

@@ -123,18 +123,6 @@ class SubuserResource extends Resource
),
])
->recordActions([
DeleteAction::make()
->label(trans('server/user.delete'))
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
->successNotificationTitle(null)
->action(function (Subuser $subuser, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuserDeletionService->handle($subuser, $server);
Notification::make()
->title(trans('server/user.notification_delete'))
->success()
->send();
}),
EditAction::make()
->label(trans('server/user.edit'))
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
@@ -179,7 +167,7 @@ class SubuserResource extends Resource
])
->formatStateUsing(fn (Subuser $subuser) => $subuser->user->email),
Actions::make([
Action::make('assignAll')
Action::make('exclude_assignAll')
->label(trans('server/user.assign_all'))
->action(function (Set $set) use ($permissionsArray) {
$permissions = $permissionsArray;
@@ -214,6 +202,19 @@ class SubuserResource extends Resource
return $data;
}),
DeleteAction::make()
->label(trans('server/user.delete'))
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
->authorize(fn () => user()?->can(SubuserPermission::UserDelete, $server))
->successNotificationTitle(null)
->action(function (Subuser $subuser, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuserDeletionService->handle($subuser, $server);
Notification::make()
->title(trans('server/user.notification_delete'))
->success()
->send();
}),
])
->toolbarActions([
CreateAction::make('invite')
@@ -243,7 +244,7 @@ class SubuserResource extends Resource
])
->required(),
Actions::make([
Action::make('assignAll')
Action::make('exclude_assignAll')
->label(trans('server/user.assign_all'))
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;

View File

@@ -10,6 +10,7 @@ use Filament\Notifications\Concerns\HasStatus;
use Filament\Notifications\Concerns\HasTitle;
use Filament\Support\Components\ViewComponent;
use Illuminate\Contracts\Support\Arrayable;
use Livewire\Livewire;
final class AlertBanner extends ViewComponent implements Arrayable
{
@@ -83,7 +84,13 @@ final class AlertBanner extends ViewComponent implements Arrayable
public function send(): AlertBanner
{
session()->push('alert-banners', $this->toArray());
$data = $this->toArray();
if (Livewire::isLivewireRequest()) {
$data['from_livewire'] = true;
}
session()->push('alert-banners', $data);
return $this;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Livewire;
use Filament\Notifications\Collection;
class AlertBannerCollection extends Collection
{
public static function fromLivewire($value): static
{
return (new static($value))->transform(
fn (array $alertBanner): AlertBanner => AlertBanner::fromArray($alertBanner),
);
}
}

View File

@@ -2,25 +2,35 @@
namespace App\Livewire;
use Filament\Notifications\Collection;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Component;
class AlertBannerContainer extends Component
{
public Collection $alertBanners;
public AlertBannerCollection $alertBanners;
public function mount(): void
{
$this->alertBanners = new Collection();
$this->pullFromSession();
$this->alertBanners = new AlertBannerCollection();
foreach (session()->pull('alert-banners', []) as $alertBanner) {
// Alerts created during Livewire requests should have been consumed by the event handler on the same page.
if (!empty($alertBanner['from_livewire'])) {
// If they weren't, then discard them instead of showing on the wrong page.
continue;
}
$alertBanner = AlertBanner::fromArray($alertBanner);
$this->alertBanners->put($alertBanner->getId(), $alertBanner);
}
}
#[On('alertBannerSent')]
public function pullFromSession(): void
{
foreach (session()->pull('alert-banners', []) as $alertBanner) {
unset($alertBanner['from_livewire']);
$alertBanner = AlertBanner::fromArray($alertBanner);
$this->alertBanners->put($alertBanner->getId(), $alertBanner);
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Livewire;
use App\Enums\TablerIcon;
use App\Models\Node;
use App\Services\Nodes\NodeJWTService;
use App\Services\Servers\GetUserPermissionsService;
use Filament\Support\Enums\IconSize;
use Filament\Tables\View\Components\Columns\IconColumnComponent\IconComponent;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\ComponentAttributeBag;
use Livewire\Attributes\Locked;
use Livewire\Component;
use function Filament\Support\generate_icon_html;
class NodeClientConnectivity extends Component
{
#[Locked]
public Node $node;
private GetUserPermissionsService $getUserPermissionsService;
private NodeJWTService $nodeJWTService;
public function boot(GetUserPermissionsService $getUserPermissionsService, NodeJWTService $nodeJWTService): void
{
$this->getUserPermissionsService = $getUserPermissionsService;
$this->nodeJWTService = $nodeJWTService;
}
public function render(): \Illuminate\Contracts\View\View
{
$httpUrl = $this->node->getConnectionAddress();
$wsUrl = null;
$wsToken = null;
$server = $this->node->servers()->first();
if ($server) {
$user = Auth::user();
$permissions = $this->getUserPermissionsService->handle($server, $user);
$wsToken = $this->nodeJWTService
->setExpiresAt(now()->addMinute()->toImmutable())
->setUser($user)
->setClaims([
'server_uuid' => $server->uuid,
'permissions' => $permissions,
])
->handle($this->node, $user->id . $server->uuid)->toString();
$wsUrl = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $this->node->getConnectionAddress());
$wsUrl .= sprintf('/api/servers/%s/ws', $server->uuid);
}
return view('livewire.node-client-connectivity', [
'httpUrl' => $httpUrl,
'wsUrl' => $wsUrl,
'wsToken' => $wsToken,
'loadingIcon' => $this->makeIcon(TablerIcon::WorldQuestion, 'warning', 'Checking...'),
'offlineIcon' => $this->makeIcon(TablerIcon::WorldX, 'danger', 'Node is not reachable from your browser'),
'onlineIcon' => $this->makeIcon(TablerIcon::WorldCheck, 'success', 'Node is reachable'),
'warningIcon' => $this->makeIcon(TablerIcon::WorldExclamation, 'warning', 'Node is reachable, but WebSocket failed. Check reverse proxy config.'),
'onlineNoWsIcon' => $this->makeIcon(TablerIcon::WorldCheck, 'success', 'Node is reachable (WebSocket not tested — no servers)'),
]);
}
private function makeIcon(TablerIcon $icon, string $color, string $tooltip): string
{
return generate_icon_html($icon, attributes: (new ComponentAttributeBag())
->merge([
'x-tooltip' => '{
content: "' . $tooltip . '",
theme: $store.theme,
allowHTML: true,
placement: "bottom",
}',
'style' => 'color: var(--dark-text, var(--text))',
], escape: false)
->color(IconComponent::class, $color), size: IconSize::Large)
->toHtml();
}
public function placeholder(): string
{
return generate_icon_html(TablerIcon::WorldQuestion, attributes: (new ComponentAttributeBag())
->color(IconComponent::class, 'warning'), size: IconSize::Large)
->toHtml();
}
}

View File

@@ -255,6 +255,8 @@ class Node extends Model implements Validatable
/**
* Gets the servers associated with a node.
*
* @return HasMany<Server, $this>
*/
public function servers(): HasMany
{

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Contracts\Plugins\HasPluginSettings;
use App\Enums\PluginCategory;
use App\Enums\PluginStatus;
use App\Exceptions\PluginIdMismatchException;
use App\Facades\Plugins;
use Exception;
use Filament\Schemas\Components\Component;
@@ -108,11 +109,14 @@ class Plugin extends Model implements HasPluginSettings
continue;
}
$plugin = Str::lower($plugin);
try {
$data = File::json($path, JSON_THROW_ON_ERROR);
$data['id'] = Str::lower($data['id']);
if ($data['id'] !== $plugin) {
throw new Exception("Plugin id mismatch for folder name ($plugin) and id in plugin.json ({$data['id']})!");
throw new PluginIdMismatchException("Plugin id mismatch for folder name ($plugin) and id in plugin.json ({$data['id']})!");
}
$panels = null;
@@ -158,12 +162,12 @@ class Plugin extends Model implements HasPluginSettings
if (!$exception instanceof JsonException) {
$plugins[] = [
'id' => $data['id'] ?? Str::uuid(),
'name' => $data['name'] ?? $plugin,
'name' => $data['name'] ?? Str::headline($plugin),
'author' => $data['author'] ?? 'Unknown',
'version' => '0.0.0',
'description' => 'Plugin.json is invalid!',
'version' => $data['version'] ?? '0.0.0',
'description' => $exception instanceof PluginIdMismatchException ? $exception->getMessage() : 'Plugin.json is invalid!',
'category' => PluginCategory::Plugin->value,
'url' => null,
'url' => $data['url'] ?? null,
'update_url' => null,
'namespace' => 'Error',
'class' => 'Error',
@@ -197,7 +201,7 @@ class Plugin extends Model implements HasPluginSettings
public function shouldLoad(?string $panelId = null): bool
{
return ($this->status === PluginStatus::Enabled || $this->status === PluginStatus::Errored) && (is_null($panelId) || !$this->panels || in_array($panelId, explode(',', $this->panels)));
return $this->fullClass() !== '\\Error\\Error' && ($this->status === PluginStatus::Enabled || $this->status === PluginStatus::Errored) && (is_null($panelId) || !$this->panels || in_array($panelId, explode(',', $this->panels)));
}
public function canEnable(): bool

View File

@@ -34,7 +34,7 @@ use Illuminate\Support\ServiceProvider;
use Livewire\Component;
use Livewire\Livewire;
use function Livewire\on;
use function Livewire\before;
use function Livewire\store;
class FilamentServiceProvider extends ServiceProvider
@@ -74,7 +74,7 @@ class FilamentServiceProvider extends ServiceProvider
fn () => Blade::render("@vite(['resources/js/app.js'])"),
);
on('dehydrate', function (Component $component) {
before('dehydrate', function (Component $component) {
if (!Livewire::isLivewireRequest()) {
return;
}

View File

@@ -29,6 +29,9 @@ abstract class PanelProvider extends BasePanelProvider
{
return $panel
->spa(fn () => !request()->routeIs('filament.server.pages.console'))
->spaUrlExceptions([
'*/oauth/redirect/*',
])
->databaseNotifications()
->brandName(config('app.name', 'Pelican'))
->brandLogo(config('app.logo'))

View File

@@ -32,10 +32,10 @@ class EggConfigurationService
*/
public function handle(Server $server): array
{
$configs = $this->replacePlaceholders(
$server,
json_decode($server->egg->inherit_config_files)
);
$configFiles = json_decode($server->egg->inherit_config_files ?? '{}');
$configs = is_object($configFiles) || is_array($configFiles)
? $this->replacePlaceholders($server, $configFiles)
: [];
return [
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),

View File

@@ -189,6 +189,18 @@ class EggImporterService
}
}
// Convert YAML booleans to strings to prevent Laravel from converting them to 1/0
// when saving to TEXT field. Required for validation rules like "in:true,false".
if (isset($parsed['variables'])) {
$parsed['variables'] = array_map(function ($variable) {
if (isset($variable['default_value']) && is_bool($variable['default_value'])) {
$variable['default_value'] = $variable['default_value'] ? 'true' : 'false';
}
return $variable;
}, $parsed['variables']);
}
// Reserved env var name handling
[$forbidden, $allowed] = collect($parsed['variables'])
->map(fn ($variable) => array_merge(

View File

@@ -54,6 +54,10 @@ class PluginService
}
}
if ($plugin->namespace === 'Error') {
continue;
}
// Always autoload src directory to make sure all class names can be resolved (e.g. in migrations)
$namespace = $plugin->namespace . '\\';
if (!array_key_exists($namespace, $classLoader->getPrefixesPsr4())) {

View File

@@ -36,7 +36,7 @@ class EggTransformer extends BaseTransformer
{
$model->loadMissing('configFrom');
$files = json_decode($model->inherit_config_files, true, 512, JSON_THROW_ON_ERROR);
$files = json_decode($model->inherit_config_files ?: '{}', true, 512, JSON_THROW_ON_ERROR);
$model->loadMissing('scriptFrom');
@@ -53,9 +53,9 @@ class EggTransformer extends BaseTransformer
'docker_images' => $model->docker_images,
'config' => [
'files' => $files,
'startup' => json_decode($model->inherit_config_startup, true),
'startup' => json_decode($model->inherit_config_startup ?: '{}', true),
'stop' => $model->inherit_config_stop,
'logs' => json_decode($model->inherit_config_logs, true),
'logs' => json_decode($model->inherit_config_logs ?: '{}', true),
'file_denylist' => $model->inherit_file_denylist,
'extends' => $model->config_from,
],

View File

@@ -15,7 +15,7 @@
"filament/filament": "^4.5",
"gboquizosanchez/filament-log-viewer": "^2.1",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.49",
"laravel/framework": "^12.51",
"laravel/helpers": "^1.8",
"laravel/sanctum": "^4.2",
"laravel/socialite": "^5.24",
@@ -55,8 +55,7 @@
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7",
"pestphp/pest-plugin-faker": "^3.0",
"pestphp/pest-plugin-livewire": "^3.0",
"spatie/laravel-ignition": "^2.9"
"pestphp/pest-plugin-livewire": "^3.0"
},
"autoload": {
"files": [

662
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,11 @@
{$CADDY_STRICT_PROXIES}
}
admin off
{$PARSED_AUTO_HTTPS}
{$PARSED_LE_EMAIL}
{$CADDY_AUTO_HTTPS}
{$CADDY_LE_EMAIL}
}
{$PARSED_APP_URL} {
{$CADDY_APP_URL} {
root * /var/www/html/public
encode gzip

View File

@@ -1,34 +1,48 @@
#!/bin/ash -e
# shellcheck shell=dash
# check for .env file or symlink and generate app keys if missing
if [ -f /var/www/html/.env ]; then
echo "external vars exist."
if [ -f /pelican-data/.env ]; then
echo ".env vars exist."
# load specific env vars from .env used in the entrypoint and they are not already set
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do if ! (printenv | grep -q ${VAR}); then export $(grep ${VAR} .env | grep -ve "^#"); fi; done
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do
echo "checking for ${VAR}"
## skip if it looks like it might try to execute code
if (grep "${VAR}" .env | grep -qE "\$\(|=\`|\$#"); then echo "var in .env may be executable or a comment, skipping"; continue; fi
# if the variable is in .env then set it
if (grep -q "${VAR}" .env); then
echo "loading ${VAR} from .env"
export "$(grep "${VAR}" .env | sed 's/"//g')"
continue
fi
## variable wasn't loaded or in the env to set
echo "didn't find variable to set"
done
else
echo "external vars don't exist."
echo ".env vars don't exist."
# webroot .env is symlinked to this path
touch /pelican-data/.env
# manually generate a key because key generate --force fails
if [ -z ${APP_KEY} ]; then
echo -e "Generating key."
if [ -z "${APP_KEY}" ]; then
echo "No key set, Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo -e "Generated app key: $APP_KEY"
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "Generated app key written to .env file"
else
echo -e "APP_KEY exists in environment, using that."
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "APP_KEY exists in environment, using that."
echo "APP_KEY=$APP_KEY" > /pelican-data/.env
fi
# enable installer
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
echo "APP_INSTALLED=false" >> /pelican-data/.env
fi
# create directories for volumes
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /pelican-data/storage/icons /pelican-data/plugins /var/www/html/storage/logs/supervisord 2>/dev/null
# if the app is installed then we need to run migrations on start. New installs will run migrations when you run the installer.
if [ "${APP_INSTALLED}" == "true" ]; then
if [ "${APP_INSTALLED}" = "true" ]; then
#if the db is anything but sqlite wait until it's accepting connections
if [ "${DB_CONNECTION}" != "sqlite" ]; then
# check for DB up before starting the panel
@@ -39,36 +53,44 @@ if [ "${APP_INSTALLED}" == "true" ]; then
# wait for 1 seconds before check again
sleep 1
done
else
echo "using sqlite database"
fi
# run migration
php artisan migrate --force
fi
echo -e "Optimizing Filament"
echo "Optimizing Filament"
php artisan filament:optimize
# default to caddy not starting
export SUPERVISORD_CADDY=false
export PARSED_APP_URL=${APP_URL}
export CADDY_APP_URL="${APP_URL}"
# checking if app url is using https
if echo "${APP_URL}" | grep -qE '^https://'; then
# checking if app url is https
if (echo "${APP_URL}" | grep -qE '^https://'); then
# check lets encrypt email was set without a proxy
if [ -z "${LE_EMAIL}" ] && [ "${BEHIND_PROXY}" != "true" ]; then
echo "when app url is https a lets encrypt email must be set when not behind a proxy"
exit 1
fi
echo "https domain found setting email var"
export PARSED_LE_EMAIL="email ${LE_EMAIL}"
export CADDY_LE_EMAIL="email ${LE_EMAIL}"
fi
# when running behind a proxy
if [ "${BEHIND_PROXY}" == "true" ]; then
if [ "${BEHIND_PROXY}" = "true" ]; then
echo "running behind proxy"
echo "listening on port 80 internally"
export PARSED_LE_EMAIL=""
export PARSED_APP_URL=":80"
export PARSED_AUTO_HTTPS="auto_https off"
export ASSET_URL=${APP_URL}
export CADDY_LE_EMAIL=""
export CADDY_APP_URL=":80"
export CADDY_AUTO_HTTPS="auto_https off"
export ASSET_URL="${APP_URL}"
fi
# disable caddy if SKIP_CADDY is set
if [ "${SKIP_CADDY:-}" == "true" ]; then
if [ "${SKIP_CADDY:-}" = "true" ]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
@@ -76,8 +98,9 @@ else
export SUPERVISORD_CADDY=true
# handle trusted proxies for caddy when variable has data
if [ ! -z ${TRUSTED_PROXIES} ]; then
export CADDY_TRUSTED_PROXIES=$(echo "trusted_proxies static ${TRUSTED_PROXIES}" | sed 's/,/ /g')
if [ -n "${TRUSTED_PROXIES:-}" ]; then
FORMATTED_PROXIES=$(echo "trusted_proxies static ${TRUSTED_PROXIES}" | sed 's/,/ /g')
export CADDY_TRUSTED_PROXIES="${FORMATTED_PROXIES}"
export CADDY_STRICT_PROXIES="trusted_proxies_strict"
fi
fi

View File

@@ -31,6 +31,7 @@ return [
'no_local_ip' => 'Local IP Addresses are not allowed',
'unsupported_format' => 'Unsupported Format. Supported Formats: :formats',
'invalid_url' => 'The provided URL is invalid',
'unknown_extension' => 'Unknown image extension',
'image_deleted' => 'Image Deleted',
'no_image' => 'No Image Provided',
'image_updated' => 'Image Updated',

View File

@@ -14,6 +14,7 @@ return [
],
'table' => [
'health' => 'Health',
'reachable' => 'Reachable',
'name' => 'Name',
'address' => 'Address',
'public' => 'Public',
@@ -39,6 +40,7 @@ return [
'ip_help' => 'Usually your machine\'s public IP unless you are port forwarding.',
'alias_help' => 'Optional display name to help you remember what these are.',
'refresh' => 'Refresh',
'custom_ip' => 'Enter Custom IP',
'domain' => 'Domain Name',
'ssl_ip' => 'You cannot connect to an IP Address over SSL',
'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,4 @@
<x-filament-panels::page>
@once
<style>
.fi-ta-header-ctn {
position: sticky;
top: 0;
z-index: 1;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
</style>
@endonce
<div
x-data="
{

View File

@@ -0,0 +1,67 @@
<div
x-data="{
status: 'loading',
async check() {
try {
await fetch('{{ $httpUrl }}', { mode: 'no-cors', signal: AbortSignal.timeout(5000) });
} catch (e) {
this.status = 'offline';
return;
}
@if ($wsUrl && $wsToken)
try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error('timeout'));
}, 10000);
const ws = new WebSocket('{{ $wsUrl }}');
ws.onerror = () => {
clearTimeout(timeout);
ws.close();
reject(new Error('ws_error'));
};
ws.onopen = () => {
ws.send(JSON.stringify({ event: 'auth', args: ['{{ $wsToken }}'] }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'auth success') {
clearTimeout(timeout);
ws.close();
resolve();
}
};
});
this.status = 'online';
} catch (e) {
this.status = 'warning';
}
@else
this.status = 'online-no-ws';
@endif
}
}"
x-init="check()"
>
<div x-show="status === 'loading'" x-cloak>
{!! $loadingIcon !!}
</div>
<div x-show="status === 'offline'" x-cloak>
{!! $offlineIcon !!}
</div>
<div x-show="status === 'online'" x-cloak>
{!! $onlineIcon !!}
</div>
<div x-show="status === 'warning'" x-cloak>
{!! $warningIcon !!}
</div>
<div x-show="status === 'online-no-ws'" x-cloak>
{!! $onlineNoWsIcon !!}
</div>
</div>