diff --git a/app/Filament/Admin/Pages/Dashboard.php b/app/Filament/Admin/Pages/Dashboard.php index b462236..2136bc5 100644 --- a/app/Filament/Admin/Pages/Dashboard.php +++ b/app/Filament/Admin/Pages/Dashboard.php @@ -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']); diff --git a/app/Filament/Admin/Pages/Support.php b/app/Filament/Admin/Pages/Support.php new file mode 100644 index 0000000..0b9e550 --- /dev/null +++ b/app/Filament/Admin/Pages/Support.php @@ -0,0 +1,30 @@ +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(); diff --git a/app/Filament/Jabali/Pages/Email.php b/app/Filament/Jabali/Pages/Email.php index ccdc46f..962ae21 100644 --- a/app/Filament/Jabali/Pages/Email.php +++ b/app/Filament/Jabali/Pages/Email.php @@ -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 { diff --git a/app/Filament/Jabali/Pages/Files.php b/app/Filament/Jabali/Pages/Files.php index 67f45f3..98004bc 100644 --- a/app/Filament/Jabali/Pages/Files.php +++ b/app/Filament/Jabali/Pages/Files.php @@ -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 diff --git a/app/Filament/Jabali/Pages/Support.php b/app/Filament/Jabali/Pages/Support.php new file mode 100644 index 0000000..f2cfeed --- /dev/null +++ b/app/Filament/Jabali/Pages/Support.php @@ -0,0 +1,30 @@ +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), ]) diff --git a/docs/docs-index.md b/docs/docs-index.md index f986ff9..5748b79 100644 --- a/docs/docs-index.md +++ b/docs/docs-index.md @@ -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. diff --git a/docs/docs-summary.md b/docs/docs-summary.md index 0df8be1..ddcbbc4 100644 --- a/docs/docs-summary.md +++ b/docs/docs-summary.md @@ -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: diff --git a/docs/installation.md b/docs/installation.md index a2093f7..c333b40 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -110,6 +110,51 @@ What is included: If you update or rebuild assets, keep the guard in place and hard‑refresh 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. diff --git a/lang/ar.json b/lang/ar.json index 14ae250..b0886d6 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -219,6 +219,9 @@ "Disk Usage": "استخدام القرص", "Display Name": "الاسم المعروض", "Document Root": "المجلد الجذر", + "Documentation": "التوثيق", + "Open Documentation": "فتح التوثيق", + "Support Chat": "دردشة الدعم", "Domain": "نطاق", "Domain Name": "اسم النطاق", "Domain Verification Code (optional)": "رمز التحقق من النطاق (اختياري)", @@ -879,4 +882,4 @@ "results": "نتائج", "selected": "محدد", "to": "إلى" -} \ No newline at end of file +} diff --git a/lang/en.json b/lang/en.json index 9c9d0ec..bdba888 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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", diff --git a/lang/es.json b/lang/es.json index 5ca043c..979866e 100644 --- a/lang/es.json +++ b/lang/es.json @@ -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", @@ -1170,4 +1173,4 @@ "results": "resultados", "selected": "seleccionado(s)", "to": "a" -} \ No newline at end of file +} diff --git a/lang/fr.json b/lang/fr.json index 0139fe1..f6e902d 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -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)", @@ -886,4 +889,4 @@ "results": "resultats", "selected": "selectionne(s)", "to": "a" -} \ No newline at end of file +} diff --git a/lang/he.json b/lang/he.json index 48ab050..ddc8804 100644 --- a/lang/he.json +++ b/lang/he.json @@ -219,6 +219,9 @@ "Disk Usage": "שימוש בדיסק", "Display Name": "שם תצוגה", "Document Root": "תיקיית שורש", + "Documentation": "תיעוד", + "Open Documentation": "פתח תיעוד", + "Support Chat": "צ'אט תמיכה", "Domain": "דומיין", "Domain Name": "שם הדומיין", "Domain Verification Code (optional)": "קוד אימות דומיין (אופציונלי)", @@ -879,4 +882,4 @@ "results": "תוצאות", "selected": "נבחרו", "to": "עד" -} \ No newline at end of file +} diff --git a/lang/pt.json b/lang/pt.json index 94c1c02..0c6591a 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -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)", @@ -879,4 +882,4 @@ "results": "resultados", "selected": "selecionado(s)", "to": "até" -} \ No newline at end of file +} diff --git a/lang/ru.json b/lang/ru.json index e4a6100..e91a829 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -220,6 +220,9 @@ "Disk Usage": "Использование диска", "Display Name": "Отображаемое имя", "Document Root": "Корневая директория", + "Documentation": "Документация", + "Open Documentation": "Открыть документацию", + "Support Chat": "Чат поддержки", "Domain": "Домен", "Domain Name": "Имя домена", "Domain Verification Code (optional)": "Код подтверждения домена (необязательно)", @@ -886,4 +889,4 @@ "results": "результатов", "selected": "выбрано", "to": "до" -} \ No newline at end of file +} diff --git a/resources/css/app.css b/resources/css/app.css index 196bbc4..2709ec2 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -7,3 +7,4 @@ [x-cloak] { display: none; } + diff --git a/resources/views/filament/admin/pages/support.blade.php b/resources/views/filament/admin/pages/support.blade.php new file mode 100644 index 0000000..de07595 --- /dev/null +++ b/resources/views/filament/admin/pages/support.blade.php @@ -0,0 +1,32 @@ + + + {{ __('Documentation') }} + + + {{ __('Open Documentation') }} + + + + + {{ __('Support Chat') }} + +
+
+ + @script + + @endscript +
diff --git a/resources/views/filament/jabali/pages/support.blade.php b/resources/views/filament/jabali/pages/support.blade.php new file mode 100644 index 0000000..de07595 --- /dev/null +++ b/resources/views/filament/jabali/pages/support.blade.php @@ -0,0 +1,32 @@ + + + {{ __('Documentation') }} + + + {{ __('Open Documentation') }} + + + + + {{ __('Support Chat') }} + +
+
+ + @script + + @endscript +
diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..1254b3c --- /dev/null +++ b/scripts/deploy.sh @@ -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." diff --git a/tests/Feature/Filament/SupportPagesTest.php b/tests/Feature/Filament/SupportPagesTest.php new file mode 100644 index 0000000..86b7383 --- /dev/null +++ b/tests/Feature/Filament/SupportPagesTest.php @@ -0,0 +1,41 @@ +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); + } +}