Compare commits
12 Commits
e95e03c4fc
...
v0.9-rc54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c4be426d | ||
|
|
adc073b751 | ||
|
|
e439204891 | ||
|
|
fd3be5b1cd | ||
|
|
758412168f | ||
|
|
66a1eaba0a | ||
|
|
b7c0419e05 | ||
|
|
6c84704476 | ||
|
|
8df76cdaae | ||
|
|
d8424ad483 | ||
|
|
a0048109ce | ||
|
|
be34afe2c8 |
13
.dockerignore
Normal 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
|
||||
12
AGENT.md
@@ -326,6 +326,18 @@ dns.get_ds_records - Get DS records for registrar
|
||||
| Admin | `https://jabali.lan/jabali-admin` | `admin@jabali.lan` | `q1w2E#R$` |
|
||||
| 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
|
||||
|
||||
| Model | Table | Description |
|
||||
|
||||
12
CONTEXT.md
@@ -1,6 +1,6 @@
|
||||
# CONTEXT.md
|
||||
|
||||
Last updated: 2026-02-01
|
||||
Last updated: 2026-02-03
|
||||
|
||||
## Stack
|
||||
- Laravel 12, Filament v5, Livewire v4
|
||||
@@ -11,6 +11,16 @@ Last updated: 2026-02-01
|
||||
- Admin panel: `/jabali-admin`
|
||||
- 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
|
||||
- Panel config DB: SQLite at `database/database.sqlite`
|
||||
- Hosting services use MariaDB/Postfix/Dovecot/etc. as configured by the agent
|
||||
|
||||
@@ -6,3 +6,8 @@
|
||||
- Asset builds must be writable for `public/build` and `node_modules`; upgrade checks both.
|
||||
- Installer builds assets as `www-data` to avoid permission issues.
|
||||
- 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: '*')`).
|
||||
|
||||
72
README.md
@@ -3,22 +3,29 @@
|
||||
</p>
|
||||
<h1 align="center">Jabali Panel</h1>
|
||||
|
||||
A modern web hosting control panel for WordPress and general PHP hosting. Jabali focuses on clean multi-tenant isolation, safe automation, and a consistent admin/user experience. It ships with an agent for privileged tasks, built-in mail and DNS management, migrations from common panels, and a security center that keeps critical services in check. The UI is designed to be fast, predictable, and easy to operate on a single server. Administrators get clear visibility into services, SSL, DNS, backups, and security posture, while users get a streamlined workflow for domains, email, WordPress, files, databases, and PHP settings. The goal is simple: reduce the operational burden of hosting by making common tasks safe, repeatable, and easy to audit.
|
||||
A modern web hosting control panel for WordPress and general PHP hosting. Jabali focuses on clean multi-tenant isolation, safe automation, and a consistent admin/user experience. It ships with a privileged agent for root-level tasks, built-in mail and DNS management, migrations from common panels, and a security center that keeps critical services in check. The UI is designed to be fast, predictable, and easy to operate on a single server.
|
||||
|
||||
Version: 0.9-rc48 (release candidate)
|
||||
Version: 0.9-rc51 (release candidate)
|
||||
|
||||
This is a release candidate. Expect rapid iteration and breaking changes until 1.0.
|
||||
|
||||
## Demo and Website
|
||||
|
||||
- Demo: https://jabali-panel.com/demo/
|
||||
- Website: https://jabali-panel.com/
|
||||
|
||||
## Highlights
|
||||
|
||||
- Per-user Linux accounts and PHP-FPM isolation
|
||||
- Root agent for DNS, SSL, mail, backups, and migrations
|
||||
- Health monitor with auto-restarts and alerts
|
||||
- cPanel and WHM migrations with step-by-step logs
|
||||
- Built-in mail stack with webmail SSO
|
||||
- DNS templates with optional DNSSEC
|
||||
- User and server backups with schedules and retention
|
||||
- WordPress management (install, updates, scans, and SSO)
|
||||
- Security center with firewall, Fail2ban, and ClamAV
|
||||
- Security center with firewall, Fail2ban, ClamAV, and scanners
|
||||
- Audit logs and admin notifications
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -33,12 +40,42 @@ Optional flags:
|
||||
- `JABALI_MINIMAL=1` for core-only install
|
||||
- `JABALI_FULL=1` to force all optional components
|
||||
|
||||
Debian packages:
|
||||
|
||||
```
|
||||
./scripts/build-jabali-deps-deb.sh
|
||||
./scripts/build-jabali-panel-deb.sh
|
||||
sudo dpkg -i ./jabali-deps_<version>_all.deb
|
||||
sudo apt-get -f install -y
|
||||
sudo dpkg -i ./jabali-panel_<version>_all.deb
|
||||
```
|
||||
|
||||
After install:
|
||||
|
||||
- Admin panel: `https://your-host/jabali-admin`
|
||||
- User panel: `https://your-host/jabali-panel`
|
||||
- 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
|
||||
|
||||
### Admin Panel
|
||||
@@ -79,33 +116,13 @@ After install:
|
||||
- Redis ACL isolation for WordPress caching
|
||||
- Multi-language UI
|
||||
|
||||
## Screenshots
|
||||
|
||||
Admin panel:
|
||||
|
||||
- Dashboard: 
|
||||
- Server Status: 
|
||||
- Server Settings: 
|
||||
- Security Center: 
|
||||
- Users: 
|
||||
- SSL Manager: 
|
||||
- DNS Zones: 
|
||||
- Backups: 
|
||||
- Services: 
|
||||
|
||||
User panel:
|
||||
|
||||
- Dashboard: 
|
||||
- Domain Management: 
|
||||
- Backups: 
|
||||
- cPanel Migration: 
|
||||
|
||||
## Architecture
|
||||
|
||||
- Control plane: Laravel app with Filament panels
|
||||
- Control plane: Laravel 12 app with Filament v5 and Livewire v4
|
||||
- Data plane: root agent handling privileged operations
|
||||
- Job queue: async tasks and migration steps
|
||||
- Logging: panel and agent logs for troubleshooting
|
||||
- Server metrics: sysstat logs via SysstatMetrics
|
||||
|
||||
Service stack (single-node default):
|
||||
|
||||
@@ -158,3 +175,8 @@ php artisan test --compact
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Documentation Notes
|
||||
|
||||
- Documentation screenshots are generated for all admin and user pages.
|
||||
- cPanel Migration tabs (Domains, Databases, Mailboxes, Forwarders, SSL) only render after a backup is analyzed. Provide a sample cPanel backup to capture those tab screenshots.
|
||||
|
||||
2
TODO.md
@@ -6,3 +6,5 @@ Keep this list current as work progresses.
|
||||
- [ ] Confirm WAF whitelist + blocked requests tables refresh correctly after changes.
|
||||
- [ ] Validate sysstat collection interval (10s) and chart intervals align.
|
||||
- [ ] 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.
|
||||
|
||||
235
app/BackupSchedule.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BackupSchedule extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'destination_id',
|
||||
'name',
|
||||
'is_active',
|
||||
'is_server_backup',
|
||||
'frequency',
|
||||
'time',
|
||||
'day_of_week',
|
||||
'day_of_month',
|
||||
'include_files',
|
||||
'include_databases',
|
||||
'include_mailboxes',
|
||||
'include_dns',
|
||||
'domains',
|
||||
'databases',
|
||||
'mailboxes',
|
||||
'users',
|
||||
'retention_count',
|
||||
'last_run_at',
|
||||
'next_run_at',
|
||||
'last_status',
|
||||
'last_error',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'is_server_backup' => 'boolean',
|
||||
'include_files' => 'boolean',
|
||||
'include_databases' => 'boolean',
|
||||
'include_mailboxes' => 'boolean',
|
||||
'include_dns' => 'boolean',
|
||||
'domains' => 'array',
|
||||
'databases' => 'array',
|
||||
'mailboxes' => 'array',
|
||||
'users' => 'array',
|
||||
'metadata' => 'array',
|
||||
'retention_count' => 'integer',
|
||||
'day_of_week' => 'integer',
|
||||
'day_of_month' => 'integer',
|
||||
'last_run_at' => 'datetime',
|
||||
'next_run_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function destination(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BackupDestination::class, 'destination_id');
|
||||
}
|
||||
|
||||
public function backups(): HasMany
|
||||
{
|
||||
return $this->hasMany(Backup::class, 'schedule_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the schedule should run now.
|
||||
*/
|
||||
public function shouldRun(): bool
|
||||
{
|
||||
if (! $this->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->next_run_at) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->next_run_at->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set the next run time.
|
||||
*/
|
||||
public function calculateNextRun(): Carbon
|
||||
{
|
||||
$timezone = $this->getSystemTimezone();
|
||||
$now = Carbon::now($timezone);
|
||||
$time = explode(':', $this->time);
|
||||
$hour = (int) ($time[0] ?? 2);
|
||||
$minute = (int) ($time[1] ?? 0);
|
||||
|
||||
$next = $now->copy()->setTime($hour, $minute, 0);
|
||||
|
||||
// If time already passed today, start from tomorrow
|
||||
if ($next->isPast()) {
|
||||
$next->addDay();
|
||||
}
|
||||
|
||||
switch ($this->frequency) {
|
||||
case 'hourly':
|
||||
$next = $now->copy()->addHour()->startOfHour();
|
||||
break;
|
||||
|
||||
case 'daily':
|
||||
// Already set to next occurrence
|
||||
break;
|
||||
|
||||
case 'weekly':
|
||||
$targetDay = $this->day_of_week ?? 0; // Default to Sunday
|
||||
while ($next->dayOfWeek !== $targetDay) {
|
||||
$next->addDay();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'monthly':
|
||||
$targetDay = $this->day_of_month ?? 1;
|
||||
$next->day = min($targetDay, $next->daysInMonth);
|
||||
if ($next->isPast()) {
|
||||
$next->addMonth();
|
||||
$next->day = min($targetDay, $next->daysInMonth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$nextUtc = $next->copy()->setTimezone('UTC');
|
||||
$this->attributes['next_run_at'] = $nextUtc->format($this->getDateFormat());
|
||||
|
||||
return $nextUtc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get frequency label for UI.
|
||||
*/
|
||||
public function getFrequencyLabelAttribute(): string
|
||||
{
|
||||
$base = match ($this->frequency) {
|
||||
'hourly' => 'Every hour',
|
||||
'daily' => 'Daily at '.$this->time,
|
||||
'weekly' => 'Weekly on '.$this->getDayName().' at '.$this->time,
|
||||
'monthly' => 'Monthly on day '.($this->day_of_month ?? 1).' at '.$this->time,
|
||||
default => ucfirst($this->frequency),
|
||||
};
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day name for weekly schedules.
|
||||
*/
|
||||
protected function getDayName(): string
|
||||
{
|
||||
$days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
return $days[$this->day_of_week ?? 0];
|
||||
}
|
||||
|
||||
protected function getSystemTimezone(): string
|
||||
{
|
||||
static $timezone = null;
|
||||
if ($timezone === null) {
|
||||
$timezone = trim((string) @file_get_contents('/etc/timezone'));
|
||||
if ($timezone === '') {
|
||||
$timezone = trim((string) @shell_exec('timedatectl show -p Timezone --value 2>/dev/null'));
|
||||
}
|
||||
if ($timezone === '') {
|
||||
$timezone = 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for active schedules.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for due schedules.
|
||||
*/
|
||||
public function scopeDue($query)
|
||||
{
|
||||
return $query->active()
|
||||
->where(function ($q) {
|
||||
$q->whereNull('next_run_at')
|
||||
->orWhere('next_run_at', '<=', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for user schedules.
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for server backup schedules.
|
||||
*/
|
||||
public function scopeServerBackups($query)
|
||||
{
|
||||
return $query->where('is_server_backup', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last status color for UI.
|
||||
*/
|
||||
public function getLastStatusColorAttribute(): string
|
||||
{
|
||||
return match ($this->last_status) {
|
||||
'success' => 'success',
|
||||
'failed' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
1616
app/Backups.php
Normal file
@@ -8,10 +8,23 @@ use App\Models\User;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||
use Filament\Auth\Pages\Login as BaseLogin;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
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
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
@@ -324,7 +324,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
->color('gray'),
|
||||
TextColumn::make('duration')
|
||||
->label(__('Duration'))
|
||||
->placeholder('-')
|
||||
->placeholder(__('-'))
|
||||
->color('gray'),
|
||||
])
|
||||
->recordActions([
|
||||
|
||||
@@ -574,7 +574,7 @@ class CpanelMigration extends Page implements HasActions, HasForms, HasInfolists
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
TextInput::make('hostname')
|
||||
->label(__('cPanel Hostname'))
|
||||
->placeholder('cpanel.example.com')
|
||||
->placeholder(__('cpanel.example.com'))
|
||||
->required(fn () => $this->sourceType === 'remote')
|
||||
->helperText(__('Your cPanel server hostname or IP address')),
|
||||
TextInput::make('port')
|
||||
@@ -610,7 +610,7 @@ class CpanelMigration extends Page implements HasActions, HasForms, HasInfolists
|
||||
->schema([
|
||||
TextInput::make('localBackupPath')
|
||||
->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')
|
||||
->helperText(__('Full path to the cPanel backup file (e.g., /var/backups/backup.tar.gz)')),
|
||||
Text::make(__('Supported formats: .tar.gz, .tgz'))->color('gray'),
|
||||
|
||||
@@ -90,7 +90,7 @@ class Dashboard extends Page implements HasActions, HasForms
|
||||
->label(__('Your Email Address'))
|
||||
->helperText(__('Enter your email to receive important server notifications.'))
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
->placeholder(__('admin@example.com')),
|
||||
])
|
||||
->modalSubmitActionLabel(__('Get Started'))
|
||||
->action(function (array $data): void {
|
||||
|
||||
@@ -254,12 +254,12 @@ class DnsZones extends Page implements HasActions, HasForms, HasTable
|
||||
->sortable(),
|
||||
TextColumn::make('priority')
|
||||
->label(__('Priority'))
|
||||
->placeholder('-')
|
||||
->placeholder(__('-'))
|
||||
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
|
||||
->sortable(),
|
||||
TextColumn::make('domain.user.username')
|
||||
->label(__('Owner'))
|
||||
->placeholder('N/A')
|
||||
->placeholder(__('N/A'))
|
||||
->sortable(),
|
||||
])
|
||||
->filters([])
|
||||
|
||||
@@ -99,7 +99,7 @@ class IpAddresses extends Page implements HasActions, HasTable
|
||||
->form([
|
||||
TextInput::make('ip')
|
||||
->label(__('IP Address'))
|
||||
->placeholder('203.0.113.10')
|
||||
->placeholder(__('203.0.113.10'))
|
||||
->live()
|
||||
->afterStateUpdated(function (?string $state, callable $set): void {
|
||||
if (! $state) {
|
||||
@@ -198,7 +198,7 @@ class IpAddresses extends Page implements HasActions, HasTable
|
||||
->getStateUsing(fn (array $record): ?string => $this->getDefaultLabel($record))
|
||||
->badge()
|
||||
->color('success')
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
])
|
||||
->recordActions([
|
||||
Action::make('setDefault')
|
||||
|
||||
@@ -65,7 +65,30 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable
|
||||
|
||||
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) {
|
||||
$this->installedVersions = $result['versions'] ?? [];
|
||||
|
||||
@@ -279,7 +279,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
Grid::make(['default' => 1, 'md' => 2])->schema([
|
||||
TextInput::make('brandingData.panel_name')
|
||||
->label(__('Control Panel Name'))
|
||||
->placeholder('Jabali')
|
||||
->placeholder(__('Jabali'))
|
||||
->helperText(__('Appears in browser title and navigation'))
|
||||
->required(),
|
||||
]),
|
||||
@@ -320,7 +320,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->schema([
|
||||
TextInput::make('hostnameData.hostname')
|
||||
->label(__('Hostname'))
|
||||
->placeholder('server.example.com')
|
||||
->placeholder(__('server.example.com'))
|
||||
->required(),
|
||||
Actions::make([
|
||||
FormAction::make('saveHostname')
|
||||
@@ -338,10 +338,10 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->icon('heroicon-o-server-stack')
|
||||
->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_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_ip')->label(__('NS2 IP Address'))->placeholder('192.168.1.2'),
|
||||
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.ns2')->label(__('NS2 Hostname'))->placeholder(__('ns2.example.com')),
|
||||
TextInput::make('dnsData.ns2_ip')->label(__('NS2 IP Address'))->placeholder(__('192.168.1.2')),
|
||||
]),
|
||||
]),
|
||||
Section::make(__('Zone Defaults'))
|
||||
@@ -349,20 +349,20 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
Grid::make(['default' => 1, 'md' => 3])->schema([
|
||||
TextInput::make('dnsData.default_ip')
|
||||
->label(__('Default Server IP'))
|
||||
->placeholder('192.168.1.1')
|
||||
->placeholder(__('192.168.1.1'))
|
||||
->helperText(__('Default A record IP for new zones')),
|
||||
TextInput::make('dnsData.default_ipv6')
|
||||
->label(__('Default IPv6'))
|
||||
->placeholder('2001:db8::1')
|
||||
->placeholder(__('2001:db8::1'))
|
||||
->helperText(__('Default AAAA record IP for new zones'))
|
||||
->rule('nullable|ipv6'),
|
||||
TextInput::make('dnsData.default_ttl')
|
||||
->label(__('Default TTL'))
|
||||
->placeholder('3600'),
|
||||
->placeholder(__('3600')),
|
||||
]),
|
||||
TextInput::make('dnsData.admin_email')
|
||||
->label(__('Admin Email (SOA)'))
|
||||
->placeholder('admin.example.com')
|
||||
->placeholder(__('admin.example.com'))
|
||||
->helperText(__('Use dots instead of @ (e.g., admin.example.com)')),
|
||||
Actions::make([
|
||||
FormAction::make('saveDns')
|
||||
@@ -386,10 +386,10 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->action('applyQuad9Resolvers'),
|
||||
])->alignment('left'),
|
||||
Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([
|
||||
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.resolver3')->label(__('Resolver 3'))->placeholder('1.1.1.1'),
|
||||
TextInput::make('resolversData.search_domain')->label(__('Search Domain'))->placeholder('example.com'),
|
||||
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.resolver3')->label(__('Resolver 3'))->placeholder(__('1.1.1.1')),
|
||||
TextInput::make('resolversData.search_domain')->label(__('Search Domain'))->placeholder(__('example.com')),
|
||||
]),
|
||||
Actions::make([
|
||||
FormAction::make('saveResolvers')
|
||||
@@ -470,7 +470,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
TextInput::make('quotaData.default_quota_mb')
|
||||
->label(__('Default Quota (MB)'))
|
||||
->numeric()
|
||||
->placeholder('5120')
|
||||
->placeholder(__('5120'))
|
||||
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
||||
]),
|
||||
Actions::make([
|
||||
@@ -487,7 +487,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(500)
|
||||
->placeholder('100')
|
||||
->placeholder(__('100'))
|
||||
->helperText(__('Maximum file size users can upload (1-500 MB)')),
|
||||
Actions::make([
|
||||
FormAction::make('saveFileManagerSettings')
|
||||
@@ -507,7 +507,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
Grid::make(['default' => 1, 'md' => 2])->schema([
|
||||
TextInput::make('emailData.mail_hostname')
|
||||
->label(__('Mail Server Hostname'))
|
||||
->placeholder('mail.example.com')
|
||||
->placeholder(__('mail.example.com'))
|
||||
->helperText(__('The hostname used for mail server identification')),
|
||||
TextInput::make('emailData.mail_default_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([
|
||||
TextInput::make('emailData.webmail_url')
|
||||
->label(__('Webmail URL'))
|
||||
->placeholder('/webmail')
|
||||
->placeholder(__('/webmail'))
|
||||
->helperText(__('URL path for Roundcube webmail')),
|
||||
TextInput::make('emailData.webmail_product_name')
|
||||
->label(__('Webmail Product Name'))
|
||||
->placeholder('Jabali Webmail')
|
||||
->placeholder(__('Jabali Webmail'))
|
||||
->helperText(__('Name displayed on the webmail login page')),
|
||||
]),
|
||||
Actions::make([
|
||||
@@ -556,7 +556,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->schema([
|
||||
TextInput::make('notificationsData.admin_email_recipients')
|
||||
->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')),
|
||||
]),
|
||||
Section::make(__('Notification Types & High Load Alerts'))
|
||||
@@ -598,14 +598,14 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->minValue(1)
|
||||
->maxValue(100)
|
||||
->step(0.5)
|
||||
->placeholder('5')
|
||||
->placeholder(__('5'))
|
||||
->helperText(__('Alert when load exceeds this value')),
|
||||
TextInput::make('notificationsData.load_alert_minutes')
|
||||
->label(__('Alert After (minutes)'))
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(60)
|
||||
->placeholder('5')
|
||||
->placeholder(__('5'))
|
||||
->helperText(__('Minutes of high load before alerting')),
|
||||
]),
|
||||
Actions::make([
|
||||
@@ -649,7 +649,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
||||
->helperText(__('Requests before worker recycle')),
|
||||
TextInput::make('phpFpmData.memory_limit')
|
||||
->label(__('Memory Limit'))
|
||||
->placeholder('512M')
|
||||
->placeholder(__('512M'))
|
||||
->helperText(__('PHP memory_limit (e.g., 512M, 1G)')),
|
||||
]),
|
||||
Grid::make(['default' => 1, 'md' => 2, 'lg' => 3])->schema([
|
||||
|
||||
@@ -123,7 +123,7 @@ class SslManager extends Page implements HasTable
|
||||
->limit(30)
|
||||
->tooltip(fn ($state) => $state)
|
||||
->color('danger')
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('ssl_status')
|
||||
|
||||
@@ -382,7 +382,7 @@ class WhmMigration extends Page implements HasActions, HasForms, HasInfolists, H
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
TextInput::make('hostname')
|
||||
->label(__('WHM Hostname'))
|
||||
->placeholder('whm.example.com')
|
||||
->placeholder(__('whm.example.com'))
|
||||
->required()
|
||||
->helperText(__('Your WHM server hostname or IP address')),
|
||||
TextInput::make('port')
|
||||
|
||||
@@ -61,7 +61,7 @@ class DnsPendingAddsTable extends Component implements HasActions, HasSchemas, H
|
||||
->label(__('TTL')),
|
||||
TextColumn::make('priority')
|
||||
->label(__('Priority'))
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
])
|
||||
->actions([
|
||||
Action::make('removePending')
|
||||
|
||||
@@ -53,7 +53,7 @@ class Fail2banLogsTable extends Component implements HasActions, HasSchemas, Has
|
||||
->label(__('IP'))
|
||||
->fontFamily('mono')
|
||||
->copyable()
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
TextColumn::make('message')
|
||||
->label(__('Message'))
|
||||
->wrap(),
|
||||
|
||||
@@ -12,9 +12,22 @@ use Filament\Facades\Filament;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
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
|
||||
{
|
||||
$panel = Filament::getPanel('jabali');
|
||||
|
||||
@@ -373,7 +373,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
->sortable(),
|
||||
TextColumn::make('duration')
|
||||
->label(__('Duration'))
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->emptyStateHeading(__('No restore history'))
|
||||
@@ -1092,7 +1092,7 @@ class Backups extends Page implements HasActions, HasForms, HasTable
|
||||
Grid::make(2)->schema([
|
||||
TextInput::make('host')
|
||||
->label(__('Host'))
|
||||
->placeholder('backup.example.com')
|
||||
->placeholder(__('backup.example.com'))
|
||||
->required(),
|
||||
TextInput::make('port')
|
||||
->label(__('Port'))
|
||||
|
||||
@@ -571,7 +571,7 @@ class CpanelMigration extends Page implements HasActions, HasForms
|
||||
Grid::make(['default' => 1, 'sm' => 2])->schema([
|
||||
TextInput::make('hostname')
|
||||
->label(__('cPanel Hostname'))
|
||||
->placeholder('cpanel.example.com')
|
||||
->placeholder(__('cpanel.example.com'))
|
||||
->required()
|
||||
->helperText(__('Your cPanel server hostname or IP address')),
|
||||
TextInput::make('port')
|
||||
|
||||
@@ -288,7 +288,7 @@ class DnsRecords extends Page implements HasActions, HasForms, HasTable
|
||||
->sortable(),
|
||||
TextColumn::make('priority')
|
||||
->label(__('Priority'))
|
||||
->placeholder('-')
|
||||
->placeholder(__('-'))
|
||||
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
|
||||
->sortable(),
|
||||
])
|
||||
|
||||
@@ -248,7 +248,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
->schema([
|
||||
TextInput::make('domain_redirect_url')
|
||||
->label(__('Redirect To'))
|
||||
->placeholder('https://newdomain.com')
|
||||
->placeholder(__('https://newdomain.com'))
|
||||
->helperText(__('All requests to this domain will be redirected to this URL'))
|
||||
->url()
|
||||
->required(fn ($get) => $get('domain_redirect_enabled'))
|
||||
@@ -275,13 +275,13 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
->schema([
|
||||
TextInput::make('source_path')
|
||||
->label(__('Source Path'))
|
||||
->placeholder('/old-page')
|
||||
->placeholder(__('/old-page'))
|
||||
->helperText(__('Path to redirect from (e.g., /old-page)'))
|
||||
->required()
|
||||
->columnSpan(['default' => 2, 'md' => 1]),
|
||||
TextInput::make('destination_url')
|
||||
->label(__('Destination URL'))
|
||||
->placeholder('https://example.com/new-page')
|
||||
->placeholder(__('https://example.com/new-page'))
|
||||
->helperText(__('Full URL to redirect to'))
|
||||
->required()
|
||||
->url()
|
||||
@@ -359,13 +359,13 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
Textarea::make('allowed_domains')
|
||||
->label(__('Allowed Domains'))
|
||||
->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)
|
||||
->columnSpan(['default' => 2, 'md' => 1]),
|
||||
TextInput::make('protected_extensions')
|
||||
->label(__('Protected File Extensions'))
|
||||
->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())
|
||||
->columnSpan(['default' => 2, 'md' => 1]),
|
||||
])
|
||||
@@ -381,7 +381,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
TextInput::make('redirect_url')
|
||||
->label(__('Redirect URL (Optional)'))
|
||||
->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()
|
||||
->columnSpan(['default' => 2, 'md' => 1]),
|
||||
])
|
||||
@@ -842,7 +842,7 @@ class Domains extends Page implements HasActions, HasForms, HasTable
|
||||
->schema([
|
||||
TextInput::make('alias')
|
||||
->label(__('Alias Domain'))
|
||||
->placeholder('alias-example.com')
|
||||
->placeholder(__('alias-example.com'))
|
||||
->required()
|
||||
->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.')),
|
||||
|
||||
@@ -185,11 +185,11 @@ class Email extends Page implements HasActions, HasForms, HasTable
|
||||
Textarea::make('whitelist')
|
||||
->label(__('Whitelist (one per line)'))
|
||||
->rows(6)
|
||||
->placeholder("friend@example.com\ntrusted.com"),
|
||||
->placeholder(__("friend@example.com\ntrusted.com")),
|
||||
Textarea::make('blacklist')
|
||||
->label(__('Blacklist (one per line)'))
|
||||
->rows(6)
|
||||
->placeholder("spam@example.com\nbad-domain.com"),
|
||||
->placeholder(__("spam@example.com\nbad-domain.com")),
|
||||
TextInput::make('score')
|
||||
->label(__('Spam Score Threshold'))
|
||||
->numeric()
|
||||
|
||||
@@ -83,7 +83,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
// Invalid path from URL - reset to home directory
|
||||
$this->currentPath = '';
|
||||
Notification::make()
|
||||
->title('Invalid path')
|
||||
->title(__('Invalid path'))
|
||||
->body('The requested path is not allowed.')
|
||||
->danger()
|
||||
->send();
|
||||
@@ -222,7 +222,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
} catch (Exception $e) {
|
||||
$this->items = [];
|
||||
Notification::make()
|
||||
->title('Error loading directory')
|
||||
->title(__('Error loading directory'))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -237,7 +237,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
$this->resetTable();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title('Invalid path')
|
||||
->title(__('Invalid path'))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -425,7 +425,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
->form([
|
||||
TextInput::make('mode')
|
||||
->label(__('Numeric Mode'))
|
||||
->placeholder('755')
|
||||
->placeholder(__('755'))
|
||||
->maxLength(4)
|
||||
->helperText(__('Enter octal mode (e.g., 755, 644)')),
|
||||
Grid::make(3)
|
||||
@@ -750,7 +750,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
$this->getAgent()->fileMove($this->getUsername(), $sourcePath, $destPath);
|
||||
|
||||
Notification::make()
|
||||
->title('Item moved successfully')
|
||||
->title(__('Item moved successfully'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -758,7 +758,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
$this->resetTable();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title('Error moving item')
|
||||
->title(__('Error moving item'))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -788,7 +788,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
);
|
||||
|
||||
Notification::make()
|
||||
->title("Uploaded: $filename")
|
||||
->title(__('Uploaded: :filename', ['filename' => $filename]))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -796,7 +796,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
$this->resetTable();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title("Upload failed: $filename")
|
||||
->title(__('Upload failed: :filename', ['filename' => $filename]))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -939,7 +939,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
$uploaded++;
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(__('Upload failed: ').$file->getClientOriginalName())
|
||||
->title(__('Upload failed: :filename', ['filename' => $file->getClientOriginalName()]))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -1007,7 +1007,7 @@ class Files extends Page implements HasActions, HasForms, HasTable
|
||||
filename: basename($path)
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()->title('Error downloading')->body($e->getMessage())->danger()->send();
|
||||
Notification::make()->title(__('Error downloading'))->body($e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ class GitDeployment extends Page implements HasActions, HasForms, HasTable
|
||||
}),
|
||||
TextInput::make('repo_url')
|
||||
->label(__('Repository URL'))
|
||||
->placeholder('git@github.com:org/repo.git')
|
||||
->placeholder(__('git@github.com:org/repo.git'))
|
||||
->required(),
|
||||
TextInput::make('branch')
|
||||
->label(__('Branch'))
|
||||
|
||||
@@ -82,7 +82,7 @@ class MailingLists extends Page implements HasActions, HasForms
|
||||
->schema([
|
||||
TextInput::make('listmonk_url')
|
||||
->label(__('Listmonk URL'))
|
||||
->placeholder('https://lists.example.com')
|
||||
->placeholder(__('https://lists.example.com'))
|
||||
->url()
|
||||
->visible(fn ($get) => $get('provider') === 'listmonk'),
|
||||
TextInput::make('listmonk_token')
|
||||
@@ -99,7 +99,7 @@ class MailingLists extends Page implements HasActions, HasForms
|
||||
->schema([
|
||||
TextInput::make('mailman_url')
|
||||
->label(__('Mailman URL'))
|
||||
->placeholder('https://lists.example.com/mailman')
|
||||
->placeholder(__('https://lists.example.com/mailman'))
|
||||
->url()
|
||||
->visible(fn ($get) => $get('provider') === 'mailman'),
|
||||
TextInput::make('mailman_admin')
|
||||
|
||||
@@ -83,6 +83,16 @@ class PhpSettings extends Page implements HasActions, HasForms
|
||||
|
||||
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', [
|
||||
'username' => $this->getUsername(),
|
||||
]);
|
||||
@@ -92,6 +102,17 @@ class PhpSettings extends Page implements HasActions, HasForms
|
||||
|
||||
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', []);
|
||||
|
||||
$this->phpVersions = [];
|
||||
@@ -120,6 +141,20 @@ class PhpSettings extends Page implements HasActions, HasForms
|
||||
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', [
|
||||
'domain' => $this->selectedDomain,
|
||||
'username' => $this->getUsername(),
|
||||
|
||||
@@ -80,6 +80,16 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
|
||||
|
||||
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', [
|
||||
'username' => $this->getUsername(),
|
||||
]);
|
||||
@@ -102,6 +112,30 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
|
||||
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', [
|
||||
'domain' => $this->selectedDomain,
|
||||
'username' => $this->getUsername(),
|
||||
@@ -239,7 +273,7 @@ class ProtectedDirectories extends Page implements HasActions, HasForms, HasTabl
|
||||
->form([
|
||||
TextInput::make('path')
|
||||
->label(__('Directory Path'))
|
||||
->placeholder('/admin')
|
||||
->placeholder(__('/admin'))
|
||||
->required()
|
||||
->helperText(__('Path relative to your document root (e.g., /admin, /private, /members)')),
|
||||
TextInput::make('name')
|
||||
|
||||
@@ -99,7 +99,7 @@ class Ssl extends Page implements HasActions, HasForms, HasTable
|
||||
? __('Expired :days days ago', ['days' => abs($record->sslCertificate->days_until_expiry)])
|
||||
: __(':days days left', ['days' => $record->sslCertificate->days_until_expiry]))
|
||||
: null)
|
||||
->placeholder('-')
|
||||
->placeholder(__('-'))
|
||||
->sortable(),
|
||||
IconColumn::make('sslCertificate.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')),
|
||||
Textarea::make('certificate')
|
||||
->label(__('Certificate (PEM format)'))
|
||||
->placeholder("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----")
|
||||
->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"))
|
||||
->rows(8)
|
||||
->required()
|
||||
->helperText(__('Paste your SSL certificate in PEM format')),
|
||||
Textarea::make('private_key')
|
||||
->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)
|
||||
->required()
|
||||
->helperText(__('Paste your private key in PEM format. Keep this secure!')),
|
||||
Textarea::make('ca_bundle')
|
||||
->label(__('CA Bundle (optional)'))
|
||||
->placeholder("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----")
|
||||
->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"))
|
||||
->rows(6)
|
||||
->helperText(__('Paste the certificate authority chain if required by your certificate provider')),
|
||||
])
|
||||
|
||||
@@ -61,7 +61,7 @@ class DnsPendingAddsTable extends Component implements HasActions, HasSchemas, H
|
||||
->label(__('TTL')),
|
||||
TextColumn::make('priority')
|
||||
->label(__('Priority'))
|
||||
->placeholder('-'),
|
||||
->placeholder(__('-')),
|
||||
])
|
||||
->actions([
|
||||
Action::make('removePending')
|
||||
|
||||
@@ -185,51 +185,3 @@ class BackupSchedule extends Model
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for active schedules.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for due schedules.
|
||||
*/
|
||||
public function scopeDue($query)
|
||||
{
|
||||
return $query->active()
|
||||
->where(function ($q) {
|
||||
$q->whereNull('next_run_at')
|
||||
->orWhere('next_run_at', '<=', now());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for user schedules.
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for server backup schedules.
|
||||
*/
|
||||
public function scopeServerBackups($query)
|
||||
{
|
||||
return $query->where('is_server_backup', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last status color for UI.
|
||||
*/
|
||||
public function getLastStatusColorAttribute(): string
|
||||
{
|
||||
return match ($this->last_status) {
|
||||
'success' => 'success',
|
||||
'failed' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
4
doccs/site/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.DS_Store
|
||||
4
doccs/site/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
doccs/site/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
doccs/site/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Starlight Starter Kit: Basics
|
||||
|
||||
[](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── content/
|
||||
│ │ └── docs/
|
||||
│ └── content.config.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
||||
36
doccs/site/astro.config.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
server: {
|
||||
allowedHosts: true,
|
||||
},
|
||||
},
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'Jabali Panel Documentation',
|
||||
description: 'Feature documentation and screenshots for the Jabali hosting panel.',
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [{ label: 'Overview', slug: 'overview' }, { label: 'Installation', slug: 'install' }, { label: 'Quickstart', slug: 'quickstart' }, { label: 'Backups and Restore', slug: 'backups-restore' }, { label: 'Migrations', slug: 'migrations' }, { label: 'DNS and Mail', slug: 'dns-mail' }, { label: 'Operations', slug: 'operations' }, { label: 'Security', slug: 'security' }, { label: 'Troubleshooting', slug: 'troubleshooting' }],
|
||||
},
|
||||
{
|
||||
label: 'Admin Panel',
|
||||
autogenerate: { directory: 'admin' },
|
||||
},
|
||||
{
|
||||
label: 'User Panel',
|
||||
autogenerate: { directory: 'user' },
|
||||
},
|
||||
{
|
||||
label: 'Platform',
|
||||
autogenerate: { directory: 'platform' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
6394
doccs/site/package-lock.json
generated
Normal file
17
doccs/site/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "doccs-site",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=jabali.lan astro dev",
|
||||
"start": "__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=jabali.lan astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.6",
|
||||
"astro": "^5.6.1",
|
||||
"sharp": "^0.34.2"
|
||||
}
|
||||
}
|
||||
1
doccs/site/public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>
|
||||
|
After Width: | Height: | Size: 696 B |
BIN
doccs/site/public/screenshots/admin-automation-api.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
doccs/site/public/screenshots/admin-backup-download.png
Normal file
|
After Width: | Height: | Size: 583 KiB |
BIN
doccs/site/public/screenshots/admin-backups--tab-backups.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 107 KiB |
BIN
doccs/site/public/screenshots/admin-backups--tab-schedules.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
doccs/site/public/screenshots/admin-backups.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
doccs/site/public/screenshots/admin-cpanel-migration.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
doccs/site/public/screenshots/admin-dashboard.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
doccs/site/public/screenshots/admin-database-tuning.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
doccs/site/public/screenshots/admin-dns-zones.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
doccs/site/public/screenshots/admin-email-logs.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
doccs/site/public/screenshots/admin-email-queue.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
doccs/site/public/screenshots/admin-geo-block-rules-5-edit.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
doccs/site/public/screenshots/admin-geo-block-rules-create.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
doccs/site/public/screenshots/admin-geo-block-rules-edit.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
doccs/site/public/screenshots/admin-geo-block-rules.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
doccs/site/public/screenshots/admin-home.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
doccs/site/public/screenshots/admin-hosting-packages-1-edit.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
doccs/site/public/screenshots/admin-hosting-packages-create.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
doccs/site/public/screenshots/admin-hosting-packages-edit.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
doccs/site/public/screenshots/admin-hosting-packages.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
doccs/site/public/screenshots/admin-ip-addresses.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
doccs/site/public/screenshots/admin-login.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 124 KiB |
BIN
doccs/site/public/screenshots/admin-migration.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
doccs/site/public/screenshots/admin-password-reset-request.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
doccs/site/public/screenshots/admin-password-reset-reset.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
doccs/site/public/screenshots/admin-php-manager.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
doccs/site/public/screenshots/admin-security--tab-antivirus.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
doccs/site/public/screenshots/admin-security--tab-fail2ban.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
doccs/site/public/screenshots/admin-security--tab-firewall.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 93 KiB |
BIN
doccs/site/public/screenshots/admin-security--tab-overview.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
doccs/site/public/screenshots/admin-security--tab-ssh.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 122 KiB |
BIN
doccs/site/public/screenshots/admin-security.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 114 KiB |
BIN
doccs/site/public/screenshots/admin-server-settings--tab-dns.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 108 KiB |
BIN
doccs/site/public/screenshots/admin-server-settings.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
doccs/site/public/screenshots/admin-server-status.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
doccs/site/public/screenshots/admin-server-updates.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
doccs/site/public/screenshots/admin-services.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
doccs/site/public/screenshots/admin-ssl-manager.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
doccs/site/public/screenshots/admin-two-factor-challenge.png
Normal file
|
After Width: | Height: | Size: 527 KiB |
BIN
doccs/site/public/screenshots/admin-users-1-edit.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
doccs/site/public/screenshots/admin-users-create.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
doccs/site/public/screenshots/admin-users-edit.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
doccs/site/public/screenshots/admin-users.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
doccs/site/public/screenshots/admin-waf.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
doccs/site/public/screenshots/admin-webhook-endpoints-1-edit.png
Normal file
|
After Width: | Height: | Size: 95 KiB |