# 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
<!-- In notification bell component -->
<script>
document.addEventListener('livewire:load', () => {
    window.Echo?.private(`user.{{ auth()->id() }}`)
        .notification((notification) => {
            // Update badge count, prepend to dropdown
            Livewire.dispatch('notificationReceived', notification);
        });
});
</script>
```

**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)