# ConstruProgress Improvement Plan > **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. **Goal:** Improve performance, security, real-time capabilities, and developer experience of the ConstruProgress Laravel + Livewire application. **Architecture:** Incremental enhancements to existing Laravel 11 + Livewire 3 + Spatie Permissions stack. No breaking changes to API. **Tech Stack:** Laravel 12, Livewire 3.6, Alpine.js, SQLite (tests) / MySQL (prod), PHP 8.2+, Pest/PHPUnit, Laravel Pint. --- ## Phase 1: Performance & Caching (High ROI, Low Risk) ### Task 1.1: Install and configure Redis **Objective:** Add Redis for caching and queue worker **Files:** - Create: `config/redis.php` (if missing) - Modify: `.env.example` — add REDIS_* vars - Modify: `config/cache.php` — set default to redis - Modify: `config/queue.php` — set default to redis - Modify: `composer.json` — add `predis/predis` or `phpredis` ext **Step 1: Add Redis dependency** ```bash cd /mnt/c/xampp/htdocs/construprogress && /mnt/c/xampp/php/php.exe composer require predis/predis --no-interaction ``` **Step 2: Configure cache and queue** ```php // config/cache.php 'default' => env('CACHE_DRIVER', 'redis'), // config/queue.php 'default' => env('QUEUE_CONNECTION', 'redis'), ``` **Step 3: Verify** ```bash /mnt/c/xampp/php/php.exe artisan config:clear && /mnt/c/xampp/php/php.exe artisan test --filter="Cache" ``` **Commit:** `feat: add Redis for caching and queues` --- ### Task 1.2: Cache ProjectDashboard queries **Objective:** Cache expensive dashboard queries (projects, tasks, issues, notifications) **Files:** - Modify: `routes/web.php:59-95` (dashboard closure) → move to `App\Http\Controllers\DashboardController` - Create: `app/Http/Controllers/DashboardController.php` - Modify: `resources/views/dashboard.blade.php` (if exists) **Step 1: Create controller with cache** ```php // app/Http/Controllers/DashboardController.php public function index() { $user = Auth::user(); $cacheKey = "dashboard:{$user->id}:v1"; $data = Cache::remember($cacheKey, 60, function () use ($user) { // ... existing query logic }); return view('dashboard', $data); } ``` **Step 2: Invalidate on relevant events** ```php // In Task/Issue/Project observers or model events Cache::forget("dashboard:{$user->id}:v1"); ``` **Step 3: Test** ```bash /mnt/c/xampp/php/php.exe artisan test --filter="Dashboard" ``` **Commit:** `perf: cache dashboard queries with 60s TTL` --- ### Task 1.3: Cache ReportController generate/preview **Objective:** Cache report generation (HTML + Excel) per project + filters **Files:** - Modify: `app/Http/Controllers/ReportController.php` (generate, preview methods) - Modify: `app/Services/ReportGenerator.php` — add cache key generation **Step 1: Add cache to ReportController::generate()** ```php $cacheKey = "report:{$project->id}:".md5(json_encode($filters->toArray())); $data = Cache::remember($cacheKey, 300, fn () => $generator->generate()); ``` **Step 2: Invalidate on progress/inspection/issue changes** ```php // Feature, Inspection, Issue observers Cache::tags(["report:project:{$project->id}"])->flush(); ``` **Commit:** `perf: cache report generation with 5min TTL` --- ### Task 1.4: Optimize N+1 queries in Livewire components **Objective:** Fix N+1 in ProjectMap, TaskManager, IssueManager **Files:** - Modify: `app/Livewire/Projects/ProjectMap.php` (mount, loadTemplates, selectFeature) - Modify: `app/Livewire/Tasks/TaskManager.php` (render) - Modify: `app/Livewire/Issues/IssueManager.php` (render) **Step 1: Audit current eager loading** ```bash grep -n "with(" app/Livewire/Projects/ProjectMap.php grep -n "with(" app/Livewire/Tasks/TaskManager.php grep -n "with(" app/Livewire/Issues/IssueManager.php ``` **Step 2: Add missing relationships** ```php // ProjectMap mount() ->with(['layers.features.template', 'layers.features.inspections', 'layers.features.issues']) // TaskManager render() ->with(['project', 'phase', 'assignee', 'creator', 'subtasks.parentTask']) // IssueManager render() ->with(['project', 'assignee', 'reporter', 'feature.layer.phase', 'checklistItems', 'comments.user']) ``` **Step 3: Verify with Laravel Debugbar or query log** ```bash /mnt/c/xampp/php/php.exe artisan test --filter="MapTablesTest|IssuesTablePageTest" ``` **Commit:** `perf: fix N+1 queries in Map, TaskManager, IssueManager` --- ## Phase 2: Real-time Notifications (High Impact) ### Task 2.1: Install Laravel Reverb **Objective:** Add WebSocket server for real-time events **Files:** - Modify: `composer.json` — add `laravel/reverb` - Create: `config/reverb.php` (via vendor:publish) - Modify: `.env.example` — REVERB_* vars - Modify: `bootstrap.js` / `resources/js/app.js` — Echo + Reverb client **Step 1: Install** ```bash /mnt/c/xampp/php/php.exe composer require laravel/reverb --no-interaction /mnt/c/xampp/php/php.exe artisan reverb:install npm install --save-dev laravel-echo pusher-js ``` **Step 2: Configure broadcasting** ```php // config/broadcasting.php 'default' => env('BROADCAST_DRIVER', 'reverb'), ``` **Step 3: Update frontend** ```javascript // resources/js/app.js import Echo from 'laravel-echo'; window.Pusher = require('pusher-js'); window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT, wssPort: import.meta.env.VITE_REVERB_PORT, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], }); ``` **Commit:** `feat: add Laravel Reverb for real-time` --- ### Task 2.2: Broadcast notifications via Reverb **Objective:** Replace polling with real-time notification delivery **Files:** - Modify: `app/Notifications/*.php` — implement `ShouldBroadcast` + `broadcastOn()` - Modify: `resources/views/layouts/navigation.blade.php` (or notification bell component) — listen for Echo events - Create: `resources/js/components/NotificationBell.vue` (or Alpine component) **Step 1: Update base notification** ```php // app/Notifications/Notification.php (or trait) use Illuminate\Contracts\Broadcasting\ShouldBroadcast; class Notification implements ShouldBroadcast { public function broadcastOn(): array { return [new PrivateChannel('user.'.$this->notifiable->id)]; } } ``` **Step 2: Frontend listener** ```html ``` **Step 3: Test with two browser tabs** ```bash # Start reverb /mnt/c/xampp/php/php.exe artisan reverb:start --debug # Trigger notification via test /mnt/c/xampp/php/php.exe artisan test --filter="IssuesEnhancementsTest::assigning a task notifies" ``` **Commit:** `feat: real-time notifications via Reverb` --- ### Task 2.3: Broadcast task/issue updates to ProjectMap **Objective:** Live updates on map when features change (progress, status, inspections) **Files:** - Modify: `app/Livewire/Projects/ProjectMap.php` — listen for broadcast events - Modify: `app/Models/Feature.php` — broadcast on update - Modify: `app/Models/Inspection.php` — broadcast on create **Step 1: Broadcast feature changes** ```php // Feature model protected $dispatchesEvents = [ 'updated' => FeatureUpdated::class, ]; // Event class FeatureUpdated implements ShouldBroadcast { public function broadcastOn(): array { return [new PrivateChannel('project.'.$this->feature->project_id)]; } } ``` **Step 2: Listen in ProjectMap** ```php #[On('echo:project.{projectId},FeatureUpdated')] public function handleFeatureUpdated($event) { $this->allFeatures = $this->allFeatures->map(fn ($f) => $f->id === $event['feature']['id'] ? (object)$event['feature'] : $f ); $this->dispatch('featureUpdated', $event['feature']); } ``` **Commit:** `feat: live map updates via Reverb` --- ## Phase 3: Validation & Security Hardening ### Task 3.1: Create FormRequests for critical Livewire forms **Objective:** Centralize validation + authorization for TaskForm, IssueForm, InspectionForm **Files:** - Create: `app/Http/Requests/TaskStoreRequest.php` - Create: `app/Http/Requests/TaskUpdateRequest.php` - Create: `app/Http/Requests/IssueStoreRequest.php` - Create: `app/Http/Requests/IssueUpdateRequest.php` - Create: `app/Http/Requests/InspectionStoreRequest.php` - Create: `app/Http/Requests/InspectionUpdateRequest.php` - Modify: `app/Livewire/Tasks/TaskForm.php` — use FormRequest - Modify: `app/Livewire/Issues/IssueForm.php` — use FormRequest - Modify: `app/Livewire/Projects/ProjectMap.php` (inspection methods) — use FormRequest **Step 1: Generate requests** ```bash /mnt/c/xampp/php/php.exe artisan make:request TaskStoreRequest /mnt/c/xampp/php/php.exe artisan make:request TaskUpdateRequest # ... repeat for Issue, Inspection ``` **Step 2: Implement rules + authorize** ```php // TaskStoreRequest public function authorize(): bool { return $this->user()->can('create tasks', $this->route('project')); } public function rules(): array { return [ 'title' => 'required|string|max:255', 'phase_id' => 'nullable|exists:phases,id', 'status' => 'required|in:pending,in_progress,completed,cancelled', // ... ]; } ``` **Step 3: Use in Livewire** ```php // TaskForm::save() $validated = app(TaskStoreRequest::class)->validated(); // or for update $validated = app(TaskUpdateRequest::class)->validated(); ``` **Step 4: Test** ```bash /mnt/c/xampp/php/php.exe artisan test --filter="TaskForm|IssueForm|InspectionForm" ``` **Commit:** `refactor: FormRequests for Task, Issue, Inspection forms` --- ### Task 3.2: Add rate limiting to all API routes **Objective:** Protect bundle, templates, projects endpoints **Files:** - Modify: `routes/api.php` — add throttle middleware to all routes **Step 1: Update routes** ```php Route::middleware(['auth:sanctum', 'ability:mobile-sync', 'throttle:120,1'])->group(function () { Route::get('projects', [ProjectApiController::class, 'index']); Route::get('projects/{project}/bundle', [ProjectApiController::class, 'bundle']); Route::get('templates', [ProjectApiController::class, 'templates']); }); Route::middleware(['auth:sanctum', 'ability:mobile-sync', 'throttle:60,1'])->group(function () { Route::post('sync', [SyncController::class, 'sync']); }); Route::middleware(['auth:sanctum', 'ability:mobile-sync', 'throttle:120,1'])->group(function () { Route::post('media', [MediaController::class, 'upload']); }); ``` **Step 2: Test rate limit** ```bash /mnt/c/xampp/php/php.exe artisan test --filter="MobileApiTest" ``` **Commit:** `security: rate limit all mobile API endpoints` --- ### Task 3.3: Add mass assignment protection audit **Objective:** Ensure all models use `$fillable` (not `$guarded = []`) **Files:** - Check: all `app/Models/*.php` - Modify: any with `$guarded = ['*']` or missing `$fillable` **Step 1: Audit** ```bash grep -r "guarded" app/Models/ --include="*.php" grep -r "fillable" app/Models/ --include="*.php" ``` **Step 2: Fix any gaps** ```php // Example fix protected $fillable = ['name', 'email', 'status']; // explicit list // NOT: protected $guarded = ['id']; // or empty ``` **Commit:** `security: audit mass assignment protection` --- ## Phase 4: Developer Experience & Testing ### Task 4.1: Add Laravel Pint to CI (already done locally) **Objective:** Ensure code style enforced in pipeline **Files:** - Create: `.github/workflows/pint.yml` (or GitLab CI equivalent) **Step 1: Create workflow** ```yaml # .github/workflows/pint.yml name: Code Style on: [push, pull_request] jobs: pint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: { php-version: '8.2' } - run: composer install --prefer-dist --no-progress - run: ./vendor/bin/pint --test ``` **Commit:** `ci: add Pint code style check` --- ### Task 4.2: Add mutation testing (Infection) **Objective:** Measure test quality beyond coverage **Files:** - Modify: `composer.json` — add `infection/infection` dev dep - Create: `infection.json5` config **Step 1: Install** ```bash /mnt/c/xampp/php/php.exe composer require --dev infection/infection --no-interaction ``` **Step 2: Configure** ```json5 // infection.json5 { "source": { "directories": ["app"] }, "mutators": { "@default": true }, "testFramework": "phpunit", "testFrameworkOptions": "--testdox", "minMsi": 80, "minCoveredMsi": 80 } ``` **Step 3: Run baseline** ```bash /mnt/c/xampp/php/php.exe vendor/bin/infection --configuration=infection.json5 ``` **Commit:** `test: add mutation testing with Infection` --- ### Task 4.3: Add browser tests (Laravel Dusk) **Objective:** Test critical user flows (login, create project, map interaction, report generation) **Files:** - Modify: `composer.json` — add `laravel/dusk` dev dep - Create: `tests/Browser/*.php` - Modify: `.env.dusk.local` (or CI config) **Step 1: Install** ```bash /mnt/c/xampp/php/php.exe composer require --dev laravel/dusk --no-interaction /mnt/c/xampp/php/php.exe artisan dusk:install ``` **Step 2: Write critical flow tests** ```php // tests/Browser/ProjectWorkflowTest.php public function test_user_can_create_project_and_add_feature() { $this->browse(function (Browser $browser) { $browser->loginAs(User::factory()->create()) ->visit('/projects/create') ->type('name', 'Test Project') ->press('Guardar') ->assertPathIs('/projects/1') ->visit('/projects/1/map') ->click('@add-feature-button') ->type('name', 'Test Feature') ->press('Guardar') ->assertSee('Test Feature'); }); } ``` **Commit:** `test: add Dusk browser tests for critical flows` --- ## Phase 5: Feature Gaps (Medium Priority) ### Task 5.1: Scheduled report email job **Objective:** Daily/weekly email with project report to stakeholders **Files:** - Create: `app/Jobs/SendScheduledReport.php` - Modify: `routes/console.php` — schedule job - Create: `resources/views/emails/scheduled-report.blade.php` - Modify: `app/Console/Commands/SendScheduledReports.php` (or use Schedule directly) **Step 1: Create job** ```php // app/Jobs/SendScheduledReport.php public function handle() { $projects = Project::whereHas('users', fn ($q) => $q->where('receive_reports', true))->get(); foreach ($projects as $project) { $users = $project->users()->where('receive_reports', true)->get(); foreach ($users as $user) { Mail::to($user)->send(new ScheduledReportMail($project, $user)); } } } ``` **Step 2: Schedule** ```php // routes/console.php Schedule::job(new SendScheduledReport)->dailyAt('08:00'); ``` **Commit:** `feat: scheduled report emails` --- ### Task 5.2: Global search (Scout + Meilisearch/Typesense) **Objective:** Cross-project search for features, issues, tasks, inspections **Files:** - Modify: `composer.json` — add `laravel/scout`, `meilisearch/meilisearch-php` - Create: `config/scout.php` (vendor:publish) - Modify: Models (Feature, Issue, Task, Inspection) — `Searchable` trait - Create: `app/Livewire/Common/GlobalSearch.php` - Modify: Navigation layout — add search input **Step 1: Install** ```bash /mnt/c/xampp/php/php.exe composer require laravel/scout meilisearch/meilisearch-php --no-interaction /mnt/c/xampp/php/php.exe artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" ``` **Step 2: Make models searchable** ```php // Feature.php use Laravel\Scout\Searchable; class Feature extends Model { use Searchable; public function toSearchableArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, 'project_id' => $this->project_id, 'status' => $this->status, ]; } } ``` **Step 3: Index & search** ```bash /mnt/c/xampp/php/php.exe artisan scout:import "App\Models\Feature" ``` **Commit:** `feat: global search with Scout + Meilisearch` --- ### Task 5.3: PWA offline support (Workbox) **Objective:** Restore offline capability for field workers **Files:** - Create: `public/sw.js` (Workbox-generated) - Modify: `vite.config.js` — add `vite-plugin-pwa` - Modify: `resources/js/app.js` — register SW - Modify: `OfflineSyncController` — ensure compatibility **Step 1: Add PWA plugin** ```bash npm install --save-dev vite-plugin-pwa workbox-window ``` **Step 2: Configure** ```javascript // vite.config.js import { VitePWA } from 'vite-plugin-pwa'; export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'ConstruProgress', short_name: 'CP', theme_color: '#2563eb', icons: [...] }, workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], runtimeCaching: [ { urlPattern: /^https:\/\/.*\/api\/v1\/.*/, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 86400 } } } ] } }) ] }); ``` **Commit:** `feat: PWA offline support with Workbox` --- ## Phase 6: Code Organization (Tech Debt) ### Task 6.1: Split ProjectMap into traits/components **Objective:** Reduce 800-line component into manageable pieces **Files:** - Create: `app/Livewire/Projects/Traits/MapLayers.php` - Create: `app/Livewire/Projects/Traits/MapInspections.php` - Create: `app/Livewire/Projects/Traits/MapIssues.php` - Create: `app/Livewire/Projects/Traits/MapFeatures.php` - Modify: `app/Livewire/Projects/ProjectMap.php` — use traits **Step 1: Extract layers logic** ```php // app/Livewire/Projects/Traits/MapLayers.php trait MapLayers { public $phases; public $activeLayers = []; public $showLayerModal = false; // ... layer methods } ``` **Step 2: Use in ProjectMap** ```php class ProjectMap extends Component { use MapLayers, MapInspections, MapIssues, MapFeatures; // ... only core map logic remains } ``` **Step 3: Test** ```bash /mnt/c/xampp/php/php.exe artisan test --filter="MapTablesTest" ``` **Commit:** `refactor: split ProjectMap into traits` --- ### Task 6.2: Extract ReportGenerator into smaller services **Objective:** Separate data aggregation from export formatting **Files:** - Create: `app/Services/Report/DataAggregator.php` - Create: `app/Services/Report/HtmlExporter.php` - Create: `app/Services/Report/ExcelExporter.php` - Modify: `app/Services/ReportGenerator.php` — delegate to services - Modify: `app/Exports/ProjectReportExport.php` — use HtmlExporter/ExcelExporter **Step 1: Create DataAggregator** ```php // app/Services/Report/DataAggregator.php class DataAggregator { public function aggregate(Project $project, ReportFilters $filters): array { // ... all the data fetching logic from ReportGenerator::generate() } } ``` **Step 2: Create exporters** ```php // app/Services/Report/HtmlExporter.php class HtmlExporter { public function export(array $data): string { return view('reports.complete', $data)->render(); } } // app/Services/Report/ExcelExporter.php class ExcelExporter { public function export(Project $project, ReportFilters $filters, array $data): BinaryFileResponse { return Excel::download(new ProjectReportExport($project, $filters, $data), $filename); } } ``` **Step 3: Simplify ReportGenerator** ```php // app/Services/ReportGenerator.php public function generate(): array { return (new DataAggregator)->aggregate($this->project, $this->filters); } ``` **Commit:** `refactor: split ReportGenerator into Aggregator + Exporters` --- ## Verification Checklist After all phases: - [ ] All 101 existing tests still pass - [ ] New tests added for each feature (target: +30 tests) - [ ] API routes unchanged (8 routes, 24 tests pass) - [ ] Pint passes on entire codebase - [ ] Mutation score ≥ 80% - [ ] Dusk tests pass for critical flows - [ ] Reverb + Echo working in local dev - [ ] Dashboard load time < 500ms (cached) - [ ] Report generation < 2s (cached) --- ## Risks & Tradeoffs | Risk | Mitigation | |------|------------| | Reverb adds infrastructure complexity | Start with single-server; scale later | | Cache invalidation bugs | Use model observers + cache tags; test thoroughly | | Breaking existing Livewire components | TDD each refactor; run full test suite after each | | Meilisearch adds external dependency | Can use Typesense or database driver as fallback | | Dusk requires Chrome in CI | Use `laravel/dusk` GitHub Action with Chrome | --- ## Open Questions 1. **Redis hosting:** Self-hosted, Docker, or managed (Redis Cloud, Upstash)? 2. **Reverb hosting:** Same server as app, or separate? 3. **Search engine:** Meilisearch (simpler) vs Typesense (better filtering)? 4. **PWA strategy:** Full offline-first or just cache-first for assets? 5. **Report scheduling:** Per-user preferences or project-level only? --- ## Execution Order (Dependencies) ``` Phase 1 (independent) → Phase 2 (needs Redis) → Phase 3 (independent) ↓ Phase 4 (needs Phase 1 cache) ← Phase 5 (needs Phase 2 Reverb) ↓ Phase 6 (refactor, anytime after tests stable) ``` **Recommended start:** Task 1.1 (Redis) → 1.2 (Dashboard cache) → 2.1 (Reverb) → 3.1 (FormRequests)