2020 lines
74 KiB
Markdown
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` | `q1w2E#R$` |
|
|
| 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"]', 'q1w2E#R$');
|
|
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>
|