Update onboarding, support pages, and deploy tooling

This commit is contained in:
2026-02-09 14:58:04 +02:00
parent c6f5b6cab8
commit 800e07d2ba
22 changed files with 709 additions and 25 deletions

View File

@@ -16,7 +16,9 @@ use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Schema;
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
{
return [
@@ -78,21 +87,63 @@ class Dashboard extends Page implements HasActions, HasForms
->color('gray')
->action(fn () => $this->redirect(request()->url())),
Action::make('onboarding')
Action::make('onboarding')->modalCancelActionLabel('Maybe later')
->label(__('Setup Wizard'))
->icon('heroicon-o-sparkles')
->visible(fn () => ! DnsSetting::get('onboarding_completed', false))
->modalHeading(__('Welcome to Jabali!'))
->modalDescription(__('Let\'s get your server control panel set up.'))
->modalWidth('md')
->modalWidth('2xl')
->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')
->label(__('Your Email Address'))
->helperText(__('Enter your email to receive important server notifications.'))
->email()
->placeholder(__('admin@example.com')),
])
->modalSubmitActionLabel(__('Get Started'))
->modalSubmitActionLabel(__("Don't show again"))
->action(function (array $data): void {
if (! empty($data['admin_email'])) {
DnsSetting::set('admin_email_recipients', $data['admin_email']);

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\RecentBackupsWidget;
use App\Filament\Jabali\Widgets\StatsOverview;
use App\Models\DnsSetting;
use BackedEnum;
use Filament\Actions\Action;
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;
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
{
$user = Auth::user();

View File

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

View File

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

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

View File

@@ -1,6 +1,6 @@
# Jabali Documentation Index
Last updated: 2026-02-06
Last updated: 2026-02-09
## Top-Level Docs
- /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.
## 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/archive-notes.md - Archived files and restore notes.
- /var/www/jabali/docs/screenshots/README.md - Screenshot generation instructions.

View File

@@ -1,6 +1,6 @@
# Documentation Summary (Jabali Panel)
Last updated: 2026-02-06
Last updated: 2026-02-09
## 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.
@@ -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.
- 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.
- 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
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
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
After every change, run a test to make sure there are no errors.

View File

@@ -219,6 +219,9 @@
"Disk Usage": "استخدام القرص",
"Display Name": "الاسم المعروض",
"Document Root": "المجلد الجذر",
"Documentation": "التوثيق",
"Open Documentation": "فتح التوثيق",
"Support Chat": "دردشة الدعم",
"Domain": "نطاق",
"Domain Name": "اسم النطاق",
"Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)",

View File

@@ -782,6 +782,8 @@
"Display Name": "Display Name",
"Document Root": "Document Root",
"Documentation": "Documentation",
"Open Documentation": "Open Documentation",
"Support Chat": "Support Chat",
"Domain": "Domain",
"Domain Aliases": "Domain Aliases",
"Domain Certificates": "Domain Certificates",

View File

@@ -308,6 +308,9 @@
"Disk Usage": "Uso de disco",
"Display Name": "Nombre para mostrar",
"Document Root": "Raíz del documento",
"Documentation": "Documentación",
"Open Documentation": "Abrir documentación",
"Support Chat": "Chat de soporte",
"Domain": "Dominio",
"Domain Configuration": "Configuración de dominio",
"Domain Count": "Cantidad de dominios",

View File

@@ -220,6 +220,9 @@
"Disk Usage": "Utilisation du disque",
"Display Name": "Nom d'affichage",
"Document Root": "Racine du document",
"Documentation": "Documentation",
"Open Documentation": "Ouvrir la documentation",
"Support Chat": "Chat de support",
"Domain": "Domaine",
"Domain Name": "Nom de domaine",
"Domain Verification Code (optional)": "Code de verification du domaine (optionnel)",

View File

@@ -219,6 +219,9 @@
"Disk Usage": "שימוש בדיסק",
"Display Name": "שם תצוגה",
"Document Root": "תיקיית שורש",
"Documentation": "תיעוד",
"Open Documentation": "פתח תיעוד",
"Support Chat": "צ'אט תמיכה",
"Domain": "דומיין",
"Domain Name": "שם הדומיין",
"Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)",

View File

@@ -219,6 +219,9 @@
"Disk Usage": "Uso de Disco",
"Display Name": "Nome de Exibição",
"Document Root": "Raiz do Documento",
"Documentation": "Documentação",
"Open Documentation": "Abrir documentação",
"Support Chat": "Chat de suporte",
"Domain": "Domínio",
"Domain Name": "Nome do Domínio",
"Domain Verification Code (optional)": "Código de Verificação do Domínio (opcional)",

View File

@@ -220,6 +220,9 @@
"Disk Usage": "Использование диска",
"Display Name": "Отображаемое имя",
"Document Root": "Корневая директория",
"Documentation": "Документация",
"Open Documentation": "Открыть документацию",
"Support Chat": "Чат поддержки",
"Domain": "Домен",
"Domain Name": "Имя домена",
"Domain Verification Code (optional)": "Код подтверждения домена (необязательно)",

View File

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

View 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>

View 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
View 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."

View 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);
}
}