Update README demo links and bump version

This commit is contained in:
codex
2026-02-04 00:30:23 +02:00
committed by root
parent be34afe2c8
commit a0048109ce
44 changed files with 3355 additions and 710 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
.git
.gitignore
node_modules
storage/logs
storage/framework/cache
storage/framework/sessions
storage/framework/views
storage/app/private
storage/app/private/*
storage/app/private/livewire-tmp
.env
.env.*
!.env.example

View File

@@ -326,6 +326,18 @@ dns.get_ds_records - Get DS records for registrar
| Admin | `https://jabali.lan/jabali-admin` | `admin@jabali.lan` | `q1w2E#R$` | | Admin | `https://jabali.lan/jabali-admin` | `admin@jabali.lan` | `q1w2E#R$` |
| User | `https://jabali.lan/jabali-panel` | `user@jabali.lan` | `wjqr9t6Z#%r&@C$4` | | User | `https://jabali.lan/jabali-panel` | `user@jabali.lan` | `wjqr9t6Z#%r&@C$4` |
### Demo Credentials
| Panel | URL | Email | Password |
|-------|-----|-------|----------|
| Admin | `https://demo.jabali-panel.com/jabali-admin` | `admin@jabali-panel.com` | `demo1234` |
| User | `https://demo.jabali-panel.com/jabali-panel` | `demo@jabali-panel.com` | `demo1234` |
**Demo mode behavior**
- `JABALI_DEMO=1` enables read-only mode via `App\Http\Middleware\DemoReadOnly`.
- Livewire `authenticate` calls are allowed for unauthenticated users.
- Some pages use static demo data to avoid agent socket calls.
- Reverse proxy must set `X-Forwarded-Proto` and the app must trust proxies for HTTPS Livewire updates.
## Models ## Models
| Model | Table | Description | | Model | Table | Description |

View File

@@ -1,6 +1,6 @@
# CONTEXT.md # CONTEXT.md
Last updated: 2026-02-01 Last updated: 2026-02-03
## Stack ## Stack
- Laravel 12, Filament v5, Livewire v4 - Laravel 12, Filament v5, Livewire v4
@@ -11,6 +11,16 @@ Last updated: 2026-02-01
- Admin panel: `/jabali-admin` - Admin panel: `/jabali-admin`
- User panel: `/jabali-panel` - User panel: `/jabali-panel`
## Demo
- Public demo: `https://demo.jabali-panel.com`
- Demo container port: `5555` behind Nginx TLS proxy
- Demo DB: `database/database-demo.sqlite`
- Demo mode: `JABALI_DEMO=1` (read-only)
- Demo credentials:
- Admin: `admin@jabali-panel.com` / `demo1234`
- User: `demo@jabali-panel.com` / `demo1234`
- Livewire HTTPS requires proxy trust (`trustProxies(at: '*')`) and `X-Forwarded-Proto`.
## Data ## Data
- Panel config DB: SQLite at `database/database.sqlite` - Panel config DB: SQLite at `database/database.sqlite`
- Hosting services use MariaDB/Postfix/Dovecot/etc. as configured by the agent - Hosting services use MariaDB/Postfix/Dovecot/etc. as configured by the agent

View File

@@ -6,3 +6,8 @@
- Asset builds must be writable for `public/build` and `node_modules`; upgrade checks both. - Asset builds must be writable for `public/build` and `node_modules`; upgrade checks both.
- Installer builds assets as `www-data` to avoid permission issues. - Installer builds assets as `www-data` to avoid permission issues.
- Default panel database is SQLite (`database/database.sqlite`). - Default panel database is SQLite (`database/database.sqlite`).
## 2026-02-03
- Demo mode is enforced by `App\Http\Middleware\DemoReadOnly` (read-only, but allow Livewire `authenticate`).
- Demo container runs without agent sockets; select pages use static demo data to avoid 500s.
- Reverse proxies must be trusted for HTTPS Livewire updates in demo (`trustProxies(at: '*')`).

View File

@@ -56,6 +56,26 @@ After install:
- User panel: `https://your-host/jabali-panel` - User panel: `https://your-host/jabali-panel`
- Webmail: `https://your-host/webmail` - Webmail: `https://your-host/webmail`
Website: `https://jabali-panel.com/`
## Demo
Public demo:
- User panel: `https://demo.jabali-panel.com/jabali-panel/`
- Admin panel: `https://demo.jabali-panel.com/jabali-admin/`
Credentials:
- Admin: `admin@jabali-panel.com` / `demo1234`
- User: `demo@jabali-panel.com` / `demo1234`
Notes:
- Demo is read-only; actions that change data are blocked.
- Some pages use static demo data where the privileged agent is unavailable
(for example: PHP Manager, PHP Settings, Protected Directories).
## Feature Map ## Feature Map
### Admin Panel ### Admin Panel

View File

@@ -6,3 +6,5 @@ Keep this list current as work progresses.
- [ ] Confirm WAF whitelist + blocked requests tables refresh correctly after changes. - [ ] Confirm WAF whitelist + blocked requests tables refresh correctly after changes.
- [ ] Validate sysstat collection interval (10s) and chart intervals align. - [ ] Validate sysstat collection interval (10s) and chart intervals align.
- [ ] Audit installer/uninstaller parity for newly added packages. - [ ] Audit installer/uninstaller parity for newly added packages.
- [ ] Demo mode: stub remaining agent-dependent pages to avoid 500s.
- [ ] Demo mode: reduce repeated "changes are disabled" notifications for blocked actions.

View File

@@ -8,10 +8,23 @@ use App\Models\User;
use Filament\Auth\Http\Responses\Contracts\LoginResponse; use Filament\Auth\Http\Responses\Contracts\LoginResponse;
use Filament\Auth\Pages\Login as BaseLogin; use Filament\Auth\Pages\Login as BaseLogin;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
class Login extends BaseLogin class Login extends BaseLogin
{ {
public function getSubheading(): string | HtmlString | null
{
if (env('JABALI_DEMO', false)) {
return new HtmlString(
__('Demo credentials') .
': <code>admin@jabali-panel.com</code> / <code>demo1234</code>'
);
}
return parent::getSubheading();
}
public function authenticate(): ?LoginResponse public function authenticate(): ?LoginResponse
{ {
$data = $this->form->getState(); $data = $this->form->getState();

View File

@@ -324,7 +324,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
->color('gray'), ->color('gray'),
TextColumn::make('duration') TextColumn::make('duration')
->label(__('Duration')) ->label(__('Duration'))
->placeholder('-') ->placeholder(__('-'))
->color('gray'), ->color('gray'),
]) ])
->recordActions([ ->recordActions([

View File

@@ -574,7 +574,7 @@ class CpanelMigration extends Page implements HasActions, HasForms, HasInfolists
Grid::make(['default' => 1, 'sm' => 2])->schema([ Grid::make(['default' => 1, 'sm' => 2])->schema([
TextInput::make('hostname') TextInput::make('hostname')
->label(__('cPanel Hostname')) ->label(__('cPanel Hostname'))
->placeholder('cpanel.example.com') ->placeholder(__('cpanel.example.com'))
->required(fn () => $this->sourceType === 'remote') ->required(fn () => $this->sourceType === 'remote')
->helperText(__('Your cPanel server hostname or IP address')), ->helperText(__('Your cPanel server hostname or IP address')),
TextInput::make('port') TextInput::make('port')
@@ -610,7 +610,7 @@ class CpanelMigration extends Page implements HasActions, HasForms, HasInfolists
->schema([ ->schema([
TextInput::make('localBackupPath') TextInput::make('localBackupPath')
->label(__('Backup File Path')) ->label(__('Backup File Path'))
->placeholder('/home/user/backups/backup-date_username.tar.gz') ->placeholder(__('/home/user/backups/backup-date_username.tar.gz'))
->required(fn () => $this->sourceType === 'local') ->required(fn () => $this->sourceType === 'local')
->helperText(__('Full path to the cPanel backup file (e.g., /var/backups/backup.tar.gz)')), ->helperText(__('Full path to the cPanel backup file (e.g., /var/backups/backup.tar.gz)')),
Text::make(__('Supported formats: .tar.gz, .tgz'))->color('gray'), Text::make(__('Supported formats: .tar.gz, .tgz'))->color('gray'),

View File

@@ -90,7 +90,7 @@ class Dashboard extends Page implements HasActions, HasForms
->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(__('Get Started'))
->action(function (array $data): void { ->action(function (array $data): void {

View File

@@ -254,12 +254,12 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
->sortable(), ->sortable(),
TextColumn::make('priority') TextColumn::make('priority')
->label(__('Priority')) ->label(__('Priority'))
->placeholder('-') ->placeholder(__('-'))
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null) ->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
->sortable(), ->sortable(),
TextColumn::make('domain.user.username') TextColumn::make('domain.user.username')
->label(__('Owner')) ->label(__('Owner'))
->placeholder('N/A') ->placeholder(__('N/A'))
->sortable(), ->sortable(),
]) ])
->filters([]) ->filters([])

View File

@@ -99,7 +99,7 @@ class IpAddresses extends Page implements HasActions, HasTable
->form([ ->form([
TextInput::make('ip') TextInput::make('ip')
->label(__('IP Address')) ->label(__('IP Address'))
->placeholder('203.0.113.10') ->placeholder(__('203.0.113.10'))
->live() ->live()
->afterStateUpdated(function (?string $state, callable $set): void { ->afterStateUpdated(function (?string $state, callable $set): void {
if (! $state) { if (! $state) {
@@ -198,7 +198,7 @@ class IpAddresses extends Page implements HasActions, HasTable
->getStateUsing(fn (array $record): ?string => $this->getDefaultLabel($record)) ->getStateUsing(fn (array $record): ?string => $this->getDefaultLabel($record))
->badge() ->badge()
->color('success') ->color('success')
->placeholder('-'), ->placeholder(__('-')),
]) ])
->recordActions([ ->recordActions([
Action::make('setDefault') Action::make('setDefault')

View File

@@ -65,7 +65,30 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable
public function loadPhpVersions(): void public function loadPhpVersions(): void
{ {
$result = $this->getAgent()->send('php.list_versions', []); if ((bool) env('JABALI_DEMO', false)) {
$this->installedVersions = [
['version' => '8.4', 'fpm_status' => 'active'],
['version' => '8.3', 'fpm_status' => 'active'],
['version' => '8.2', 'fpm_status' => 'inactive'],
['version' => '8.1', 'fpm_status' => 'inactive'],
];
$this->defaultVersion = '8.4';
$allVersions = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'];
$installed = array_column($this->installedVersions, 'version');
$this->availableVersions = array_diff($allVersions, $installed);
return;
}
try {
$result = $this->getAgent()->send('php.list_versions', []);
} catch (\Exception $e) {
$this->installedVersions = [];
$this->defaultVersion = null;
$this->availableVersions = [];
return;
}
if ($result['success'] ?? false) { if ($result['success'] ?? false) {
$this->installedVersions = $result['versions'] ?? []; $this->installedVersions = $result['versions'] ?? [];

View File

@@ -279,7 +279,7 @@ class ServerSettings extends Page implements HasActions, HasForms
Grid::make(['default' => 1, 'md' => 2])->schema([ Grid::make(['default' => 1, 'md' => 2])->schema([
TextInput::make('brandingData.panel_name') TextInput::make('brandingData.panel_name')
->label(__('Control Panel Name')) ->label(__('Control Panel Name'))
->placeholder('Jabali') ->placeholder(__('Jabali'))
->helperText(__('Appears in browser title and navigation')) ->helperText(__('Appears in browser title and navigation'))
->required(), ->required(),
]), ]),
@@ -320,7 +320,7 @@ class ServerSettings extends Page implements HasActions, HasForms
->schema([ ->schema([
TextInput::make('hostnameData.hostname') TextInput::make('hostnameData.hostname')
->label(__('Hostname')) ->label(__('Hostname'))
->placeholder('server.example.com') ->placeholder(__('server.example.com'))
->required(), ->required(),
Actions::make([ Actions::make([
FormAction::make('saveHostname') FormAction::make('saveHostname')
@@ -338,10 +338,10 @@ class ServerSettings extends Page implements HasActions, HasForms
->icon('heroicon-o-server-stack') ->icon('heroicon-o-server-stack')
->schema([ ->schema([
Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([ Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([
TextInput::make('dnsData.ns1')->label(__('NS1 Hostname'))->placeholder('ns1.example.com'), TextInput::make('dnsData.ns1')->label(__('NS1 Hostname'))->placeholder(__('ns1.example.com')),
TextInput::make('dnsData.ns1_ip')->label(__('NS1 IP Address'))->placeholder('192.168.1.1'), TextInput::make('dnsData.ns1_ip')->label(__('NS1 IP Address'))->placeholder(__('192.168.1.1')),
TextInput::make('dnsData.ns2')->label(__('NS2 Hostname'))->placeholder('ns2.example.com'), TextInput::make('dnsData.ns2')->label(__('NS2 Hostname'))->placeholder(__('ns2.example.com')),
TextInput::make('dnsData.ns2_ip')->label(__('NS2 IP Address'))->placeholder('192.168.1.2'), TextInput::make('dnsData.ns2_ip')->label(__('NS2 IP Address'))->placeholder(__('192.168.1.2')),
]), ]),
]), ]),
Section::make(__('Zone Defaults')) Section::make(__('Zone Defaults'))
@@ -349,20 +349,20 @@ class ServerSettings extends Page implements HasActions, HasForms
Grid::make(['default' => 1, 'md' => 3])->schema([ Grid::make(['default' => 1, 'md' => 3])->schema([
TextInput::make('dnsData.default_ip') TextInput::make('dnsData.default_ip')
->label(__('Default Server IP')) ->label(__('Default Server IP'))
->placeholder('192.168.1.1') ->placeholder(__('192.168.1.1'))
->helperText(__('Default A record IP for new zones')), ->helperText(__('Default A record IP for new zones')),
TextInput::make('dnsData.default_ipv6') TextInput::make('dnsData.default_ipv6')
->label(__('Default IPv6')) ->label(__('Default IPv6'))
->placeholder('2001:db8::1') ->placeholder(__('2001:db8::1'))
->helperText(__('Default AAAA record IP for new zones')) ->helperText(__('Default AAAA record IP for new zones'))
->rule('nullable|ipv6'), ->rule('nullable|ipv6'),
TextInput::make('dnsData.default_ttl') TextInput::make('dnsData.default_ttl')
->label(__('Default TTL')) ->label(__('Default TTL'))
->placeholder('3600'), ->placeholder(__('3600')),
]), ]),
TextInput::make('dnsData.admin_email') TextInput::make('dnsData.admin_email')
->label(__('Admin Email (SOA)')) ->label(__('Admin Email (SOA)'))
->placeholder('admin.example.com') ->placeholder(__('admin.example.com'))
->helperText(__('Use dots instead of @ (e.g., admin.example.com)')), ->helperText(__('Use dots instead of @ (e.g., admin.example.com)')),
Actions::make([ Actions::make([
FormAction::make('saveDns') FormAction::make('saveDns')
@@ -386,10 +386,10 @@ class ServerSettings extends Page implements HasActions, HasForms
->action('applyQuad9Resolvers'), ->action('applyQuad9Resolvers'),
])->alignment('left'), ])->alignment('left'),
Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([ Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([
TextInput::make('resolversData.resolver1')->label(__('Resolver 1'))->placeholder('8.8.8.8'), TextInput::make('resolversData.resolver1')->label(__('Resolver 1'))->placeholder(__('8.8.8.8')),
TextInput::make('resolversData.resolver2')->label(__('Resolver 2'))->placeholder('8.8.4.4'), TextInput::make('resolversData.resolver2')->label(__('Resolver 2'))->placeholder(__('8.8.4.4')),
TextInput::make('resolversData.resolver3')->label(__('Resolver 3'))->placeholder('1.1.1.1'), TextInput::make('resolversData.resolver3')->label(__('Resolver 3'))->placeholder(__('1.1.1.1')),
TextInput::make('resolversData.search_domain')->label(__('Search Domain'))->placeholder('example.com'), TextInput::make('resolversData.search_domain')->label(__('Search Domain'))->placeholder(__('example.com')),
]), ]),
Actions::make([ Actions::make([
FormAction::make('saveResolvers') FormAction::make('saveResolvers')
@@ -470,7 +470,7 @@ class ServerSettings extends Page implements HasActions, HasForms
TextInput::make('quotaData.default_quota_mb') TextInput::make('quotaData.default_quota_mb')
->label(__('Default Quota (MB)')) ->label(__('Default Quota (MB)'))
->numeric() ->numeric()
->placeholder('5120') ->placeholder(__('5120'))
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')), ->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
]), ]),
Actions::make([ Actions::make([
@@ -487,7 +487,7 @@ class ServerSettings extends Page implements HasActions, HasForms
->numeric() ->numeric()
->minValue(1) ->minValue(1)
->maxValue(500) ->maxValue(500)
->placeholder('100') ->placeholder(__('100'))
->helperText(__('Maximum file size users can upload (1-500 MB)')), ->helperText(__('Maximum file size users can upload (1-500 MB)')),
Actions::make([ Actions::make([
FormAction::make('saveFileManagerSettings') FormAction::make('saveFileManagerSettings')
@@ -507,7 +507,7 @@ class ServerSettings extends Page implements HasActions, HasForms
Grid::make(['default' => 1, 'md' => 2])->schema([ Grid::make(['default' => 1, 'md' => 2])->schema([
TextInput::make('emailData.mail_hostname') TextInput::make('emailData.mail_hostname')
->label(__('Mail Server Hostname')) ->label(__('Mail Server Hostname'))
->placeholder('mail.example.com') ->placeholder(__('mail.example.com'))
->helperText(__('The hostname used for mail server identification')), ->helperText(__('The hostname used for mail server identification')),
TextInput::make('emailData.mail_default_quota_mb') TextInput::make('emailData.mail_default_quota_mb')
->label(__('Default Mailbox Quota (MB)')) ->label(__('Default Mailbox Quota (MB)'))
@@ -527,11 +527,11 @@ class ServerSettings extends Page implements HasActions, HasForms
Grid::make(['default' => 1, 'md' => 2])->schema([ Grid::make(['default' => 1, 'md' => 2])->schema([
TextInput::make('emailData.webmail_url') TextInput::make('emailData.webmail_url')
->label(__('Webmail URL')) ->label(__('Webmail URL'))
->placeholder('/webmail') ->placeholder(__('/webmail'))
->helperText(__('URL path for Roundcube webmail')), ->helperText(__('URL path for Roundcube webmail')),
TextInput::make('emailData.webmail_product_name') TextInput::make('emailData.webmail_product_name')
->label(__('Webmail Product Name')) ->label(__('Webmail Product Name'))
->placeholder('Jabali Webmail') ->placeholder(__('Jabali Webmail'))
->helperText(__('Name displayed on the webmail login page')), ->helperText(__('Name displayed on the webmail login page')),
]), ]),
Actions::make([ Actions::make([
@@ -556,7 +556,7 @@ class ServerSettings extends Page implements HasActions, HasForms
->schema([ ->schema([
TextInput::make('notificationsData.admin_email_recipients') TextInput::make('notificationsData.admin_email_recipients')
->label(__('Email Addresses')) ->label(__('Email Addresses'))
->placeholder('admin@example.com, alerts@example.com') ->placeholder(__('admin@example.com, alerts@example.com'))
->helperText(__('Comma-separated list of email addresses to receive notifications')), ->helperText(__('Comma-separated list of email addresses to receive notifications')),
]), ]),
Section::make(__('Notification Types & High Load Alerts')) Section::make(__('Notification Types & High Load Alerts'))
@@ -598,14 +598,14 @@ class ServerSettings extends Page implements HasActions, HasForms
->minValue(1) ->minValue(1)
->maxValue(100) ->maxValue(100)
->step(0.5) ->step(0.5)
->placeholder('5') ->placeholder(__('5'))
->helperText(__('Alert when load exceeds this value')), ->helperText(__('Alert when load exceeds this value')),
TextInput::make('notificationsData.load_alert_minutes') TextInput::make('notificationsData.load_alert_minutes')
->label(__('Alert After (minutes)')) ->label(__('Alert After (minutes)'))
->numeric() ->numeric()
->minValue(1) ->minValue(1)
->maxValue(60) ->maxValue(60)
->placeholder('5') ->placeholder(__('5'))
->helperText(__('Minutes of high load before alerting')), ->helperText(__('Minutes of high load before alerting')),
]), ]),
Actions::make([ Actions::make([
@@ -649,7 +649,7 @@ class ServerSettings extends Page implements HasActions, HasForms
->helperText(__('Requests before worker recycle')), ->helperText(__('Requests before worker recycle')),
TextInput::make('phpFpmData.memory_limit') TextInput::make('phpFpmData.memory_limit')
->label(__('Memory Limit')) ->label(__('Memory Limit'))
->placeholder('512M') ->placeholder(__('512M'))
->helperText(__('PHP memory_limit (e.g., 512M, 1G)')), ->helperText(__('PHP memory_limit (e.g., 512M, 1G)')),
]), ]),
Grid::make(['default' => 1, 'md' => 2, 'lg' => 3])->schema([ Grid::make(['default' => 1, 'md' => 2, 'lg' => 3])->schema([

View File

@@ -123,7 +123,7 @@ class SslManager extends Page implements HasTable
->limit(30) ->limit(30)
->tooltip(fn ($state) => $state) ->tooltip(fn ($state) => $state)
->color('danger') ->color('danger')
->placeholder('-'), ->placeholder(__('-')),
]) ])
->filters([ ->filters([
SelectFilter::make('ssl_status') SelectFilter::make('ssl_status')

View File

@@ -382,7 +382,7 @@ class WhmMigration extends Page implements HasActions, HasForms, HasInfolists, H
Grid::make(['default' => 1, 'sm' => 2])->schema([ Grid::make(['default' => 1, 'sm' => 2])->schema([
TextInput::make('hostname') TextInput::make('hostname')
->label(__('WHM Hostname')) ->label(__('WHM Hostname'))
->placeholder('whm.example.com') ->placeholder(__('whm.example.com'))
->required() ->required()
->helperText(__('Your WHM server hostname or IP address')), ->helperText(__('Your WHM server hostname or IP address')),
TextInput::make('port') TextInput::make('port')

View File

@@ -61,7 +61,7 @@ class DnsPendingAddsTable extends Component implements HasActions, HasSchemas, H
->label(__('TTL')), ->label(__('TTL')),
TextColumn::make('priority') TextColumn::make('priority')
->label(__('Priority')) ->label(__('Priority'))
->placeholder('-'), ->placeholder(__('-')),
]) ])
->actions([ ->actions([
Action::make('removePending') Action::make('removePending')

View File

@@ -53,7 +53,7 @@ class Fail2banLogsTable extends Component implements HasActions, HasSchemas, Has
->label(__('IP')) ->label(__('IP'))
->fontFamily('mono') ->fontFamily('mono')
->copyable() ->copyable()
->placeholder('-'), ->placeholder(__('-')),
TextColumn::make('message') TextColumn::make('message')
->label(__('Message')) ->label(__('Message'))
->wrap(), ->wrap(),

View File

@@ -12,9 +12,22 @@ use Filament\Facades\Filament;
use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\FilamentUser;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\HtmlString;
class Login extends BaseLogin class Login extends BaseLogin
{ {
public function getSubheading(): string | HtmlString | null
{
if (env('JABALI_DEMO', false)) {
return new HtmlString(
__('Demo credentials') .
': <code>demo@jabali-panel.com</code> / <code>demo1234</code>'
);
}
return parent::getSubheading();
}
public function authenticate(): ?LoginResponse public function authenticate(): ?LoginResponse
{ {
$panel = Filament::getPanel('jabali'); $panel = Filament::getPanel('jabali');

View File

@@ -373,7 +373,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
->sortable(), ->sortable(),
TextColumn::make('duration') TextColumn::make('duration')
->label(__('Duration')) ->label(__('Duration'))
->placeholder('-'), ->placeholder(__('-')),
]) ])
->defaultSort('created_at', 'desc') ->defaultSort('created_at', 'desc')
->emptyStateHeading(__('No restore history')) ->emptyStateHeading(__('No restore history'))
@@ -1092,7 +1092,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
Grid::make(2)->schema([ Grid::make(2)->schema([
TextInput::make('host') TextInput::make('host')
->label(__('Host')) ->label(__('Host'))
->placeholder('backup.example.com') ->placeholder(__('backup.example.com'))
->required(), ->required(),
TextInput::make('port') TextInput::make('port')
->label(__('Port')) ->label(__('Port'))

View File

@@ -571,7 +571,7 @@ class CpanelMigration extends Page implements HasActions, HasForms
Grid::make(['default' => 1, 'sm' => 2])->schema([ Grid::make(['default' => 1, 'sm' => 2])->schema([
TextInput::make('hostname') TextInput::make('hostname')
->label(__('cPanel Hostname')) ->label(__('cPanel Hostname'))
->placeholder('cpanel.example.com') ->placeholder(__('cpanel.example.com'))
->required() ->required()
->helperText(__('Your cPanel server hostname or IP address')), ->helperText(__('Your cPanel server hostname or IP address')),
TextInput::make('port') TextInput::make('port')

View File

@@ -288,7 +288,7 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
->sortable(), ->sortable(),
TextColumn::make('priority') TextColumn::make('priority')
->label(__('Priority')) ->label(__('Priority'))
->placeholder('-') ->placeholder(__('-'))
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null) ->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
->sortable(), ->sortable(),
]) ])

View File

@@ -248,7 +248,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
->schema([ ->schema([
TextInput::make('domain_redirect_url') TextInput::make('domain_redirect_url')
->label(__('Redirect To')) ->label(__('Redirect To'))
->placeholder('https://newdomain.com') ->placeholder(__('https://newdomain.com'))
->helperText(__('All requests to this domain will be redirected to this URL')) ->helperText(__('All requests to this domain will be redirected to this URL'))
->url() ->url()
->required(fn ($get) => $get('domain_redirect_enabled')) ->required(fn ($get) => $get('domain_redirect_enabled'))
@@ -275,13 +275,13 @@ class Domains extends Page implements HasActions, HasForms, HasTable
->schema([ ->schema([
TextInput::make('source_path') TextInput::make('source_path')
->label(__('Source Path')) ->label(__('Source Path'))
->placeholder('/old-page') ->placeholder(__('/old-page'))
->helperText(__('Path to redirect from (e.g., /old-page)')) ->helperText(__('Path to redirect from (e.g., /old-page)'))
->required() ->required()
->columnSpan(['default' => 2, 'md' => 1]), ->columnSpan(['default' => 2, 'md' => 1]),
TextInput::make('destination_url') TextInput::make('destination_url')
->label(__('Destination URL')) ->label(__('Destination URL'))
->placeholder('https://example.com/new-page') ->placeholder(__('https://example.com/new-page'))
->helperText(__('Full URL to redirect to')) ->helperText(__('Full URL to redirect to'))
->required() ->required()
->url() ->url()
@@ -359,13 +359,13 @@ class Domains extends Page implements HasActions, HasForms, HasTable
Textarea::make('allowed_domains') Textarea::make('allowed_domains')
->label(__('Allowed Domains')) ->label(__('Allowed Domains'))
->helperText(__('One domain per line that can link to your files (your own domain is always allowed)')) ->helperText(__('One domain per line that can link to your files (your own domain is always allowed)'))
->placeholder("example.com\ntrusted-site.com") ->placeholder(__("example.com\ntrusted-site.com"))
->rows(4) ->rows(4)
->columnSpan(['default' => 2, 'md' => 1]), ->columnSpan(['default' => 2, 'md' => 1]),
TextInput::make('protected_extensions') TextInput::make('protected_extensions')
->label(__('Protected File Extensions')) ->label(__('Protected File Extensions'))
->helperText(__('Comma-separated list of file extensions to protect')) ->helperText(__('Comma-separated list of file extensions to protect'))
->placeholder('jpg,jpeg,png,gif,webp,svg,mp4,mp3,pdf') ->placeholder(__('jpg,jpeg,png,gif,webp,svg,mp4,mp3,pdf'))
->default(DomainHotlinkSetting::getDefaultExtensions()) ->default(DomainHotlinkSetting::getDefaultExtensions())
->columnSpan(['default' => 2, 'md' => 1]), ->columnSpan(['default' => 2, 'md' => 1]),
]) ])
@@ -381,7 +381,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
TextInput::make('redirect_url') TextInput::make('redirect_url')
->label(__('Redirect URL (Optional)')) ->label(__('Redirect URL (Optional)'))
->helperText(__('Redirect blocked requests to this URL instead of showing an error')) ->helperText(__('Redirect blocked requests to this URL instead of showing an error'))
->placeholder('https://example.com/hotlink-blocked.png') ->placeholder(__('https://example.com/hotlink-blocked.png'))
->url() ->url()
->columnSpan(['default' => 2, 'md' => 1]), ->columnSpan(['default' => 2, 'md' => 1]),
]) ])
@@ -842,7 +842,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
->schema([ ->schema([
TextInput::make('alias') TextInput::make('alias')
->label(__('Alias Domain')) ->label(__('Alias Domain'))
->placeholder('alias-example.com') ->placeholder(__('alias-example.com'))
->required() ->required()
->rule('regex:/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\\.[a-z]{2,}$/') ->rule('regex:/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\\.[a-z]{2,}$/')
->helperText(__('Enter a full domain name.')), ->helperText(__('Enter a full domain name.')),

View File

@@ -185,11 +185,11 @@ class Email extends Page implements HasActions, HasForms, HasTable
Textarea::make('whitelist') Textarea::make('whitelist')
->label(__('Whitelist (one per line)')) ->label(__('Whitelist (one per line)'))
->rows(6) ->rows(6)
->placeholder("friend@example.com\ntrusted.com"), ->placeholder(__("friend@example.com\ntrusted.com")),
Textarea::make('blacklist') Textarea::make('blacklist')
->label(__('Blacklist (one per line)')) ->label(__('Blacklist (one per line)'))
->rows(6) ->rows(6)
->placeholder("spam@example.com\nbad-domain.com"), ->placeholder(__("spam@example.com\nbad-domain.com")),
TextInput::make('score') TextInput::make('score')
->label(__('Spam Score Threshold')) ->label(__('Spam Score Threshold'))
->numeric() ->numeric()

View File

@@ -83,7 +83,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
// Invalid path from URL - reset to home directory // Invalid path from URL - reset to home directory
$this->currentPath = ''; $this->currentPath = '';
Notification::make() Notification::make()
->title('Invalid path') ->title(__('Invalid path'))
->body('The requested path is not allowed.') ->body('The requested path is not allowed.')
->danger() ->danger()
->send(); ->send();
@@ -222,7 +222,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
} catch (Exception $e) { } catch (Exception $e) {
$this->items = []; $this->items = [];
Notification::make() Notification::make()
->title('Error loading directory') ->title(__('Error loading directory'))
->body($e->getMessage()) ->body($e->getMessage())
->danger() ->danger()
->send(); ->send();
@@ -237,7 +237,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
$this->resetTable(); $this->resetTable();
} catch (Exception $e) { } catch (Exception $e) {
Notification::make() Notification::make()
->title('Invalid path') ->title(__('Invalid path'))
->body($e->getMessage()) ->body($e->getMessage())
->danger() ->danger()
->send(); ->send();
@@ -425,7 +425,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
->form([ ->form([
TextInput::make('mode') TextInput::make('mode')
->label(__('Numeric Mode')) ->label(__('Numeric Mode'))
->placeholder('755') ->placeholder(__('755'))
->maxLength(4) ->maxLength(4)
->helperText(__('Enter octal mode (e.g., 755, 644)')), ->helperText(__('Enter octal mode (e.g., 755, 644)')),
Grid::make(3) Grid::make(3)
@@ -750,7 +750,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
$this->getAgent()->fileMove($this->getUsername(), $sourcePath, $destPath); $this->getAgent()->fileMove($this->getUsername(), $sourcePath, $destPath);
Notification::make() Notification::make()
->title('Item moved successfully') ->title(__('Item moved successfully'))
->success() ->success()
->send(); ->send();
@@ -758,7 +758,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
$this->resetTable(); $this->resetTable();
} catch (Exception $e) { } catch (Exception $e) {
Notification::make() Notification::make()
->title('Error moving item') ->title(__('Error moving item'))
->body($e->getMessage()) ->body($e->getMessage())
->danger() ->danger()
->send(); ->send();
@@ -788,7 +788,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
); );
Notification::make() Notification::make()
->title("Uploaded: $filename") ->title(__('Uploaded: :filename', ['filename' => $filename]))
->success() ->success()
->send(); ->send();
@@ -796,7 +796,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
$this->resetTable(); $this->resetTable();
} catch (Exception $e) { } catch (Exception $e) {
Notification::make() Notification::make()
->title("Upload failed: $filename") ->title(__('Upload failed: :filename', ['filename' => $filename]))
->body($e->getMessage()) ->body($e->getMessage())
->danger() ->danger()
->send(); ->send();
@@ -939,7 +939,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
$uploaded++; $uploaded++;
} catch (Exception $e) { } catch (Exception $e) {
Notification::make() Notification::make()
->title(__('Upload failed: ').$file->getClientOriginalName()) ->title(__('Upload failed: :filename', ['filename' => $file->getClientOriginalName()]))
->body($e->getMessage()) ->body($e->getMessage())
->danger() ->danger()
->send(); ->send();
@@ -1007,7 +1007,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
filename: basename($path) filename: basename($path)
); );
} catch (Exception $e) { } catch (Exception $e) {
Notification::make()->title('Error downloading')->body($e->getMessage())->danger()->send(); Notification::make()->title(__('Error downloading'))->body($e->getMessage())->danger()->send();
} }
} }

View File

@@ -276,7 +276,7 @@ class GitDeployment extends Page implements HasActions, HasForms, HasTable
}), }),
TextInput::make('repo_url') TextInput::make('repo_url')
->label(__('Repository URL')) ->label(__('Repository URL'))
->placeholder('git@github.com:org/repo.git') ->placeholder(__('git@github.com:org/repo.git'))
->required(), ->required(),
TextInput::make('branch') TextInput::make('branch')
->label(__('Branch')) ->label(__('Branch'))

View File

@@ -82,7 +82,7 @@ class MailingLists extends Page implements HasActions, HasForms
->schema([ ->schema([
TextInput::make('listmonk_url') TextInput::make('listmonk_url')
->label(__('Listmonk URL')) ->label(__('Listmonk URL'))
->placeholder('https://lists.example.com') ->placeholder(__('https://lists.example.com'))
->url() ->url()
->visible(fn ($get) => $get('provider') === 'listmonk'), ->visible(fn ($get) => $get('provider') === 'listmonk'),
TextInput::make('listmonk_token') TextInput::make('listmonk_token')
@@ -99,7 +99,7 @@ class MailingLists extends Page implements HasActions, HasForms
->schema([ ->schema([
TextInput::make('mailman_url') TextInput::make('mailman_url')
->label(__('Mailman URL')) ->label(__('Mailman URL'))
->placeholder('https://lists.example.com/mailman') ->placeholder(__('https://lists.example.com/mailman'))
->url() ->url()
->visible(fn ($get) => $get('provider') === 'mailman'), ->visible(fn ($get) => $get('provider') === 'mailman'),
TextInput::make('mailman_admin') TextInput::make('mailman_admin')

View File

@@ -83,6 +83,16 @@ class PhpSettings extends Page implements HasActions, HasForms
protected function loadDomains(): void protected function loadDomains(): void
{ {
if ((bool) env('JABALI_DEMO', false)) {
$this->domains = [
['domain' => 'jabali-panel.com'],
['domain' => 'demo-site.com'],
['domain' => 'store.demo'],
];
return;
}
$result = $this->getAgent()->send('domain.list', [ $result = $this->getAgent()->send('domain.list', [
'username' => $this->getUsername(), 'username' => $this->getUsername(),
]); ]);
@@ -92,6 +102,17 @@ class PhpSettings extends Page implements HasActions, HasForms
protected function loadPhpVersions(): void protected function loadPhpVersions(): void
{ {
if ((bool) env('JABALI_DEMO', false)) {
$this->phpVersions = [
'8.4' => 'PHP 8.4',
'8.3' => 'PHP 8.3',
'8.2' => 'PHP 8.2',
'8.1' => 'PHP 8.1',
];
return;
}
$result = $this->getAgent()->send('php.list_versions', []); $result = $this->getAgent()->send('php.list_versions', []);
$this->phpVersions = []; $this->phpVersions = [];
@@ -120,6 +141,20 @@ class PhpSettings extends Page implements HasActions, HasForms
return; return;
} }
if ((bool) env('JABALI_DEMO', false)) {
$this->data = [
'php_version' => array_key_first($this->phpVersions),
'memory_limit' => '256M',
'upload_max_filesize' => '64M',
'post_max_size' => '64M',
'max_input_vars' => '3000',
'max_execution_time' => '300',
'max_input_time' => '300',
];
return;
}
$result = $this->getAgent()->send('php.getSettings', [ $result = $this->getAgent()->send('php.getSettings', [
'domain' => $this->selectedDomain, 'domain' => $this->selectedDomain,
'username' => $this->getUsername(), 'username' => $this->getUsername(),

View File

@@ -80,6 +80,16 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
protected function loadDomains(): void protected function loadDomains(): void
{ {
if ((bool) env('JABALI_DEMO', false)) {
$this->domains = [
['domain' => 'jabali-panel.com'],
['domain' => 'demo-site.com'],
['domain' => 'store.demo'],
];
return;
}
$result = $this->getAgent()->send('domain.list', [ $result = $this->getAgent()->send('domain.list', [
'username' => $this->getUsername(), 'username' => $this->getUsername(),
]); ]);
@@ -102,6 +112,30 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
return; return;
} }
if ((bool) env('JABALI_DEMO', false)) {
$this->protectedDirs = [
[
'path' => '/admin',
'name' => 'Restricted Area',
'users_count' => 2,
'users' => [
['username' => 'demo', 'created_at' => now()->subDays(10)->toDateTimeString()],
['username' => 'editor', 'created_at' => now()->subDays(3)->toDateTimeString()],
],
],
[
'path' => '/private',
'name' => 'Private Files',
'users_count' => 1,
'users' => [
['username' => 'staff', 'created_at' => now()->subDays(1)->toDateTimeString()],
],
],
];
return;
}
$result = $this->getAgent()->send('domain.list_protected_dirs', [ $result = $this->getAgent()->send('domain.list_protected_dirs', [
'domain' => $this->selectedDomain, 'domain' => $this->selectedDomain,
'username' => $this->getUsername(), 'username' => $this->getUsername(),
@@ -239,7 +273,7 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
->form([ ->form([
TextInput::make('path') TextInput::make('path')
->label(__('Directory Path')) ->label(__('Directory Path'))
->placeholder('/admin') ->placeholder(__('/admin'))
->required() ->required()
->helperText(__('Path relative to your document root (e.g., /admin, /private, /members)')), ->helperText(__('Path relative to your document root (e.g., /admin, /private, /members)')),
TextInput::make('name') TextInput::make('name')

View File

@@ -99,7 +99,7 @@ class Ssl extends Page implements HasActions, HasForms, HasTable
? __('Expired :days days ago', ['days' => abs($record->sslCertificate->days_until_expiry)]) ? __('Expired :days days ago', ['days' => abs($record->sslCertificate->days_until_expiry)])
: __(':days days left', ['days' => $record->sslCertificate->days_until_expiry])) : __(':days days left', ['days' => $record->sslCertificate->days_until_expiry]))
: null) : null)
->placeholder('-') ->placeholder(__('-'))
->sortable(), ->sortable(),
IconColumn::make('sslCertificate.auto_renew') IconColumn::make('sslCertificate.auto_renew')
->label(__('Auto-Renew')) ->label(__('Auto-Renew'))
@@ -421,19 +421,19 @@ class Ssl extends Page implements HasActions, HasForms, HasTable
->helperText(__('Select the domain to install the certificate on')), ->helperText(__('Select the domain to install the certificate on')),
Textarea::make('certificate') Textarea::make('certificate')
->label(__('Certificate (PEM format)')) ->label(__('Certificate (PEM format)'))
->placeholder("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----") ->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"))
->rows(8) ->rows(8)
->required() ->required()
->helperText(__('Paste your SSL certificate in PEM format')), ->helperText(__('Paste your SSL certificate in PEM format')),
Textarea::make('private_key') Textarea::make('private_key')
->label(__('Private Key (PEM format)')) ->label(__('Private Key (PEM format)'))
->placeholder("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----") ->placeholder(__("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"))
->rows(8) ->rows(8)
->required() ->required()
->helperText(__('Paste your private key in PEM format. Keep this secure!')), ->helperText(__('Paste your private key in PEM format. Keep this secure!')),
Textarea::make('ca_bundle') Textarea::make('ca_bundle')
->label(__('CA Bundle (optional)')) ->label(__('CA Bundle (optional)'))
->placeholder("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----") ->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"))
->rows(6) ->rows(6)
->helperText(__('Paste the certificate authority chain if required by your certificate provider')), ->helperText(__('Paste the certificate authority chain if required by your certificate provider')),
]) ])

View File

@@ -61,7 +61,7 @@ class DnsPendingAddsTable extends Component implements HasActions, HasSchemas, H
->label(__('TTL')), ->label(__('TTL')),
TextColumn::make('priority') TextColumn::make('priority')
->label(__('Priority')) ->label(__('Priority'))
->placeholder('-'), ->placeholder(__('-')),
]) ])
->actions([ ->actions([
Action::make('removePending') Action::make('removePending')

View File

@@ -12,6 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '*');
$middleware->append(\App\Http\Middleware\SecurityHeaders::class); $middleware->append(\App\Http\Middleware\SecurityHeaders::class);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {

49
docker/Dockerfile.demo Normal file
View File

@@ -0,0 +1,49 @@
FROM php:8.4-cli
WORKDIR /var/www/jabali
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
unzip \
sqlite3 \
libsqlite3-dev \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libicu-dev \
libonig-dev \
libxml2-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
bcmath \
exif \
gd \
intl \
mbstring \
pdo \
pdo_sqlite \
zip \
&& rm -rf /var/lib/apt/lists/*
COPY . /var/www/jabali
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENV APP_ENV=demo \
APP_DEBUG=false \
APP_URL=http://localhost:5555 \
DB_CONNECTION=sqlite \
DB_DATABASE=/var/www/jabali/database/demo.sqlite \
JABALI_DEMO=1 \
JABALI_DEMO_EMAIL=demo-admin@jabali-panel.com \
JABALI_DEMO_PASSWORD=demo12345 \
SESSION_DRIVER=file \
CACHE_STORE=file \
QUEUE_CONNECTION=sync
EXPOSE 5555
ENTRYPOINT ["/entrypoint.sh"]

27
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env sh
set -eu
cd /var/www/jabali
mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views storage/logs bootstrap/cache
if [ ! -f .env ]; then
cp .env.example .env
fi
if [ -n "${APP_URL:-}" ]; then
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|" .env
fi
if [ -n "${ASSET_URL:-}" ]; then
sed -i "s|^ASSET_URL=.*|ASSET_URL=${ASSET_URL}|" .env
fi
php -r "if (trim(getenv('APP_KEY') ?: '') === '') { echo 'Generating APP_KEY...\n'; }"
php artisan key:generate --force >/dev/null 2>&1 || true
php artisan storage:link >/dev/null 2>&1 || true
chmod -R ug+rw storage bootstrap/cache
exec php -S 0.0.0.0:5555 -t public

View File

@@ -120,3 +120,9 @@ This blueprint describes a modern web hosting control panel (cPanel/DirectAdmin-
- Local + S3 target - Local + S3 target
- Schedule + retention - Schedule + retention
- Restore job with step logs - Restore job with step logs
## 9) Demo mode considerations
- Read-only middleware should block data mutations but allow authentication.
- Demo deployments may run without privileged agent sockets; provide static demo data for agent-dependent pages.
- Reverse proxy must be trusted to ensure Livewire update URLs are HTTPS.

View File

@@ -10,3 +10,11 @@ Archived to `/opt/jabali-archive/cleanup-20260124/repo-root`:
- `jabali_logo.svg` (duplicate of `public/images/jabali_logo.svg`) - `jabali_logo.svg` (duplicate of `public/images/jabali_logo.svg`)
Restore by moving the files back to the repo root if needed. Restore by moving the files back to the repo root if needed.
## Demo housekeeping (2026-02-03)
Disk cleanup steps used on demo hosts:
- Vacuum systemd journals (`journalctl --vacuum-time=7d`)
- Clean apt cache (`apt-get clean` + remove `/var/lib/apt/lists/*`)
- Prune Docker build cache (`docker builder prune -af`)

View File

@@ -37,6 +37,63 @@ After install, systemd services are enabled and started:
- `jabali-queue` - `jabali-queue`
- `jabali-health-monitor` - `jabali-health-monitor`
## Demo Docker (single container)
Demo images run the panel in read-only mode with demo data preloaded.
Key requirements:
- `JABALI_DEMO=1`
- `DB_DATABASE` must point to the demo SQLite file: `database/database-demo.sqlite`
- Trust reverse proxy so Livewire update URLs are HTTPS
Example container run (port 5555):
```
docker run -d --name jabali-panel-demo \
--restart unless-stopped \
-p 5555:5555 \
-e APP_URL=https://demo.jabali-panel.com \
-e ASSET_URL=https://demo.jabali-panel.com \
-e DB_DATABASE=/var/www/jabali/database/database-demo.sqlite \
-e JABALI_DEMO=1 \
-e JABALI_DEMO_ADMIN_EMAIL=admin@jabali-panel.com \
-e JABALI_DEMO_ADMIN_PASSWORD=demo1234 \
-e JABALI_DEMO_USER_EMAIL=demo@jabali-panel.com \
-e JABALI_DEMO_USER_PASSWORD=demo1234 \
jabali-panel-demo-current
```
Nginx reverse proxy (HTTPS):
```
server {
listen 443 ssl http2;
server_name demo.jabali-panel.com;
ssl_certificate /etc/letsencrypt/live/demo.jabali-panel.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/demo.jabali-panel.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
}
```
Reverse proxy trust (required for Livewire HTTPS):
- Ensure `bootstrap/app.php` includes `trustProxies(at: '*')`.
Demo limitations:
- No agent socket in the container: pages that rely on the agent should use
static demo data to avoid 500s.
## Panel notifications (admin + user) ## Panel notifications (admin + user)
Jabali ships with a hardened Filament notifications setup that prevents Livewire Jabali ships with a hardened Filament notifications setup that prevents Livewire

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,11 @@
<x-application-logo class="block h-12 w-auto" /> <x-application-logo class="block h-12 w-auto" />
<h1 class="mt-8 text-2xl font-medium text-gray-900"> <h1 class="mt-8 text-2xl font-medium text-gray-900">
Welcome to your Jetstream application! {{ __('Welcome to your Jetstream application!') }}
</h1> </h1>
<p class="mt-6 text-gray-500 leading-relaxed"> <p class="mt-6 text-gray-500 leading-relaxed">
Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed {{ __('Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel ecosystem to be a breath of fresh air. We hope you love it.') }}
to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe
you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel
ecosystem to be a breath of fresh air. We hope you love it.
</p> </p>
</div> </div>
@@ -20,17 +17,17 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg> </svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900"> <h2 class="ms-3 text-xl font-semibold text-gray-900">
<a href="https://laravel.com/docs">Documentation</a> <a href="https://laravel.com/docs">{{ __('Documentation') }}</a>
</h2> </h2>
</div> </div>
<p class="mt-4 text-gray-500 text-sm leading-relaxed"> <p class="mt-4 text-gray-500 text-sm leading-relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you're new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end. {{ __('Laravel has wonderful documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end.') }}
</p> </p>
<p class="mt-4 text-sm"> <p class="mt-4 text-sm">
<a href="https://laravel.com/docs" class="inline-flex items-center font-semibold text-indigo-700"> <a href="https://laravel.com/docs" class="inline-flex items-center font-semibold text-indigo-700">
Explore the documentation {{ __('Explore the documentation') }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500">
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
@@ -45,17 +42,17 @@
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" /> <path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
</svg> </svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900"> <h2 class="ms-3 text-xl font-semibold text-gray-900">
<a href="https://laracasts.com">Laracasts</a> <a href="https://laracasts.com">{{ __('Laracasts') }}</a>
</h2> </h2>
</div> </div>
<p class="mt-4 text-gray-500 text-sm leading-relaxed"> <p class="mt-4 text-gray-500 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process. {{ __('Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.') }}
</p> </p>
<p class="mt-4 text-sm"> <p class="mt-4 text-sm">
<a href="https://laracasts.com" class="inline-flex items-center font-semibold text-indigo-700"> <a href="https://laracasts.com" class="inline-flex items-center font-semibold text-indigo-700">
Start watching Laracasts {{ __('Start watching Laracasts') }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 size-5 fill-indigo-500">
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
@@ -70,12 +67,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg> </svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900"> <h2 class="ms-3 text-xl font-semibold text-gray-900">
<a href="https://tailwindcss.com/">Tailwind</a> <a href="https://tailwindcss.com/">{{ __('Tailwind') }}</a>
</h2> </h2>
</div> </div>
<p class="mt-4 text-gray-500 text-sm leading-relaxed"> <p class="mt-4 text-gray-500 text-sm leading-relaxed">
Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that doesn't get in your way. You'll be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips. {{ __('Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that does not get in your way. You will be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips.') }}
</p> </p>
</div> </div>
@@ -85,12 +82,12 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg> </svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900"> <h2 class="ms-3 text-xl font-semibold text-gray-900">
Authentication {{ __('Authentication') }}
</h2> </h2>
</div> </div>
<p class="mt-4 text-gray-500 text-sm leading-relaxed"> <p class="mt-4 text-gray-500 text-sm leading-relaxed">
Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you're free to get started with what matters most: building your application. {{ __('Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you are free to get started with what matters most: building your application.') }}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -16,9 +16,9 @@
<x-slot name="description">{{ __('Use these endpoints with a token that has the automation ability.') }}</x-slot> <x-slot name="description">{{ __('Use these endpoints with a token that has the automation ability.') }}</x-slot>
<div class="space-y-2 fi-section-header-description"> <div class="space-y-2 fi-section-header-description">
<div><span class="font-mono">GET</span> /api/automation/users</div> <div><span class="font-mono">{{ __('GET') }}</span> {{ __('/api/automation/users') }}</div>
<div><span class="font-mono">POST</span> /api/automation/users</div> <div><span class="font-mono">{{ __('POST') }}</span> {{ __('/api/automation/users') }}</div>
<div><span class="font-mono">POST</span> /api/automation/domains</div> <div><span class="font-mono">{{ __('POST') }}</span> {{ __('/api/automation/domains') }}</div>
</div> </div>
</x-filament::section> </x-filament::section>

View File

@@ -48,7 +48,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<x-filament::badge color="success">{{ $add['type'] }}</x-filament::badge> <x-filament::badge color="success">{{ $add['type'] }}</x-filament::badge>
<span class="font-mono fi-section-header-heading">{{ $add['name'] }}</span> <span class="font-mono fi-section-header-heading">{{ $add['name'] }}</span>
<span class="fi-section-header-description">&rarr;</span> <span class="fi-section-header-description">{{ __('→') }}</span>
<span class="font-mono fi-section-header-description truncate max-w-xs" title="{{ $add['content'] }}">{{ Str::limit($add['content'], 40) }}</span> <span class="font-mono fi-section-header-description truncate max-w-xs" title="{{ $add['content'] }}">{{ Str::limit($add['content'], 40) }}</span>
</div> </div>
<x-filament::icon-button <x-filament::icon-button

View File

@@ -37,13 +37,13 @@
<div class="flex w-full items-center gap-3 sm:w-auto sm:justify-end"> <div class="flex w-full items-center gap-3 sm:w-auto sm:justify-end">
{{-- CPU --}} {{-- CPU --}}
<div class="text-center"> <div class="text-center">
<div class="fi-section-header-description">CPU</div> <div class="fi-section-header-description">{{ __('CPU') }}</div>
<x-filament::badge :color="$cpuColor">{{ $proc['cpu'] }}%</x-filament::badge> <x-filament::badge :color="$cpuColor">{{ $proc['cpu'] }}%</x-filament::badge>
</div> </div>
{{-- Memory --}} {{-- Memory --}}
<div class="text-center"> <div class="text-center">
<div class="fi-section-header-description">MEM</div> <div class="fi-section-header-description">{{ __('MEM') }}</div>
<x-filament::badge :color="$memColor">{{ $proc['memory'] }}%</x-filament::badge> <x-filament::badge :color="$memColor">{{ $proc['memory'] }}%</x-filament::badge>
</div> </div>
</div> </div>

View File

@@ -9,9 +9,9 @@
</x-slot> </x-slot>
<x-slot name="description"> <x-slot name="description">
{{ __('Deleting or modifying system files can break your website. Avoid editing files in the') }} {{ __('Deleting or modifying system files can break your website. Avoid editing files in the') }}
<code class="px-1.5 py-0.5 rounded bg-warning-100 dark:bg-warning-900/50 fi-color-warning fi-text-color-700 dark:fi-text-color-300 font-mono fi-section-header-description">conf</code> <code class="px-1.5 py-0.5 rounded bg-warning-100 dark:bg-warning-900/50 fi-color-warning fi-text-color-700 dark:fi-text-color-300 font-mono fi-section-header-description">{{ __('conf') }}</code>
{{ __('and') }} {{ __('and') }}
<code class="px-1.5 py-0.5 rounded bg-warning-100 dark:bg-warning-900/50 fi-color-warning fi-text-color-700 dark:fi-text-color-300 font-mono fi-section-header-description">logs</code> <code class="px-1.5 py-0.5 rounded bg-warning-100 dark:bg-warning-900/50 fi-color-warning fi-text-color-700 dark:fi-text-color-300 font-mono fi-section-header-description">{{ __('logs') }}</code>
{{ __('folders unless you know what you are doing.') }} {{ __('folders unless you know what you are doing.') }}
</x-slot> </x-slot>
</x-filament::section> </x-filament::section>

View File

@@ -28,21 +28,21 @@
href="{{ url('/dashboard') }}" href="{{ url('/dashboard') }}"
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal" class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal"
> >
Dashboard {{ __('Dashboard') }}
</a> </a>
@else @else
<a <a
href="{{ route('login') }}" href="{{ route('login') }}"
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] text-[#1b1b18] border border-transparent hover:border-[#19140035] dark:hover:border-[#3E3E3A] rounded-sm text-sm leading-normal" class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] text-[#1b1b18] border border-transparent hover:border-[#19140035] dark:hover:border-[#3E3E3A] rounded-sm text-sm leading-normal"
> >
Log in {{ __('Log in') }}
</a> </a>
@if (Route::has('register')) @if (Route::has('register'))
<a <a
href="{{ route('register') }}" href="{{ route('register') }}"
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal"> class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal">
Register {{ __('Register') }}
</a> </a>
@endif @endif
@endauth @endauth
@@ -52,8 +52,8 @@
<div class="flex items-center justify-center w-full transition-opacity opacity-100 duration-750 lg:grow starting:opacity-0"> <div class="flex items-center justify-center w-full transition-opacity opacity-100 duration-750 lg:grow starting:opacity-0">
<main class="flex max-w-[335px] w-full flex-col-reverse lg:max-w-4xl lg:flex-row"> <main class="flex max-w-[335px] w-full flex-col-reverse lg:max-w-4xl lg:flex-row">
<div class="text-[13px] leading-[20px] flex-1 p-6 pb-12 lg:p-20 bg-white dark:bg-[#161615] dark:text-[#EDEDEC] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d] rounded-bl-lg rounded-br-lg lg:rounded-tl-lg lg:rounded-br-none"> <div class="text-[13px] leading-[20px] flex-1 p-6 pb-12 lg:p-20 bg-white dark:bg-[#161615] dark:text-[#EDEDEC] shadow-[inset_0px_0px_0px_1px_rgba(26,26,0,0.16)] dark:shadow-[inset_0px_0px_0px_1px_#fffaed2d] rounded-bl-lg rounded-br-lg lg:rounded-tl-lg lg:rounded-br-none">
<h1 class="mb-1 font-medium">Let's get started</h1> <h1 class="mb-1 font-medium">{{ __('Let's get started') }}</h1>
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">Laravel has an incredibly rich ecosystem. <br>We suggest starting with the following.</p> <p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">{{ __('Laravel has an incredibly rich ecosystem.') }} <br>{{ __('We suggest starting with the following.') }}</p>
<ul class="flex flex-col mb-4 lg:mb-6"> <ul class="flex flex-col mb-4 lg:mb-6">
<li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:top-1/2 before:bottom-0 before:left-[0.4rem] before:absolute"> <li class="flex items-center gap-4 py-2 relative before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A] before:top-1/2 before:bottom-0 before:left-[0.4rem] before:absolute">
<span class="relative py-1 bg-white dark:bg-[#161615]"> <span class="relative py-1 bg-white dark:bg-[#161615]">
@@ -62,9 +62,9 @@
</span> </span>
</span> </span>
<span> <span>
Read the {{ __('Read the') }}
<a href="https://laravel.com/docs" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1"> <a href="https://laravel.com/docs" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
<span>Documentation</span> <span>{{ __('Documentation') }}</span>
<svg <svg
width="10" width="10"
height="11" height="11"
@@ -89,9 +89,9 @@
</span> </span>
</span> </span>
<span> <span>
Watch video tutorials at {{ __('Watch video tutorials at') }}
<a href="https://laracasts.com" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1"> <a href="https://laracasts.com" target="_blank" class="inline-flex items-center space-x-1 font-medium underline underline-offset-4 text-[#f53003] dark:text-[#FF4433] ml-1">
<span>Laracasts</span> <span>{{ __('Laracasts') }}</span>
<svg <svg
width="10" width="10"
height="11" height="11"
@@ -113,7 +113,7 @@
<ul class="flex gap-3 text-sm leading-normal"> <ul class="flex gap-3 text-sm leading-normal">
<li> <li>
<a href="https://cloud.laravel.com" target="_blank" class="inline-block dark:bg-[#eeeeec] dark:border-[#eeeeec] dark:text-[#1C1C1A] dark:hover:bg-white dark:hover:border-white hover:bg-black hover:border-black px-5 py-1.5 bg-[#1b1b18] rounded-sm border border-black text-white text-sm leading-normal"> <a href="https://cloud.laravel.com" target="_blank" class="inline-block dark:bg-[#eeeeec] dark:border-[#eeeeec] dark:text-[#1C1C1A] dark:hover:bg-white dark:hover:border-white hover:bg-black hover:border-black px-5 py-1.5 bg-[#1b1b18] rounded-sm border border-black text-white text-sm leading-normal">
Deploy now {{ __('Deploy now') }}
</a> </a>
</li> </li>
</ul> </ul>