Replaces "+ New category" with a Note/Numerical type-aware picker, extends
CategoryBucket to render type badges, bulk-add, and custom-bucket remove controls,
and deletes the addCategory() back-compat shim from Task 7.
Implement TDD for new route that accepts CSV configuration with validated buckets.
Route uses auditMiddleware to log config changes and delegates validation to
validateCsvConfig helper from Task 2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AnalysisSummaryCard gains an "Edit manually" affordance powered by
the new ManualMetricsForm (add/remove categories, edit counts,
recompute total + success rate).
- New PastAnalysesViewer renders a tab-style selector of saved CSV
analyses with an inline AnalysisResultsView panel.
- Reorganizes the page so the manual-metrics path is reachable when no
analysis_summary exists yet.
- Sync package-lock to the recharts ^2.15.4 already declared in package.json.
12-task TDD plan covering DB backup, schema migration, backend
PATCH endpoint with validator, frontend lib helpers (extractNumber,
computeNumericSeries), CsvAnalysis state refactor, type-aware field
picker, numerical plot rendering, and per-animal config persistence.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Spec for adding a numerical bucket type to CSV analysis that plots
note values (numeric portion) against frame/timestamp, persisted on
the Animal model so subsequent uploads reuse the configuration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New file tests/experimentDailyStatuses.test.js — 9 tests covering
GET /api/experiments/:id/daily-statuses:
- 404 on unknown experiment
- all statuses returned when no filter
- analysisOnly=true keeps only non-null analysis_summary records
- analysisOnly=true returns [] when none have analysis_summary
- the exact null/object mix that caused the production 500 now passes
- analysisOnly=false (explicit) returns all records
- ?date= param is forwarded to Prisma correctly
- empty-array response on zero statuses
- returned records include expected fields
Fixed tests/calendar.test.js: existing tests were silently broken by the
cache added in the perf commit. Both test files now mock lib/cache to
always miss, so each test exercises the real route logic in isolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prisma 5 rejects both null and Prisma.DbNull as WHERE filter values for
Json? fields (no clean IS NOT NULL operator). Filter the result array in
JS after the query — the output is immediately cached so this only runs
once per cache period.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Each chart card now has a "▾ customize" button. Clicking it reveals a
settings bar (only rendered when active — zero cost when collapsed):
Size — S / M / L height selector
Y range — min / max inputs to set the YAxis domain
X zoom — min / max inputs to clip the displayed X domain
X data — per-chart X field override, independent from the shared
global X selector (each chart can use a different field)
Y lines — (counts chart only) toggle individual category lines on/off
↺ Reset — shown only when any setting has been changed from default
The global X-axis selector and tick-step control remain at the component
level as the persistent defaults. Per-chart overrides are session-only
(non-persistent), resetting on page load.
Performance: animalRows computation in ExperimentAnalysisCharts is now
split from the per-chart data step, so a metric/X change only recomputes
the affected chart. Per-chart data only recomputes when that chart's
xFieldOverride changes; shared data is reused otherwise.
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>
FileDropZone now appears on each subject's individual daily status
page, scoped to that one status. Files are matched against that
subject's animal_id_string and animal_name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-word names ("Rat A") now allow any word separator in the
filename — space, underscore, or dash — so "Rat_A_session.csv"
and "Rat-A-data.bin" both match. Single-word names and numeric IDs
are unchanged. Partial overlaps still rejected ("Rat" won't hit
"Rationale"). Tests: 5 new name-based match cases (24 total).
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>
Clicking any table cell opens an inline input (text or textarea per
field type). Enter or blur saves; Escape cancels. Unchanged values
skip the API call. Custom fields merge with existing custom_fields;
builtin fields use their key directly.
Prev/next buttons at the bottom navigate to adjacent days that have
data for the selected field, preserving the ?field= param. Day list
is fetched via getCalendar and sorted chronologically.
Tests: 41 total (added 15 new tests for cell editing and day nav).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers loading/error states, breadcrumb, row filtering by selected
field (custom and builtin), table structure, subject-name navigation,
edit modal open/close/submit, session metrics bar chart visibility,
and API call parameters.
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>
5 tests cover the bar chart rules:
- Shows when analysis_summary.total > 0
- Hidden when no summary or total=0
- Multiple animals each with their own summary
- Animals excluded from day view (empty field) also excluded from chart
- animalsWithData now filters by selected field value (not just record presence)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
X-axis: animals with valid field data on that day.
Bars: Success / Failure / Total from status.analysis_summary for that day.
Each day shows its own session metrics. Animals without analysis_summary
or with total=0 are excluded from the chart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
X-axis: animal names, Y-axis: the selected field's numeric value recorded
that specific day. Animals with empty or non-numeric values are excluded.
Each day's chart is independent — different animals, different values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows Total / Success / Failure bars across ALL sessions for each animal
that has at least one valid (non-empty) value for the chosen field.
Animals with zero valid field entries are excluded from the chart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously showed cumulative success/failure counts across all days.
Now shows the selected field's actual value for each subject on that
specific day — numeric values only, subjects with empty or non-numeric
values are excluded from the chart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>