frontend: surface spot-finding upload failures and send sliders on release

This was the only panel firing the generated call directly, with the
rejection routed to console.log. Because the poll then kept returning the
unchanged server value - which still equalled lastDownloadedS - the
resync effect never fired, so the dashboard showed a threshold the broker
was not using, indefinitely and silently. It now goes through useUpload
like its siblings, so a failure raises the snackbar, and onError puts the
server's value back in the panel.

The eight sliders also applied from onChange, which MUI fires for every
intermediate position while dragging: one drag across the ice-ring width
sent ~200 PUTs plus ~200 forced /statistics refetches, each reconfiguring
spot finding on the running acquisition. Dragging now only moves the
panel; the value is sent from onChangeCommitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:50:39 +02:00
co-authored by Claude Opus 5
parent 16145cd55c
commit f8beee7e87
@@ -1,8 +1,10 @@
import {ChangeEvent, memo, useEffect, useState} from 'react';
import {Grid, Slider, Switch, Typography} from "@mui/material";
import {putConfigSpotFinding, spot_finding_settings} from "../client";
import {spot_finding_settings} from "../client";
import {putConfigSpotFindingMutation} from "../client/@tanstack/react-query.gen";
import SettingsPanel from "./SettingsPanel";
import {useUpload} from "./useUpload";
import _ from "lodash";
type MyProps = {
@@ -25,17 +27,20 @@ const default_spot_finding_settings: spot_finding_settings = {
high_res_gap_Q_recipA: 1.5
};
function putValues(x: spot_finding_settings) {
putConfigSpotFinding({ body: x, throwOnError: true })
.catch(error => console.log(error) );
}
function DataProcessingSettings({s: serverS, update}: MyProps) {
const [s, setS] = useState<spot_finding_settings>(default_spot_finding_settings);
const [lastDownloadedS, setLastDownloadedS] = useState<spot_finding_settings>(default_spot_finding_settings);
const [highResGap, setHighResGap] = useState(1.5);
const [highResLimit, setHighResLimit] = useState(2.5);
const { submit, snackbar } = useUpload({
...putConfigSpotFindingMutation(),
// A rejected PUT would otherwise leave the panel showing a value the broker is not using:
// the poll keeps returning the old value, which still equals lastDownloadedS, so the
// effect below never corrects it. Put the server's value back.
onError: () => { if (serverS !== undefined) setS(serverS); },
});
// Only adopt the server copy when it actually changed, otherwise the
// 1 s statistics poll would overwrite edits the user is making.
useEffect(() => {
@@ -50,38 +55,20 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
const apply = (next: spot_finding_settings) => {
setS(next);
putValues(next);
submit(next);
update();
};
const setPhotonCountThreshold = (event: Event, newValue: number | number[]) =>
apply({...s, photon_count_threshold: newValue as number});
const setSignalToNoiseThreshold = (event: Event, newValue: number | number[]) =>
apply({...s, signal_to_noise_threshold: newValue as number});
const setMinPixPerSpot = (event: Event, newValue: number | number[]) =>
apply({...s, min_pix_per_spot: newValue as number});
const setLowResolutionLimit = (event: Event, newValue: number | number[]) =>
apply({...s, low_resolution_limit: newValue as number});
const setHighResolutionLimit = (event: Event, newValue: number | number[]) => {
const v = newValue as number;
setHighResLimit(v);
apply({...s, high_resolution_limit: v});
};
// While the knob is dragged only the panel moves; the value is sent on release. MUI fires
// onChange for every intermediate position, and each one is a PUT that reconfigures spot
// finding on the running acquisition - one drag across the ice-ring width was ~200 of them.
const preview = (patch: Partial<spot_finding_settings>) =>
setS(prev => ({...prev, ...patch}));
// No high-resolution limit means spot finding goes as far as the detector reaches.
const autoHighResolutionLimitToggle = (event: ChangeEvent<HTMLInputElement>) =>
apply({...s, high_resolution_limit: event.target.checked ? undefined : highResLimit});
const setHighResolutionLimitForCountingLowResSpots = (event: Event, newValue: number | number[]) =>
apply({...s, high_resolution_limit_for_spot_count_low_res: newValue as number});
const setIceRingWidth = (event: Event, newValue: number | number[]) =>
apply({...s, ice_ring_width_q_recipA: newValue as number});
const enableSpotFindingToggle = (event: ChangeEvent<HTMLInputElement>) =>
apply({...s, enable: event.target.checked});
@@ -94,14 +81,8 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
const enableHighResGapToggle = (event: ChangeEvent<HTMLInputElement>) =>
apply({...s, high_res_gap_Q_recipA: event.target.checked ? highResGap : undefined});
const handleHighResGap = (event: Event, newValue: number | number[]) => {
const v = newValue as number;
setHighResGap(v);
apply({...s, high_res_gap_Q_recipA: v});
};
return (
<SettingsPanel title="Spot finding parameters" sx={{ minHeight: 800 }}>
<SettingsPanel title="Spot finding parameters" sx={{ minHeight: 800 }} snackbar={snackbar}>
<Grid container spacing={0}>
<Grid item xs={1}/>
@@ -113,13 +94,15 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
<Typography gutterBottom> Count threshold </Typography>
<Slider disabled={!s.enable}
value={Number(s.photon_count_threshold)}
onChange={setPhotonCountThreshold}
onChange={(e, v) => preview({photon_count_threshold: v as number})}
onChangeCommitted={(e, v) => apply({...s, photon_count_threshold: v as number})}
min={1} max={50} step={1} valueLabelDisplay="auto"/>
<br/><Typography> Signal-to-noise threshold </Typography>
<Slider disabled={!s.enable}
value={Number(s.signal_to_noise_threshold)}
onChange={setSignalToNoiseThreshold}
onChange={(e, v) => preview({signal_to_noise_threshold: v as number})}
onChangeCommitted={(e, v) => apply({...s, signal_to_noise_threshold: v as number})}
min={2}
max={10}
step={0.5}
@@ -130,12 +113,14 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
<br/><Typography> Minimum pixel / spot </Typography>
<Slider disabled={!s.enable}
value={Number(s.min_pix_per_spot)}
onChange={setMinPixPerSpot}
onChange={(e, v) => preview({min_pix_per_spot: v as number})}
onChangeCommitted={(e, v) => apply({...s, min_pix_per_spot: v as number})}
min={1} max={8} step={1} valueLabelDisplay="auto"/>
<Typography> Low resolution limit [&#8491;] </Typography>
<Slider disabled={!s.enable}
value={Number(s.low_resolution_limit)}
onChange={setLowResolutionLimit}
onChange={(e, v) => preview({low_resolution_limit: v as number})}
onChangeCommitted={(e, v) => apply({...s, low_resolution_limit: v as number})}
min={10} max={100} step={0.1} valueLabelDisplay="auto"
valueLabelFormat={(value) => value.toFixed(1)}
/>
@@ -147,21 +132,24 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
<Typography> High resolution limit [&#8491;] </Typography>
<Slider disabled={!s.enable || s.high_resolution_limit === undefined}
value={Number(s.high_resolution_limit ?? highResLimit)}
onChange={setHighResolutionLimit}
onChange={(e, v) => { setHighResLimit(v as number); preview({high_resolution_limit: v as number}); }}
onChangeCommitted={(e, v) => apply({...s, high_resolution_limit: v as number})}
min={1} max={5} step={0.1} valueLabelDisplay="auto"
valueLabelFormat={(value) => value.toFixed(1)}
/>
<Typography> High resolution limit for counting low resolution spots [&#8491;] </Typography>
<Slider disabled={!s.enable}
value={Number(s.high_resolution_limit_for_spot_count_low_res)}
onChange={setHighResolutionLimitForCountingLowResSpots}
onChange={(e, v) => preview({high_resolution_limit_for_spot_count_low_res: v as number})}
onChangeCommitted={(e, v) => apply({...s, high_resolution_limit_for_spot_count_low_res: v as number})}
min={2} max={8} step={0.1} valueLabelDisplay="auto"
valueLabelFormat={(value) => value.toFixed(1)}
/>
<Typography> Ice ring width in Q-space [&#8491;<sup>-1</sup>] </Typography>
<Slider disabled={!s.enable}
value={Number(s.ice_ring_width_q_recipA)}
onChange={setIceRingWidth}
onChange={(e, v) => preview({ice_ring_width_q_recipA: v as number})}
onChangeCommitted={(e, v) => apply({...s, ice_ring_width_q_recipA: v as number})}
min={0.0} max={0.2} step={0.001} valueLabelDisplay="auto"
valueLabelFormat={(value) => value.toFixed(3)}
/>
@@ -176,7 +164,8 @@ function DataProcessingSettings({s: serverS, update}: MyProps) {
<Slider
disabled={!s.enable || s.high_res_gap_Q_recipA === undefined}
value={Number(s.high_res_gap_Q_recipA ?? highResGap)}
onChange={handleHighResGap}
onChange={(e, v) => { setHighResGap(v as number); preview({high_res_gap_Q_recipA: v as number}); }}
onChangeCommitted={(e, v) => apply({...s, high_res_gap_Q_recipA: v as number})}
min={0.1}
max={5.0}
step={0.01}