feat(treatment): add voice detail entry #65
@@ -93,6 +93,9 @@ describe('createJsonBodyParser', () => {
|
||||
'/api/voice/extract/extra',
|
||||
'/api/voice',
|
||||
'/voice/extract',
|
||||
// Express ignores one trailing slash, not two — this one never routes, so it must
|
||||
// not get the large parser either.
|
||||
'/api/voice/extract//',
|
||||
]) {
|
||||
const res = await request(buildApp()).post(path).send(bodyOfKb(300));
|
||||
expect(res.status).toBe(413);
|
||||
|
||||
@@ -31,7 +31,10 @@ export const VOICE_BODY_LIMIT = '10mb';
|
||||
* recording — a failure that looks like a broken microphone, not a routing detail.
|
||||
*/
|
||||
function isVoiceExtractPath(path: string): boolean {
|
||||
return path.toLowerCase().replace(/\/+$/, '') === VOICE_EXTRACT_PATH;
|
||||
// Exactly one trailing slash, because that is exactly what Express ignores. Stripping
|
||||
// every trailing slash would hand the 10 MB parser to `/api/voice/extract//`, which
|
||||
// buffers the body and then 404s — memory spent on a request that never routes.
|
||||
return path.toLowerCase().replace(/\/$/, '') === VOICE_EXTRACT_PATH;
|
||||
}
|
||||
|
||||
export function createJsonBodyParser(): RequestHandler {
|
||||
|
||||
@@ -2052,6 +2052,14 @@ export function TreatmentWorkspace({
|
||||
void (async () => {
|
||||
try {
|
||||
const saved = await persistDraft({ force: true });
|
||||
// persistDraft returns a *preview* treatment rather than saving when the
|
||||
// details are not persistable — one blank detail, the kind the workspace opens
|
||||
// with, is enough. A preview's detail id falls back to the client id, so
|
||||
// posting lab cases against it would send the server an id it has never seen
|
||||
// and fail the whole save. Check what came back, not the precondition, so this
|
||||
// holds for every early return persistDraft has.
|
||||
const savedDetail = saved.details.find((d) => d.clientId === detail.clientId);
|
||||
if (!savedDetail?.id || savedDetail.id === detail.clientId) return;
|
||||
await persistLabCases(saved, updatedLabCases);
|
||||
} catch (error: unknown) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorSaveLabShipments')));
|
||||
|
||||
@@ -139,8 +139,16 @@ export function useVoiceCapture({
|
||||
);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
// No recorder means nothing will fire `onstop`, so nothing else will move the phase.
|
||||
// Optional-chaining into a no-op here left the bar recording forever with a running
|
||||
// timer, and only Cancel could get out of it.
|
||||
if (!recorderRef.current) {
|
||||
teardown();
|
||||
setPhase('idle');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
recorderRef.current?.stop();
|
||||
recorderRef.current.stop();
|
||||
} catch {
|
||||
teardown();
|
||||
setPhase('idle');
|
||||
@@ -150,7 +158,9 @@ export function useVoiceCapture({
|
||||
const onStart = useCallback(() => {
|
||||
if (phase !== 'idle' || startingRef.current) return;
|
||||
if (!isMediaRecorderSupported()) {
|
||||
onError(clientError('VOICE_MIC_DENIED'));
|
||||
// Not a permission problem: this browser cannot record at all. Saying "microphone
|
||||
// denied" sends the clinician to hunt for a permission nothing ever asked for.
|
||||
onError(clientError('VOICE_UNSUPPORTED_FORMAT'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,8 +187,9 @@ export function useVoiceCapture({
|
||||
|
||||
const mimeType = pickRecordingMimeType();
|
||||
if (mimeType === null) {
|
||||
// The browser records, but in no container the transcription API accepts.
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
onError(clientError('VOICE_MIC_DENIED'));
|
||||
onError(clientError('VOICE_UNSUPPORTED_FORMAT'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +237,9 @@ export function useVoiceCapture({
|
||||
// recording indicator stays lit until the workspace unmounts.
|
||||
teardown();
|
||||
setPhase('idle');
|
||||
onError(clientError('VOICE_MIC_DENIED'));
|
||||
// The permission was already granted by this point — what failed is the recorder
|
||||
// itself, so this is "this browser cannot record", not "you denied the mic".
|
||||
onError(clientError('VOICE_UNSUPPORTED_FORMAT'));
|
||||
} finally {
|
||||
startingRef.current = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user