docs: refresh CLAUDE.md + parked-work for 15 May session wins

CLAUDE.md changes:
- 'What's mid-flight' breadcrumb updated: SiteReport/Absences
  migrations are LIVE on prod; only the 4 latest UX commits await
  a pull-and-restart (no migration / collectstatic needed).
- URL Routes table entries for /workers/ and /history/ now document
  the new ?team= filter (and team=none for unassigned / no-team cases).
- Worker Management UI inline description mentions the team filter
  + Absences tab on the worker detail page.
- Two new Coding Style gotchas captured: (1) Bootstrap dropdowns
  inside .card elements get clipped by sibling cards — fix is to
  lift the wrapping card with position:relative + z-index;
  (2) JS reading from data-worker-id was unreliable on production —
  read input[name="workers"][value] directly (the attendance-form
  pattern that's been working for years).

parked-work.md changes:
- Pending-deploy section rewritten: the BIG deploy (migrations +
  collectstatic) is DONE; only 4 small commits await a pull +
  service restart.
- 'Recently shipped' grew two new entries: the team filters on
  /workers/ + /history/, and the two absences UX polish fixes.
- Updated timestamp to 15 May 2026.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-15 00:50:47 +02:00
parent 398a5b21ab
commit bde6f24bb1
2 changed files with 64 additions and 67 deletions

View File

@ -3,17 +3,20 @@
## What's mid-flight — read this first ## What's mid-flight — read this first
**Parked / deferred work:** see `docs/plans/parked-work.md`. **Parked / deferred work:** see `docs/plans/parked-work.md`.
**⚠ Operator action pending:** SiteReport (`0013`) + Absences (`0014` **Production status (15 May 2026):** migrations `0013_add_site_report`,
+ `0015`) migrations need to run on production. Visit `0014_add_absence`, `0015_absence_project` are deployed; `/history/`
`https://foxlog.flatlogic.app/run-migrate/` once + ask Gemini to is no longer crashing on the production VM. The Worker Absences
run `collectstatic` + restart `django-dev.service`. The /history/ feature shipped on 14 May 2026 (commits `bf6f0a5``27fe05e` on
page is currently 500ing on production until this is done — see `ai-dev`). Subsequent UX polish (multi-checkbox-dropdown stacking
parked-work.md "Production deploy" section for the full sequence. fix, absence-form team-filter bug fix, team filter added to
`/workers/` and `/history/`) is on `ai-dev` HEAD but not yet on
production — needs a `git pull` + `sudo systemctl restart
django-dev.service` whenever convenient (no migrations or
collectstatic required for those commits).
Phase A.2 (manual JournalEntry UI) and Phase B (Letterly inbound Phase A.2 (manual JournalEntry UI) and Phase B (Letterly inbound
webhook) from the Site Work Logging design are parked pending Q5 / Q7 webhook) from the Site Work Logging design are parked pending Q5 / Q7
answers. The Worker Absences feature shipped on 14 May 2026 (commits answers — see `docs/plans/parked-work.md`.
`bf6f0a5``27fe05e` on `ai-dev`).
## Coding Style ## Coding Style
- Always add clear section header comments using the format: # === SECTION NAME === - Always add clear section header comments using the format: # === SECTION NAME ===
@ -22,6 +25,8 @@ answers. The Worker Absences feature shipped on 14 May 2026 (commits
- When creating or editing code, maintain the existing comment structure - When creating or editing code, maintain the existing comment structure
- **Django template comments `{# ... #}` are SINGLE-LINE only.** Multi-line blocks need `{% comment %}...{% endcomment %}`. A `{#` on line N with no closing `#}` on the same line renders the whole block as literal text onto the page (and silently — no error). This bit us 4× during the Adjustments feature. Also: the literal tokens `{#` and `#}` cannot appear inside a `{% comment %}` block — they'll be parsed as a nested comment marker. Rephrase meta-notes about comment syntax OUTSIDE the block. - **Django template comments `{# ... #}` are SINGLE-LINE only.** Multi-line blocks need `{% comment %}...{% endcomment %}`. A `{#` on line N with no closing `#}` on the same line renders the whole block as literal text onto the page (and silently — no error). This bit us 4× during the Adjustments feature. Also: the literal tokens `{#` and `#}` cannot appear inside a `{% comment %}` block — they'll be parsed as a nested comment marker. Rephrase meta-notes about comment syntax OUTSIDE the block.
- **Duplicate `id=""` attributes cause silent bugs.** `document.getElementById()` returns only the FIRST match in DOM order, so adding a second element with an existing id silently steals the handler from the original. Grep the template before assigning any new id (caught `adjSelectAll` collision in Task 6 — header checkbox stole the Add-Adjustment modal's Select-All handler). - **Duplicate `id=""` attributes cause silent bugs.** `document.getElementById()` returns only the FIRST match in DOM order, so adding a second element with an existing id silently steals the handler from the original. Grep the template before assigning any new id (caught `adjSelectAll` collision in Task 6 — header checkbox stole the Add-Adjustment modal's Select-All handler).
- **Bootstrap dropdowns inside `.card` elements get clipped by sibling cards.** A `.dropdown-menu` with `z-index: 1050` rendered inside a filter `.card` will STILL appear behind a sibling table `.card` that follows in document order. Bootstrap's `transform: translate(...)` Popper positioning creates a new stacking context — the z-index is measured INSIDE the parent card, not globally. The fix: lift the wrapping element (e.g. the filter `<form class="card">`) with `style="position: relative; z-index: 10;"` so the entire card sits above its siblings. The dropdown's local z-index then resolves correctly. Bit us on the Absences filter dropdown (May 2026).
- **JS reading from `data-worker-id` was unreliable; read from `<input name="workers">[value]` directly.** Round A's first absence-form team filter rendered `data-worker-id="{{ worker.choice_value }}"` on the row `<div>` and read it via `row.dataset.workerId`. On production this hid ALL workers when a team was selected — likely a stale-template / template-render mismatch. The proven pattern (used by `attendance_log.html` for years) is to read `row.querySelector('input[name="workers"]').value`. The form widget's `<input value="<pk>">` is the source of truth; data attributes are an unnecessary indirection.
## Project Overview ## Project Overview
Django payroll management system for FoxFitt Construction, a civil works contractor specializing in solar farm foundation installations. Manages field worker attendance, payroll processing, employee loans, and business expenses for solar farm projects. Django payroll management system for FoxFitt Construction, a civil works contractor specializing in solar farm foundation installations. Manages field worker attendance, payroll processing, employee loans, and business expenses for solar farm projects.
@ -335,7 +340,7 @@ numbers on hot pages.
- Quick Adjust Button: Each pending payments row has an "Adjust" button (slider icon) that opens the Add Adjustment modal with that worker pre-checked and their most recent project pre-selected. The header "Add Adjustment" button resets the modal to a clean state. Uses `_quickAdjustOpen` flag to distinguish between the two open paths. - Quick Adjust Button: Each pending payments row has an "Adjust" button (slider icon) that opens the Add Adjustment modal with that worker pre-checked and their most recent project pre-selected. The header "Add Adjustment" button resets the modal to a clean state. Uses `_quickAdjustOpen` flag to distinguish between the two open paths.
- Worker Lookup Modal: Clicking any worker name on the payroll dashboard (or using the "Worker Lookup" button) opens a modal with a comprehensive report card — amount payable, outstanding loans, paid this month/year, loans this year, recent activity (last payslip, loan, repayment, advance), active loans table, current project + days on project, PPE sizing, drivers license, and notes. Uses `worker_lookup_ajax` AJAX endpoint. Worker dropdown in modal allows switching workers without closing. - Worker Lookup Modal: Clicking any worker name on the payroll dashboard (or using the "Worker Lookup" button) opens a modal with a comprehensive report card — amount payable, outstanding loans, paid this month/year, loans this year, recent activity (last payslip, loan, repayment, advance), active loans table, current project + days on project, PPE sizing, drivers license, and notes. Uses `worker_lookup_ajax` AJAX endpoint. Worker dropdown in modal allows switching workers without closing.
- Team & Project Management UIs: Friendlier alternatives to `/admin/core/team/` and `/admin/core/project/`. Reachable via the "Resources" dropdown in the topbar (admin only). **Team pages**: `/teams/` (list + search/filter), `/teams/<id>/` (detail with Profile/Pay Schedule/Workers/History tabs — Pay Schedule tab uses the existing `get_pay_period()` helper to show current + next 2 periods), `/teams/<id>/edit/` (single-page form for name, supervisor, pay schedule, and workers M2M). **Project pages**: `/projects/`, `/projects/<id>/` (tabs: Profile/Supervisors/Teams/Workers/History), `/projects/<id>/edit/` (form for name, description, dates, supervisors M2M). Uses `TeamForm` and `ProjectForm` from `core/forms.py` (both simple ModelForms, no inline formsets). Batch reports at `/teams/report/` and `/projects/report/` with CSV exports; PDF exports deferred as a follow-up. Dashboard "Manage Resources" card now has "Manage All Workers/Projects/Teams" footer links on each tab. Django admin remains fully functional as a fallback. - Team & Project Management UIs: Friendlier alternatives to `/admin/core/team/` and `/admin/core/project/`. Reachable via the "Resources" dropdown in the topbar (admin only). **Team pages**: `/teams/` (list + search/filter), `/teams/<id>/` (detail with Profile/Pay Schedule/Workers/History tabs — Pay Schedule tab uses the existing `get_pay_period()` helper to show current + next 2 periods), `/teams/<id>/edit/` (single-page form for name, supervisor, pay schedule, and workers M2M). **Project pages**: `/projects/`, `/projects/<id>/` (tabs: Profile/Supervisors/Teams/Workers/History), `/projects/<id>/edit/` (form for name, description, dates, supervisors M2M). Uses `TeamForm` and `ProjectForm` from `core/forms.py` (both simple ModelForms, no inline formsets). Batch reports at `/teams/report/` and `/projects/report/` with CSV exports; PDF exports deferred as a follow-up. Dashboard "Manage Resources" card now has "Manage All Workers/Projects/Teams" footer links on each tab. Django admin remains fully functional as a fallback.
- Worker Management UI: A friendlier alternative to `/admin/core/worker/`. Reachable via the "Resources" topbar dropdown → Workers (admin-only). Pages: `/workers/` (list with search + status filter), `/workers/<id>/` (detail with Profile/Certifications/Warnings/History tabs), `/workers/<id>/edit/` or `/workers/new/` (single-page form with sections for Personal & Pay, PPE, Documents, Driver's License, plus inline formsets for certifications and warnings). Uses `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` from `core/forms.py`. The "+ Add Certification" / "+ Add Warning" buttons clone a `<template>` element via `content.cloneNode()` (DOM-safe, no innerHTML) and rewrite `__PREFIX__` in input names to the next formset index. File uploads validated at 5 MB max via `validate_max_5mb()` in `forms.py`. Django admin (`/admin/core/worker/`) remains fully functional as a fallback — both UIs coexist. - Worker Management UI: A friendlier alternative to `/admin/core/worker/`. Reachable via the "Resources" topbar dropdown → Workers (admin-only). Pages: `/workers/` (list with search + status + team filter — team filter uses Team.workers M2M membership, special value `none` matches workers not assigned to any team), `/workers/<id>/` (detail with Profile/Certifications/Warnings/**Absences**/History tabs — Absences tab shows YTD totals chip row + 50 most-recent absence rows), `/workers/<id>/edit/` or `/workers/new/` (single-page form with sections for Personal & Pay, PPE, Documents, Driver's License, plus inline formsets for certifications and warnings). Uses `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` from `core/forms.py`. The "+ Add Certification" / "+ Add Warning" buttons clone a `<template>` element via `content.cloneNode()` (DOM-safe, no innerHTML) and rewrite `__PREFIX__` in input names to the next formset index. File uploads validated at 5 MB max via `validate_max_5mb()` in `forms.py`. Django admin (`/admin/core/worker/`) remains fully functional as a fallback — both UIs coexist.
- Worker Batch Report: `/workers/report/` shows every worker with aggregated lifetime history — days worked, projects worked on, teams, first/last payslip dates, total paid, cert status (active/total + expired/expiring counts), warning count. Filter by status, project, team. CSV export via `/workers/report/csv/`, PDF via `/workers/report/pdf/` (landscape A4, same amber-accent typography as the payroll report). Built on the reusable `_build_worker_report_context()` helper which uses `annotate(Min/Max/Count/Sum)` + prefetch for efficient aggregation. - Worker Batch Report: `/workers/report/` shows every worker with aggregated lifetime history — days worked, projects worked on, teams, first/last payslip dates, total paid, cert status (active/total + expired/expiring counts), warning count. Filter by status, project, team. CSV export via `/workers/report/csv/`, PDF via `/workers/report/pdf/` (landscape A4, same amber-accent typography as the payroll report). Built on the reusable `_build_worker_report_context()` helper which uses `annotate(Min/Max/Count/Sum)` + prefetch for efficient aggregation.
- Dashboard cert-expiry card: The admin dashboard shows a "Certifications Need Attention" stat card with count of expired + expiring-within-30-days certs (active workers only). Card is CONDITIONAL — renders only when count > 0, so it disappears when everything is in good standing. Clicking it goes to the worker batch report. Counts come from `index()` view adding `certs_expired_count`, `certs_expiring_count`, `certs_alert_total` to context. - Dashboard cert-expiry card: The admin dashboard shows a "Certifications Need Attention" stat card with count of expired + expiring-within-30-days certs (active workers only). Card is CONDITIONAL — renders only when count > 0, so it disappears when everything is in good standing. Clicking it goes to the worker batch report. Counts come from `index()` view adding `certs_expired_count`, `certs_expiring_count`, `certs_alert_total` to context.
- **Inline Filters on the Report page**: `/report/` has three pill-buttons (Date / Projects / Teams) in a sticky strip. Clicking a pill opens an inline popover with the editor for that filter. Popover's OK rebuilds the URL and navigates — no "dirty state", no global Apply. Date popover has a Single/Range/Custom mode toggle; Projects + Teams use Choices.js multi-select. Bidirectional project↔team cross-filter disables workers/projects that never paired in the selected date range. Context key `project_team_pairs_json` is consumed via `|json_script` (raw Python list — NEVER `json.dumps` it first; the filter does the serialisation and double-encoding silently breaks `.forEach(...)`). Deleted the old `_report_config_modal.html`; Dashboard "Generate Report" button is now a plain link with the current month pre-filled. - **Inline Filters on the Report page**: `/report/` has three pill-buttons (Date / Projects / Teams) in a sticky strip. Clicking a pill opens an inline popover with the editor for that filter. Popover's OK rebuilds the URL and navigates — no "dirty state", no global Apply. Date popover has a Single/Range/Custom mode toggle; Projects + Teams use Choices.js multi-select. Bidirectional project↔team cross-filter disables workers/projects that never paired in the selected date range. Context key `project_team_pairs_json` is consumed via `|json_script` (raw Python list — NEVER `json.dumps` it first; the filter does the serialisation and double-encoding silently breaks `.forEach(...)`). Deleted the old `_report_config_modal.html`; Dashboard "Generate Report" button is now a plain link with the current month pre-filled.
@ -348,7 +353,7 @@ numbers on hot pages.
|------|------|---------| |------|------|---------|
| `/` | `index` | Dashboard (admin stats / supervisor work view) | | `/` | `index` | Dashboard (admin stats / supervisor work view) |
| `/attendance/log/` | `attendance_log` | Log daily work with date range support | | `/attendance/log/` | `attendance_log` | Log daily work with date range support |
| `/history/` | `work_history` | Work logs table with filters | | `/history/` | `work_history` | Work logs table with filters. Query params: `?worker=`, `?project=`, `?team=` (digit = WorkLog.team FK match; `none` = logs with no team set), `?status=paid|unpaid`. CSV export at `/history/export/` honors the same filters. |
| `/history/export/` | `export_work_log_csv` | Download filtered logs as CSV | | `/history/export/` | `export_work_log_csv` | Download filtered logs as CSV |
| `/site-report/<work_log_id>/edit/` | `site_report_edit` | Create-or-update the optional SiteReport for a WorkLog (auto-redirected here after `/attendance/log/` POST) | | `/site-report/<work_log_id>/edit/` | `site_report_edit` | Create-or-update the optional SiteReport for a WorkLog (auto-redirected here after `/attendance/log/` POST) |
| `/site-report/<work_log_id>/` | `site_report_detail` | Read-only view of the SiteReport (404 if none — use the edit URL to create) | | `/site-report/<work_log_id>/` | `site_report_detail` | Read-only view of the SiteReport (404 if none — use the edit URL to create) |
@ -359,7 +364,7 @@ numbers on hot pages.
| `/absences/<id>/delete/` | `absence_delete` | POST-only; cascades unpaid adjustment; refuses if paid. | | `/absences/<id>/delete/` | `absence_delete` | POST-only; cascades unpaid adjustment; refuses if paid. |
| `/absences/export/` | `absence_export_csv` | Admin-only CSV; honors all list filters. | | `/absences/export/` | `absence_export_csv` | Admin-only CSV; honors all list filters. |
| `/workers/export/` | `export_workers_csv` | Admin: export all workers to CSV | | `/workers/export/` | `export_workers_csv` | Admin: export all workers to CSV |
| `/workers/` | `worker_list` | Admin: friendly worker list with search + status filter | | `/workers/` | `worker_list` | Admin: friendly worker list. Query params: `?q=` (name/ID/phone search), `?status=active\|inactive\|all`, `?team=` (digit = Team.workers M2M membership; `none` = workers not on any team). |
| `/workers/new/` | `worker_edit` | Admin: blank worker-create form | | `/workers/new/` | `worker_edit` | Admin: blank worker-create form |
| `/workers/<id>/` | `worker_detail` | Admin: worker profile with profile/certs/warnings/history tabs | | `/workers/<id>/` | `worker_detail` | Admin: worker profile with profile/certs/warnings/history tabs |
| `/workers/<id>/edit/` | `worker_edit` | Admin: edit worker + inline cert/warning formsets | | `/workers/<id>/edit/` | `worker_edit` | Admin: edit worker + inline cert/warning formsets |

View File

@ -1,67 +1,38 @@
# Parked / deferred work # Parked / deferred work
> Updated 14 May 2026 (late evening). A small index of features that > Updated 15 May 2026. A small index of features that are designed,
> are designed, half-built, blocked on input, or pending an operator > half-built, blocked on input, or pending an operator step. When a
> step. When a fresh session opens, glance here first to see what's > fresh session opens, glance here first to see what's already on
> already on the workbench. > the workbench.
--- ---
## ⚠ Needs operator action (production) ## ⚠ Pending pull-and-restart on production
### Production deploy of SiteReport + Absences (pending Konrad) **Status:** The big absences/SiteReport deploy (migrations
`0013`/`0014`/`0015` + collectstatic + service restart) was
completed on 14 May 2026 — `/history/` no longer crashes,
`/absences/*` is reachable, badge CSS is collected. ✓
**Status:** All code committed and pushed to `origin/ai-dev` (HEAD Since that deploy, four small commits have shipped to `origin/ai-dev`
`27fe05e` as of 14 May 2026 late evening). Production at that are NOT yet on production:
`https://foxlog.flatlogic.app/` has pulled the new code but is
crashing on `/history/` with:
``` | Commit | What it does |
ProgrammingError: (1146, "Table 'app_38686.core_sitereport' doesn't exist") |---|---|
``` | `4368e53` | Fixes the absence-form team filter (workers were all disappearing when a team was selected — read input.value instead of data-attr) |
| `02c6d4d` | Fixes the Reasons multi-checkbox dropdown stacking-context bug on `/absences/` (lifted the filter card above the table card) |
| `4b57cff` | Adds a Team filter to `/workers/` |
| `398a5b2` | Adds a Team filter to `/history/` (and CSV export) |
**Why:** Flatlogic auto-pulled the new code but didn't run migrations. **To deploy these:**
The new template logic references `log.site_report`, which queries a - No migrations needed (pure template + view changes; no schema changes).
table that doesn't exist yet on the production MySQL. - No `collectstatic` needed (no new CSS or JS files).
- Just: ask Gemini in Flatlogic to `git pull origin ai-dev` and then `sudo systemctl restart django-dev.service`.
**The fix (~2 minutes total):** That's the whole sequence. Production should pick up all four
improvements instantly. Rollback is `git reset --hard <previous SHA>`
1. **Backup first** (safety net) — visit on the VM if anything looks wrong (no data migrations were involved,
`https://foxlog.flatlogic.app/backup-data/` while logged in as so rollback is safe).
admin. Download the `.json` file to a safe location.
2. **Run pending migrations** — visit
`https://foxlog.flatlogic.app/run-migrate/`. Applies three
migrations:
- `0013_add_site_report` — creates `core_sitereport` table (fixes
the immediate /history/ error)
- `0014_add_absence` — creates `core_absence` table
- `0015_absence_project` — adds `project` FK to absence
3. **Refresh static files + restart service** — ask Gemini in
Flatlogic to run:
```
python3 manage.py collectstatic --noinput
sudo systemctl restart django-dev.service
```
Needed because `static/css/custom.css` has new
`.badge-absence-*` rules — without `collectstatic`, the reason
badges on the new pages will render with no color.
4. **Smoke test (incognito)** — visit `foxlog.flatlogic.app/history/`
(should load), `/absences/` (should load), `Resources →
Absences` (admin dropdown should now have it). Log one test
absence end-to-end.
**Rollback if anything breaks:** visit
`https://foxlog.flatlogic.app/restore-data/`, upload the backup
from step 1, ask Gemini to restart the service.
**Why this is parked, not urgent:** The /history/ error only fires
when someone actively uses the History page. The dashboard,
attendance log, and payroll pages all work fine. Konrad chose to
defer the deploy step — that's the right call when you don't have
time to babysit a deploy properly.
**Reference:** the deployment hazard is documented in CLAUDE.md
under "Migrations" in the Flatlogic/AppWizzy Deployment section.
--- ---
@ -162,6 +133,25 @@ From Q9, Q4 of the Site Work Logging brainstorm:
## Recently shipped (for context, so a fresh session knows what just landed) ## Recently shipped (for context, so a fresh session knows what just landed)
- **Team filters on `/workers/` and `/history/`** (commits `4b57cff`,
`398a5b2`, 15 May 2026): Both pages now have a Team dropdown in
their filter row. `/workers/?team=<id>` filters by Team.workers
M2M membership (who's CURRENTLY on the team); `/history/?team=<id>`
filters by WorkLog.team FK (logs TAGGED with the team when
created — different semantics, intentional). Both accept
`team=none` for the "no team assigned" / "ad-hoc work log" case.
CSV export at `/history/export/` honours the same param.
+8 regression tests.
- **Absences UX polish** (commits `4368e53`, `02c6d4d`, 15 May 2026):
Two production-found bugs fixed. (1) Absence-form team filter was
hiding ALL workers when a team was selected — switched the JS to
read `<input name="workers">[value]` directly instead of going
through `data-worker-id` (proven attendance-form pattern). (2)
Reasons multi-checkbox dropdown on `/absences/` was rendering
behind the table — lifted the filter card with
`position: relative; z-index: 10` so the whole card sits above
its sibling table card in the stacking order. See the Coding
Style section in CLAUDE.md for both gotchas.
- **Worker Absences feature** (commits `bf6f0a5``27fe05e`, - **Worker Absences feature** (commits `bf6f0a5``27fe05e`,
14 May 2026): Complete absence-tracking system. 8 reason choices, 14 May 2026): Complete absence-tracking system. 8 reason choices,
optional project FK (auto-attributes paid-absence Bonus optional project FK (auto-attributes paid-absence Bonus
@ -172,9 +162,11 @@ From Q9, Q4 of the Site Work Logging brainstorm:
export. Worker-detail "Absences" tab with YTD totals. Dashboard export. Worker-detail "Absences" tab with YTD totals. Dashboard
alert card "X absent in last 7 days". Bidirectional cascade with alert card "X absent in last 7 days". Bidirectional cascade with
PayrollAdjustment via `_sync_absence_payroll_adjustment` helper PayrollAdjustment via `_sync_absence_payroll_adjustment` helper
(single-chokepoint design, transaction.atomic-wrapped). All 12 (single-chokepoint design, transaction.atomic-wrapped). 12
commits pushed to `origin/ai-dev`. Test count: 85 → 149 (+64 commits pushed to `origin/ai-dev`. Test count: 85 → 149 (+64
tests). All ai-dev tests green. tests). Migrations `0013_add_site_report`, `0014_add_absence`,
`0015_absence_project` are LIVE on production as of late 14 May
2026.
- **Phase A.1 — SiteReport** (commit `864ae72`, 14 May 2026): - **Phase A.1 — SiteReport** (commit `864ae72`, 14 May 2026):
Model, migration `0013_add_site_report.py`, form, two-step flow Model, migration `0013_add_site_report.py`, form, two-step flow
from attendance log, 16 new tests. `CLAUDE.md` updated with model from attendance log, 16 new tests. `CLAUDE.md` updated with model