perf: server-side caching + remove bulk status fetch from experiment page

- 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>
This commit is contained in:
Experiments DB Dev
2026-04-23 14:03:59 -04:00
parent 52d5fca5d1
commit d2c8d69718
6 changed files with 102 additions and 22 deletions
+8 -14
View File
@@ -4,10 +4,9 @@ import {
getDay, addMonths, subMonths,
} from 'date-fns';
import { useNavigate } from 'react-router-dom';
import { experimentsApi } from '../api/client';
const BUILTIN_KEYS_SET = new Set(['experiment_description', 'vitals', 'treatment', 'notes']);
export default function ExperimentCalendar({ experimentId, template, allStatuses }) {
export default function ExperimentCalendar({ experimentId, template }) {
const navigate = useNavigate();
const [currentMonth, setCurrentMonth] = useState(() => new Date());
const [selectedField, setSelectedField] = useState(null);
@@ -26,17 +25,12 @@ export default function ExperimentCalendar({ experimentId, template, allStatuses
useEffect(() => {
if (!selectedField) return;
const isBuiltin = BUILTIN_KEYS_SET.has(selectedField);
const days = {};
allStatuses.forEach((s) => {
const val = isBuiltin ? s[selectedField] : s.custom_fields?.[selectedField];
if (val !== null && val !== undefined && String(val).trim() !== '') {
const key = s.date.slice(0, 10);
days[key] = (days[key] || 0) + 1;
}
});
setCalendarDays(days);
}, [allStatuses, selectedField]);
let cancelled = false;
experimentsApi.getCalendar(experimentId, selectedField)
.then(({ days }) => { if (!cancelled) setCalendarDays(days); })
.catch(() => {});
return () => { cancelled = true; };
}, [experimentId, selectedField]);
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);