Compare commits

...

11 Commits

Author SHA1 Message Date
root
e95e03c4fc Add backups, migration, and DNS/mail guides 2026-02-04 17:18:09 +02:00
root
f7ba963730 Add ops, security, and troubleshooting docs 2026-02-04 17:16:29 +02:00
root
58fff86ca8 Add installation and uninstall docs 2026-02-04 17:14:03 +02:00
root
9054f60035 Allow jabali.lan for docs dev server 2026-02-04 17:10:41 +02:00
root
ef7b6419ac Allow all Vite hosts for dev 2026-02-04 17:00:25 +02:00
root
0c75933c57 Allow jabali.lan in Vite dev server 2026-02-04 16:58:59 +02:00
root
872285f2e3 Update panels, docs, and screenshots 2026-02-04 05:55:09 +02:00
codex
edc67cd361 Revert "Update demo password"
This reverts commit e977d66335.
2026-02-04 00:43:22 +02:00
codex
e977d66335 Update demo password 2026-02-04 00:40:51 +02:00
codex
14253ee70e Make website/demo links clickable 2026-02-04 00:31:16 +02:00
codex
2ef4ac69e2 Update README demo links and bump version 2026-02-04 00:30:23 +02:00
360 changed files with 15413 additions and 781 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$` |
| 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 |

View File

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

View File

@@ -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: '*')`).

View File

@@ -39,6 +39,26 @@ After install:
- 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,27 +99,6 @@ After install:
- Redis ACL isolation for WordPress caching
- Multi-language UI
## Screenshots
Admin panel:
- Dashboard: ![Admin Dashboard](docs/screenshots/admin-dashboard.png)
- Server Status: ![Server Status](docs/screenshots/admin-server-status.png)
- Server Settings: ![Server Settings](docs/screenshots/admin-server-settings.png)
- Security Center: ![Security Center](docs/screenshots/admin-security.png)
- Users: ![User Management](docs/screenshots/admin-users.png)
- SSL Manager: ![SSL Manager](docs/screenshots/admin-ssl-manager.png)
- DNS Zones: ![DNS Zones](docs/screenshots/admin-dns-zones.png)
- Backups: ![Admin Backups](docs/screenshots/admin-backups.png)
- Services: ![Services](docs/screenshots/admin-services.png)
User panel:
- Dashboard: ![User Dashboard](docs/screenshots/user-dashboard.png)
- Domain Management: ![User Domains](docs/screenshots/user-domains.png)
- Backups: ![User Backups](docs/screenshots/user-backups.png)
- cPanel Migration: ![cPanel Migration](docs/screenshots/user-cpanel-migration.png)
## Architecture
- Control plane: Laravel app with Filament panels
@@ -158,3 +157,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.

View File

@@ -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.

View File

@@ -1 +1 @@
VERSION=0.9-rc51
VERSION=0.9-rc54

235
app/BackupSchedule.php Normal file
View 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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

@@ -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'),

View File

@@ -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 {

View File

@@ -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([])

View File

@@ -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')

View File

@@ -65,7 +65,30 @@ class PhpManager extends Page implements HasActions, HasForms, HasTable
public function loadPhpVersions(): void
{
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'] ?? [];

View File

@@ -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([

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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(),

View File

@@ -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');

View File

@@ -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'))

View File

@@ -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')

View File

@@ -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(),
])

View File

@@ -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.')),

View File

@@ -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()

View File

@@ -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();
}
}

View File

@@ -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'))

View File

@@ -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')

View File

@@ -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(),

View File

@@ -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')

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)])
: __(':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')),
])

View File

@@ -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')

View File

@@ -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',
};
}
}

View File

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

@@ -0,0 +1,4 @@
node_modules/
dist/
.astro/
.DS_Store

4
doccs/site/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
doccs/site/.vscode/launch.json vendored Normal file
View 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
View File

@@ -0,0 +1,49 @@
# Starlight Starter Kit: Basics
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](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 [Starlights docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

View 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

File diff suppressed because it is too large Load Diff

17
doccs/site/package.json Normal file
View 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"
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Some files were not shown because too many files have changed in this diff Show More