# Changelog

All notable changes to this project. Format inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and **Semantic Versioning**.

## [Unreleased] — `advanced` branch

### Real-time Dashboard & Custom Widgets (2026-05-16)

#### Added

- **`DashboardStatsUpdated` event** (`app/Events/DashboardStatsUpdated.php`) — implements `ShouldBroadcast`, broadcasts slim stats payload (cpu, ram, disk, users, queues, uptime) to the `admin.monitoring` private channel as `stats.updated`. Replaces the previous `setInterval` polling loop.
- **`BroadcastDashboardStats` artisan command** (`app/Console/Commands/BroadcastDashboardStats.php`) — signature: `dashboard:broadcast-stats`. Clears `monitoring_full_bundle` cache, calls `SystemMonitoringService::getAll()`, dispatches `DashboardStatsUpdated`. Scheduled every minute with `withoutOverlapping()`.
- **Dashboard real-time listener** — `window.Echo.private('admin.monitoring').listen('.stats.updated', applyStats)` in `dashboard.blade.php`. Falls back to `setInterval(refreshStats, 30000)` only when Reverb is not connected.
- **`dashboard_widget_preferences` table** (migration `2026_05_16_220000_create_dashboard_widget_preferences_table.php`) — columns: `user_id` (FK → users, cascade delete), `widget_key` (varchar 64), `visible` (boolean), `sort_order` (smallint). Unique constraint on `(user_id, widget_key)`.
- **`DashboardWidgetPreference` model** (`app/Models/DashboardWidgetPreference.php`) — `forUser(int $userId)` merges static defaults with saved DB prefs, sorted by `sort_order`. `defaults()` defines 7 widgets: `cpu`, `ram`, `disk`, `live_users`, `queues`, `activity_feed`, `ai_insight`.
- **Widget partials** in `resources/views/pages/dashboard/`:
  - `widget-cpu.blade.php`, `widget-ram.blade.php`, `widget-disk.blade.php`
  - `widget-live-users.blade.php`, `widget-queues.blade.php`
  - `widget-quick-actions.blade.php` (quick nav links to common admin pages)
- **Dashboard Customize panel** — floating "Customize" button reveals a slide-down panel with per-widget toggle switches, drag-to-reorder via **SortableJS**, Save Layout button (POSTs to `dashboard.widgets.save`), and Reset to Default button.
- **`DashboardController@saveWidgetPreferences`** — validates and upserts widget prefs (key, visible, sort_order) per user.
- **`DashboardController@resetWidgetPreferences`** — deletes all prefs for authenticated user, restoring defaults.
- **Route** `POST /dashboard/widgets` → `DashboardController@saveWidgetPreferences` named `dashboard.widgets.save`.
- **SortableJS** loaded via CDN in `dashboard.blade.php` for drag-to-reorder widget grid.

---

### Granular Tab Permission System (2026-05-16)

#### Added

- **85 granular tab-level permissions** covering every settings tab in Global Settings and Mobile Settings panels (e.g., `view global settings branding`, `manage global settings password policy`, `view mobile settings kill switch`).
- **`CheckTabPermission` middleware** — enforces tab-level access control when navigating to specific settings tabs.
- **`@cantab` / `@managetab` Blade directives** — shorthand for checking tab-level permissions in views.
- **Tree-structured UI in role modals** — permission list in Create/Edit Role modals shows permissions grouped in a collapsible tree, making it easier to assign tab-level permissions.
- **Two-panel drag-and-drop permission picker** — role modals have an Available panel and an Assigned panel; items can be dragged between panels or double-clicked to move. Both panels display **category group headers** that appear/disappear dynamically as items move between panels. Multi-select supported.

---

### UI & UX Improvements (2026-05-16)

#### Added

- **Modal border-radius** — `.modal-content` globally styled with `border-radius: 20px` and `overflow: clip` (clip prevents scroll container creation while still clipping visually).
- **Modal width** — `modal-xl` and `modal-permission` modals set to `calc(70vw - 40px)` globally via `app.blade.php`.

#### Fixed

- **Modal scroll broken** — `overflow: hidden` was replaced with `overflow: clip`. `clip` clips visually without creating a new scroll container, so scrollable children inside modals work correctly.
- **Sidebar submenu cannot close** — replaced Alpine.js approach (which was intercepted by the AdminUIUX theme) with vanilla JS `initSidebarSubmenus()` in `navigation.blade.php`. Uses `data-sidebar-toggle` attribute pattern, `e.stopPropagation()`, and `cloneNode()` to prevent duplicate event listeners. Chevron rotates 180° on open via inline style transform, not Alpine transitions.

---

### Tests (+84, total 371 passing) (2026-05-16)

- Added test coverage for tab permission middleware, widget preference CRUD, and real-time broadcast event.

---

### Data Retention Wave (2026-05-15)

#### Added

- **`OtpCode` — Prunable trait**: OTP code yang sudah melewati `expires_at` dipangkas otomatis setiap kali `model:prune` berjalan. Sebelumnya record expired menumpuk tanpa batas.
- **`UserTrustedDevice` — Prunable trait**: Device entry yang sudah melewati `expires_at` dipangkas otomatis. Konsisten dengan OtpCode — keduanya prune berdasarkan kolom `expires_at` yang sudah ada.
- **`AiHealingLog` — Prunable trait, retensi 90 hari**: Log AI self-healing sebelumnya tidak memiliki retention policy. Sekarang dipangkas setelah 90 hari (`created_at`), sejajar dengan `ai_usage_logs` dan `mobile_error_logs`.
- **`PasswordHistory` — Prunable trait, retensi 365 hari**: History password disimpan untuk N-reuse check. Record di luar 365 hari tidak lagi relevan untuk kebijakan reuse dan kini dipangkas otomatis.
- **`telescope:prune --hours=48`** dijadwalkan harian pukul 03:05: Telescope debugging data sebelumnya tumbuh tanpa batas di production. Entry lebih dari 48 jam dipangkas setelah prune model selesai.

#### Summary Retention Matrix (setelah wave ini)

| Model / Table | Retensi | Basis |
|---|---|---|
| `otp_codes` | Setelah expired | `expires_at < now()` |
| `user_trusted_devices` | Setelah expired | `expires_at < now()` |
| `ai_healing_logs` | 90 hari | `created_at` |
| `password_histories` | 365 hari | `created_at` |
| `mobile_error_logs` | 90 hari | `occurred_at` |
| `ai_usage_logs` | 90 hari | `created_at` |
| `mobile_sync_logs` | 30 hari | `synced_at` |
| `notifications` | 30 hari | `created_at` |
| `activity_log` | 365 hari | Spatie config |
| `telescope_entries` | 48 jam | `telescope:prune` |

---

### Added

#### Tests (+287, subtotal 287 passing)

- **Auth security** — `TwoFactorTest` (10), `SocialAuthTest` (9), `ImpersonateTest` (9), `WebAuthnConfigTest` (6).
- **Authorization & gating** — `RoleManagementTest` (13), `PermissionManagementTest` (10), `UserManagementTest` (14), `CheckActivePermissionTest` (4), `IpAccessControlTest` (8), `CheckLegalAgreementTest` (4), `PasswordExpiryMiddlewareTest` (4), `SecurityHeadersTest` (6).
- **Service layer** — `SystemConfigServiceTest` (15), `PasswordPolicyServiceTest` (9), `BackupManagementServiceTest` (7).
- **Pure unit** (no Laravel boot) — `SettingValueCasterTest` (23), `ActivityFormatterTest` (22), `MonitoringFormatterTest` (14), `SessionHelperTest` (12), `PasswordRuleHelperTest` (8), `ApiResponseTest` (10), `CustomExceptionsTest` (4).
- **Database integrity** — `CascadeIntegrityTest` (9) locking FK + soft-delete contracts.
- **Performance** — `NPlusOneTest` (3) regression locks for datatables.
- **Rate limiting** — `RateLimitTest` (6) covering login/register/forgot-password/OTP/2FA + per-IP isolation.
- **API contracts** — `HealthTest` rewrites, `MobileConfigTest` rewrites (ETag + 304).

#### Schema

- `2026_05_12_120000_add_social_columns_to_users_table` — `google_id`, `facebook_id`, `github_id` columns (previously referenced in `User::$fillable` with no migration).
- `2026_05_14_100000_add_performance_indexes` — composite/secondary indexes:
  - `password_histories(user_id, created_at)`
  - `system_setting_revisions(key, created_at)` and `(changed_by)`
  - `notifications(notifiable_type, notifiable_id, read_at)`
- `2026_05_14_110000_add_fk_to_audit_columns` — FK constraints for `roles.{created_by,updated_by}` and `permissions.{created_by,updated_by}` with `ON DELETE SET NULL`. Orphan ids are nulled before constraint creation.

#### Code

- `App\Services\Monitoring\MonitoringFormatter` — extracted from `SystemMonitoringService`. Methods: `bytes()`, `parseBytes()`, `duration()`.
- `App\Services\SystemConfig\{SettingDefinitions,SettingValueCaster,SettingFileUploader}` — extracted from a 1.272-line god class.
- `App\Exceptions\SystemConfigException` — factories: `unknownKey()`, `imageUploadFailed()`.
- `App\Exceptions\BackupOperationException` — factories: `missingBinary()`, `diskNotConfigured()`, `restoreFailed()`.
- `App\Exceptions\MonitoringException` — factories: `unsupportedOs()`, `probeFailed()`.

#### Tooling & Docs

- **Larastan level 5** with baseline (`phpstan.neon` + `phpstan-baseline.neon`). 82 pre-existing findings are silenced; new code must stay clean.
- **Laravel Pint** applied across the codebase (267 files PSR-12 compliant).
- **CI workflow** (`.github/workflows/ci.yml`) — 3 jobs: `test`, `lint` (pint + composer audit + permissions:audit), `static-analysis` (Larastan).
- **`SECURITY.md`** — vulnerability reporting + supply-chain advisory record + defense-in-depth overview.
- **`CHANGELOG.md`** — this file.
- All major Markdown docs refreshed (`README.md`, `TECH_STACK.md`, `USER_GUIDE.md`, `DEPLOYMENT_GUIDE.md`, `mobile/README.md`).

### Changed

- `SystemConfigService` — **1.272 → 197 lines** (-85%). Public API preserved.
- `BackupManagementService` — private `parseBytes`/`formatBytes` delegate to `MonitoringFormatter`. Single canonical implementation.
- `routes/web.php` — `/auth/callback` now declared before `/auth/{provider}` so OAuth callback is reachable. `{provider}` constrained to `google|facebook|github`.
- `app/Http/Controllers/Api/HealthController` — returns `503` only when at least one check has `status=fail`; `warn` keeps `200` with `status=warn`. Matches OpenAPI annotation.
- `declare(strict_types=1)` on core utility classes (`MonitoringFormatter`, `SettingValueCaster`, `SettingFileUploader`, `ApiResponse`, all new exceptions).

### Fixed

- **`SystemConfigService` stale static cache** — `update()` did not reset `$resolvedSettings`, causing a second update in the same request to compare against pre-update values and silently skip the write. Fixed by always nulling the static in `invalidateCache()`.
- **OAuth callback unreachable** — `/auth/{provider}` ordered before `/auth/callback` caused the wildcard to swallow `/auth/callback` and resolve it as `redirect('callback')`, which 404s on the missing feature flag. Fixed by reordering routes.
- **OAuth identity-overwrite** — callback fell through to `User::create()` on email collision with a different provider id, throwing a unique-violation 500. Fixed by explicitly refusing the link and redirecting to `/login` with a generic error (avoids account enumeration).
- **Missing migration for OAuth columns** — `users.{google_id,facebook_id,github_id}` were in `$fillable` but no migration created them. Added.
- **`HealthController` 503 false-alarm** — `every($checks, status === 'ok')` treated storage `warn` (>90% disk) as unhealthy. Now: only `fail` triggers 503.
- **Pest flake in `UserManagementTest`** — `fake()->name()` could produce strings outside the controller's `/^[a-zA-Z\s]+$/` regex, intermittently failing the update tests under full-suite runs. Hardcoded display names.

### Security

- `SecurityHeaders` middleware (wired globally) — `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, `X-XSS-Protection`, HSTS (HTTPS + opt-in).
- Test coverage for the entire auth boundary: 2FA, Impersonate, SocialAuth (incl. identity-overwrite protection), 4 middleware (`IpAccessControl`, `CheckActivePermission`, `PasswordExpiry`, `CheckLegalAgreement`).
- Rate-limit regression tests prevent silent throttle removal.

### Known Follow-ups

- `SystemMonitoringService` (~600 lines, `shell_exec`/`disk_*`/`sys_getloadavg`) still tightly coupled to OS calls. Refactor target: extract `SystemInfoProvider` interface + `LinuxSystemInfoProvider`/`WindowsSystemInfoProvider`/`FakeSystemInfoProvider` so the service can be unit-tested without mocking globals. Deferred to a dedicated change.
- `laragear/webauthn` is marked **abandoned** upstream. Replacement: `laravel/passkeys`. Tracked separately because the public surface differs.
- Mobile `expo`/`@expo/cli`/`@expo/metro-config`/`postcss` chain has **4 moderate** CVEs reachable only in dev-build tooling. Fix requires Expo SDK bump (breaking change).
