feat: session file registry with drag-and-drop on day view

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>
This commit is contained in:
Experiments DB Dev
2026-04-23 13:12:45 -04:00
parent dc46af8d31
commit daf36908ef
25 changed files with 1008 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
#!/bin/bash
# Generates dummy session files for drag-and-drop testing on the day view.
# Files are tiny (just named correctly) — the browser File API only reads metadata.
#
# Usage: bash generate_test_files.sh [DATE] [IDS...]
# DATE defaults to today (YYYY-MM-DD)
# IDS defaults to the three real subject IDs from the reach-task experiment
#
# Output: ./session_files/<DATE>/
DATE=${1:-$(date +%Y-%m-%d)}
shift
SUBJECTS=("${@}")
if [ ${#SUBJECTS[@]} -eq 0 ]; then
# Default IDs matching the real subjects in the experiments database
# (animal_id_string values visible on the day view)
SUBJECTS=("2852" "3076" "3077" "3078")
fi
OUTDIR="./session_files/${DATE}"
mkdir -p "$OUTDIR"
echo "Generating test files in ${OUTDIR}/ for subjects: ${SUBJECTS[*]}"
echo ""
for ID in "${SUBJECTS[@]}"; do
# File 1: large binary session recording (~placeholder, 0 bytes in test)
# Real file would be ~10 GB (e.g. raw ephys .bin)
BIN1="${OUTDIR}/${DATE}_${ID}_ephys_raw.bin"
touch "$BIN1"
echo " [~10 GB] ${DATE}_${ID}_ephys_raw.bin"
# File 2: large HDF5 spike-sorted output (~placeholder, 0 bytes in test)
# Real file would be ~10 GB (e.g. kilosort output .h5)
BIN2="${OUTDIR}/${DATE}_${ID}_kilosort.h5"
touch "$BIN2"
echo " [~10 GB] ${DATE}_${ID}_kilosort.h5"
# File 3: CSV trial events (~10 MB real, 1 KB here)
CSV1="${OUTDIR}/${DATE}_${ID}_trials.csv"
{
echo "trial_num,timestamp,outcome,reaction_time_ms,reward"
for i in $(seq 1 20); do
OUTCOME=$( [ $((RANDOM % 3)) -eq 0 ] && echo "Failure" || echo "Success" )
printf "%d,%.3f,%s,%d,%d\n" \
"$i" "$(echo "$i * 0.823 + 1.0" | bc -l 2>/dev/null || echo $i)" \
"$OUTCOME" "$((RANDOM % 400 + 50))" "$((RANDOM % 2))"
done
} > "$CSV1"
echo " [~10 MB] ${DATE}_${ID}_trials.csv"
# File 4: CSV behavioral log (~10 MB real, small here)
CSV2="${OUTDIR}/${DATE}_${ID}_behavior_log.csv"
{
echo "ts,event,value,notes"
echo "0.000,session_start,1,"
echo "1.234,trial_start,1,"
echo "1.856,reach_onset,1,"
echo "2.102,reach_end,1,"
echo "2.103,outcome,Success,"
echo "3.500,trial_start,2,"
echo "4.100,reach_onset,1,"
echo "4.950,outcome,Failure,"
echo "9999.9,session_end,1,"
} > "$CSV2"
echo " [~10 MB] ${DATE}_${ID}_behavior_log.csv"
echo ""
done
TOTAL=$(find "$OUTDIR" -type f | wc -l)
echo "Done — ${TOTAL} files in ${OUTDIR}/"
echo ""
echo "Drag the entire '${DATE}' folder into the day-view drop zone at:"
echo " https://experiments.sam-tran.com/experiments/<id>/day/${DATE}?field=<fieldId>"
@@ -0,0 +1,10 @@
ts,event,value,notes
0.000,session_start,1,
1.234,trial_start,1,
1.856,reach_onset,1,
2.102,reach_end,1,
2.103,outcome,Success,
3.500,trial_start,2,
4.100,reach_onset,1,
4.950,outcome,Failure,
9999.9,session_end,1,
1 ts event value notes
2 0.000 session_start 1
3 1.234 trial_start 1
4 1.856 reach_onset 1
5 2.102 reach_end 1
6 2.103 outcome Success
7 3.500 trial_start 2
8 4.100 reach_onset 1
9 4.950 outcome Failure
10 9999.9 session_end 1
@@ -0,0 +1,21 @@
trial_num,timestamp,outcome,reaction_time_ms,reward
1,1.823,Failure,176,0
2,2.646,Failure,378,0
3,3.469,Failure,138,1
4,4.292,Success,150,1
5,5.115,Success,204,1
6,5.938,Success,209,0
7,6.761,Success,369,0
8,7.584,Failure,84,0
9,8.407,Failure,403,0
10,9.230,Success,340,0
11,10.053,Success,442,1
12,10.876,Success,355,0
13,11.699,Success,280,0
14,12.522,Failure,430,1
15,13.345,Success,60,1
16,14.168,Failure,102,0
17,14.991,Success,213,0
18,15.814,Success,54,1
19,16.637,Failure,88,0
20,17.460,Failure,290,1
1 trial_num timestamp outcome reaction_time_ms reward
2 1 1.823 Failure 176 0
3 2 2.646 Failure 378 0
4 3 3.469 Failure 138 1
5 4 4.292 Success 150 1
6 5 5.115 Success 204 1
7 6 5.938 Success 209 0
8 7 6.761 Success 369 0
9 8 7.584 Failure 84 0
10 9 8.407 Failure 403 0
11 10 9.230 Success 340 0
12 11 10.053 Success 442 1
13 12 10.876 Success 355 0
14 13 11.699 Success 280 0
15 14 12.522 Failure 430 1
16 15 13.345 Success 60 1
17 16 14.168 Failure 102 0
18 17 14.991 Success 213 0
19 18 15.814 Success 54 1
20 19 16.637 Failure 88 0
21 20 17.460 Failure 290 1
@@ -0,0 +1,10 @@
ts,event,value,notes
0.000,session_start,1,
1.234,trial_start,1,
1.856,reach_onset,1,
2.102,reach_end,1,
2.103,outcome,Success,
3.500,trial_start,2,
4.100,reach_onset,1,
4.950,outcome,Failure,
9999.9,session_end,1,
1 ts event value notes
2 0.000 session_start 1
3 1.234 trial_start 1
4 1.856 reach_onset 1
5 2.102 reach_end 1
6 2.103 outcome Success
7 3.500 trial_start 2
8 4.100 reach_onset 1
9 4.950 outcome Failure
10 9999.9 session_end 1
@@ -0,0 +1,21 @@
trial_num,timestamp,outcome,reaction_time_ms,reward
1,1.823,Failure,56,0
2,2.646,Failure,299,1
3,3.469,Failure,335,0
4,4.292,Failure,70,1
5,5.115,Success,273,0
6,5.938,Success,59,1
7,6.761,Success,159,0
8,7.584,Success,268,1
9,8.407,Success,330,0
10,9.230,Success,365,1
11,10.053,Success,371,0
12,10.876,Success,230,1
13,11.699,Success,74,0
14,12.522,Failure,381,1
15,13.345,Success,70,1
16,14.168,Failure,215,0
17,14.991,Failure,310,0
18,15.814,Success,78,1
19,16.637,Success,79,1
20,17.460,Failure,303,1
1 trial_num timestamp outcome reaction_time_ms reward
2 1 1.823 Failure 56 0
3 2 2.646 Failure 299 1
4 3 3.469 Failure 335 0
5 4 4.292 Failure 70 1
6 5 5.115 Success 273 0
7 6 5.938 Success 59 1
8 7 6.761 Success 159 0
9 8 7.584 Success 268 1
10 9 8.407 Success 330 0
11 10 9.230 Success 365 1
12 11 10.053 Success 371 0
13 12 10.876 Success 230 1
14 13 11.699 Success 74 0
15 14 12.522 Failure 381 1
16 15 13.345 Success 70 1
17 16 14.168 Failure 215 0
18 17 14.991 Failure 310 0
19 18 15.814 Success 78 1
20 19 16.637 Success 79 1
21 20 17.460 Failure 303 1
@@ -0,0 +1,10 @@
ts,event,value,notes
0.000,session_start,1,
1.234,trial_start,1,
1.856,reach_onset,1,
2.102,reach_end,1,
2.103,outcome,Success,
3.500,trial_start,2,
4.100,reach_onset,1,
4.950,outcome,Failure,
9999.9,session_end,1,
1 ts event value notes
2 0.000 session_start 1
3 1.234 trial_start 1
4 1.856 reach_onset 1
5 2.102 reach_end 1
6 2.103 outcome Success
7 3.500 trial_start 2
8 4.100 reach_onset 1
9 4.950 outcome Failure
10 9999.9 session_end 1
@@ -0,0 +1,21 @@
trial_num,timestamp,outcome,reaction_time_ms,reward
1,1.823,Failure,169,1
2,2.646,Success,127,0
3,3.469,Failure,265,0
4,4.292,Failure,266,1
5,5.115,Success,295,1
6,5.938,Success,94,1
7,6.761,Failure,78,1
8,7.584,Success,182,1
9,8.407,Success,249,1
10,9.230,Success,201,0
11,10.053,Failure,167,0
12,10.876,Failure,124,0
13,11.699,Failure,361,0
14,12.522,Success,306,1
15,13.345,Failure,311,0
16,14.168,Success,186,0
17,14.991,Success,289,0
18,15.814,Failure,152,0
19,16.637,Failure,379,1
20,17.460,Success,332,0
1 trial_num timestamp outcome reaction_time_ms reward
2 1 1.823 Failure 169 1
3 2 2.646 Success 127 0
4 3 3.469 Failure 265 0
5 4 4.292 Failure 266 1
6 5 5.115 Success 295 1
7 6 5.938 Success 94 1
8 7 6.761 Failure 78 1
9 8 7.584 Success 182 1
10 9 8.407 Success 249 1
11 10 9.230 Success 201 0
12 11 10.053 Failure 167 0
13 12 10.876 Failure 124 0
14 13 11.699 Failure 361 0
15 14 12.522 Success 306 1
16 15 13.345 Failure 311 0
17 16 14.168 Success 186 0
18 17 14.991 Success 289 0
19 18 15.814 Failure 152 0
20 19 16.637 Failure 379 1
21 20 17.460 Success 332 0
@@ -0,0 +1,10 @@
ts,event,value,notes
0.000,session_start,1,
1.234,trial_start,1,
1.856,reach_onset,1,
2.102,reach_end,1,
2.103,outcome,Success,
3.500,trial_start,2,
4.100,reach_onset,1,
4.950,outcome,Failure,
9999.9,session_end,1,
1 ts event value notes
2 0.000 session_start 1
3 1.234 trial_start 1
4 1.856 reach_onset 1
5 2.102 reach_end 1
6 2.103 outcome Success
7 3.500 trial_start 2
8 4.100 reach_onset 1
9 4.950 outcome Failure
10 9999.9 session_end 1
@@ -0,0 +1,21 @@
trial_num,timestamp,outcome,reaction_time_ms,reward
1,1.823,Failure,81,1
2,2.646,Success,243,0
3,3.469,Success,218,1
4,4.292,Success,348,1
5,5.115,Failure,418,0
6,5.938,Failure,249,1
7,6.761,Success,133,0
8,7.584,Success,386,1
9,8.407,Failure,116,1
10,9.230,Success,226,0
11,10.053,Success,438,1
12,10.876,Success,405,1
13,11.699,Success,419,1
14,12.522,Success,95,0
15,13.345,Failure,89,1
16,14.168,Success,360,1
17,14.991,Failure,424,1
18,15.814,Success,73,1
19,16.637,Failure,269,0
20,17.460,Success,236,0
1 trial_num timestamp outcome reaction_time_ms reward
2 1 1.823 Failure 81 1
3 2 2.646 Success 243 0
4 3 3.469 Success 218 1
5 4 4.292 Success 348 1
6 5 5.115 Failure 418 0
7 6 5.938 Failure 249 1
8 7 6.761 Success 133 0
9 8 7.584 Success 386 1
10 9 8.407 Failure 116 1
11 10 9.230 Success 226 0
12 11 10.053 Success 438 1
13 12 10.876 Success 405 1
14 13 11.699 Success 419 1
15 14 12.522 Success 95 0
16 15 13.345 Failure 89 1
17 16 14.168 Success 360 1
18 17 14.991 Failure 424 1
19 18 15.814 Success 73 1
20 19 16.637 Failure 269 0
21 20 17.460 Success 236 0