Files
biiproject-kit-v1/CHANGELOG.md
T

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

  • 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 listenerwindow.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/widgetsDashboardController@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 widthmodal-xl and modal-permission modals set to calc(70vw - 40px) globally via app.blade.php.

Fixed

  • Modal scroll brokenoverflow: 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 securityTwoFactorTest (10), SocialAuthTest (9), ImpersonateTest (9), WebAuthnConfigTest (6).
  • Authorization & gatingRoleManagementTest (13), PermissionManagementTest (10), UserManagementTest (14), CheckActivePermissionTest (4), IpAccessControlTest (8), CheckLegalAgreementTest (4), PasswordExpiryMiddlewareTest (4), SecurityHeadersTest (6).
  • Service layerSystemConfigServiceTest (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 integrityCascadeIntegrityTest (9) locking FK + soft-delete contracts.
  • PerformanceNPlusOneTest (3) regression locks for datatables.
  • Rate limitingRateLimitTest (6) covering login/register/forgot-password/OTP/2FA + per-IP isolation.
  • API contractsHealthTest rewrites, MobileConfigTest rewrites (ETag + 304).

Schema

  • 2026_05_12_120000_add_social_columns_to_users_tablegoogle_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

  • SystemConfigService1.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 cacheupdate() 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 columnsusers.{google_id,facebook_id,github_id} were in $fillable but no migration created them. Added.
  • HealthController 503 false-alarmevery($checks, status === 'ok') treated storage warn (>90% disk) as unhealthy. Now: only fail triggers 503.
  • Pest flake in UserManagementTestfake()->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).