12 KiB
12 KiB
Changelog
All notable changes to this project. Format inspired by Keep a Changelog and Semantic Versioning.
[Unreleased] — advanced branch
Real-time Dashboard & Custom Widgets (2026-05-16)
Added
DashboardStatsUpdatedevent (app/Events/DashboardStatsUpdated.php) — implementsShouldBroadcast, broadcasts slim stats payload (cpu, ram, disk, users, queues, uptime) to theadmin.monitoringprivate channel asstats.updated. Replaces the previoussetIntervalpolling loop.BroadcastDashboardStatsartisan command (app/Console/Commands/BroadcastDashboardStats.php) — signature:dashboard:broadcast-stats. Clearsmonitoring_full_bundlecache, callsSystemMonitoringService::getAll(), dispatchesDashboardStatsUpdated. Scheduled every minute withwithoutOverlapping().- Dashboard real-time listener —
window.Echo.private('admin.monitoring').listen('.stats.updated', applyStats)indashboard.blade.php. Falls back tosetInterval(refreshStats, 30000)only when Reverb is not connected. dashboard_widget_preferencestable (migration2026_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).DashboardWidgetPreferencemodel (app/Models/DashboardWidgetPreference.php) —forUser(int $userId)merges static defaults with saved DB prefs, sorted bysort_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.phpwidget-live-users.blade.php,widget-queues.blade.phpwidget-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@saveWidgetPreferencesnameddashboard.widgets.save. - SortableJS loaded via CDN in
dashboard.blade.phpfor 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). CheckTabPermissionmiddleware — enforces tab-level access control when navigating to specific settings tabs.@cantab/@managetabBlade 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-contentglobally styled withborder-radius: 20pxandoverflow: clip(clip prevents scroll container creation while still clipping visually). - Modal width —
modal-xlandmodal-permissionmodals set tocalc(70vw - 40px)globally viaapp.blade.php.
Fixed
- Modal scroll broken —
overflow: hiddenwas replaced withoverflow: clip.clipclips 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()innavigation.blade.php. Usesdata-sidebar-toggleattribute pattern,e.stopPropagation(), andcloneNode()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 melewatiexpires_atdipangkas otomatis setiap kalimodel:pruneberjalan. Sebelumnya record expired menumpuk tanpa batas.UserTrustedDevice— Prunable trait: Device entry yang sudah melewatiexpires_atdipangkas otomatis. Konsisten dengan OtpCode — keduanya prune berdasarkan kolomexpires_atyang 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 denganai_usage_logsdanmobile_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=48dijadwalkan 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 —
HealthTestrewrites,MobileConfigTestrewrites (ETag + 304).
Schema
2026_05_12_120000_add_social_columns_to_users_table—google_id,facebook_id,github_idcolumns (previously referenced inUser::$fillablewith 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 forroles.{created_by,updated_by}andpermissions.{created_by,updated_by}withON DELETE SET NULL. Orphan ids are nulled before constraint creation.
Code
App\Services\Monitoring\MonitoringFormatter— extracted fromSystemMonitoringService. 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— privateparseBytes/formatBytesdelegate toMonitoringFormatter. Single canonical implementation.routes/web.php—/auth/callbacknow declared before/auth/{provider}so OAuth callback is reachable.{provider}constrained togoogle|facebook|github.app/Http/Controllers/Api/HealthController— returns503only when at least one check hasstatus=fail;warnkeeps200withstatus=warn. Matches OpenAPI annotation.declare(strict_types=1)on core utility classes (MonitoringFormatter,SettingValueCaster,SettingFileUploader,ApiResponse, all new exceptions).
Fixed
SystemConfigServicestale 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 ininvalidateCache().- OAuth callback unreachable —
/auth/{provider}ordered before/auth/callbackcaused the wildcard to swallow/auth/callbackand resolve it asredirect('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/loginwith a generic error (avoids account enumeration). - Missing migration for OAuth columns —
users.{google_id,facebook_id,github_id}were in$fillablebut no migration created them. Added. HealthController503 false-alarm —every($checks, status === 'ok')treated storagewarn(>90% disk) as unhealthy. Now: onlyfailtriggers 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
SecurityHeadersmiddleware (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: extractSystemInfoProviderinterface +LinuxSystemInfoProvider/WindowsSystemInfoProvider/FakeSystemInfoProviderso the service can be unit-tested without mocking globals. Deferred to a dedicated change.laragear/webauthnis marked abandoned upstream. Replacement:laravel/passkeys. Tracked separately because the public surface differs.- Mobile
expo/@expo/cli/@expo/metro-config/postcsschain has 4 moderate CVEs reachable only in dev-build tooling. Fix requires Expo SDK bump (breaking change).