- 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)
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— addpredis/predisorphpredisext
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 toApp\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— addlaravel/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— implementShouldBroadcast+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— addinfection/infectiondev dep - Create:
infection.json5config
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— addlaravel/duskdev 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— addlaravel/scout,meilisearch/meilisearch-php - Create:
config/scout.php(vendor:publish) - Modify: Models (Feature, Issue, Task, Inspection) —
Searchabletrait - 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— addvite-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
- Redis hosting: Self-hosted, Docker, or managed (Redis Cloud, Upstash)?
- Reverb hosting: Same server as app, or separate?
- Search engine: Meilisearch (simpler) vs Typesense (better filtering)?
- PWA strategy: Full offline-first or just cache-first for assets?
- 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)