Final code review of the configurable-data-export feature: ExperimentDetail
was forwarding its on-page groupField state (defaults to '__name__') into
ExportDataModal, leaking a clustering default into the default (untouched)
export. Now passes the raw saved localStorage value, defaulting to
'__none__'. The modal also now validates the incoming groupField against
'__none__'/'__name__'/'__id__'/active subjectTemplate fields and coerces to
'__none__' otherwise, guarding against stale/deleted subject_info fields.
Plus a stale module comment fix and optional-chaining consistency fix in
listSubjectMembers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
- 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.
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>
- Day modal now only shows animals that have a status record for that day
- Removed '+ Add' button and 'No data' placeholder for absent subjects
- Fixed failure bars disappearing in recharts: removed radius from all bars,
added minPointSize=2 on failure bar so zero-value bars remain visible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Calendar moved out of List/Group tab toggle — always visible below cross-
subject analytics (ExperimentAnalysisCharts)
- Day modal shows all subjects: existing data as read-only + Edit button,
missing data as + Add button; both open DailyStatusForm inline
- Status changes/creates propagate up to parent via onStatusChange/onStatusCreate
and calendar counts recompute locally (no extra API round-trip)
- Bar chart (recharts) at bottom: Total/Success/Failure bars per subject plus
% complete text, grouped by subject name
- Tests updated: 17 ExperimentCalendar tests, 76 total passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use module-level EMPTY_TEMPLATE constant as default prop to prevent
useEffect infinite loop (new [] each render changed identity every cycle)
- Add htmlFor to date and dynamic field labels for proper a11y association
- Add role=alert to date error paragraph so tests can find it
- Update tests to pass a template with builtin fields (required by template-
driven component design) and mock experimentsApi
- Add --max-old-space-size=8192 to test scripts to prevent OOM on full suite
All 72 frontend tests now pass across 7 suites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents OOM crashes when running the full Jest suite — the act() warning
stack traces were generating 16k+ lines of output which exhausted Node heap.
All assertions still run; only the noisy console.error spam is silenced.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>