docs(claude): capture session's new patterns + gotchas
Three additions from this session's work: 1. Django ORM gotcha — PayrollAdjustment project double-attribution. Documents the Coalesce pattern that solved the Apr 2026 perf-pass double-count bug on Overtime adjustments. 2. Payroll dashboard query-count baselines — target ranges for / and the four /payroll/ tabs after the perf pass, plus the "spotting a regression" heuristic (>50% jump = N+1 reintroduced). 3. Profiling locally — Django Debug Toolbar — what it is, how it's triple-gated, how to use it for N+1 hunting. Flags that the package is already in requirements.txt so future sessions don't need to install it. Net: +35 lines, three new sections, no deletions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f495064c3
commit
b43892f712
51
CLAUDE.md
51
CLAUDE.md
@ -99,6 +99,25 @@ outer queryset JOIN-free. See `_build_report_context` in `core/views.py` and
|
||||
`ReportContextFilterInflationTests` in `core/tests.py` for the reference
|
||||
implementation (commit f1e246c, Apr 2026).
|
||||
|
||||
## Django ORM gotcha — PayrollAdjustment project double-attribution
|
||||
`PayrollAdjustment` has TWO project FKs: a direct `adj.project` and an
|
||||
indirect `adj.work_log.project`. For every **Overtime** adjustment these
|
||||
always point at the same project (see `price_overtime()` — it sets
|
||||
BOTH). When rolling up "costs per project" you typically want the
|
||||
OR-union — "adjustments where either FK points to project P".
|
||||
|
||||
- **Correct**: `Q(project_id__in=ids) | Q(work_log__project_id__in=ids)` filter
|
||||
+ `.annotate(effective_project_id=Coalesce('project_id', 'work_log__project_id'))`
|
||||
+ `.values('effective_project_id', ...).annotate(total=Sum('amount'))`.
|
||||
Each row contributes to exactly ONE project.
|
||||
- **WRONG**: two separate filtered querysets (one per FK) summed in
|
||||
Python. Any row with BOTH FKs set (every Overtime) gets counted twice.
|
||||
Bit us during the Apr 2026 perf pass — Coalesce fix is commit
|
||||
`167c821`. Regression test: `PayrollDashboardAdjustmentAggregationTests`
|
||||
in `core/tests.py`. See `payroll_dashboard()` in `core/views.py` for
|
||||
the reference implementation on both the unpaid-outstanding card and
|
||||
the paid-monthly stacked chart.
|
||||
|
||||
## PayrollAdjustment Type Handling
|
||||
- **Bonus / Deduction** — standalone, require a linked Project
|
||||
- **New Loan** — creates a `Loan` record (`loan_type='loan'`); has a "Pay Immediately" checkbox (checked by default) that auto-processes the loan (creates PayrollRecord, sends payslip to Spark, marks as paid). When unchecked, the loan sits in Pending Payments for the next pay cycle. Editing syncs loan amount/balance/reason; deleting cascades to Loan + unpaid repayments
|
||||
@ -114,6 +133,22 @@ The dashboard's outstanding amount uses **per-worker** checking, not per-log:
|
||||
- This correctly handles partially-paid logs (e.g., one worker paid, another not)
|
||||
- Unpaid adjustments: additive types increase outstanding, deductive types decrease it
|
||||
|
||||
## Payroll dashboard query-count baselines (post Apr 2026 perf pass)
|
||||
Target ranges after `payroll_dashboard()` was optimized with batched
|
||||
aggregates + `Prefetch(to_attr='active_workers_cached')` + Coalesce-based
|
||||
project attribution (commits `61c485f` + `167c821`):
|
||||
- `/` (admin dashboard) — ~15 queries
|
||||
- `/payroll/?status=pending` — ~24
|
||||
- `/payroll/?status=history` — ~24
|
||||
- `/payroll/?status=loans` — ~25
|
||||
- `/payroll/?status=adjustments` — ~32
|
||||
|
||||
If any of these jumps meaningfully (>50%) after a future change, an N+1
|
||||
was reintroduced. Profile with Django Debug Toolbar (see Profiling
|
||||
section below) to find it. The test suite does NOT have `assertNumQueries`
|
||||
guards on these views — deliberate YAGNI for now, worth adding if
|
||||
regressions become a pattern.
|
||||
|
||||
## Commands
|
||||
```bash
|
||||
# Local development (SQLite)
|
||||
@ -131,6 +166,22 @@ python manage.py check # System check
|
||||
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
```
|
||||
|
||||
## Profiling locally — Django Debug Toolbar
|
||||
Installed as a dev-only dependency in `requirements.txt` since Apr 2026.
|
||||
Triple-gated in `config/settings.py`: only loads when **DEBUG=true AND
|
||||
USE_SQLITE=true AND NOT running tests**. Never loads in production —
|
||||
prod has neither flag, and the test-run gate exists because the toolbar
|
||||
emits an E001 system-check error + breaks template rendering when
|
||||
DEBUG=false (which Django forces during `manage.py test`).
|
||||
|
||||
To profile a page: start the dev server normally (`run_dev.bat` or
|
||||
inline `USE_SQLITE=true DJANGO_DEBUG=true python manage.py runserver`),
|
||||
log in as admin, navigate to any URL, click the toolbar tab on the
|
||||
right edge. The **SQL panel** shows query count + highlights any
|
||||
duplicate-query groups — the go-to tool for N+1 hunting. See the
|
||||
"Payroll dashboard query-count baselines" section for expected
|
||||
numbers on hot pages.
|
||||
|
||||
## Development Workflow
|
||||
- Active development branch: `ai-dev` (PR target: `master`)
|
||||
- Local dev uses SQLite: set `USE_SQLITE=true` environment variable
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user