Compare commits

...

9 Commits

Author SHA1 Message Date
c1599f5dd1 Bump VERSION to 0.9-rc59 2026-02-10 18:27:55 +02:00
6064de6c81 Refine support page content 2026-02-10 18:27:31 +02:00
f7902105de Bump VERSION to 0.9-rc58 2026-02-09 15:34:36 +02:00
b049d338d8 Expand support page help options 2026-02-09 15:34:31 +02:00
8573d96719 Bump VERSION to 0.9-rc57 2026-02-09 14:58:24 +02:00
800e07d2ba Update onboarding, support pages, and deploy tooling 2026-02-09 14:58:04 +02:00
c6f5b6cab8 Replace custom HTML activity log table with Filament EmbeddedTable
The activity tab on the user Logs page used a raw HTML table with
Tailwind classes. This replaces it with a proper Filament embedded
table widget (ActivityLogTable) for consistent styling, pagination,
badges, and dark mode support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:50:49 +00:00
root
8acc55a799 Refine database warning banner and docs 2026-02-06 18:46:16 +00:00
root
a5742a3156 Add confirmations for service disabling 2026-02-06 17:00:13 +00:00
34 changed files with 1017 additions and 90 deletions

View File

@@ -620,7 +620,15 @@ All administrative actions are logged to the `audit_logs` table.
- **USE Tailwind classes** - Only when absolutely necessary for minor adjustments - **USE Tailwind classes** - Only when absolutely necessary for minor adjustments
- **MUST be responsive** - All pages must work on mobile, tablet, and desktop - **MUST be responsive** - All pages must work on mobile, tablet, and desktop
### Warning Banners
- Use Filament `Section::make()` for warning banners (no raw HTML).
- Always set `->icon('heroicon-o-exclamation-triangle')` and `->iconColor('warning')`.
- Keep banners non-collapsible: `->collapsed(false)->collapsible(false)`.
- Put the full message in `->description()` and keep the heading short.
### Allowed Components ### Allowed Components
Use these Filament native components exclusively: Use these Filament native components exclusively:
| Category | Components | | Category | Components |

View File

@@ -1 +1 @@
VERSION=0.9-rc54 VERSION=0.9-rc59

View File

@@ -16,7 +16,9 @@ use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms; use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Schemas\Components\EmbeddedTable; use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Htmlable;
@@ -50,6 +52,13 @@ class Dashboard extends Page implements HasActions, HasForms
]; ];
} }
public function mount(): void
{
if (! DnsSetting::get('onboarding_completed', false)) {
$this->defaultAction = 'onboarding';
}
}
protected function getForms(): array protected function getForms(): array
{ {
return [ return [
@@ -78,21 +87,63 @@ class Dashboard extends Page implements HasActions, HasForms
->color('gray') ->color('gray')
->action(fn () => $this->redirect(request()->url())), ->action(fn () => $this->redirect(request()->url())),
Action::make('onboarding') Action::make('onboarding')->modalCancelActionLabel('Maybe later')
->label(__('Setup Wizard')) ->label(__('Setup Wizard'))
->icon('heroicon-o-sparkles') ->icon('heroicon-o-sparkles')
->visible(fn () => ! DnsSetting::get('onboarding_completed', false))
->modalHeading(__('Welcome to Jabali!')) ->modalHeading(__('Welcome to Jabali!'))
->modalDescription(__('Let\'s get your server control panel set up.')) ->modalDescription(__('Let\'s get your server control panel set up.'))
->modalWidth('md') ->modalWidth('2xl')
->form([ ->form([
Section::make(__('Next Steps'))
->description(__('Here is a quick setup path to get your first site online.'))
->icon('heroicon-o-check-circle')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact()
->schema([
Grid::make(['default' => 1, 'md' => 2])
->schema([
Section::make(__('1. Configure Server Settings'))
->description(__('Set hostname, DNS, email, storage, and PHP defaults.'))
->icon('heroicon-o-cog-6-tooth')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('2. Create a Hosting Package'))
->description(__('Define limits and features for your plans.'))
->icon('heroicon-o-cube')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('3. Create a User'))
->description(__('Assign the hosting package and set credentials.'))
->icon('heroicon-o-user-plus')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('4. Add a Domain'))
->description(__('Issue SSL and deploy your site files.'))
->icon('heroicon-o-globe-alt')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
]),
Text::make(__('Optional: review Services and Server Status to confirm everything is healthy.'))
->color('gray'),
]),
TextInput::make('admin_email') TextInput::make('admin_email')
->label(__('Your Email Address')) ->label(__('Your Email Address'))
->helperText(__('Enter your email to receive important server notifications.')) ->helperText(__('Enter your email to receive important server notifications.'))
->email() ->email()
->placeholder(__('admin@example.com')), ->placeholder(__('admin@example.com')),
]) ])
->modalSubmitActionLabel(__('Get Started')) ->modalSubmitActionLabel(__("Don't show again"))
->action(function (array $data): void { ->action(function (array $data): void {
if (! empty($data['admin_email'])) { if (! empty($data['admin_email'])) {
DnsSetting::set('admin_email_recipients', $data['admin_email']); DnsSetting::set('admin_email_recipients', $data['admin_email']);

View File

@@ -575,7 +575,9 @@ class Security extends Page implements HasActions, HasForms, HasTable
->visible(fn () => $this->fail2banRunning) ->visible(fn () => $this->fail2banRunning)
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(__('Disable Fail2ban')) ->modalHeading(__('Disable Fail2ban'))
->modalDescription(__('Fail2ban will be stopped and disabled. You can re-enable it later from this tab.')) ->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(__('Warning: Fail2ban will be stopped and disabled. You can re-enable it later from this tab.'))
->action('disableFail2ban'), ->action('disableFail2ban'),
]) ])
->schema([ ->schema([
@@ -665,9 +667,12 @@ class Security extends Page implements HasActions, HasForms, HasTable
->color(fn () => $this->clamavRunning ? 'warning' : 'success') ->color(fn () => $this->clamavRunning ? 'warning' : 'success')
->size('sm') ->size('sm')
->action(fn () => $this->clamavRunning ? $this->disableClamav() : $this->enableClamav()) ->action(fn () => $this->clamavRunning ? $this->disableClamav() : $this->enableClamav())
->requiresConfirmation(fn () => ! $this->clamavRunning) ->requiresConfirmation()
->modalHeading(fn () => $this->clamavRunning ? __('Disable ClamAV') : __('Enable ClamAV'))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn () => $this->clamavRunning ->modalDescription(fn () => $this->clamavRunning
? __('ClamAV will be stopped and disabled. You can re-enable it later.') ? __('Warning: This will stop and disable ClamAV. You can re-enable it later.')
: __('Starting ClamAV daemon uses ~500MB RAM. Continue?')), : __('Starting ClamAV daemon uses ~500MB RAM. Continue?')),
FormAction::make('updateSignatures') FormAction::make('updateSignatures')
->label(__('Update Signatures')) ->label(__('Update Signatures'))

View File

@@ -692,17 +692,17 @@ class ServerSettings extends Page implements HasActions, HasForms
protected function databaseTabContent(): array protected function databaseTabContent(): array
{ {
return [ return [
Section::make(__('Warning: Changing database settings can impact performance or cause outages'))
->description(__('Apply changes only if you understand their effects, and prefer doing so during maintenance windows.'))
->icon('heroicon-o-exclamation-triangle')
->iconColor('warning')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('Database Tuning')) Section::make(__('Database Tuning'))
->description(__('Adjust MariaDB/MySQL global variables.')) ->description(__('Adjust MariaDB/MySQL global variables.'))
->icon('heroicon-o-circle-stack') ->icon('heroicon-o-circle-stack')
->schema([ ->schema([
Placeholder::make('database_tuning_warning')
->content(new HtmlString(
'<div class="rounded-lg bg-warning-500/10 p-4 text-sm text-warning-700 dark:text-warning-400">'.
'<strong>'.__('Warning:').'</strong> '.
__('Changing database settings can impact performance or cause outages. Apply changes only if you understand their effects, and prefer doing so during maintenance windows.').
'</div>'
)),
EmbeddedTable::make(DatabaseTuningTable::class), EmbeddedTable::make(DatabaseTuningTable::class),
]), ]),
]; ];

View File

@@ -208,7 +208,9 @@ class Services extends Page implements HasActions, HasForms, HasTable
->visible(fn (array $record): bool => $record['is_active']) ->visible(fn (array $record): bool => $record['is_active'])
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(__('Stop Service')) ->modalHeading(__('Stop Service'))
->modalDescription(fn (array $record): string => __('Are you sure you want to stop :service? This may affect running websites and services.', ['service' => $record['name']])) ->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn (array $record): string => __('Warning: This will stop :service and may affect running websites and services. Are you sure you want to continue?', ['service' => $record['name']]))
->modalSubmitActionLabel(__('Stop Service')) ->modalSubmitActionLabel(__('Stop Service'))
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'stop')), ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'stop')),
Action::make('restart') Action::make('restart')
@@ -236,7 +238,9 @@ class Services extends Page implements HasActions, HasForms, HasTable
->visible(fn (array $record): bool => $record['is_enabled']) ->visible(fn (array $record): bool => $record['is_enabled'])
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(__('Disable Service')) ->modalHeading(__('Disable Service'))
->modalDescription(fn (array $record): string => __("Are you sure you want to disable :service? It won't start automatically on boot.", ['service' => $record['name']])) ->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->modalDescription(fn (array $record): string => __('Warning: This will disable :service and it will not start automatically on boot. Are you sure you want to continue?', ['service' => $record['name']]))
->modalSubmitActionLabel(__('Disable Service')) ->modalSubmitActionLabel(__('Disable Service'))
->action(fn (array $record) => $this->executeServiceAction($record['service'], 'disable')), ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'disable')),
]) ])

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Filament\Admin\Pages;
use BackedEnum;
use Filament\Pages\Page;
use Illuminate\Contracts\Support\Htmlable;
class Support extends Page
{
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-question-mark-circle';
protected static ?int $navigationSort = 23;
protected static ?string $slug = 'support';
protected string $view = 'filament.admin.pages.support';
public static function getNavigationLabel(): string
{
return __('Support');
}
public function getTitle(): string|Htmlable
{
return __('Support');
}
}

View File

@@ -9,8 +9,13 @@ use App\Filament\Jabali\Widgets\DomainsWidget;
use App\Filament\Jabali\Widgets\MailboxesWidget; use App\Filament\Jabali\Widgets\MailboxesWidget;
use App\Filament\Jabali\Widgets\RecentBackupsWidget; use App\Filament\Jabali\Widgets\RecentBackupsWidget;
use App\Filament\Jabali\Widgets\StatsOverview; use App\Filament\Jabali\Widgets\StatsOverview;
use App\Models\DnsSetting;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action;
use Filament\Pages\Dashboard as BaseDashboard; use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Text;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class Dashboard extends BaseDashboard class Dashboard extends BaseDashboard
@@ -41,6 +46,75 @@ class Dashboard extends BaseDashboard
]; ];
} }
public function mount(): void
{
if (! DnsSetting::get('user_onboarding_completed_'.(string) Auth::id(), false)) {
$this->defaultAction = 'onboarding';
}
}
protected function getHeaderActions(): array
{
return [
Action::make('onboarding')->modalCancelActionLabel('Maybe later')
->label(__('Setup Wizard'))
->icon('heroicon-o-sparkles')
->modalHeading(__('Welcome to Jabali!'))
->modalDescription(__('Here is a quick path to launch your first site.'))
->modalWidth('2xl')
->form([
Section::make(__('Next Steps'))
->description(__('Follow these steps to get online quickly.'))
->icon('heroicon-o-check-circle')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact()
->schema([
Grid::make(['default' => 1, 'md' => 2])
->schema([
Section::make(__('1. Add a Domain'))
->description(__('Point your DNS to this server or update nameservers.'))
->icon('heroicon-o-globe-alt')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('2. Issue SSL'))
->description(__('Enable HTTPS for your site with SSL certificates.'))
->icon('heroicon-o-lock-closed')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('3. Upload or Install'))
->description(__('Upload files or install WordPress to deploy your site.'))
->icon('heroicon-o-arrow-up-tray')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
Section::make(__('4. Create Email & Databases'))
->description(__('Set up mailboxes and databases for your app.'))
->icon('heroicon-o-envelope')
->iconColor('info')
->collapsed(false)
->collapsible(false)
->compact(),
]),
Text::make(__('Optional: configure backups, cron jobs, and SSH keys for day-to-day operations.'))
->color('gray'),
]),
])
->modalSubmitActionLabel(__("Don't show again"))
->action(function (): void {
DnsSetting::set('user_onboarding_completed_'.(string) Auth::id(), '1');
DnsSetting::clearCache();
}),
];
}
public function getSubheading(): ?string public function getSubheading(): ?string
{ {
$user = Auth::user(); $user = Auth::user();

View File

@@ -32,6 +32,7 @@ use Filament\Pages\Page;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\View; use Filament\Schemas\Components\View;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
@@ -1017,21 +1018,26 @@ class Email extends Page implements HasActions, HasForms, HasTable
->label(__('Domain')) ->label(__('Domain'))
->options(fn () => Domain::where('user_id', Auth::id())->pluck('domain', 'id')->toArray()) ->options(fn () => Domain::where('user_id', Auth::id())->pluck('domain', 'id')->toArray())
->required() ->required()
->searchable(), ->searchable()
->live()
->live(),
TextInput::make('local_part') TextInput::make('local_part')
->label(__('Email Address')) ->label(__('Email Address'))
->required() ->required(fn (Get $get): bool => filled($get('domain_id')))
->visible(fn (Get $get): bool => filled($get('domain_id')))
->regex('/^[a-zA-Z0-9._%+-]+$/') ->regex('/^[a-zA-Z0-9._%+-]+$/')
->maxLength(64) ->maxLength(64)
->helperText(__('The part before the @ symbol')), ->helperText(__('The part before the @ symbol')),
TextInput::make('name') TextInput::make('name')
->label(__('Display Name')) ->label(__('Display Name'))
->visible(fn (Get $get): bool => filled($get('domain_id')))
->maxLength(255), ->maxLength(255),
TextInput::make('password') TextInput::make('password')
->label(__('Password')) ->label(__('Password'))
->password() ->password()
->revealable() ->revealable()
->required() ->required(fn (Get $get): bool => filled($get('domain_id')))
->visible(fn (Get $get): bool => filled($get('domain_id')))
->minLength(8) ->minLength(8)
->rules([ ->rules([
'regex:/[a-z]/', // lowercase 'regex:/[a-z]/', // lowercase
@@ -1063,6 +1069,7 @@ class Email extends Page implements HasActions, HasForms, HasTable
TextInput::make('quota_mb') TextInput::make('quota_mb')
->label(__('Quota (MB)')) ->label(__('Quota (MB)'))
->numeric() ->numeric()
->visible(fn (Get $get): bool => filled($get('domain_id')))
->default(1024) ->default(1024)
->minValue(100) ->minValue(100)
->maxValue(10240) ->maxValue(10240)
@@ -1236,16 +1243,19 @@ class Email extends Page implements HasActions, HasForms, HasTable
->label(__('Domain')) ->label(__('Domain'))
->options(fn () => Domain::where('user_id', Auth::id())->pluck('domain', 'id')->toArray()) ->options(fn () => Domain::where('user_id', Auth::id())->pluck('domain', 'id')->toArray())
->required() ->required()
->searchable(), ->searchable()
->live(),
TextInput::make('local_part') TextInput::make('local_part')
->label(__('Email Address')) ->label(__('Email Address'))
->required() ->required(fn (Get $get): bool => filled($get('domain_id')))
->visible(fn (Get $get): bool => filled($get('domain_id')))
->regex('/^[a-zA-Z0-9._%+-]+$/') ->regex('/^[a-zA-Z0-9._%+-]+$/')
->maxLength(64) ->maxLength(64)
->helperText(__('The part before the @ symbol')), ->helperText(__('The part before the @ symbol')),
TextInput::make('destinations') TextInput::make('destinations')
->label(__('Forward To')) ->label(__('Forward To'))
->required() ->required(fn (Get $get): bool => filled($get('domain_id')))
->visible(fn (Get $get): bool => filled($get('domain_id')))
->helperText(__('Comma-separated email addresses to forward to')), ->helperText(__('Comma-separated email addresses to forward to')),
]) ])
->action(function (array $data): void { ->action(function (array $data): void {

View File

@@ -60,7 +60,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
public function getTitle(): string|Htmlable public function getTitle(): string|Htmlable
{ {
return 'File Manager'; return __('File Manager');
} }
public function getAgent(): AgentClient public function getAgent(): AgentClient

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Filament\Jabali\Pages; namespace App\Filament\Jabali\Pages;
use App\Models\AuditLog; use App\Filament\Jabali\Widgets\ActivityLogTable;
use App\Services\Agent\AgentClient; use App\Services\Agent\AgentClient;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action; use Filament\Actions\Action;
@@ -16,6 +16,7 @@ use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\View; use Filament\Schemas\Components\View;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Htmlable;
@@ -119,7 +120,7 @@ class Logs extends Page implements HasActions, HasForms
'activity' => Tab::make(__('Activity Log')) 'activity' => Tab::make(__('Activity Log'))
->icon('heroicon-o-clipboard-document-list') ->icon('heroicon-o-clipboard-document-list')
->schema([ ->schema([
View::make('filament.jabali.pages.logs-tab-activity'), EmbeddedTable::make(ActivityLogTable::class),
]), ]),
]), ]),
]); ]);
@@ -227,15 +228,6 @@ class Logs extends Page implements HasActions, HasForms
->send(); ->send();
} }
public function getActivityLogs()
{
return AuditLog::query()
->where('user_id', Auth::id())
->latest()
->limit(50)
->get();
}
public function generateStats(): void public function generateStats(): void
{ {
if (! $this->selectedDomain) { if (! $this->selectedDomain) {

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Filament\Jabali\Pages;
use BackedEnum;
use Filament\Pages\Page;
use Illuminate\Contracts\Support\Htmlable;
class Support extends Page
{
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-question-mark-circle';
protected static ?int $navigationSort = 23;
protected static ?string $slug = 'support';
protected string $view = 'filament.jabali.pages.support';
public static function getNavigationLabel(): string
{
return __('Support');
}
public function getTitle(): string|Htmlable
{
return __('Support');
}
}

View File

@@ -22,6 +22,7 @@ use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ViewColumn; use Filament\Tables\Columns\ViewColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
@@ -448,24 +449,29 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
->options($domainOptions) ->options($domainOptions)
->required() ->required()
->searchable() ->searchable()
->live()
->placeholder(__('Select a domain...')) ->placeholder(__('Select a domain...'))
->helperText(__('The domain where WordPress will be installed')), ->helperText(__('The domain where WordPress will be installed')),
Toggle::make('use_www') Toggle::make('use_www')
->label(__('Use www prefix')) ->label(__('Use www prefix'))
->visible(fn (Get $get): bool => filled($get('domain')))
->helperText(__('Install on www.domain.com instead of domain.com')) ->helperText(__('Install on www.domain.com instead of domain.com'))
->default(false), ->default(false),
TextInput::make('path') TextInput::make('path')
->label(__('Directory (optional)')) ->label(__('Directory (optional)'))
->visible(fn (Get $get): bool => filled($get('domain')))
->placeholder(__('Leave empty to install in root')) ->placeholder(__('Leave empty to install in root'))
->helperText(__('e.g., "blog" to install at domain.com/blog')), ->helperText(__('e.g., "blog" to install at domain.com/blog')),
TextInput::make('site_title') TextInput::make('site_title')
->label(__('Site Title')) ->label(__('Site Title'))
->required() ->required(fn (Get $get): bool => filled($get('domain')))
->visible(fn (Get $get): bool => filled($get('domain')))
->default(__('My WordPress Site')) ->default(__('My WordPress Site'))
->helperText(__('The name of your WordPress site')), ->helperText(__('The name of your WordPress site')),
TextInput::make('admin_user') TextInput::make('admin_user')
->label(__('Admin Username')) ->label(__('Admin Username'))
->required() ->required(fn (Get $get): bool => filled($get('domain')))
->visible(fn (Get $get): bool => filled($get('domain')))
->default('admin') ->default('admin')
->alphaNum() ->alphaNum()
->helperText(__('Username for the WordPress admin account')), ->helperText(__('Username for the WordPress admin account')),
@@ -473,7 +479,8 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
->label(__('Admin Password')) ->label(__('Admin Password'))
->password() ->password()
->revealable() ->revealable()
->required() ->required(fn (Get $get): bool => filled($get('domain')))
->visible(fn (Get $get): bool => filled($get('domain')))
->default(fn () => $this->generateSecurePassword()) ->default(fn () => $this->generateSecurePassword())
->minLength(8) ->minLength(8)
->rules([ ->rules([
@@ -504,7 +511,8 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
->helperText(__('Minimum 8 characters with uppercase, lowercase, and numbers')), ->helperText(__('Minimum 8 characters with uppercase, lowercase, and numbers')),
TextInput::make('admin_email') TextInput::make('admin_email')
->label(__('Admin Email')) ->label(__('Admin Email'))
->required() ->required(fn (Get $get): bool => filled($get('domain')))
->visible(fn (Get $get): bool => filled($get('domain')))
->email() ->email()
->default(Auth::user()->email ?? '') ->default(Auth::user()->email ?? '')
->helperText(__('Email address for the WordPress admin account')), ->helperText(__('Email address for the WordPress admin account')),
@@ -538,14 +546,17 @@ class WordPress extends Page implements HasActions, HasForms, HasTable
]) ])
->default('en_US') ->default('en_US')
->searchable() ->searchable()
->required() ->required(fn (Get $get): bool => filled($get('domain')))
->visible(fn (Get $get): bool => filled($get('domain')))
->helperText(__('Default language for WordPress admin and content')), ->helperText(__('Default language for WordPress admin and content')),
Toggle::make('enable_cache') Toggle::make('enable_cache')
->label(__('Enable Jabali Cache')) ->label(__('Enable Jabali Cache'))
->visible(fn (Get $get): bool => filled($get('domain')))
->helperText(__('Install Redis object caching for better performance')) ->helperText(__('Install Redis object caching for better performance'))
->default(true), ->default(true),
Toggle::make('enable_auto_update') Toggle::make('enable_auto_update')
->label(__('Enable Auto-Updates')) ->label(__('Enable Auto-Updates'))
->visible(fn (Get $get): bool => filled($get('domain')))
->helperText(__('Automatically update WordPress, plugins, and themes')) ->helperText(__('Automatically update WordPress, plugins, and themes'))
->default(false), ->default(false),
]) ])

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Filament\Jabali\Widgets;
use App\Models\AuditLog;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class ActivityLogTable extends Component implements HasTable, HasSchemas, HasActions
{
use InteractsWithTable;
use InteractsWithSchemas;
use InteractsWithActions;
public function table(Table $table): Table
{
return $table
->query(
AuditLog::query()
->where('user_id', Auth::id())
->latest()
)
->columns([
TextColumn::make('created_at')
->label(__('Time'))
->dateTime('M d, H:i')
->color('gray'),
TextColumn::make('category')
->label(__('Category'))
->badge()
->color(fn (string $state): string => match ($state) {
'domain' => 'info',
'email' => 'primary',
'database' => 'warning',
'auth' => 'gray',
'firewall' => 'danger',
'service' => 'success',
default => 'gray',
}),
TextColumn::make('action')
->label(__('Action'))
->badge()
->color(fn (string $state): string => match ($state) {
'create', 'created' => 'success',
'update', 'updated' => 'warning',
'delete', 'deleted' => 'danger',
'login' => 'info',
default => 'gray',
}),
TextColumn::make('description')
->label(__('Description'))
->limit(60)
->wrap(),
TextColumn::make('ip_address')
->label(__('IP'))
->color('gray'),
])
->defaultPaginationPageOption(25)
->striped()
->emptyStateHeading(__('No activity recorded yet'))
->emptyStateDescription(__('Recent actions performed in your account will appear here.'))
->emptyStateIcon('heroicon-o-clipboard-document-list');
}
public function render()
{
return $this->getTable()->render();
}
}

View File

@@ -185,3 +185,5 @@ class BackupSchedule extends Model
return $timezone; return $timezone;
} }
}

View File

@@ -17,7 +17,7 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"filament/blueprint": "^2.0", "filament/blueprint": "^2.1",
"laravel/boost": "*", "laravel/boost": "*",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",
"laravel/pint": "^1.24", "laravel/pint": "^1.24",

8
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "194d87cc129a30c6e832109fb820097a", "content-hash": "7083b0b087c4b503b50d3aa23cfbbfac",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -8817,14 +8817,14 @@
}, },
{ {
"name": "filament/blueprint", "name": "filament/blueprint",
"version": "v2.0.1", "version": "v2.1.0",
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://packages.filamentphp.com/composer/10/127/download" "url": "https://packages.filamentphp.com/composer/10/473/download"
}, },
"require": { "require": {
"filament/support": "^5.0", "filament/support": "^5.0",
"laravel/boost": "^1.8" "laravel/boost": "^1.8|^2.0"
}, },
"type": "library", "type": "library",
"license": [ "license": [

View File

@@ -1,6 +1,6 @@
# Jabali Documentation Index # Jabali Documentation Index
Last updated: 2026-02-06 Last updated: 2026-02-09
## Top-Level Docs ## Top-Level Docs
- /var/www/jabali/README.md - Product overview, features, install, upgrade, and architecture summary. - /var/www/jabali/README.md - Product overview, features, install, upgrade, and architecture summary.
@@ -11,7 +11,7 @@ Last updated: 2026-02-06
- /var/www/jabali/TODO.md - Active checklist items. - /var/www/jabali/TODO.md - Active checklist items.
## Docs Folder ## Docs Folder
- /var/www/jabali/docs/installation.md - Debian package install path and Filament notifications patch. - /var/www/jabali/docs/installation.md - Debian package install path, Filament notifications patch, and deploy script usage.
- /var/www/jabali/docs/architecture/control-panel-blueprint.md - High-level blueprint for a hosting panel. - /var/www/jabali/docs/architecture/control-panel-blueprint.md - High-level blueprint for a hosting panel.
- /var/www/jabali/docs/archive-notes.md - Archived files and restore notes. - /var/www/jabali/docs/archive-notes.md - Archived files and restore notes.
- /var/www/jabali/docs/screenshots/README.md - Screenshot generation instructions. - /var/www/jabali/docs/screenshots/README.md - Screenshot generation instructions.

View File

@@ -1,6 +1,6 @@
# Documentation Summary (Jabali Panel) # Documentation Summary (Jabali Panel)
Last updated: 2026-02-06 Last updated: 2026-02-09
## Product Overview ## Product Overview
Jabali Panel is a modern web hosting control panel for WordPress and general PHP hosting. It provides an admin panel for server-wide operations and a user panel for per-tenant management. The core goals are safe automation, clean multi-tenant isolation, and operational clarity. Jabali Panel is a modern web hosting control panel for WordPress and general PHP hosting. It provides an admin panel for server-wide operations and a user panel for per-tenant management. The core goals are safe automation, clean multi-tenant isolation, and operational clarity.
@@ -41,6 +41,7 @@ DNSSEC can be enabled per domain and generates KSK/ZSK keys, DS records, and sig
- Target OS: Fresh Debian 12/13 install with no pre-existing web/mail stack. - Target OS: Fresh Debian 12/13 install with no pre-existing web/mail stack.
- Installer: install.sh, builds assets as www-data and ensures permissions. - Installer: install.sh, builds assets as www-data and ensures permissions.
- Upgrade: php artisan jabali:upgrade manages dependencies, caches, and permissions for public/build and node_modules. - Upgrade: php artisan jabali:upgrade manages dependencies, caches, and permissions for public/build and node_modules.
- Deploy helper: scripts/deploy.sh syncs code to a server, runs composer/npm, migrations, and caches, and can push to Gitea/GitHub with automatic VERSION bump.
## Packaging ## Packaging
Debian packaging is supported via scripts: Debian packaging is supported via scripts:

View File

@@ -110,6 +110,51 @@ What is included:
If you update or rebuild assets, keep the guard in place and hardrefresh the If you update or rebuild assets, keep the guard in place and hardrefresh the
browser (Ctrl+Shift+R) after deployment. browser (Ctrl+Shift+R) after deployment.
## Deploy script
The repository ships with a deploy helper at `scripts/deploy.sh`. It syncs the
project to a remote server over SSH, then runs composer/npm, migrations, and
cache rebuilds as the web user.
Defaults (override via flags or env vars):
- Host: `192.168.100.50`
- User: `root`
- Path: `/var/www/jabali`
- Web user: `www-data`
Common usage:
```
# Basic deploy to the default host
scripts/deploy.sh
# Target a different host/path/user
scripts/deploy.sh --host 192.168.100.50 --user root --path /var/www/jabali --www-user www-data
# Dry-run rsync only
scripts/deploy.sh --dry-run
# Skip npm build and cache steps
scripts/deploy.sh --skip-npm --skip-cache
```
Push to Git remotes (optional):
```
# Push to Gitea and/or GitHub before deploying
scripts/deploy.sh --push-gitea --push-github
# Push to explicit URLs
scripts/deploy.sh --push-gitea --gitea-url http://192.168.100.100:3001/shukivaknin/jabali-panel.git \
--push-github --github-url git@github.com:shukiv/jabali-panel.git
```
Notes:
- `--push-gitea` / `--push-github` require a clean worktree.
- When pushing, the script bumps `VERSION`, updates the `install.sh` fallback,
and commits the version bump before pushing.
- Rsync excludes `.env`, `storage/`, `vendor/`, `node_modules/`, `public/build/`,
`bootstrap/cache/`, and SQLite DB files. Handle those separately if needed.
- `--delete` passes `--delete` to rsync (dangerous).
## Testing after changes ## Testing after changes
After every change, run a test to make sure there are no errors. After every change, run a test to make sure there are no errors.

View File

@@ -16,7 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")" JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")"
fi fi
JABALI_VERSION="${JABALI_VERSION:-0.9-rc54}" JABALI_VERSION="${JABALI_VERSION:-0.9-rc59}"
# Colors # Colors
RED='\033[0;31m' RED='\033[0;31m'

View File

@@ -219,6 +219,20 @@
"Disk Usage": "استخدام القرص", "Disk Usage": "استخدام القرص",
"Display Name": "الاسم المعروض", "Display Name": "الاسم المعروض",
"Document Root": "المجلد الجذر", "Document Root": "المجلد الجذر",
"Documentation": "التوثيق",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "اعثر على إجابات في التوثيق أو تحدث مع روبوت الدعم المدرَّب لدينا. اطّلع على أدلة الإعداد وخطوات استكشاف الأخطاء وأفضل الممارسات.",
"Chat with our AI support bot.": "تحدث مع روبوت الدعم بالذكاء الاصطناعي.",
"GitHub Issues": "مشكلات GitHub",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "أبلغ عن الأخطاء أو اطلب الميزات. أرفق الخطوات والسجلات ولقطات الشاشة لنتمكن من إعادة الإنتاج بسرعة.",
"Open GitHub Issues": "فتح مشكلات GitHub",
"Paid Support": "دعم مدفوع",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "احصل على مساعدة احترافية للترحيل وتحسين الأداء والإصلاحات ذات الأولوية. تتضمن الخطط الإعداد والدعم المخصص.",
"View Support Plans": "عرض خطط الدعم",
"Response Time": "وقت الاستجابة",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "نستجيب عادة خلال 4-8 ساعات. للحوادث الحرجة، استخدم دعم الطوارئ للحصول على استجابة أسرع.",
"Emergency Support": "دعم الطوارئ",
"Open Documentation": "فتح التوثيق",
"Support Chat": "دردشة الدعم",
"Domain": "نطاق", "Domain": "نطاق",
"Domain Name": "اسم النطاق", "Domain Name": "اسم النطاق",
"Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)", "Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)",
@@ -879,4 +893,4 @@
"results": "نتائج", "results": "نتائج",
"selected": "محدد", "selected": "محدد",
"to": "إلى" "to": "إلى"
} }

View File

@@ -782,6 +782,19 @@
"Display Name": "Display Name", "Display Name": "Display Name",
"Document Root": "Document Root", "Document Root": "Document Root",
"Documentation": "Documentation", "Documentation": "Documentation",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.",
"Chat with our AI support bot.": "Chat with our AI support bot.",
"GitHub Issues": "GitHub Issues",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.",
"Open GitHub Issues": "Open GitHub Issues",
"Paid Support": "Paid Support",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.",
"View Support Plans": "View Support Plans",
"Response Time": "Response Time",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.",
"Emergency Support": "Emergency Support",
"Open Documentation": "Open Documentation",
"Support Chat": "Support Chat",
"Domain": "Domain", "Domain": "Domain",
"Domain Aliases": "Domain Aliases", "Domain Aliases": "Domain Aliases",
"Domain Certificates": "Domain Certificates", "Domain Certificates": "Domain Certificates",

View File

@@ -308,6 +308,20 @@
"Disk Usage": "Uso de disco", "Disk Usage": "Uso de disco",
"Display Name": "Nombre para mostrar", "Display Name": "Nombre para mostrar",
"Document Root": "Raíz del documento", "Document Root": "Raíz del documento",
"Documentation": "Documentación",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "Encuentra respuestas en nuestra documentación o habla con nuestro bot de soporte entrenado. Explora guías de configuración, pasos de solución de problemas y buenas prácticas.",
"Chat with our AI support bot.": "Chatea con nuestro bot de soporte con IA.",
"GitHub Issues": "Issues de GitHub",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "Reporta errores o solicita funciones. Incluye pasos, registros y capturas de pantalla para que podamos reproducirlo rápido.",
"Open GitHub Issues": "Abrir issues de GitHub",
"Paid Support": "Soporte de pago",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "Obtén asistencia profesional para migraciones, ajustes de rendimiento y correcciones prioritarias. Los planes incluyen incorporación y soporte dedicado.",
"View Support Plans": "Ver planes de soporte",
"Response Time": "Tiempo de respuesta",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "Normalmente respondemos en 4-8 horas. Para incidentes críticos, usa el Soporte de Emergencia para una respuesta más rápida.",
"Emergency Support": "Soporte de emergencia",
"Open Documentation": "Abrir documentación",
"Support Chat": "Chat de soporte",
"Domain": "Dominio", "Domain": "Dominio",
"Domain Configuration": "Configuración de dominio", "Domain Configuration": "Configuración de dominio",
"Domain Count": "Cantidad de dominios", "Domain Count": "Cantidad de dominios",
@@ -1170,4 +1184,4 @@
"results": "resultados", "results": "resultados",
"selected": "seleccionado(s)", "selected": "seleccionado(s)",
"to": "a" "to": "a"
} }

View File

@@ -220,6 +220,20 @@
"Disk Usage": "Utilisation du disque", "Disk Usage": "Utilisation du disque",
"Display Name": "Nom d'affichage", "Display Name": "Nom d'affichage",
"Document Root": "Racine du document", "Document Root": "Racine du document",
"Documentation": "Documentation",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "Trouvez des réponses dans notre documentation ou discutez avec notre bot d'assistance entraîné. Explorez les guides de configuration, les étapes de dépannage et les bonnes pratiques.",
"Chat with our AI support bot.": "Discutez avec notre bot de support IA.",
"GitHub Issues": "Issues GitHub",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "Signalez des bugs ou demandez des fonctionnalités. Incluez les étapes, les journaux et des captures d'écran pour une reproduction rapide.",
"Open GitHub Issues": "Ouvrir les issues GitHub",
"Paid Support": "Support payant",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "Obtenez une assistance professionnelle pour les migrations, l'optimisation des performances et les correctifs prioritaires. Les plans incluent l'onboarding et un support dédié.",
"View Support Plans": "Voir les plans de support",
"Response Time": "Délai de réponse",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "Nous répondons généralement sous 4-8 heures. Pour les incidents critiques, utilisez le support d'urgence pour une réponse plus rapide.",
"Emergency Support": "Support d'urgence",
"Open Documentation": "Ouvrir la documentation",
"Support Chat": "Chat de support",
"Domain": "Domaine", "Domain": "Domaine",
"Domain Name": "Nom de domaine", "Domain Name": "Nom de domaine",
"Domain Verification Code (optional)": "Code de verification du domaine (optionnel)", "Domain Verification Code (optional)": "Code de verification du domaine (optionnel)",
@@ -886,4 +900,4 @@
"results": "resultats", "results": "resultats",
"selected": "selectionne(s)", "selected": "selectionne(s)",
"to": "a" "to": "a"
} }

View File

@@ -219,6 +219,20 @@
"Disk Usage": "שימוש בדיסק", "Disk Usage": "שימוש בדיסק",
"Display Name": "שם תצוגה", "Display Name": "שם תצוגה",
"Document Root": "תיקיית שורש", "Document Root": "תיקיית שורש",
"Documentation": "תיעוד",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "מצאו תשובות בתיעוד שלנו או דברו עם בוט התמיכה המאומן שלנו. עיינו במדריכי התקנה, שלבי פתרון תקלות ושיטות עבודה מומלצות.",
"Chat with our AI support bot.": "שוחחו עם בוט התמיכה שלנו.",
"GitHub Issues": "Issues של GitHub",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "דווחו על באגים או בקשו פיצ'רים. צרפו שלבים, לוגים וצילומי מסך כדי שנוכל לשחזר במהירות.",
"Open GitHub Issues": "פתח Issues ב-GitHub",
"Paid Support": "תמיכה בתשלום",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "קבלו סיוע מקצועי במיגרציות, שיפור ביצועים ותיקונים בעדיפות גבוהה. התוכניות כוללות קליטה ותמיכה ייעודית.",
"View Support Plans": "צפו בתוכניות התמיכה",
"Response Time": "זמן תגובה",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "אנחנו בדרך כלל מגיבים תוך 4-8 שעות. לאירועים קריטיים, השתמשו בתמיכת חירום לקבלת מענה מהיר יותר.",
"Emergency Support": "תמיכת חירום",
"Open Documentation": "פתח תיעוד",
"Support Chat": "צ'אט תמיכה",
"Domain": "דומיין", "Domain": "דומיין",
"Domain Name": "שם הדומיין", "Domain Name": "שם הדומיין",
"Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)", "Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)",
@@ -879,4 +893,4 @@
"results": "תוצאות", "results": "תוצאות",
"selected": "נבחרו", "selected": "נבחרו",
"to": "עד" "to": "עד"
} }

View File

@@ -219,6 +219,20 @@
"Disk Usage": "Uso de Disco", "Disk Usage": "Uso de Disco",
"Display Name": "Nome de Exibição", "Display Name": "Nome de Exibição",
"Document Root": "Raiz do Documento", "Document Root": "Raiz do Documento",
"Documentation": "Documentação",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "Encontre respostas na nossa documentação ou fale com o nosso bot de suporte treinado. Explore guias de configuração, etapas de solução de problemas e boas práticas.",
"Chat with our AI support bot.": "Converse com nosso bot de suporte com IA.",
"GitHub Issues": "Issues do GitHub",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "Reporte bugs ou solicite recursos. Inclua passos, logs e capturas de tela para podermos reproduzir rapidamente.",
"Open GitHub Issues": "Abrir issues do GitHub",
"Paid Support": "Suporte pago",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "Obtenha assistência profissional para migrações, ajustes de desempenho e correções prioritárias. Os planos incluem onboarding e suporte dedicado.",
"View Support Plans": "Ver planos de suporte",
"Response Time": "Tempo de resposta",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "Normalmente respondemos em 4-8 horas. Para incidentes críticos, use o Suporte de Emergência para uma resposta mais rápida.",
"Emergency Support": "Suporte de emergência",
"Open Documentation": "Abrir documentação",
"Support Chat": "Chat de suporte",
"Domain": "Domínio", "Domain": "Domínio",
"Domain Name": "Nome do Domínio", "Domain Name": "Nome do Domínio",
"Domain Verification Code (optional)": "Código de Verificação do Domínio (opcional)", "Domain Verification Code (optional)": "Código de Verificação do Domínio (opcional)",
@@ -879,4 +893,4 @@
"results": "resultados", "results": "resultados",
"selected": "selecionado(s)", "selected": "selecionado(s)",
"to": "até" "to": "até"
} }

View File

@@ -220,6 +220,20 @@
"Disk Usage": "Использование диска", "Disk Usage": "Использование диска",
"Display Name": "Отображаемое имя", "Display Name": "Отображаемое имя",
"Document Root": "Корневая директория", "Document Root": "Корневая директория",
"Documentation": "Документация",
"Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.": "Найдите ответы в нашей документации или поговорите с нашим обученным ботом поддержки. Изучите руководства по настройке, шаги по устранению неполадок и лучшие практики.",
"Chat with our AI support bot.": "Чат с нашим AI-ботом поддержки.",
"GitHub Issues": "GitHub Issues",
"Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.": "Сообщайте о багах или запрашивайте функции. Укажите шаги, логи и скриншоты, чтобы мы могли быстро воспроизвести.",
"Open GitHub Issues": "Открыть GitHub Issues",
"Paid Support": "Платная поддержка",
"Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.": "Профессиональная помощь по миграциям, оптимизации производительности и приоритетным исправлениям. Планы включают онбординг и выделенную поддержку.",
"View Support Plans": "Посмотреть планы поддержки",
"Response Time": "Время ответа",
"We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.": "Обычно отвечаем в течение 48 часов. Для критических инцидентов используйте экстренную поддержку для более быстрого ответа.",
"Emergency Support": "Экстренная поддержка",
"Open Documentation": "Открыть документацию",
"Support Chat": "Чат поддержки",
"Domain": "Домен", "Domain": "Домен",
"Domain Name": "Имя домена", "Domain Name": "Имя домена",
"Domain Verification Code (optional)": "Код подтверждения домена (необязательно)", "Domain Verification Code (optional)": "Код подтверждения домена (необязательно)",
@@ -886,4 +900,4 @@
"results": "результатов", "results": "результатов",
"selected": "выбрано", "selected": "выбрано",
"to": "до" "to": "до"
} }

View File

@@ -7,3 +7,4 @@
[x-cloak] { [x-cloak] {
display: none; display: none;
} }

View File

@@ -0,0 +1,78 @@
<x-filament-panels::page>
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
<x-filament::section
icon="heroicon-o-book-open"
icon-color="primary"
>
<x-slot name="heading">{{ __('Documentation') }}</x-slot>
<x-slot name="description">{{ __('Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/docs/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ __('Open Documentation') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-bug-ant"
icon-color="warning"
>
<x-slot name="heading">{{ __('GitHub Issues') }}</x-slot>
<x-slot name="description">{{ __('Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.') }}</x-slot>
<x-filament::button
tag="a"
href="https://github.com/shukiv/jabali-panel/issues"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
color="gray"
>
{{ __('Open GitHub Issues') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-lifebuoy"
icon-color="primary"
>
<x-slot name="heading">{{ __('Paid Support') }}</x-slot>
<x-slot name="description">{{ __('Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/support/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ __('View Support Plans') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-clock"
icon-color="gray"
compact
>
<x-slot name="heading">{{ __('Emergency Support') }}</x-slot>
<x-slot name="description">{{ __('We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/emergency/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
color="warning"
>
{{ __('Emergency Support') }}
</x-filament::button>
</x-filament::section>
</div>
</x-filament-panels::page>

View File

@@ -1,35 +0,0 @@
<x-filament::section class="mt-4" icon="heroicon-o-clipboard-document-list">
<x-slot name="heading">{{ __('Activity Log') }}</x-slot>
<x-slot name="description">{{ __('Recent actions performed in your account.') }}</x-slot>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Time') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Category') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Action') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('Description') }}</th>
<th class="px-4 py-3 text-left fi-section-header-heading">{{ __('IP') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($this->getActivityLogs() as $log)
<tr>
<td class="px-4 py-3 fi-section-header-description">{{ $log->created_at?->format('Y-m-d H:i') }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->category }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->action }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->description }}</td>
<td class="px-4 py-3 fi-section-header-description">{{ $log->ip_address }}</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-6 text-center fi-section-header-description">
{{ __('No activity recorded yet.') }}
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</x-filament::section>

View File

@@ -0,0 +1,78 @@
<x-filament-panels::page>
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
<x-filament::section
icon="heroicon-o-book-open"
icon-color="primary"
>
<x-slot name="heading">{{ __('Documentation') }}</x-slot>
<x-slot name="description">{{ __('Find answers in our docs or talk with our trainned support bot. Explore setup guides, troubleshooting steps, and best practices.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/docs/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ __('Open Documentation') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-bug-ant"
icon-color="warning"
>
<x-slot name="heading">{{ __('GitHub Issues') }}</x-slot>
<x-slot name="description">{{ __('Report bugs or request features. Include steps, logs, and screenshots so we can reproduce quickly.') }}</x-slot>
<x-filament::button
tag="a"
href="https://github.com/shukiv/jabali-panel/issues"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
color="gray"
>
{{ __('Open GitHub Issues') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-lifebuoy"
icon-color="primary"
>
<x-slot name="heading">{{ __('Paid Support') }}</x-slot>
<x-slot name="description">{{ __('Get professional assistance for migrations, performance tuning, and priority fixes. Plans include onboarding and dedicated support.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/support/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ __('View Support Plans') }}
</x-filament::button>
</x-filament::section>
<x-filament::section
icon="heroicon-o-clock"
icon-color="gray"
compact
>
<x-slot name="heading">{{ __('Emergency Support') }}</x-slot>
<x-slot name="description">{{ __('We typically respond within 4-8 hours. For critical incidents, use Emergency Support for faster response.') }}</x-slot>
<x-filament::button
tag="a"
href="https://jabali-panel.com/emergency/"
target="_blank"
rel="noopener"
icon="heroicon-o-arrow-top-right-on-square"
color="warning"
>
{{ __('Emergency Support') }}
</x-filament::button>
</x-filament::section>
</div>
</x-filament-panels::page>

323
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,323 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEPLOY_HOST="${DEPLOY_HOST:-192.168.100.50}"
DEPLOY_USER="${DEPLOY_USER:-root}"
DEPLOY_PATH="${DEPLOY_PATH:-/var/www/jabali}"
WWW_USER="${WWW_USER:-www-data}"
NPM_CACHE_DIR="${NPM_CACHE_DIR:-}"
GITEA_REMOTE="${GITEA_REMOTE:-gitea}"
GITEA_URL="${GITEA_URL:-}"
GITHUB_REMOTE="${GITHUB_REMOTE:-origin}"
GITHUB_URL="${GITHUB_URL:-}"
PUSH_BRANCH="${PUSH_BRANCH:-}"
SKIP_SYNC=0
SKIP_COMPOSER=0
SKIP_NPM=0
SKIP_MIGRATE=0
SKIP_CACHE=0
DELETE_REMOTE=0
DRY_RUN=0
PUSH_GITEA=0
PUSH_GITHUB=0
SET_VERSION=""
usage() {
cat <<'EOF'
Usage: scripts/deploy.sh [options]
Options:
--host HOST Remote host (default: 192.168.100.50)
--user USER SSH user (default: root)
--path PATH Remote path (default: /var/www/jabali)
--www-user USER Remote runtime user (default: www-data)
--skip-sync Skip rsync sync step
--skip-composer Skip composer install
--skip-npm Skip npm install/build
--skip-migrate Skip php artisan migrate
--skip-cache Skip cache clear/rebuild
--delete Pass --delete to rsync (dangerous)
--dry-run Dry-run rsync only
--push-gitea Push current branch to Gitea before deploy
--gitea-remote NAME Gitea git remote name (default: gitea)
--gitea-url URL Push to this URL instead of a named remote
--push-github Push current branch to GitHub before deploy
--github-remote NAME GitHub git remote name (default: origin)
--github-url URL Push to this URL instead of a named remote
--version VALUE Set VERSION to a specific value before push
-h, --help Show this help
Environment overrides:
DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH, WWW_USER, NPM_CACHE_DIR, GITEA_REMOTE, GITEA_URL, GITHUB_REMOTE, GITHUB_URL, PUSH_BRANCH
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--host)
DEPLOY_HOST="$2"
shift 2
;;
--user)
DEPLOY_USER="$2"
shift 2
;;
--path)
DEPLOY_PATH="$2"
shift 2
;;
--www-user)
WWW_USER="$2"
shift 2
;;
--skip-sync)
SKIP_SYNC=1
shift
;;
--skip-composer)
SKIP_COMPOSER=1
shift
;;
--skip-npm)
SKIP_NPM=1
shift
;;
--skip-migrate)
SKIP_MIGRATE=1
shift
;;
--skip-cache)
SKIP_CACHE=1
shift
;;
--delete)
DELETE_REMOTE=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--push-gitea)
PUSH_GITEA=1
shift
;;
--gitea-remote)
GITEA_REMOTE="$2"
shift 2
;;
--gitea-url)
GITEA_URL="$2"
shift 2
;;
--push-github)
PUSH_GITHUB=1
shift
;;
--github-remote)
GITHUB_REMOTE="$2"
shift 2
;;
--github-url)
GITHUB_URL="$2"
shift 2
;;
--version)
SET_VERSION="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}"
ensure_clean_worktree() {
if ! git -C "$ROOT_DIR" diff --quiet || ! git -C "$ROOT_DIR" diff --cached --quiet; then
echo "Working tree is dirty. Commit or stash changes before pushing."
exit 1
fi
}
get_current_version() {
sed -n 's/^VERSION=//p' "$ROOT_DIR/VERSION"
}
bump_version() {
local current new base num
current="$(get_current_version)"
if [[ -n "$SET_VERSION" ]]; then
new="$SET_VERSION"
else
if [[ "$current" =~ ^(.+-rc)([0-9]+)?$ ]]; then
base="${BASH_REMATCH[1]}"
num="${BASH_REMATCH[2]}"
if [[ -z "$num" ]]; then
num=1
else
num=$((num + 1))
fi
new="${base}${num}"
elif [[ "$current" =~ ^(.+?)([0-9]+)$ ]]; then
new="${BASH_REMATCH[1]}$((BASH_REMATCH[2] + 1))"
else
echo "Cannot auto-bump VERSION from '$current'. Use --version to set it explicitly."
exit 1
fi
fi
if [[ "$new" == "$current" ]]; then
echo "VERSION is already '$current'. Use --version to set a new value."
exit 1
fi
printf 'VERSION=%s\n' "$new" > "$ROOT_DIR/VERSION"
perl -0pi -e "s/JABALI_VERSION=\\\"\\$\\{JABALI_VERSION:-[^\\\"]+\\}\\\"/JABALI_VERSION=\\\"\\$\\{JABALI_VERSION:-$new\\}\\\"/g" "$ROOT_DIR/install.sh"
git -C "$ROOT_DIR" add VERSION install.sh
if ! git -C "$ROOT_DIR" diff --cached --quiet; then
git -C "$ROOT_DIR" commit -m "Bump VERSION to $new"
fi
}
prepare_push() {
ensure_clean_worktree
bump_version
}
push_remote() {
local label="$1"
local remote_name="$2"
local remote_url="$3"
local target
if [[ -n "$remote_url" ]]; then
target="$remote_url"
else
if ! git -C "$ROOT_DIR" remote get-url "$remote_name" >/dev/null 2>&1; then
echo "$label remote '$remote_name' not found. Use --${label,,}-url or --${label,,}-remote."
exit 1
fi
target="$remote_name"
fi
if [[ -z "$PUSH_BRANCH" ]]; then
PUSH_BRANCH="$(git -C "$ROOT_DIR" rev-parse --abbrev-ref HEAD)"
fi
git -C "$ROOT_DIR" push "$target" "$PUSH_BRANCH"
}
rsync_project() {
local -a rsync_opts
rsync_opts=(-az --info=progress2)
if [[ "$DELETE_REMOTE" -eq 1 ]]; then
rsync_opts+=(--delete)
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
rsync_opts+=(--dry-run)
fi
rsync "${rsync_opts[@]}" \
--exclude ".git/" \
--exclude "node_modules/" \
--exclude "vendor/" \
--exclude "storage/" \
--exclude "bootstrap/cache/" \
--exclude "public/build/" \
--exclude ".env" \
--exclude ".env.*" \
--exclude "database/*.sqlite" \
--exclude "database/*.sqlite-wal" \
--exclude "database/*.sqlite-shm" \
"$ROOT_DIR/" \
"${REMOTE}:${DEPLOY_PATH}/"
}
remote_run() {
ssh -o StrictHostKeyChecking=no "$REMOTE" "bash -lc '$1'"
}
remote_run_www() {
ssh -o StrictHostKeyChecking=no "$REMOTE" "bash -lc 'cd \"$DEPLOY_PATH\" && sudo -u \"$WWW_USER\" -H bash -lc \"$1\"'"
}
ensure_remote_permissions() {
local parent_dir
parent_dir="$(dirname "$DEPLOY_PATH")"
if [[ -z "$NPM_CACHE_DIR" ]]; then
NPM_CACHE_DIR="${parent_dir}/.npm"
fi
remote_run "mkdir -p \"$DEPLOY_PATH/storage\" \"$DEPLOY_PATH/bootstrap/cache\" \"$DEPLOY_PATH/public/build\" \"$DEPLOY_PATH/node_modules\" \"$DEPLOY_PATH/database\" \"$NPM_CACHE_DIR\""
remote_run "chown -R \"$WWW_USER\":\"$WWW_USER\" \"$DEPLOY_PATH/storage\" \"$DEPLOY_PATH/bootstrap/cache\" \"$DEPLOY_PATH/public\" \"$DEPLOY_PATH/public/build\" \"$DEPLOY_PATH/node_modules\" \"$DEPLOY_PATH/database\" \"$NPM_CACHE_DIR\""
remote_run "if [[ -f \"$DEPLOY_PATH/auth.json\" ]]; then chown \"$WWW_USER\":\"$WWW_USER\" \"$DEPLOY_PATH/auth.json\" && chmod 600 \"$DEPLOY_PATH/auth.json\"; fi"
}
echo "Deploying to ${REMOTE}:${DEPLOY_PATH}"
if [[ "$PUSH_GITEA" -eq 1 ]]; then
prepare_push
echo "Pushing to Gitea..."
push_remote "Gitea" "$GITEA_REMOTE" "$GITEA_URL"
fi
if [[ "$PUSH_GITHUB" -eq 1 ]]; then
if [[ "$PUSH_GITEA" -ne 1 ]]; then
prepare_push
fi
echo "Pushing to GitHub..."
push_remote "GitHub" "$GITHUB_REMOTE" "$GITHUB_URL"
fi
if [[ "$SKIP_SYNC" -eq 0 ]]; then
echo "Syncing project files..."
rsync_project
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "Dry run complete. No remote commands executed."
exit 0
fi
echo "Ensuring remote permissions..."
ensure_remote_permissions
if [[ "$SKIP_COMPOSER" -eq 0 ]]; then
echo "Installing composer dependencies..."
remote_run_www "composer install --no-interaction --prefer-dist --optimize-autoloader"
fi
if [[ "$SKIP_NPM" -eq 0 ]]; then
echo "Building frontend assets..."
remote_run_www "npm ci"
remote_run_www "npm run build"
fi
if [[ "$SKIP_MIGRATE" -eq 0 ]]; then
echo "Running migrations..."
remote_run_www "php artisan migrate --force"
fi
if [[ "$SKIP_CACHE" -eq 0 ]]; then
echo "Refreshing caches..."
remote_run_www "php artisan optimize:clear"
remote_run_www "php artisan config:cache"
remote_run_www "php artisan route:cache"
remote_run_www "php artisan view:cache"
fi
echo "Deploy complete."

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Filament;
use App\Filament\Admin\Pages\Support as AdminSupport;
use App\Filament\Jabali\Pages\Support as UserSupport;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class SupportPagesTest extends TestCase
{
use RefreshDatabase;
public function test_admin_support_page_renders_support_links(): void
{
$admin = User::factory()->admin()->create();
$this->actingAs($admin);
Livewire::test(AdminSupport::class)
->assertStatus(200)
->assertSee('Open Documentation')
->assertSee('GitHub Issues')
->assertSee('Paid Support');
}
public function test_user_support_page_renders_support_links(): void
{
$user = User::factory()->create();
$this->actingAs($user);
Livewire::test(UserSupport::class)
->assertStatus(200)
->assertSee('Open Documentation')
->assertSee('GitHub Issues')
->assertSee('Paid Support');
}
}