Files
construprogress/.hermes/plans/2026-08-28_140000-construprogress-improvements.md
T
Javier Braña 78d1dbf45a feat: Phase 1 - Redis config, Dashboard cache, Report cache, N+1 fixes, observers
- Add predis/predis for Redis support
- Configure cache/queue defaults to redis in config + .env.example
- Create DashboardController with 60s cache for dashboard queries
- Add cache (5min) to ReportController::generate() and preview()
- Add FeatureObserver, InspectionObserver, IssueObserver for cache invalidation
- Fix N+1 in ProjectMap (eager load template, images), TaskManager (subtasks.parentTask)
- Register observers in AppServiceProvider

Tests: 101 passing (319 assertions)
2026-08-31 09:44:33 +02:00

22 KiB

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

cd /mnt/c/xampp/htdocs/construprogress && /mnt/c/xampp/php/php.exe composer require predis/predis --no-interaction

Step 2: Configure cache and queue

// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),

// config/queue.php
'default' => env('QUEUE_CONNECTION', 'redis'),

Step 3: Verify

/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

// 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

// In Task/Issue/Project observers or model events
Cache::forget("dashboard:{$user->id}:v1");

Step 3: Test

/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()

$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

// 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

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

// 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

/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

/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

// config/broadcasting.php
'default' => env('BROADCAST_DRIVER', 'reverb'),

Step 3: Update frontend

// 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

// 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

<!-- 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

# 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

// 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

#[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

/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

// 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

// TaskForm::save()
$validated = app(TaskStoreRequest::class)->validated();
// or for update
$validated = app(TaskUpdateRequest::class)->validated();

Step 4: Test

/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

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

/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

grep -r "guarded" app/Models/ --include="*.php"
grep -r "fillable" app/Models/ --include="*.php"

Step 2: Fix any gaps

// 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

# .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

/mnt/c/xampp/php/php.exe composer require --dev infection/infection --no-interaction

Step 2: Configure

// infection.json5
{
    "source": {
        "directories": ["app"]
    },
    "mutators": {
        "@default": true
    },
    "testFramework": "phpunit",
    "testFrameworkOptions": "--testdox",
    "minMsi": 80,
    "minCoveredMsi": 80
}

Step 3: Run baseline

/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

/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

// 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

// 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

// 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

/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

// 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

/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

npm install --save-dev vite-plugin-pwa workbox-window

Step 2: Configure

// 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

// app/Livewire/Projects/Traits/MapLayers.php
trait MapLayers
{
    public $phases;
    public $activeLayers = [];
    public $showLayerModal = false;
    // ... layer methods
}

Step 2: Use in ProjectMap

class ProjectMap extends Component
{
    use MapLayers, MapInspections, MapIssues, MapFeatures;
    // ... only core map logic remains
}

Step 3: Test

/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

// 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

// 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

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