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:
@@ -33,7 +33,11 @@ export const experimentsApi = {
|
||||
updateTemplate: (id, template) => api.put(`/experiments/${id}/template`, template).then((r) => r.data),
|
||||
getSubjectTemplate: (id) => api.get(`/experiments/${id}/subject-template`).then((r) => r.data),
|
||||
updateSubjectTemplate: (id, template) => api.put(`/experiments/${id}/subject-template`, template).then((r) => r.data),
|
||||
getDailyStatuses: (id) => api.get(`/experiments/${id}/daily-statuses`).then((r) => r.data),
|
||||
// opts: { date?: string, analysisOnly?: boolean }
|
||||
getDailyStatuses: (id, opts) => {
|
||||
const params = typeof opts === 'string' ? { date: opts } : (opts ?? {});
|
||||
return api.get(`/experiments/${id}/daily-statuses`, { params }).then((r) => r.data);
|
||||
},
|
||||
getDayStatuses: (id, date) => api.get(`/experiments/${id}/daily-statuses`, { params: { date } }).then((r) => r.data),
|
||||
getCalendar: (id, field) => api.get(`/experiments/${id}/calendar`, { params: { field } }).then((r) => r.data),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -95,7 +95,9 @@ export default function ExperimentDetail() {
|
||||
animalsApi.list(id),
|
||||
experimentsApi.getSubjectTemplate(id),
|
||||
experimentsApi.getTemplate(id),
|
||||
experimentsApi.getDailyStatuses(id),
|
||||
// Only fetch statuses that have saved analysis_summary — charts are the only consumer,
|
||||
// and they filter for analysis_summary.total != null anyway.
|
||||
experimentsApi.getDailyStatuses(id, { analysisOnly: true }),
|
||||
]);
|
||||
setExperiment(exp);
|
||||
setAnimals(anims);
|
||||
@@ -371,7 +373,6 @@ export default function ExperimentDetail() {
|
||||
<ExperimentCalendar
|
||||
experimentId={id}
|
||||
template={dailyTemplate}
|
||||
allStatuses={experimentStatuses}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user