Prisma 5 rejects JS null as a where-clause value on Json? fields.
Use Prisma.DbNull (SQL NULL sentinel) so the NOT filter compiles correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/src/lib/cache.js — in-process TTL Map cache with
prefix-based invalidation; no external dependencies
- Cache /experiments/:id/calendar?field=X per (experimentId, field).
Invalidated immediately on any DailyStatus write.
- Cache /experiments/:id/daily-statuses per (experimentId, date, analysisOnly).
Invalidated immediately on any DailyStatus write (create/update/delete
and analysis-summary saves all clear the relevant prefix).
- Add ?analysisOnly=true to /daily-statuses: filters rows where
analysis_summary IS NOT NULL. Charts are the only consumer and already
discard rows without analysis data; pre-filtering server-side avoids
sending the full status payload (71 rows → 1 for this experiment).
- ExperimentCalendar: remove allStatuses prop, fetch calendar data
directly via API. The component now issues one small GET per field
selection change instead of the parent loading thousands of rows upfront.
- ExperimentDetail: pass analysisOnly:true to getDailyStatuses;
drop allStatuses prop from ExperimentCalendar. Cold experiment page
load no longer fetches every daily status record.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each saved file now records three additional fields:
matched_identifier — the literal animal_id_string or animal_name
substring that matched in the filename
animal_id_string_snap — snapshot of animal.animal_id_string at
registration time
animal_name_snap — snapshot of animal.animal_name at
registration time
This lets future consumers (scripts, exports, re-matching) identify
the subject from the file record alone, even if animal data changes.
The matched identifier is also shown in the drop preview UI (↳ 2852).
Tests: 25 total (+2 for new payload fields).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a FileDropZone at the bottom of the day view where users can
drop all session files at once. Filenames are regex-matched to
subjects using their animal_id_string (word-boundary aware, so
"2852" won't match "28520"). Only metadata is recorded — files
are never uploaded.
Backend: new session_files table (id, daily_status_id, filename,
file_size BIGINT, file_type, last_modified, notes). Routes:
POST /api/daily-statuses/:id/files (batch create)
GET /api/daily-statuses/:id/files
DELETE /api/session-files/:id
Frontend: FileDropZone shows per-subject match groups, unmatched
files, and a warning for subjects with no match. After saving,
registered files are displayed with delete capability.
test-files/: generate_test_files.sh creates 4 dummy files per
subject (2×bin, 2×csv) with correct naming for drag-and-drop
testing. Pre-generated for 2026-04-23 with IDs 2852/3076/3077/3078.
Tests: 19 tests covering drag state, regex matching, save payload,
partial-overlap prevention, multi-subject batching, and delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Calendar day clicks now navigate to /experiments/:id/day/:date
(with ?field= param). ExperimentDayView shows a sticky-column table
of all subjects with data that day, inline edit via modal, and a
session-metrics bar chart from analysis_summary. Subject name links
to /daily-statuses/:id. Backend adds ?date= filtering to
GET /experiments/:id/daily-statuses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New ExperimentCalendar component added as a third view mode (List / Group / Calendar)
on the Experiment Detail page.
Backend:
- GET /api/experiments/:id/calendar?field=<fieldIdOrBuiltinKey>
Returns { field, days: { "YYYY-MM-DD": count } } where count = number of
subjects with a non-null/non-empty value for the chosen field on that date.
Supports both builtin fields (vitals, treatment, notes, experiment_description)
and custom fields addressed by fieldId UUID.
Frontend:
- ExperimentCalendar component: navigable monthly grid, field selector
(defaults to first field with "weight" in label, else first active field),
blue badges showing subject count per day, click to open modal with all
subjects' data for that day.
- Integrated into ExperimentDetail as displayMode "calendar", persisted
in localStorage alongside the existing list/group modes.
- experimentsApi.getCalendar() added to API client.
Tests:
- backend/tests/calendar.test.js: 8 unit tests (404, empty days, builtin
count, custom field count, whitespace ignored, multi-month, field echo,
missing param).
- frontend/tests/ExperimentCalendar.test.jsx: 13 RTL tests covering
rendering, default field selection, count badges, month navigation,
field-change re-fetch, day click modal, and multi-subject display.
All 47 backend + 13 calendar frontend tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>