38686-vm/docs/plans/2026-04-21-worker-management-expansion.md
Konrad du Plessis 3c28387dd3 WIP: 2026-04-22 session checkpoint
Complete working state of the session. Will be split into two deploy
phases (safety scaffolding then feature release) before merging to ai-dev.

Includes:
- Security fixes (email creds / SECRET_KEY / DEBUG / CSRF)
- Backup + restore management commands and browser endpoints
- WeasyPrint migration (replaces xhtml2pdf)
- New Worker fields + WorkerCertificate + WorkerWarning models
- Worker / Team / Project friendly management UIs
- Dashboard cert-expiry card + Manage All buttons
- Bootstrap tooltips (global init + theme-aware CSS)
- Django admin template override (taller M2M pickers)
- Money filter for ZAR currency formatting
- Resources dropdown nav
- Massive CLAUDE.md expansion + deploy plan docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:15 +02:00

664 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Worker Management Expansion — Design & Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. REQUIRED SUB-SKILL: Use superpowers:test-driven-development where tests are called for.
**Goal:** Extend the Worker model with certifications + disciplinary records, add a friendly in-app worker-edit form (replacing the need to use Django admin for common edits), and build a batch worker report showing projects, teams, days worked, and first/last payslip dates.
**Architecture:**
- Two new models (`WorkerCertificate`, `WorkerWarning`) with ForeignKey to existing `Worker`.
- A worker list page + worker edit page, both admin-only, replacing most everyday admin needs.
- A batch-report page (HTML + CSV + PDF) summarising each worker's full work history.
- All new work lives inside the existing `core/` app — no new apps. Reuses existing patterns: `is_admin()` gating, `@login_required`, Bootstrap modals, `render_to_pdf()`, inline formsets.
**Tech Stack:** Django 5.2.7, Python 3.13, SQLite (local) / MySQL (prod), Bootstrap 5, WeasyPrint for PDFs, existing `format_tags.money` filter.
---
## Context
**Why this is being built:**
The owner (Konrad) currently manages workers via `/admin/core/worker/` — which is functional but lacks:
- Certification tracking (Skills, PDP, First Aid, Medical, Work at Height) with expiry dates and document uploads
- A disciplinary/warnings record per worker
- A friendlier edit UI than Django admin's default form
- A consolidated worker-history report (projects worked, days, payslips) for review or regulatory/auditor purposes
This plan closes all four gaps in one coherent feature.
**Outcomes when complete:**
1. Per worker, the admin can see and maintain: all 5 cert types, all warnings, in one page.
2. The admin has a worker list and worker edit page that are easier than Django admin.
3. The admin can generate a batch report of all workers' project/team/day/payslip history — viewable, CSV-exportable, PDF-exportable.
---
## Scope (explicit)
### In scope
- New models: `WorkerCertificate`, `WorkerWarning`
- Django admin registrations for both (inline on Worker)
- Worker list page (admin-only): `/workers/`
- Worker edit page (admin-only): `/workers/<id>/edit/` and `/workers/new/`
- Worker detail page (admin-only, read-only view with history): `/workers/<id>/`
- Batch worker report: `/workers/report/` (HTML), `/workers/report/csv/`, `/workers/report/pdf/`
- Nav links added to base.html
- Migrations for the two new models
- Updates to `CLAUDE.md` documenting the new pieces
### Out of scope (not in this plan — can be follow-ups)
- Cert expiry email alerts
- Worker self-service portal (only admins use this)
- Photo cropping / file optimisation
- Bulk cert upload (edit one worker at a time)
---
## Files to Create / Modify
| File | Action | Purpose |
|------|--------|---------|
| `core/models.py` | Modify | Add `WorkerCertificate` and `WorkerWarning` model classes |
| `core/migrations/0009_worker_certificates_warnings.py` | Create (auto) | Schema changes |
| `core/admin.py` | Modify | Register new models; add inlines to WorkerAdmin |
| `core/forms.py` | Modify | `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` |
| `core/views.py` | Modify | 6 new views: worker_list, worker_detail, worker_edit, worker_batch_report, worker_batch_report_csv, worker_batch_report_pdf |
| `core/urls.py` | Modify | 6 new routes |
| `core/templates/core/workers/list.html` | Create | Worker list with search + filters + "Add Worker" button |
| `core/templates/core/workers/edit.html` | Create | Section-based edit form with inline certs + warnings |
| `core/templates/core/workers/detail.html` | Create | Read-only worker profile with history tabs |
| `core/templates/core/workers/batch_report.html` | Create | HTML batch report (extends base.html) |
| `core/templates/core/pdf/workers_report_pdf.html` | Create | PDF version of batch report (uses WeasyPrint) |
| `core/templates/base.html` | Modify | Add "Workers" nav link (admin only) |
| `CLAUDE.md` | Modify | Document new models, URLs, admin patterns |
---
## Data Model
### `WorkerCertificate`
```python
class WorkerCertificate(models.Model):
"""A certification held by a worker (Skills, PDP, First Aid, etc.).
One row per (worker, cert_type) — existence of the row means the
worker currently holds this certification. Delete the row to record
that they no longer hold it. Use `valid_until` to track expiry.
"""
CERT_TYPES = [
('skills', 'Skills Certificate'),
('pdp', 'PDP (Professional Driving Permit)'),
('first_aid', 'First Aid'),
('medical', 'Medical'),
('work_at_height', 'Work at Height'),
]
worker = models.ForeignKey(
Worker, related_name='certificates', on_delete=models.CASCADE,
)
cert_type = models.CharField(max_length=30, choices=CERT_TYPES)
document = models.FileField(
upload_to='workers/certificates/', blank=True, null=True,
help_text='Scan or photo of the certificate',
)
issued_date = models.DateField(blank=True, null=True)
valid_until = models.DateField(
blank=True, null=True,
help_text='Expiry date — leave blank if the cert does not expire',
)
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [('worker', 'cert_type')]
ordering = ['worker', 'cert_type']
def __str__(self):
return f'{self.worker.name}{self.get_cert_type_display()}'
@property
def is_expired(self):
if not self.valid_until:
return False
return self.valid_until < timezone.now().date()
@property
def expires_soon(self):
"""True if the cert expires within the next 30 days."""
if not self.valid_until:
return False
today = timezone.now().date()
return today <= self.valid_until <= today + datetime.timedelta(days=30)
```
### `WorkerWarning`
```python
class WorkerWarning(models.Model):
"""A disciplinary warning issued to a worker."""
SEVERITY_CHOICES = [
('verbal', 'Verbal Warning'),
('written', 'Written Warning'),
('final', 'Final Warning'),
]
worker = models.ForeignKey(
Worker, related_name='warnings', on_delete=models.CASCADE,
)
date = models.DateField(default=timezone.now)
severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES)
reason = models.CharField(
max_length=200,
help_text='Short summary — e.g. "Repeated lateness"',
)
description = models.TextField(
blank=True,
help_text='Full context of what happened',
)
issued_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True,
related_name='warnings_issued',
)
document = models.FileField(
upload_to='workers/warnings/', blank=True, null=True,
help_text='Signed warning form (optional)',
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-date']
def __str__(self):
return f'{self.worker.name}{self.get_severity_display()} ({self.date})'
```
### Migration
`python manage.py makemigrations core` → produces `0009_worker_certificates_warnings.py`.
Safe migration: two new tables, no changes to existing ones. Reversible.
---
## URL Routes (new)
Add to `core/urls.py` before the `# === EXPENSE RECEIPTS ===` section:
```python
# === WORKERS ===
# Admin-friendly worker management UI (alternative to /admin/core/worker/)
path('workers/', views.worker_list, name='worker_list'),
path('workers/new/', views.worker_edit, name='worker_new'),
path('workers/<int:worker_id>/', views.worker_detail, name='worker_detail'),
path('workers/<int:worker_id>/edit/', views.worker_edit, name='worker_edit'),
# Batch report (table of all workers with aggregated history)
path('workers/report/', views.worker_batch_report, name='worker_batch_report'),
path('workers/report/csv/', views.worker_batch_report_csv, name='worker_batch_report_csv'),
path('workers/report/pdf/', views.worker_batch_report_pdf, name='worker_batch_report_pdf'),
```
---
## Views (new)
All admin-gated via `@login_required` + `is_admin(request.user)` → 403 if not admin.
### `worker_list(request)`
- Fetches all Workers (not just active).
- Filters: `?q=search_term`, `?status=active|inactive|all`.
- Template: `core/workers/list.html`.
- Shows table: name, ID, phone, salary, days worked, active toggle.
### `worker_detail(request, worker_id)`
- Worker profile (read-only).
- Shows: personal info, PPE, license, certs (with expiry highlights), warnings, and a "History" tab with projects/teams/days/payslips.
- Template: `core/workers/detail.html`.
### `worker_edit(request, worker_id=None)`
- GET: renders form pre-filled (or blank if `worker_id is None`).
- POST: validates + saves Worker + inline formsets for certs + warnings.
- Redirect on success: → `worker_detail`.
- Template: `core/workers/edit.html`.
### `worker_batch_report(request)`
- Builds per-worker aggregates using a shared `_build_worker_report_context()` helper (parallel to `_build_report_context`).
- Filters: `?status=`, `?project=`, `?team=`.
- Template: `core/workers/batch_report.html`.
### `worker_batch_report_csv(request)`
- Same context builder; streams a CSV with all columns.
### `worker_batch_report_pdf(request)`
- Same context builder; uses `render_to_pdf('core/pdf/workers_report_pdf.html', context)`.
### Shared helper `_build_worker_report_context(status=None, project_id=None, team_id=None)`
Returns a list of dicts, one per worker:
```python
{
'worker': worker_obj,
'projects': ['Solar Farm Alpha', 'Solar Farm Beta'], # distinct project names
'teams': ['Team Alpha'], # distinct team names
'days_worked': 47, # distinct WorkLog dates
'first_payslip_date': date(2025, 3, 14) or None,
'last_payslip_date': date(2026, 4, 5) or None,
'total_paid_lifetime': Decimal('127450.00'),
'payslip_count': 12,
'active_certs': 3,
'expiring_certs': 1, # expires within 30 days
'expired_certs': 0,
'active_warnings_count': 1, # warnings issued in last 12 months
}
```
Aggregation approach (efficient — one query per aggregate, not per worker):
```python
qs = Worker.objects.annotate(
days_worked=Count('work_logs__date', distinct=True),
first_payslip_date=Min('payroll_records__date'),
last_payslip_date=Max('payroll_records__date'),
total_paid_lifetime=Sum('payroll_records__amount_paid'),
payslip_count=Count('payroll_records', distinct=True),
)
# then a separate prefetch for projects/teams
```
---
## Forms (new)
```python
# core/forms.py
class WorkerForm(forms.ModelForm):
"""Main worker edit form — covers the flat fields on Worker."""
class Meta:
model = Worker
fields = [
'name', 'id_number', 'phone_number', 'monthly_salary',
'employment_date', 'active', 'notes',
'shoe_size', 'overall_top_size', 'pants_size', 'tshirt_size',
'photo', 'id_document',
'has_drivers_license', 'drivers_license',
]
widgets = {
'employment_date': forms.DateInput(attrs={'type': 'date'}),
'notes': forms.Textarea(attrs={'rows': 3}),
}
WorkerCertificateFormSet = inlineformset_factory(
Worker, WorkerCertificate,
fields=['cert_type', 'document', 'issued_date', 'valid_until', 'notes'],
extra=0, # no blank rows by default
can_delete=True,
widgets={
'issued_date': forms.DateInput(attrs={'type': 'date'}),
'valid_until': forms.DateInput(attrs={'type': 'date'}),
'notes': forms.Textarea(attrs={'rows': 2}),
},
)
WorkerWarningFormSet = inlineformset_factory(
Worker, WorkerWarning,
fields=['date', 'severity', 'reason', 'description', 'document'],
extra=0,
can_delete=True,
widgets={
'date': forms.DateInput(attrs={'type': 'date'}),
'description': forms.Textarea(attrs={'rows': 3}),
},
)
```
In the edit view, `request.POST`/`request.FILES` flow through all three (form + two formsets); all must be valid before saving.
---
## Templates (new)
### `core/workers/list.html`
- Search box (name/ID) + status filter dropdown
- Table columns: Name, ID, Phone, Salary, Days Worked, Active, Actions (View / Edit / Toggle)
- Buttons: "Add Worker", "Batch Report", "Export CSV"
- Styled with existing stat-card / resource-row patterns from index.html
### `core/workers/edit.html`
- Section-based layout (no tabs — long-form scroll for easier visual review):
1. **Personal & Pay** — name, id_number, phone, salary, employment_date, active, notes
2. **PPE Sizing** — shoe, overall top, pants, t-shirt
3. **Documents** — photo, id_document
4. **Driver's License** — has_drivers_license, drivers_license file
5. **Certifications** (formset with + add button, × delete)
6. **Warnings & Disciplinary** (formset with + add button, × delete)
- Client-side JS: "Add Certification" / "Add Warning" buttons clone a hidden blank formset row and bump the TOTAL_FORMS counter (standard Django formset pattern)
- Submit button at the bottom; Cancel goes back to `worker_detail`
### `core/workers/detail.html`
- Header: worker photo, name, ID, active badge
- Tabs:
1. **Profile** — personal, PPE, license info
2. **Certifications** — list with colored badges: green (valid > 30 days), amber (expires within 30), red (expired)
3. **Warnings** — chronological list
4. **History** — projects worked, teams, days, last 10 payslips
- "Edit" button links to `worker_edit`
### `core/workers/batch_report.html`
- Report header + filter bar (status / project / team)
- Table with columns:
- Name | ID | Salary | Active | Days Worked | Projects | Teams | First Payslip | Last Payslip | Total Paid | Certs (n/m) | Warnings
- "Export CSV" + "Download PDF" buttons at top-right
- Row click → `worker_detail`
### `core/pdf/workers_report_pdf.html`
- Print-optimized A4 layout using WeasyPrint
- Header: "FoxFitt Construction — Worker Roster Report"
- Filter summary subhead
- Table (narrower columns, landscape orientation may be needed for many fields)
- Uses the same amber accent and typography as `report_pdf.html`
---
## Navigation
`base.html` desktop topbar: add a "Workers" link after "Receipts" and before "Admin" (admin-only):
```html
{% if user.is_staff %}
<a href="{% url 'worker_list' %}" class="topbar-nav__link {% if 'worker' in request.resolver_match.url_name %}active{% endif %}">
<i class="fas fa-hard-hat"></i><span>Workers</span>
</a>
{% endif %}
```
Also add matching entries to the mobile menu (the `.mobile-menu__nav` block) and the bottom tab bar (if room).
---
## Django Admin Enhancements
Register the new models:
```python
# core/admin.py
class WorkerCertificateInline(admin.TabularInline):
model = WorkerCertificate
extra = 0
class WorkerWarningInline(admin.TabularInline):
model = WorkerWarning
extra = 0
readonly_fields = ['created_at']
@admin.register(WorkerCertificate)
class WorkerCertificateAdmin(admin.ModelAdmin):
list_display = ('worker', 'cert_type', 'valid_until', 'is_expired')
list_filter = ('cert_type',)
search_fields = ('worker__name',)
@admin.register(WorkerWarning)
class WorkerWarningAdmin(admin.ModelAdmin):
list_display = ('worker', 'date', 'severity', 'reason')
list_filter = ('severity',)
search_fields = ('worker__name', 'reason')
```
Then update `WorkerAdmin` to include the inlines:
```python
class WorkerAdmin(admin.ModelAdmin):
# ...existing config...
inlines = [WorkerCertificateInline, WorkerWarningInline]
```
This means the Django admin ALSO gets the new sections — the in-app edit page is a better UX, but admin remains fully functional.
---
## Task-by-Task Execution Plan
Each task is 515 minutes. Execute in order.
### Task 1: Add the two new models + migration
**Files:**
- Modify: `core/models.py` (append `WorkerCertificate`, `WorkerWarning` classes at end of file)
- Create: `core/migrations/0009_worker_certificates_warnings.py` (auto-generated)
**Steps:**
1. Add the two model classes (code above)
2. `USE_SQLITE=true python manage.py makemigrations core`
3. `USE_SQLITE=true python manage.py migrate`
4. Verify: `python manage.py check` → no issues
5. Commit
### Task 2: Register models in Django admin
**Files:**
- Modify: `core/admin.py`
**Steps:**
1. Add `WorkerCertificateInline`, `WorkerWarningInline`, `WorkerCertificateAdmin`, `WorkerWarningAdmin` classes
2. Add `inlines = [...]` to `WorkerAdmin`
3. Verify in browser: `/admin/core/worker/<id>/change/` shows certs + warnings sections
4. Commit
### Task 3: Add WorkerForm and formsets
**Files:**
- Modify: `core/forms.py`
**Steps:**
1. Add `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet`
2. Verify: `python manage.py shell` → import forms → no errors
3. Commit
### Task 4: Add worker_list view and template
**Files:**
- Modify: `core/views.py` (+ `import` updates)
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/list.html`
**Steps:**
1. Add URL route `workers/``views.worker_list`
2. Add view `worker_list(request)` with search + status filter
3. Create template with search bar, table, action buttons
4. Verify: visit `/workers/` as admin → list shows workers
5. Commit
### Task 5: Add worker_edit view + template (the big one)
**Files:**
- Modify: `core/views.py`
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/edit.html`
**Steps:**
1. Add two URL routes: `workers/new/`, `workers/<id>/edit/`
2. Add view `worker_edit(request, worker_id=None)` handling both create and update
3. Create section-based template with formsets for certs + warnings
4. Add JS for "+ Add Certification" / "+ Add Warning" buttons (formset clone pattern)
5. Verify: add a worker, edit a worker, add cert, add warning → all persist
6. Commit
### Task 6: Add worker_detail view + template
**Files:**
- Modify: `core/views.py`
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/detail.html`
**Steps:**
1. Add URL `workers/<id>/`
2. Add view with tab-context (profile, certs, warnings, history)
3. Template with Bootstrap tabs; cert badges styled by expiry
4. Link "Edit" button to edit view
5. Verify
6. Commit
### Task 7: Add batch report (HTML, CSV, PDF)
**Files:**
- Modify: `core/views.py` (add `_build_worker_report_context`, 3 views)
- Modify: `core/urls.py`
- Create: `core/templates/core/workers/batch_report.html`
- Create: `core/templates/core/pdf/workers_report_pdf.html`
**Steps:**
1. Write `_build_worker_report_context()` helper with the annotate/prefetch pattern
2. Add 3 URLs + 3 views
3. Create HTML template with filter bar + table
4. Create PDF template (derive from existing `report_pdf.html` structure — cover, section headings, ledger-style table)
5. Verify HTML, CSV, PDF all render correctly
6. Commit
### Task 8: Add nav links
**Files:**
- Modify: `core/templates/base.html`
**Steps:**
1. Add desktop nav link (admin-only)
2. Add mobile menu link
3. Verify on desktop and mobile layouts
4. Commit
### Task 9: Update CLAUDE.md
**Files:**
- Modify: `CLAUDE.md`
**Steps:**
1. Add `WorkerCertificate` and `WorkerWarning` to Key Models section
2. Add 7 new URL routes to URL Routes table
3. Add "Worker Management" subsection under Development Workflow
4. Commit
### Task 10: Verification pass
- Visit every new page; click every button; upload a test file to each upload field
- Try edge cases: blank forms, duplicate cert_type for same worker (should fail unique constraint), expired certs
- Regenerate PDF; open to verify layout
- Run `python manage.py check`
- Smoke-test existing features (payroll report, payment, receipt) still work
---
## Open Questions for User
The following decisions need your input before I start executing. Defaults in brackets are what I'll use if you don't answer.
### Q1: Certification types list
I'm proposing these 5 types:
- Skills Certificate
- PDP (Professional Driving Permit)
- First Aid
- Medical
- Work at Height
**Any additions, removals, or renames?** [Default: use exactly these 5]
### Q2: Warning severity levels
I'm proposing: Verbal → Written → Final. **Accept, or add a fourth (e.g. "Informal"), or use different names?** [Default: Verbal/Written/Final]
### Q3: Navigation placement
**Where does the "Workers" link go?**
- (a) Top desktop nav, admin-only, after Receipts — prominent, 1-click access
- (b) Only accessible from a button on the Dashboard — less cluttered nav
- (c) Inside the Admin dropdown / submenu
[Default: (a) — matches how Payroll is currently linked]
### Q4: Worker list — default sort?
- (a) Alphabetical by name
- (b) By employment date (newest first)
- (c) By active status, then name
[Default: (a) alphabetical]
### Q5: Coexist with Django admin?
The new worker list/edit pages would be more user-friendly, but Django admin at `/admin/core/worker/` remains fully functional.
**Keep Django admin working as a fallback for power-user edits?** [Default: yes — keep both]
### Q6: Cert expiry alerts on dashboard
Would you like the Dashboard to show a stat card like "3 certs expiring in 30 days"?
- (a) Yes, show it — helpful operationally
- (b) No, keep dashboard unchanged for now
- (c) Show, but only if count > 0 (hide the card when everything's fine)
[Default: (c) conditional display]
### Q7: File upload size limit
Certificates and warnings support file uploads. Currently no limit. Should I add a max size to prevent someone accidentally uploading a 50MB scan?
- (a) No limit
- (b) 5 MB max
- (c) 10 MB max
[Default: (b) 5 MB]
### Q8: Batch report columns
I'm proposing this column set:
Name | ID | Salary | Active | Days Worked | Projects | Teams | First Payslip | Last Payslip | Total Paid | Certs (active/total) | Warnings
**Anything to add or remove?** Common additions could be: employment_date, phone, has_drivers_license, last-active-date.
[Default: use the list above]
### Q9: Scope bailout
If I discover during execution that any task is significantly more complex than the estimate (e.g. Task 5 turns out to need 2 hours instead of 30 minutes), should I:
- (a) Keep going, add a new task, push complete
- (b) Stop, flag the issue, let you decide
- (c) Deliver a smaller version (e.g. certs-only, no warnings) and flag warnings as follow-up
[Default: (b) stop and flag]
---
## Verification (end-to-end)
Run after all tasks complete:
```
USE_SQLITE=true python manage.py check
USE_SQLITE=true python manage.py migrate --plan # confirm no pending migrations
```
Browser smoke tests (as admin):
1. Visit `/workers/` — list renders, search works
2. Click "Add Worker" — blank form loads
3. Fill name/ID/salary, click Save → redirected to detail view
4. Click "Edit" on detail — form pre-filled
5. Click "+ Add Certification" → blank cert row appears
6. Fill cert (Medical, valid_until=next month), upload a test PDF, Save
7. Verify cert appears on detail page with green "valid" badge
8. Click "+ Add Warning", fill, Save — verify it appears on Warnings tab
9. Visit `/workers/report/` — table shows all workers with aggregates
10. Click "Export CSV" — downloads, opens in Excel/LibreOffice cleanly
11. Click "Download PDF" — renders with correct layout
12. Visit `/admin/core/worker/<id>/change/` — Django admin still works, inlines show
13. As a non-admin user, visit `/workers/` — should get 403
## Rollback
The change touches two new models with ForeignKey to Worker. If we need to undo:
1. `git reset --hard` to the last commit before this plan started
2. `python manage.py migrate core 0008` (the pre-existing migration) — drops the two new tables
3. Branches stay isolated until merged; worst case the whole thing sits on a local branch forever
---
## Estimated Timeline
- Task 1 (models + migration): 10 min
- Task 2 (admin): 5 min
- Task 3 (forms): 10 min
- Task 4 (list view): 15 min
- Task 5 (edit view + template): 40 min — biggest task; has formset JS
- Task 6 (detail view): 20 min
- Task 7 (batch report): 30 min
- Task 8 (nav): 5 min
- Task 9 (CLAUDE.md): 5 min
- Task 10 (verification): 15 min
**Total: ~2.5 hours of supervised execution**, or ~90 minutes if I can auto-execute with good test coverage.