Files
jabali-panel/AGENT.md
2026-01-27 23:38:27 +02:00

2020 lines
74 KiB
Markdown

# Jabali Web Hosting Panel
A modern web hosting control panel built with Laravel 12, Filament v5, and Livewire 4.
## Installation Requirements
**Critical for mail server functionality:**
- Fresh Debian 12/13 installation (no existing web/mail software)
- Domain with glue records pointing to server IP (`ns1.domain.com` → IP)
- PTR record (reverse DNS) pointing to mail hostname (`IP``mail.domain.com`)
- Port 25 open (check with VPS provider)
See README.md "Prerequisites" section for detailed DNS setup instructions.
### Subdomain Installation
The panel can be installed on a subdomain (e.g., `panel.example.com`). The installer automatically:
- Extracts the root domain (`example.com`) for DNS zone creation
- Sets nameservers as `ns1.example.com` and `ns2.example.com`
- Creates an A record for the subdomain (`panel` → server IP)
- Configures email at the root domain (`webmaster@example.com`)
**Example:** Installing on `panel.example.com`
```
DNS Zone: example.com
NS Records: ns1.example.com, ns2.example.com
A Records: @, www, panel, mail, ns1, ns2 → server IP
Email: webmaster@example.com
```
## Database
The panel uses **SQLite** by default (not MySQL). The database file is located at:
```
/var/www/jabali/database/database.sqlite
```
## Quick Reference
```bash
# IMPORTANT: All artisan commands must be run from /var/www/jabali/
cd /var/www/jabali
# Development
composer dev # Start all dev servers (artisan, queue, pail, vite)
composer test # Run PHPUnit tests
./vendor/bin/pint # Format PHP code
php artisan serve # Web server only
php artisan tinker # Interactive REPL
# Production
php artisan migrate # Run migrations
php artisan config:cache # Cache configuration
php artisan route:cache # Cache routes
```
## Git Workflow
**Important:** Only push to git when explicitly requested by the user. Do not auto-push after commits.
### Version Numbers
**IMPORTANT:** Before every push, bump the `VERSION` in the `VERSION` file:
```bash
# VERSION file format:
VERSION=0.9-rc
# Bump before pushing:
# 0.9-rc → 0.9-rc1 → 0.9-rc2 → 0.9-rc3 → ...
```
| Field | When to Bump | Format |
|-------|--------------|--------|
| `VERSION` | Every push | `0.9-rc`, `0.9-rc1`, `0.9-rc2`, ... |
## Project Structure
```
/var/www/jabali/
├── app/
│ ├── Filament/
│ │ ├── Admin/ # Admin panel (route: /admin)
│ │ │ ├── Pages/ # Admin pages (Dashboard, Services, etc.)
│ │ │ ├── Resources/# Admin resources (Users)
│ │ │ └── Widgets/ # Admin widgets (Stats, Disk, Network)
│ │ └── Jabali/ # User panel (route: /panel)
│ │ ├── Pages/ # User pages (Domains, Email, WordPress, etc.)
│ │ └── Widgets/ # User widgets (Stats, Disk, Domains)
│ ├── Models/ # Eloquent models (22 models)
│ ├── Services/ # Business logic services
│ └── Console/Commands/ # Artisan commands
├── bin/
│ ├── jabali-agent # Privileged operations daemon (runs as root)
│ └── screenshot # Chromium screenshot capture script
├── config/ # Laravel config files
│ └── languages.php # Supported languages configuration
├── database/migrations/ # Database migrations
├── lang/ # Translation files (JSON)
│ ├── en.json # English (base)
│ ├── es.json # Spanish
│ ├── fr.json # French
│ ├── ru.json # Russian
│ ├── pt.json # Portuguese
│ ├── ar.json # Arabic (RTL)
│ └── he.json # Hebrew (RTL)
└── resources/views/filament/ # Blade templates for Filament pages
```
## Architecture
### Two Panels
- **Admin Panel** (`/admin`): Server-wide management, user administration
- **User Panel** (`/panel`): Per-user domain, email, database management
### Privileged Agent
The `bin/jabali-agent` daemon runs as root and handles operations requiring elevated privileges:
- System user creation/deletion
- Domain/vhost configuration
- Email (Postfix/Dovecot) management
- SSL certificate operations
- Database operations
- Backup operations
Communication via Unix socket at `/var/run/jabali/agent.sock`.
### Key Services
- **AgentClient**: PHP client for communicating with jabali-agent
- **AdminNotificationService**: System notifications (SSL, backups, quotas)
### Self-Healing Services
The `bin/jabali-health-monitor` daemon automatically monitors and restarts critical services when they fail.
**Monitored Services:**
- nginx, mariadb, jabali-agent, php-fpm
- postfix, dovecot, named (if installed)
- redis-server, fail2ban (if installed)
**Features:**
- Checks services every 30 seconds
- Automatic restart on failure (up to 3 attempts)
- Email notifications via `AdminNotificationService`
- Systemd restart policies as backup protection
**Files:**
| File | Purpose |
|------|---------|
| `bin/jabali-health-monitor` | Health monitoring daemon |
| `/etc/systemd/system/jabali-health-monitor.service` | Systemd service unit |
| `/var/log/jabali/health-monitor.log` | Event log |
| `/var/run/jabali/health-monitor.state` | Service state tracking |
**Commands:**
```bash
# Check status
systemctl status jabali-health-monitor
# View logs
journalctl -u jabali-health-monitor -f
# Manual notification test
php artisan notify:service-health down nginx --description="Web Server"
```
**Notification Setting:** `notify_service_health` in dns_settings table (Admin > Server Settings)
### Admin Notifications & Monitoring
The system sends email notifications to configured admin recipients for various events.
**Configuration:** Admin > Server Settings > Notifications tab
**Notification Types:**
| Type | Setting | Description |
|------|---------|-------------|
| SSL Errors | `notify_ssl_errors` | Certificate errors and expiration warnings |
| Backup Failures | `notify_backup_failures` | Failed scheduled backups |
| Disk Quota | `notify_disk_quota` | Users reaching 90% quota |
| Login Failures | `notify_login_failures` | Brute force and Fail2ban alerts |
| SSH Logins | `notify_ssh_logins` | Successful SSH login alerts |
| System Updates | `notify_system_updates` | Panel update availability |
| Service Health | `notify_service_health` | Service failures and auto-restarts |
| High Load | `notify_high_load` | Server load exceeds threshold |
**Notification Log:**
All sent notifications are logged in `notification_logs` table and viewable in Admin > Server Settings > Notifications tab.
**High Load Monitoring:**
- Monitors server load average every minute via `jabali-health-monitor`
- Configurable threshold (default: 5.0) and duration (default: 5 minutes)
- Sends alert when load exceeds threshold for configured duration
- Settings: `load_threshold`, `load_alert_minutes` in dns_settings
**Commands:**
```bash
# Manual high load notification test
php artisan notify:high-load
# Test email
# Use "Send Test Email" button in Server Settings > Notifications
```
### Backup System
The panel provides comprehensive backup functionality for both users and administrators.
**Backup Types:**
| Type | Description | Storage |
|------|-------------|---------|
| User Backup | Single user's domains, databases, mailboxes | `/home/{user}/backups/` |
| Server Backup (Full) | All users as tar.gz archive | `/var/backups/jabali/` |
| Server Backup (Incremental) | Rsync to remote destination | Remote SFTP/NFS |
**Admin Backup Features:**
- **Create Server Backup**: Backup all users or selected users
- **Restore Backup**: Selective restore with modal UI
- **Download Backup**: Download local backups (creates zip for directories)
**Restore Options:**
| Option | Description |
|--------|-------------|
| Website Files | Restore domain files to `/home/{user}/domains/` |
| Databases | Restore MySQL databases with security sanitization |
| MySQL Users | Restore MySQL users and their permissions |
| Mailboxes | Restore email mailboxes and messages |
| SSL Certificates | Restore SSL certificates for domains |
| DNS Zones | Restore DNS zone files |
**Backup Contents:**
```
backup_folder/
├── manifest.json # Backup metadata
├── {username}.tar.gz # Per-user archive containing:
│ ├── files/ # Domain files
│ │ └── {domain}/
│ ├── mysql/ # Database dumps
│ │ ├── {database}.sql.gz
│ │ └── users.sql # MySQL users and grants
│ ├── mail/ # Mailbox data
│ │ └── {domain}/{user}/
│ ├── ssl/ # SSL certificates
│ │ └── {domain}/
│ └── dns/ # DNS zone files
│ └── {domain}.zone
```
**Security Validations (Restore):**
The backup restore process includes comprehensive security checks to prevent privilege escalation:
| Check | Description |
|-------|-------------|
| Database Prefix | Only restore databases matching user's prefix (`{username}_*`) |
| MySQL Users | Only restore users with correct prefix, block global grants |
| SQL Sanitization | Remove DEFINER, GRANT, SET GLOBAL from dumps |
| Domain Ownership | Verify user owns domains before restoring files/SSL/DNS |
| Symlink Prevention | Remove dangerous symlinks pointing outside backup |
| Path Traversal | Block `..` sequences in paths |
| DNS Validation | Validate zone files with `named-checkzone` |
**Agent Actions:**
```
backup.create - Create user backup
backup.restore - Restore backup with selective options
backup.get_info - Get backup manifest/contents
backup.create_server - Create server-wide backup
backup.delete - Delete backup file
backup.upload_remote - Upload backup to remote destination
backup.download_remote - Download backup from remote
backup.incremental - Create incremental backup via rsync
backup.test_destination - Test remote backup destination
```
**Download Route:**
```
GET /jabali-admin/backup-download?id={backup_id} # Admin (requires is_admin)
GET /jabali-panel/backup-download?path={base64} # User (validates ownership)
```
**Files:**
| File | Purpose |
|------|---------|
| `app/Filament/Admin/Pages/Backups.php` | Admin backup management UI |
| `app/Filament/Jabali/Pages/Backups.php` | User backup management UI |
| `app/Http/Controllers/BackupDownloadController.php` | Download handlers |
| `bin/jabali-agent` | Backup/restore implementation |
| `/var/log/jabali/agent.log` | Security audit log for blocked operations |
### DNSSEC Support
DNSSEC (Domain Name System Security Extensions) adds cryptographic signatures to DNS records.
**Management:** Admin > Server Settings > DNS tab > DNSSEC section
**Features:**
- Enable/disable DNSSEC per domain
- Automatic KSK (Key Signing Key) and ZSK (Zone Signing Key) generation
- Uses ECDSAP256SHA256 algorithm
- Auto-generates DS records for registrar configuration
- Inline zone signing with auto-dnssec maintain
**Agent Actions:**
```
dns.enable_dnssec - Generate keys and sign zone
dns.disable_dnssec - Remove keys and unsign zone
dns.get_dnssec_status - Check DNSSEC status and keys
dns.get_ds_records - Get DS records for registrar
```
**Files:**
| Path | Description |
|------|-------------|
| `/etc/bind/keys/{domain}/` | DNSSEC keys directory |
| `/etc/bind/zones/db.{domain}` | Unsigned zone file |
| `/etc/bind/zones/db.{domain}.signed` | Signed zone file |
**Setup Process:**
1. Enable DNSSEC for domain in Server Settings
2. Copy DS record from modal
3. Add DS record to domain registrar
4. Wait for DNS propagation (up to 48 hours)
### Test Credentials
| Panel | URL | Email | Password |
|-------|-----|-------|----------|
| Admin | `https://jabali.lan/jabali-admin` | `admin@jabali.lan` | `123123123` |
| User | `https://jabali.lan/jabali-panel` | `user@jabali.lan` | `wjqr9t6Z#%r&@C$4` |
## Models
| Model | Table | Description |
|-------|-------|-------------|
| User | users | Panel users (system users) |
| Domain | domains | Hosted domains |
| EmailDomain | email_domains | Email-enabled domains |
| Mailbox | mailboxes | Email mailboxes |
| EmailForwarder | email_forwarders | Email forwarding rules |
| DnsRecord | dns_records | DNS zone records |
| DnsSetting | dns_settings | Key-value settings store |
| SslCertificate | ssl_certificates | SSL/TLS certificates |
| MysqlCredential | mysql_credentials | Database credentials |
| Backup | backups | User backups |
| BackupSchedule | backup_schedules | Scheduled backups |
| BackupDestination | backup_destinations | Remote backup targets |
| CronJob | cron_jobs | User cron jobs |
| AuditLog | audit_logs | Admin audit trail |
| NotificationLog | notification_logs | Admin notification history |
## Filament Pages
### Admin Panel
- Dashboard, Services, ServerStatus, ServerSettings
- SslManager, PhpManager, EmailSettings, DnsZones
- Backups, AuditLogs, Fail2ban, ClamAV, Security, ServerImports
### User Panel
- Dashboard, Domains, DnsRecords, Files
- Email, WordPress, Databases, Ssl
- Backups, CronJobs, SshKeys, PhpSettings, Logs
## Dashboard Configurations
### Admin Dashboard (`/jabali-admin`)
**Location:** `App\Filament\Admin\Pages\Dashboard`
**Header Widgets:**
- `DashboardStatsWidget` - Stats cards showing Users, Domains, Mailboxes, Databases, SSL Certificates
- Uses `<x-filament::section>` components with icon, value (bold), label
- Responsive: 1 col mobile, 2 cols tablet, 5 cols desktop
**Schema Components:**
- `RecentActivityTable` - Embedded audit log table via `EmbeddedTable::make()`
**Header Actions:**
- Refresh - Reloads the page
- Setup Wizard - Onboarding modal (visible until completed)
- Take Tour - Starts the admin panel tour
**Files:**
| File | Purpose |
|------|---------|
| `app/Filament/Admin/Pages/Dashboard.php` | Dashboard page class |
| `app/Filament/Admin/Widgets/DashboardStatsWidget.php` | Stats widget |
| `resources/views/filament/admin/widgets/dashboard-stats.blade.php` | Stats template |
| `app/Filament/Admin/Widgets/Dashboard/RecentActivityTable.php` | Activity table widget |
### User Dashboard (`/jabali-panel`)
**Location:** `App\Filament\Jabali\Pages\Dashboard`
**Widgets (in order):**
1. `StatsOverview` - Stats cards showing Domains, Mailboxes, Databases, SSL Certificates
- Responsive: 1 col mobile, 2 cols tablet, 4 cols desktop
2. `DiskUsageWidget` - Disk quota visualization with progress bar
3. `DomainsWidget` - Recent domains table with SSL status
4. `MailboxesWidget` - Recent mailboxes table
5. `RecentBackupsWidget` - Latest backups table
**Layout:** 2-column responsive grid (1 col mobile, 2 cols desktop)
**Subheading:** Personalized welcome message ("Welcome back, {name}!")
**Files:**
| File | Purpose |
|------|---------|
| `app/Filament/Jabali/Pages/Dashboard.php` | Dashboard page class |
| `app/Filament/Jabali/Widgets/StatsOverview.php` | Stats widget |
| `resources/views/filament/jabali/widgets/stats-overview.blade.php` | Stats template |
| `app/Filament/Jabali/Widgets/DiskUsageWidget.php` | Disk usage widget |
| `app/Filament/Jabali/Widgets/DomainsWidget.php` | Domains table widget |
| `app/Filament/Jabali/Widgets/MailboxesWidget.php` | Mailboxes table widget |
| `app/Filament/Jabali/Widgets/RecentBackupsWidget.php` | Backups table widget |
## Agent Actions
The jabali-agent supports these action categories:
```
user.* - System user management
domain.* - Domain/vhost operations
wp.* - WordPress installation/management
email.* - Email domain/mailbox operations
mysql.* - Database operations
dns.* - DNS zone management (includes DNSSEC)
php.* - PHP version management
ssl.* - SSL certificate operations
backup.* - Backup/restore operations (see Backup System section)
service.* - System service control
ufw.* - Firewall management
file.* - File operations
ssh.* - SSH key management
cron.* - Cron job management
quota.* - Disk quota management
clamav.* - Antivirus scanning
metrics.* - System metrics
scanner.* - Security scanning (Lynis, Nikto)
logs.* - Log access
redis.* - Redis user management
```
**Detailed Action Reference:**
| Action | Parameters | Description |
|--------|------------|-------------|
| `dns.enable_dnssec` | `domain` | Generate keys and sign zone |
| `dns.disable_dnssec` | `domain` | Remove keys and unsign zone |
| `dns.get_dnssec_status` | `domain` | Check DNSSEC status and keys |
| `dns.get_ds_records` | `domain` | Get DS records for registrar |
| `backup.create` | `username`, `options` | Create user backup |
| `backup.restore` | `username`, `backup_path`, `restore_*` | Restore with selective options |
| `backup.get_info` | `backup_path` | Get backup manifest |
| `backup.create_server` | `path`, `options` | Create server backup |
| `backup.delete` | `path` | Delete backup |
| `backup.upload_remote` | `local_path`, `config` | Upload to remote |
| `backup.download_remote` | `remote_path`, `local_path`, `config` | Download from remote |
| `backup.incremental` | `config`, `options` | Incremental rsync backup |
| `backup.test_destination` | `config` | Test remote destination |
| `backup.delete_remote` | `remote_path`, `config` | Delete backup from remote |
## Backup System
The backup system supports both user-level and server-wide backups with local and remote storage.
### Backup Types
| Type | Description | Storage |
|------|-------------|---------|
| **Full (tar.gz)** | Complete archive of all data | Local or remote |
| **Incremental (rsync)** | Space-efficient with hard links | Remote only (SFTP/NFS) |
### Remote Destinations
Supported destination types:
- **SFTP** - SSH-based file transfer to remote servers
- **NFS** - Network File System mounts
- **S3** - S3-compatible object storage (AWS, MinIO, etc.)
### Backup Schedules & Retention
Scheduled backups run via the `backups:run-schedules` artisan command (called by cron). Each schedule has:
- **Frequency**: Hourly, daily, weekly, or monthly
- **Retention Count**: Number of backups to keep (default: 7)
- **Destination**: Local or remote storage
**Retention Policy:**
- Applied automatically after each backup completes
- Deletes oldest backups beyond the retention count
- Works for both local files and remote destinations
- Implemented in `App\Jobs\RunServerBackup::applyRetention()`
**Important:** The queue worker must be running for scheduled backups and retention:
```bash
systemctl status jabali-queue # Check status
systemctl restart jabali-queue # Restart to pick up code changes
```
### Backup Files
| Path | Description |
|------|-------------|
| `/var/backups/jabali/` | Default server backup location |
| `/home/{user}/backups/` | User backup location |
| `metadata/panel_data.json` | Panel database records (domains, mailboxes, etc.) |
| `mysql/` | Database dumps (.sql.gz) |
| `zones/` | DNS zone files |
| `ssl/` | SSL certificates |
| `mail/` | Mailbox data |
### Related Models
- `Backup` - Individual backup records
- `BackupSchedule` - Schedule configuration with retention settings
- `BackupDestination` - Remote storage configuration
### Related Jobs
- `RunServerBackup` - Executes backup and applies retention
- `IndexRemoteBackups` - Indexes backups on remote destinations
## Security Features
### Security Page (Admin)
Located at `/jabali-admin/security`, provides:
- **Overview** - System security status and recent audit logs
- **Firewall** - UFW rules management
- **Fail2ban** - Intrusion prevention with jail management
- **Antivirus** - ClamAV scanning
- **SSH** - SSH hardening settings
- **Vulnerability Scanner** - Security scanning tools
### Security Scanning Tools
| Tool | Purpose | Installation |
|------|---------|--------------|
| **Lynis** | System security auditing | Pre-installed |
| **Nikto** | Web server vulnerability scanner | `/opt/nikto` (GitHub clone) |
| **WPScan** | WordPress vulnerability scanner | Ruby gem |
**Nikto Configuration:**
- Installed from GitHub at `/opt/nikto`
- Symlinked to `/usr/local/bin/nikto`
- Called with full path in timeout commands (PATH restriction)
**WPScan Configuration:**
- Cache directory: `/var/www/.wpscan` (owned by www-data)
- Run with `HOME=/var/www` environment variable
- Version displayed without ASCII banner using `grep -i 'version'`
### Audit Logs
All administrative actions are logged to the `audit_logs` table.
**Logged Events:**
- Authentication (login, logout, login_failed)
- CRUD operations on domains, mailboxes, users
- System configuration changes
- Security-related actions
**Audit Log Retention:**
- Configured via `audit_log_retention_days` setting (default: 90 days)
- Pruned daily at 2:00 AM via scheduled task
- Method: `AuditLog::prune()`
**Viewing Logs:**
- Admin Panel: Security page → Overview tab (paginated table)
- Direct page: `/jabali-admin/audit-logs` (hidden from sidebar)
### Related Files
| File | Description |
|------|-------------|
| `app/Models/AuditLog.php` | Audit log model with prune method |
| `app/Filament/Admin/Pages/Security.php` | Security dashboard |
| `app/Filament/Admin/Widgets/Security/AuditLogsTable.php` | Audit logs table widget |
| `routes/console.php` | Scheduled audit log pruning |
## Code Style
- PHP 8.2+ with strict types
- Follow Laravel conventions
- Use `DnsSetting::get()` / `DnsSetting::set()` for key-value storage
- All privileged operations go through jabali-agent
## Filament v4 UI Guidelines
**CRITICAL: Build using ONLY Filament v4 native components and styles with proper spacing. Always use pure Filament native components with proper icons and colors.**
### Architecture Guidelines
1. **Use Filament Resources for Eloquent Models**: When data is backed by an Eloquent model, prefer creating a Filament Resource (`php artisan make:filament-resource`) over custom pages.
2. **Use Tables for List Data**: When displaying list data (arrays or collections), ALWAYS use proper Filament tables via `EmbeddedTable::make()` or the `HasTable` trait.
3. **Use Schema-based Pages for Complex UIs**: For pages with multiple sections, tabs, and mixed content (forms + tables + stats), use Schema-based pages with embedded table widgets.
4. **Prefer Existing Patterns**: Look at existing pages in the codebase (e.g., `Security.php`, `DnsRecords.php`, `SshKeys.php`) for examples of how to implement similar features.
### Absolute Rules (NO EXCEPTIONS)
- **NO custom HTML** - Never use raw `<div>`, `<span>`, `<p>`, `<ul>` etc. in Filament pages
- **NO inline styles** - Never use `style="..."` attributes
- **NO custom CSS** - Never create CSS files for Filament components
- **NO custom blade templates** - Don't create View components with custom HTML for UI elements
- **USE Filament components** - Sections, badges, buttons, tables, infolists, stats widgets, grids
- **USE proper icons** - Always use `->icon('heroicon-o-*')` on sections and actions
- **USE proper colors** - Always use `->iconColor('success'|'danger'|'warning'|'gray')` for status indication
- **USE Tailwind classes** - Only when absolutely necessary for minor adjustments
- **MUST be responsive** - All pages must work on mobile, tablet, and desktop
### Allowed Components
Use these Filament native components exclusively:
| Category | Components |
|----------|------------|
| Layout | `Section::make()`, `Grid::make()`, `Group::make()`, `Tabs::make()` |
| Display | `Text::make()`, `<x-filament::badge>`, `<x-filament::icon>` |
| Actions | `Actions::make()`, `Action::make()`, `<x-filament::button>` |
| Forms | TextInput, Select, Toggle, FileUpload, Checkbox, etc. |
| Data | Tables, Infolists, Stats Widgets, `EmbeddedTable::make()` |
| Feedback | `<x-filament::modal>`, Notifications |
### Tables with Array Data (IMPORTANT)
When displaying list data, **ALWAYS use proper Filament tables**, not Sections or custom HTML. Use `EmbeddedTable::make()` to embed table widgets within Schema-based pages.
**Creating a Self-Refreshing Table Widget:**
For tables with actions that modify data (enable/disable, delete, etc.), the widget should reload its own data directly rather than dispatching events to the parent (which causes full page re-renders).
```php
<?php
namespace App\Filament\Admin\Widgets\Security;
use App\Services\Agent\AgentClient;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Notifications\Notification;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Livewire\Component;
class MyDataTable extends Component implements HasTable, HasSchemas, HasActions
{
use InteractsWithTable;
use InteractsWithSchemas;
use InteractsWithActions;
public array $items = []; // Data passed from parent
// Reload data directly from source (agent, API, database)
protected function reloadData(): void
{
try {
$agent = new AgentClient();
$result = $agent->send('some.action', []);
if ($result['success'] ?? false) {
$this->items = $result['items'] ?? [];
$this->resetTable(); // Force table to re-render with new data
}
} catch (\Exception $e) {
// Keep existing data on error
}
}
public function makeFilamentTranslatableContentDriver(): ?\Filament\Support\Contracts\TranslatableContentDriver
{
return null;
}
public function table(Table $table): Table
{
return $table
->records(fn () => $this->items)
->columns([
TextColumn::make('name')->label(__('Name'))->searchable(),
IconColumn::make('enabled')
->label(__('Status'))
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('gray'),
])
->actions([
Action::make('toggle')
->label(fn (array $record): string => ($record['enabled'] ?? false) ? __('Disable') : __('Enable'))
->icon(fn (array $record): string => ($record['enabled'] ?? false) ? 'heroicon-o-x-circle' : 'heroicon-o-check-circle')
->color(fn (array $record): string => ($record['enabled'] ?? false) ? 'danger' : 'success')
->action(function (array $record): void {
try {
$agent = new AgentClient();
$result = $agent->send('item.toggle', ['id' => $record['id']]);
if ($result['success'] ?? false) {
Notification::make()
->title(__('Status updated'))
->success()
->send();
// Reload data directly - don't dispatch events to parent
$this->reloadData();
} else {
throw new \Exception($result['error'] ?? __('Operation failed'));
}
} catch (\Exception $e) {
Notification::make()
->title(__('Error'))
->body($e->getMessage())
->danger()
->send();
}
}),
])
->striped()
->emptyStateHeading(__('No items'))
->emptyStateIcon('heroicon-o-inbox');
}
public function render()
{
return $this->getTable()->render();
}
}
```
**Embedding Table in Schema:**
```php
use Filament\Schemas\Components\EmbeddedTable;
use App\Filament\Admin\Widgets\Security\MyDataTable;
// In your page's schema
Section::make(__('Data List'))
->icon('heroicon-o-list-bullet')
->schema([
EmbeddedTable::make(MyDataTable::class, ['items' => $this->items]),
])
```
**Key Points:**
- Implement `HasTable`, `HasSchemas`, and `HasActions` interfaces
- Use `InteractsWithTable`, `InteractsWithSchemas`, and `InteractsWithActions` traits
- Use `->records(fn () => $this->arrayData)` for array/collection data
- Use `->query(Model::query())` for Eloquent models
- Use `->actions([])` for row actions on array-based tables
- Always implement `makeFilamentTranslatableContentDriver()` returning null
- Return `$this->getTable()->render()` in render method (NOT `view('filament-tables::index')`)
- Import actions from `Filament\Actions\Action` (NOT `Filament\Tables\Actions\Action`)
- **After modifying data, call `$this->resetTable()` to force the table to re-render**
- **Reload data directly in the widget rather than dispatching events to parent** (avoids full page re-renders)
### Section with Icons and Colors
Always use Section's native icon and color support:
```php
use Filament\Schemas\Components\Section;
// Status card with icon and color
Section::make(__('Active'))
->description(__('Firewall'))
->icon('heroicon-o-shield-check')
->iconColor('success') // success, danger, warning, gray
// Section with header actions
Section::make(__('Settings'))
->icon('heroicon-o-cog')
->headerActions([
Action::make('save')->label(__('Save')),
])
->schema([...])
```
### Responsive Grid Layout
Use Grid with responsive column configuration:
```php
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
// 3-column responsive grid for stat cards
Grid::make(['default' => 1, 'sm' => 3])
->schema([
Section::make(__('Active'))
->description(__('Firewall'))
->icon('heroicon-o-shield-check')
->iconColor('success'),
Section::make('0')
->description(__('IPs Banned'))
->icon('heroicon-o-lock-closed')
->iconColor('success'),
Section::make('0')
->description(__('Threats'))
->icon('heroicon-o-bug-ant')
->iconColor('gray'),
])
```
### Stats Overview Widget Pattern
For dashboard stats (domains count, mailboxes count, etc.), use a custom Widget with `<x-filament::section>` components in a blade template.
**Widget class:**
```php
<?php
declare(strict_types=1);
namespace App\Filament\Admin\Widgets;
use Filament\Widgets\Widget;
class DashboardStatsWidget extends Widget
{
protected static ?int $sort = 1;
protected int|string|array $columnSpan = 'full';
protected string $view = 'filament.admin.widgets.dashboard-stats';
public function getStats(): array
{
return [
[
'value' => $userCount,
'label' => __('Users'),
'icon' => 'heroicon-o-users',
'color' => 'primary',
],
[
'value' => $domainCount,
'label' => __('Domains'),
'icon' => 'heroicon-o-globe-alt',
'color' => 'success',
],
// ... more stats
];
}
}
```
**Blade template** (`resources/views/filament/{panel}/widgets/dashboard-stats.blade.php`):
```blade
<x-filament-widgets::widget>
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 16px;
}
@media (min-width: 640px) {
.stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.stats-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
</style>
<div class="stats-grid">
@foreach($this->getStats() as $stat)
<x-filament::section :icon="$stat['icon']" :icon-color="$stat['color']">
<x-slot name="heading">
<span class="text-2xl font-bold">{{ $stat['value'] }}</span>
</x-slot>
<x-slot name="description">{{ $stat['label'] }}</x-slot>
</x-filament::section>
@endforeach
</div>
</x-filament-widgets::widget>
```
**Key points:**
- Use custom Widget extending `Filament\Widgets\Widget` with a blade view
- Use `<x-filament::section>` for each stat card with icon and icon-color
- Value goes in `heading` slot with bold styling, label goes in `description` slot
- Use CSS media queries in `<style>` tag for responsive grid (Tailwind dynamic classes may not work)
- Adjust grid columns based on number of stats (e.g., 5 cols for admin, 4 cols for user panel)
### Responsive Design Verification
**CRITICAL: Always test responsive design on three viewport sizes:**
1. **Mobile** (375px width) - iPhone size
2. **Tablet** (768px width) - iPad size
3. **Desktop** (1400px width) - Standard desktop
**Puppeteer viewport examples:**
```javascript
// Mobile
await page.setViewport({ width: 375, height: 812 });
// Tablet
await page.setViewport({ width: 768, height: 1024 });
// Desktop
await page.setViewport({ width: 1400, height: 900 });
```
**Common responsive issues to check:**
- Stats/cards should stack on mobile, grid on tablet/desktop
- Tables should be scrollable on mobile
- Navigation should collapse to hamburger menu on mobile
- Buttons and inputs should be touch-friendly (min 44px tap target)
- Text should not overflow or be cut off
### Tabs with Dynamic Table Content (IMPORTANT)
**Problem:** When using Filament's `Tabs::make()` with tables inside `View::make()`, table row actions (modals, confirmations) don't work because the action mounting breaks when nested inside the Tabs schema.
**Solution:** Render the table OUTSIDE the form schema, and use a custom tabs navigation component with `wire:click` for tab switching.
**1. Create a custom tabs nav component** (`resources/views/filament/{panel}/components/my-tabs-nav.blade.php`):
```blade
@php
$tabs = [
'first' => ['label' => __('First Tab'), 'icon' => 'heroicon-o-home'],
'second' => ['label' => __('Second Tab'), 'icon' => 'heroicon-o-cog'],
'third' => ['label' => __('Third Tab'), 'icon' => 'heroicon-o-list-bullet'],
];
@endphp
<nav class="fi-tabs flex max-w-full gap-x-1 overflow-x-auto mx-auto rounded-xl bg-white p-2 shadow-sm ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10" role="tablist">
@foreach($tabs as $key => $tab)
<button
type="button"
role="tab"
aria-selected="{{ $this->activeTab === $key ? 'true' : 'false' }}"
wire:click="setTab('{{ $key }}')"
@class([
'fi-tabs-item group flex items-center gap-x-2 rounded-lg px-3 py-2 text-sm font-medium outline-none transition duration-75',
'fi-active bg-gray-50 dark:bg-white/5' => $this->activeTab === $key,
'hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5' => $this->activeTab !== $key,
])
>
<x-filament::icon
:icon="$tab['icon']"
@class([
'fi-tabs-item-icon h-5 w-5 shrink-0 transition duration-75',
'text-primary-600 dark:text-primary-400' => $this->activeTab === $key,
'text-gray-400 group-hover:text-gray-500 group-focus-visible:text-gray-500 dark:text-gray-500 dark:group-hover:text-gray-400 dark:group-focus-visible:text-gray-400' => $this->activeTab !== $key,
])
/>
<span @class([
'fi-tabs-item-label transition duration-75',
'text-primary-600 dark:text-primary-400' => $this->activeTab === $key,
'text-gray-500 group-hover:text-gray-700 group-focus-visible:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200 dark:group-focus-visible:text-gray-200' => $this->activeTab !== $key,
])>
{{ $tab['label'] }}
</span>
</button>
@endforeach
</nav>
```
**2. Page class:**
```php
<?php
namespace App\Filament\Jabali\Pages;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Livewire\Attributes\Url;
class MyTabbedPage extends Page implements HasTable
{
use InteractsWithTable;
#[Url(as: 'tab')]
public ?string $activeTab = 'first';
public function mount(): void
{
$this->activeTab = $this->normalizeTabName($this->activeTab);
}
protected function normalizeTabName(?string $tab): string
{
return match ($tab) {
'first', 'second', 'third' => $tab,
default => 'first',
};
}
protected function getForms(): array
{
return ['myForm'];
}
// Form renders ONLY the tabs nav (and optional info section)
public function myForm(Schema $schema): Schema
{
return $schema->schema([
Section::make(__('Page Title'))
->description(__('Description of this page'))
->icon('heroicon-o-information-circle')
->iconColor('info'),
View::make('filament.jabali.components.my-tabs-nav'),
]);
}
public function setTab(string $tab): void
{
$this->activeTab = $this->normalizeTabName($tab);
$this->resetTable();
}
// Table rendered separately - NOT inside the form schema
public function table(Table $table): Table
{
return match ($this->activeTab) {
'first' => $this->firstTable($table),
'second' => $this->secondTable($table),
'third' => $this->thirdTable($table),
default => $this->firstTable($table),
};
}
protected function firstTable(Table $table): Table
{
return $table
->query(MyModel::query())
->columns([...])
->recordActions([
// These actions will work because table is outside the form schema
Action::make('edit')->modalHeading('Edit Item')->form([...])->action(...),
Action::make('delete')->requiresConfirmation()->action(...),
]);
}
}
```
**3. Blade template** - render table OUTSIDE the form with negative margin:
```blade
<x-filament-panels::page>
{{ $this->myForm }}
<div class="-mt-4">
{{ $this->table }}
</div>
<x-filament-actions::modals />
</x-filament-panels::page>
```
**Key points:**
- **DO NOT** put `View::make('...table')` inside `Tabs\Tab::make()->schema([])` - table actions won't work
- Render the table DIRECTLY in the blade template with `{{ $this->table }}`
- Use `<div class="-mt-4">` to compensate for the parent's row-gap CSS
- Custom tabs nav uses `wire:click="setTab('tabname')"` for proper Livewire updates
- `setTab()` normalizes the tab name and calls `$this->resetTable()` to refresh the table
- `#[Url(as: 'tab')]` syncs the active tab with the URL query string
**Examples in codebase:**
- `app/Filament/Jabali/Pages/Email.php` - Email page with mailboxes/forwarders/catchall/logs tabs
- `app/Filament/Jabali/Pages/Backups.php` - Backups page with local/remote/history tabs
### Examples
**Stats Widget (correct):**
```php
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class MyStatsWidget extends StatsOverviewWidget
{
protected function getStats(): array
{
return [
Stat::make('Total', 100)->icon('heroicon-o-users'),
Stat::make('Active', 80)->color('success'),
];
}
}
```
**Section with content (correct):**
```blade
<x-filament::section icon="heroicon-o-cog" collapsible>
<x-slot name="heading">Settings</x-slot>
<x-slot name="description">Configure options</x-slot>
{{-- Use Filament form components here --}}
</x-filament::section>
```
**Table columns (correct):**
```php
TextColumn::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'active' => 'success',
'pending' => 'warning',
default => 'gray',
})
```
### Progress Bar Widget Pattern
For displaying progress (disk usage, quotas, etc.), use a Widget with Filament Section and Tailwind-based progress bar. Use Tailwind's dynamic color classes with `@class` directive:
**Widget class:**
```php
<?php
declare(strict_types=1);
namespace App\Filament\Jabali\Widgets;
use Filament\Widgets\Widget;
class DiskUsageWidget extends Widget
{
protected static ?int $sort = 2;
protected int|string|array $columnSpan = 1;
protected string $view = 'filament.jabali.widgets.disk-usage';
public function getData(): array
{
$percent = 25.0;
return [
'used' => '2.5 GB',
'quota' => '10 GB',
'free' => '7.5 GB',
'percent' => $percent,
'has_quota' => true,
'home' => '/home/user',
'color' => $this->getColor($percent),
];
}
protected function getColor(float $percent): string
{
if ($percent >= 90) return 'danger';
if ($percent >= 70) return 'warning';
return 'success';
}
}
```
**Blade template (resources/views/filament/{panel}/widgets/disk-usage.blade.php):**
```blade
<x-filament-widgets::widget>
<x-filament::section icon="heroicon-o-circle-stack">
<x-slot name="heading">{{ __('Disk Usage') }}</x-slot>
<x-slot name="description">{{ $this->getData()['home'] }}</x-slot>
@php
$data = $this->getData();
$percent = min(100, max(0, $data['percent']));
@endphp
<div class="space-y-4">
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">
{{ $data['used'] }} {{ __('used') }}
</span>
<x-filament::badge :color="$data['color']">
{{ number_format($percent, 1) }}%
</x-filament::badge>
</div>
<div class="w-full h-3 bg-gray-200 rounded-full dark:bg-gray-700 overflow-hidden">
<div
@class([
'h-full rounded-full transition-all duration-500',
'bg-success-500' => $data['color'] === 'success',
'bg-warning-500' => $data['color'] === 'warning',
'bg-danger-500' => $data['color'] === 'danger',
])
style="width: {{ $percent }}%"
></div>
</div>
</div>
</div>
</x-filament::section>
</x-filament-widgets::widget>
```
**Key points:**
- Use `@class` directive with conditional Tailwind classes for colors
- Only use inline `style` for truly dynamic values like percentage width (Tailwind can't generate dynamic classes)
- Use Filament's native `<x-filament::badge>` and `<x-filament::section>` components
- Use Filament color names (`success`, `warning`, `danger`) for consistency
### Anti-patterns (avoid)
```blade
{{-- BAD: Custom HTML --}}
<div class="custom-card">
<h3>Title</h3>
<p>Content</p>
</div>
{{-- GOOD: Use Filament section --}}
<x-filament::section>
<x-slot name="heading">Title</x-slot>
Content
</x-filament::section>
```
```blade
{{-- BAD: Inline styles --}}
<div style="display: flex; gap: 1rem;">
{{-- GOOD: Tailwind classes if needed --}}
<div class="flex gap-4">
```
### Filament v4 Namespaces (IMPORTANT)
Filament v4 uses `Schema` instead of `Form` for page layouts, and layout components are in different namespaces:
```php
// CORRECT Filament v4 imports
use Filament\Schemas\Schema; // NOT Filament\Forms\Form
use Filament\Schemas\Components\Tabs; // NOT Filament\Forms\Components\Tabs
use Filament\Schemas\Components\Grid; // Layout components
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Components\View;
use Filament\Schemas\Components\Actions as FormActions;
// Form INPUT components stay in Forms namespace
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
```
**Method signatures:**
```php
// CORRECT - Filament v4
public function myForm(Schema $schema): Schema
// WRONG - Old Filament v3 style
public function myForm(Form $form): Form
```
### Schema-based Tabs (For Form-Only Content)
Use `Filament\Schemas\Components\Tabs` within a form schema **ONLY when tabs contain form fields or static content** - NOT when tabs contain tables with row actions.
**When to use Schema-based Tabs:**
- Tabs with form inputs (TextInput, Select, Toggle, etc.)
- Tabs with static content (Text, View with no interactive elements)
- Settings pages where each tab is a different category of settings
**When NOT to use Schema-based Tabs:**
- Tabs with tables that have row actions (edit, delete, view modals)
- Use the "Tabs with Dynamic Table Content" pattern above instead
**Example - Settings page with form tabs:**
```php
public function settingsForm(Schema $schema): Schema
{
return $schema->schema([
Tabs::make('Settings')
->tabs([
Tabs\Tab::make(__('General'))
->icon('heroicon-o-cog')
->schema([
TextInput::make('site_name')->label(__('Site Name')),
Toggle::make('maintenance_mode')->label(__('Maintenance Mode')),
]),
Tabs\Tab::make(__('Email'))
->icon('heroicon-o-envelope')
->schema([
TextInput::make('smtp_host')->label(__('SMTP Host')),
TextInput::make('smtp_port')->label(__('SMTP Port')),
]),
])
->persistTabInQueryString(),
]);
}
```
**Key Points:**
- `persistTabInQueryString()` syncs tab state with URL (client-side only)
- For tables with actions, see "Tabs with Dynamic Table Content" pattern above
**Text component (no label method):**
```php
// CORRECT - Text takes content directly
Text::make(__('Your message here'))
Text::make($this->someVariable)
// WRONG - Text doesn't have label() or content() methods
Text::make('name')->label('Label')->content('Content')
```
## Debugging & Verification
### IMPORTANT: Always Clear Laravel Cache After Tasks
After completing any task that modifies PHP files, views, or configuration, **ALWAYS clear Laravel cache**:
```bash
cd /var/www/jabali && php artisan optimize:clear
```
This is equivalent to running:
```bash
php artisan config:clear
php artisan view:clear
php artisan cache:clear
php artisan route:clear
php artisan event:clear
```
**When to clear cache:**
- After modifying any PHP file in `app/`
- After modifying any Blade template in `resources/views/`
- After modifying `.env` or `config/` files
- After adding or removing files
- Before taking screenshots to verify changes
### Always Diagnose with Artisan
When encountering errors or unexpected behavior, use `php artisan` commands to diagnose:
```bash
# Check PHP syntax errors
php -l app/Filament/Admin/Pages/YourPage.php
# Clear all caches (do this after code changes)
php artisan config:clear
php artisan view:clear
php artisan cache:clear
php artisan route:clear
# Rebuild caches for production
php artisan config:cache
php artisan view:cache
php artisan route:cache
# Check routes are registered
php artisan route:list --name=filament
# Debug with tinker
php artisan tinker
# Check for missing migrations
php artisan migrate:status
# View logs in real-time
php artisan pail
```
### Common Error Patterns
| Error | Cause | Fix |
|-------|-------|-----|
| `TypeError: must be of type X, Y given` | Wrong type hint (e.g., `Form` vs `Schema` in Filament v4) | Check method signatures match framework version |
| `Unable to locate component` | Blade component doesn't exist | Check component name spelling and namespace |
| `Class not found` | Missing import or autoload | Run `composer dump-autoload`, check `use` statements |
| `View not found` | Missing blade template | Create the view file in correct location |
### Screenshots for UI Verification
Use Puppeteer scripts in `/tmp/` to take screenshots of authenticated pages. Always save screenshots to `/tmp/`:
```bash
# For authenticated admin panel pages, use Puppeteer scripts in /tmp/
# Example: /tmp/security-overview.js
node /tmp/security-overview.js
# Then use the Read tool to view the screenshot
# Read /tmp/security-overview.png to see the result
```
**Example Puppeteer script** (`/tmp/screenshot-page.js`):
```javascript
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors']
});
const page = await browser.newPage();
await page.setViewport({ width: 1600, height: 1200 });
// Login to admin panel
await page.goto('https://jabali.lan/jabali-admin/login', { waitUntil: 'networkidle0' });
await new Promise(r => setTimeout(r, 1000));
await page.type('input[type="email"]', 'admin@jabali.lan');
await page.type('input[type="password"]', '123123123');
await page.click('button[type="submit"]');
await new Promise(r => setTimeout(r, 5000));
// Navigate to target page
await page.goto('https://jabali.lan/jabali-admin/security', { waitUntil: 'networkidle0', timeout: 30000 });
await new Promise(r => setTimeout(r, 3000));
await page.screenshot({ path: '/tmp/security.png', fullPage: true });
await browser.close();
})();
```
**Important:** Always save screenshots to `/tmp/` directory (e.g., `/tmp/page.png`).
**When to use screenshots:**
- After making UI/layout changes to verify they look correct
- When debugging visual issues that are hard to diagnose from code
- To check responsive design on different viewport sizes
- Before committing changes to ensure nothing is visually broken
**Workflow:**
1. Make code changes
2. Run `php -l` to check syntax
3. Clear caches with `php artisan view:clear`
4. Create/update Puppeteer script in `/tmp/` and run with `node`
5. Use Read tool to view the screenshot
## Testing
```bash
composer test # Run all tests
php artisan test --filter=ClassName # Run specific test
```
## Common Tasks
### Adding a New Filament Page
1. Create page class in `app/Filament/{Panel}/Pages/`
2. Create blade view in `resources/views/filament/{panel}/pages/`
3. Register in panel provider if needed
### Adding Agent Actions
1. Add action handler in `bin/jabali-agent` (match statement)
2. Implement function with proper validation
3. Call from PHP:
```php
use App\Services\Agent\AgentClient;
$client = new AgentClient();
$response = $client->send('category.action', ['param1' => 'value1']);
```
### Agent Best Practices
**Service Restart Operations:**
When agent actions restart services (fail2ban, nginx, postfix, etc.), add a wait period before returning success. This ensures the UI gets accurate status when it reloads:
```php
function myServiceToggle(array $params): array
{
// ... modify config ...
// Restart the service
exec('systemctl restart myservice 2>&1', $output, $code);
// Wait for service to be fully ready before returning
if ($code === 0) {
sleep(2); // Give service time to fully start
exec('systemctl is-active myservice 2>/dev/null', $statusOutput);
}
return ['success' => $code === 0, 'error' => $code !== 0 ? implode("\n", $output) : null];
}
```
**Fallback Data:**
For data that may not be available from the source (e.g., descriptions from filter files), provide fallback mappings:
```php
$descriptions = [
'postfix-sasl' => 'Postfix SASL authentication protection',
'dovecot' => 'Dovecot IMAP/POP3 server protection',
// ... more fallbacks
];
$description = getFromSource() ?: ($descriptions[$name] ?? '');
```
### Working with Settings
```php
use App\Models\DnsSetting;
// Get setting with default
$value = DnsSetting::get('setting_key', 'default');
// Set setting
DnsSetting::set('setting_key', $value);
```
## File Paths
| Path | Description |
|------|-------------|
| `/home/{user}/domains/{domain}/public_html` | Domain web root |
| `/home/{user}/backups/` | User backups (tar.gz files) |
| `/var/backups/jabali/` | Server backups (admin-created) |
| `/var/vmail/{domain}/{user}` | Email mailbox storage |
| `/var/run/jabali/agent.sock` | Agent socket |
| `/var/log/jabali/agent.log` | Agent log (includes security audit) |
| `/etc/nginx/sites-available/` | Nginx vhosts |
| `/etc/letsencrypt/` | SSL certificates |
| `/etc/bind/zones/` | DNS zone files |
| `/etc/bind/keys/{domain}/` | DNSSEC keys |
## Environment Variables
Key `.env` settings:
- `APP_URL` - Panel URL
- `PANEL_DOMAIN` - Panel hostname
- `DB_*` - Database connection
- `MAIL_*` - Mail configuration
## Dependencies
- PHP 8.2+, Laravel 12, Filament 4, Livewire 3
- Nginx, PHP-FPM, MariaDB/MySQL
- Postfix, Dovecot, OpenDKIM
- BIND9 (named), Certbot
- Redis (optional caching)
## Internationalization (i18n) & Translations
The panel supports multiple languages with full RTL (right-to-left) support for Arabic and Hebrew.
### Supported Languages
| Code | Language | Native Name | Direction |
|------|----------|-------------|-----------|
| `en` | English | English | LTR |
| `es` | Spanish | Español | LTR |
| `fr` | French | Français | LTR |
| `ru` | Russian | Русский | LTR |
| `pt` | Portuguese | Português | LTR |
| `ar` | Arabic | العربية | RTL |
| `he` | Hebrew | עברית | RTL |
### Configuration
**Language settings:** `config/languages.php`
```php
// Get supported languages
$languages = config('languages.supported');
// Check if language is RTL
$isRtl = config('languages.supported.ar.direction') === 'rtl';
```
### Translation Files
| File | Description | Entries |
|------|-------------|---------|
| `lang/en.json` | English (base) | 602 |
| `lang/es.json` | Spanish | 1171 |
| `lang/fr.json` | French | 887 |
| `lang/ru.json` | Russian | 887 |
| `lang/pt.json` | Portuguese | 880 |
| `lang/ar.json` | Arabic | 880 |
| `lang/he.json` | Hebrew | 880 |
### Using Translations
**In PHP/Blade:**
```php
// Simple translation
__('Dashboard')
// With parameters
__('Welcome, :name', ['name' => $user->name])
// In Filament components
TextColumn::make('status')->label(__('Status'))
Section::make(__('Settings'))->description(__('Configure your options'))
```
**Best Practices:**
- Always wrap user-facing strings with `__()`
- Use English as the key: `__('Create Domain')` not `__('domain.create')`
- Parameters use `:name` syntax for substitution
- Keep translations concise and contextually appropriate
### Adding New Translations
1. **Add the English string** in your code using `__('Your string')`
2. **Add translations** to each language file in `lang/`:
```bash
# Example: Adding "New Feature" translation
# Edit each file: lang/es.json, lang/fr.json, etc.
```
```json
{
"New Feature": "Nueva Función"
}
```
3. **For bulk additions**, create entries in all language files:
```php
// lang/es.json
{
"New Feature": "Nueva Función",
"Another String": "Otra Cadena"
}
// lang/fr.json
{
"New Feature": "Nouvelle Fonctionnalité",
"Another String": "Une Autre Chaîne"
}
```
### RTL Language Support
Arabic and Hebrew use right-to-left text direction. Filament handles RTL automatically when the locale is set. For custom components:
```blade
{{-- Use Tailwind RTL utilities --}}
<div class="rtl:text-right ltr:text-left">Content</div>
<div class="rtl:mr-4 ltr:ml-4">Indented content</div>
```
### Login Page Word Clouds
Both admin and user panels feature typographic word cloud backgrounds on login pages:
- **Admin Panel:** "Administrator" in all supported languages (red, diagonal pattern)
- **User Panel:** "Client Dashboard" in all supported languages (blue, diagonal pattern)
The word clouds use fonts that support all scripts: `"Segoe UI", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", sans-serif`
### Extracting Translatable Strings
To find all translatable strings in the codebase:
```bash
# Find all __() calls in PHP files
grep -roh "__('[^']*')" app/ resources/ --include="*.php" | sort | uniq
# Find all __() calls in Blade files
grep -roh "__('[^']*')" resources/ --include="*.blade.php" | sort | uniq
```
### Language Switcher
Users can change their language preference through their profile settings. The selected language is stored in the session and persists across requests.
===
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.16
- filament/filament (FILAMENT) - v5
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- livewire/livewire (LIVEWIRE) - v4
- laravel/mcp (MCP) - v0
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- phpunit/phpunit (PHPUNIT) - v11
- tailwindcss (TAILWINDCSS) - v4
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters.
## URLs
- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'.
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit".
3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order.
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit".
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms.
=== php rules ===
## PHP
- Always use strict typing at the head of a `.php` file: `declare(strict_types=1);`.
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries.
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
## Laravel 12
- Use the `search-docs` tool to get version-specific documentation.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
### Laravel 12 Structure
- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`.
- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration.
- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration.
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== livewire/core rules ===
## Livewire
- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests.
- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components.
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle Hook Examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire Component Test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing Livewire Component Exists on Page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
=== phpunit/core rules ===
## PHPUnit
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test.
- If you see a test using "Pest", convert it to PHPUnit.
- Every time a test has been updated, run that singular test.
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
- Tests should test all of the happy paths, failure paths, and weird paths.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.
### Running Tests
- Run the minimal number of tests, using an appropriate filter, before finalizing.
- To run all tests: `php artisan test --compact`.
- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file).
=== tailwindcss/core rules ===
## Tailwind CSS
- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.).
- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically.
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing; don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v4 rules ===
## Tailwind CSS 4
- Always use Tailwind CSS v4; do not use the deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed.
<code-snippet name="Extending Theme in CSS" lang="css">
@theme {
--color-brand: oklch(0.72 0.11 178);
}
</code-snippet>
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff">
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
### Replaced Utilities
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement.
- Opacity values are still numeric.
| Deprecated | Replacement |
|------------+--------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
=== filament/blueprint rules ===
## Filament Blueprint
You are writing Filament v5 implementation plans. Plans must be specific enough
that an implementing agent can write code without making decisions.
**Start here**: Read
`/vendor/filament/blueprint/resources/markdown/planning/overview.md` for plan format,
required sections, and what to clarify with the user before planning.
### Filament v5 Key Namespaces
| Element | Namespace Pattern |
| -------------- | ----------------------------------------- |
| Form fields | `Filament\Forms\Components\{Component}` |
| Table columns | `Filament\Tables\Columns\{Column}` |
| Table filters | `Filament\Tables\Filters\{Filter}` |
| Actions | `Filament\Actions\{Action}` |
| Infolist | `Filament\Infolists\Components\{Entry}` |
| Layout | `Filament\Schemas\Components\{Component}` |
| Reactive utils | `Filament\Schemas\Components\Utilities\*` |
### Blueprint Planning Files
When creating Filament implementation plans, reference these files:
| File | Purpose |
| -------------------- | -------------------------------------- |
| overview.md | Plan structure and required sections |
| models.md | Model attributes, relationships, enums |
| resources.md | Resource specification format |
| custom-pages.md | Standalone pages and resource pages |
| forms.md | Form field components and validation |
| tables.md | Table columns, filters, summarizers |
| infolists.md | Infolist entries for View pages |
| schema-layouts.md | Layout components (Section, Tabs, etc) |
| relationships.md | Relationship handling decisions |
| actions.md | Custom actions with modals |
| reactive-fields.md | Reactive fields (Get/Set) |
| authorization.md | Policies and permissions |
| widgets.md | Dashboard widgets |
| testing.md | Test specifications |
| checklist.md | Pre-submission checklist |
Files located at: `/vendor/filament/blueprint/resources/markdown/planning/`
</laravel-boost-guidelines>