Compare commits
2 Commits
c6f5b6cab8
...
8573d96719
| Author | SHA1 | Date | |
|---|---|---|---|
| 8573d96719 | |||
| 800e07d2ba |
@@ -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']);
|
||||||
|
|||||||
30
app/Filament/Admin/Pages/Support.php
Normal file
30
app/Filament/Admin/Pages/Support.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
30
app/Filament/Jabali/Pages/Support.php
Normal file
30
app/Filament/Jabali/Pages/Support.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -110,6 +110,51 @@ What is included:
|
|||||||
If you update or rebuild assets, keep the guard in place and hard‑refresh the
|
If you update or rebuild assets, keep the guard in place and hard‑refresh 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.
|
||||||
|
|||||||
@@ -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-rc56}"
|
JABALI_VERSION="${JABALI_VERSION:-0.9-rc57}"
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|||||||
@@ -219,6 +219,9 @@
|
|||||||
"Disk Usage": "استخدام القرص",
|
"Disk Usage": "استخدام القرص",
|
||||||
"Display Name": "الاسم المعروض",
|
"Display Name": "الاسم المعروض",
|
||||||
"Document Root": "المجلد الجذر",
|
"Document Root": "المجلد الجذر",
|
||||||
|
"Documentation": "التوثيق",
|
||||||
|
"Open Documentation": "فتح التوثيق",
|
||||||
|
"Support Chat": "دردشة الدعم",
|
||||||
"Domain": "نطاق",
|
"Domain": "نطاق",
|
||||||
"Domain Name": "اسم النطاق",
|
"Domain Name": "اسم النطاق",
|
||||||
"Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)",
|
"Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)",
|
||||||
|
|||||||
@@ -782,6 +782,8 @@
|
|||||||
"Display Name": "Display Name",
|
"Display Name": "Display Name",
|
||||||
"Document Root": "Document Root",
|
"Document Root": "Document Root",
|
||||||
"Documentation": "Documentation",
|
"Documentation": "Documentation",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -308,6 +308,9 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -220,6 +220,9 @@
|
|||||||
"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",
|
||||||
|
"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)",
|
||||||
|
|||||||
@@ -219,6 +219,9 @@
|
|||||||
"Disk Usage": "שימוש בדיסק",
|
"Disk Usage": "שימוש בדיסק",
|
||||||
"Display Name": "שם תצוגה",
|
"Display Name": "שם תצוגה",
|
||||||
"Document Root": "תיקיית שורש",
|
"Document Root": "תיקיית שורש",
|
||||||
|
"Documentation": "תיעוד",
|
||||||
|
"Open Documentation": "פתח תיעוד",
|
||||||
|
"Support Chat": "צ'אט תמיכה",
|
||||||
"Domain": "דומיין",
|
"Domain": "דומיין",
|
||||||
"Domain Name": "שם הדומיין",
|
"Domain Name": "שם הדומיין",
|
||||||
"Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)",
|
"Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)",
|
||||||
|
|||||||
@@ -219,6 +219,9 @@
|
|||||||
"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",
|
||||||
|
"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)",
|
||||||
|
|||||||
@@ -220,6 +220,9 @@
|
|||||||
"Disk Usage": "Использование диска",
|
"Disk Usage": "Использование диска",
|
||||||
"Display Name": "Отображаемое имя",
|
"Display Name": "Отображаемое имя",
|
||||||
"Document Root": "Корневая директория",
|
"Document Root": "Корневая директория",
|
||||||
|
"Documentation": "Документация",
|
||||||
|
"Open Documentation": "Открыть документацию",
|
||||||
|
"Support Chat": "Чат поддержки",
|
||||||
"Domain": "Домен",
|
"Domain": "Домен",
|
||||||
"Domain Name": "Имя домена",
|
"Domain Name": "Имя домена",
|
||||||
"Domain Verification Code (optional)": "Код подтверждения домена (необязательно)",
|
"Domain Verification Code (optional)": "Код подтверждения домена (необязательно)",
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
[x-cloak] {
|
[x-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
resources/views/filament/admin/pages/support.blade.php
Normal file
32
resources/views/filament/admin/pages/support.blade.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<x-filament::section
|
||||||
|
icon="heroicon-o-book-open"
|
||||||
|
icon-color="primary"
|
||||||
|
>
|
||||||
|
<x-slot name="heading">{{ __('Documentation') }}</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-chat-bubble-left-right"
|
||||||
|
icon-color="info"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<x-slot name="heading">{{ __('Support Chat') }}</x-slot>
|
||||||
|
|
||||||
|
<div id="jabali-support-chat"></div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
@script
|
||||||
|
<script src="https://portal.jabali-panel.com/js/support-widget.js?v=10" data-api-url="https://portal.jabali-panel.com" data-open="true"></script>
|
||||||
|
@endscript
|
||||||
|
</x-filament-panels::page>
|
||||||
32
resources/views/filament/jabali/pages/support.blade.php
Normal file
32
resources/views/filament/jabali/pages/support.blade.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<x-filament::section
|
||||||
|
icon="heroicon-o-book-open"
|
||||||
|
icon-color="primary"
|
||||||
|
>
|
||||||
|
<x-slot name="heading">{{ __('Documentation') }}</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-chat-bubble-left-right"
|
||||||
|
icon-color="info"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<x-slot name="heading">{{ __('Support Chat') }}</x-slot>
|
||||||
|
|
||||||
|
<div id="jabali-support-chat"></div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
@script
|
||||||
|
<script src="https://portal.jabali-panel.com/js/support-widget.js?v=10" data-api-url="https://portal.jabali-panel.com" data-open="true"></script>
|
||||||
|
@endscript
|
||||||
|
</x-filament-panels::page>
|
||||||
306
scripts/deploy.sh
Executable file
306
scripts/deploy.sh
Executable file
@@ -0,0 +1,306 @@
|
|||||||
|
#!/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}"
|
||||||
|
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, 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\"'"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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."
|
||||||
41
tests/Feature/Filament/SupportPagesTest.php
Normal file
41
tests/Feature/Filament/SupportPagesTest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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_docs_and_chat(): void
|
||||||
|
{
|
||||||
|
$admin = User::factory()->admin()->create();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test(AdminSupport::class)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSee('Open Documentation')
|
||||||
|
->assertSee('jabali-support-chat', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_user_support_page_renders_docs_and_chat(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
Livewire::test(UserSupport::class)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSee('Open Documentation')
|
||||||
|
->assertSee('jabali-support-chat', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user