fix(mobile-recording): never lose a meeting capture on offline stop, oversize, or re-record
This commit is contained in:
parent
80538c0f23
commit
5b1621d314
9 changed files with 1238 additions and 56 deletions
|
|
@ -0,0 +1,231 @@
|
|||
-- Re-recording a meeting must not destroy its existing transcript before the
|
||||
-- new capture has produced one.
|
||||
--
|
||||
-- 20260821000022 made mobile_begin_meeting_recording wipe raw/edited
|
||||
-- transcript, minutes, transcripts rows and unlink the meeting's audio the
|
||||
-- moment a recording STARTS. A re-record that was then cancelled, failed or
|
||||
-- lost (offline stop, oversized capture) left the meeting in 'error' with its
|
||||
-- previous content gone for good, and since 20260929000004 the unlinked audio
|
||||
-- is also queued for storage purge.
|
||||
--
|
||||
-- New split of responsibilities:
|
||||
-- begin -> only marks the meeting as recording (status and timing).
|
||||
-- complete -> on the FIRST successful completion of a processing job,
|
||||
-- replaces the previous content: deletes old transcript rows,
|
||||
-- clears minutes/LLM metadata and detaches older audio. A replay
|
||||
-- of an already-succeeded job leaves derived content untouched.
|
||||
-- cancel -> a meeting that still holds earlier content returns to
|
||||
-- 'completed' instead of 'error'.
|
||||
-- Signatures, grants and return shapes are unchanged.
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.mobile_begin_meeting_recording(p_meeting_id uuid)
|
||||
RETURNS public.meetings
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
result public.meetings;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
UPDATE public.meetings
|
||||
SET status = 'recording',
|
||||
started_at = now(),
|
||||
ended_at = NULL,
|
||||
error_message = NULL
|
||||
WHERE id = p_meeting_id
|
||||
AND user_id = current_user_id
|
||||
RETURNING * INTO result;
|
||||
|
||||
IF result.id IS NULL THEN
|
||||
RAISE EXCEPTION 'meeting not found or not owned' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.mobile_cancel_meeting_recording(p_meeting_id uuid)
|
||||
RETURNS public.meetings
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
result public.meetings;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
UPDATE public.meetings m
|
||||
SET status = CASE WHEN has_content.present THEN 'completed' ELSE 'error' END,
|
||||
ended_at = now(),
|
||||
error_message = CASE
|
||||
WHEN has_content.present THEN NULL
|
||||
ELSE 'Recording was cancelled before processing.'
|
||||
END
|
||||
FROM (
|
||||
SELECT (
|
||||
mm.raw_transcript IS NOT NULL
|
||||
OR mm.edited_transcript IS NOT NULL
|
||||
OR mm.minutes_markdown IS NOT NULL
|
||||
OR mm.minutes_json IS NOT NULL
|
||||
OR EXISTS (SELECT 1 FROM public.transcripts t WHERE t.meeting_id = mm.id)
|
||||
) AS present
|
||||
FROM public.meetings mm
|
||||
WHERE mm.id = p_meeting_id
|
||||
) AS has_content
|
||||
WHERE m.id = p_meeting_id
|
||||
AND m.user_id = current_user_id
|
||||
AND m.status = 'recording'
|
||||
RETURNING m.* INTO result;
|
||||
IF result.id IS NULL THEN
|
||||
RAISE EXCEPTION 'active meeting recording not found' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Same as 20260821000025 (language SSOT binding) plus the first-completion
|
||||
-- replacement of the previous recording's content.
|
||||
CREATE OR REPLACE FUNCTION public.mobile_complete_meeting_processing(
|
||||
p_meeting_id uuid,
|
||||
p_audio_file_id uuid,
|
||||
p_idempotency_key text,
|
||||
p_transcript text,
|
||||
p_language text,
|
||||
p_provider text,
|
||||
p_duration_ms bigint,
|
||||
p_stt_latency_ms integer
|
||||
)
|
||||
RETURNS public.meetings
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
safe_language text := lower(trim(p_language));
|
||||
owned_audio public.audio_files;
|
||||
owned_job public.processing_jobs;
|
||||
first_completion boolean;
|
||||
result public.meetings;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF p_transcript IS NULL OR length(p_transcript) > 1000000
|
||||
OR p_language IS NULL
|
||||
OR char_length(safe_language) NOT BETWEEN 2 AND 35
|
||||
OR safe_language !~ '^[a-z]{2,3}(-[a-z0-9]{2,8})*$'
|
||||
OR p_provider IS NULL OR length(p_provider) NOT BETWEEN 1 AND 120
|
||||
OR p_duration_ms < 0 OR p_stt_latency_ms < 0 THEN
|
||||
RAISE EXCEPTION 'invalid transcription result' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT * INTO owned_audio
|
||||
FROM public.audio_files
|
||||
WHERE id = p_audio_file_id
|
||||
AND user_id = current_user_id
|
||||
AND meeting_id = p_meeting_id
|
||||
AND history_id IS NULL
|
||||
AND upload_status = 'uploaded'
|
||||
FOR UPDATE;
|
||||
IF owned_audio.id IS NULL THEN
|
||||
RAISE EXCEPTION 'uploaded meeting audio not found' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT * INTO owned_job
|
||||
FROM public.processing_jobs
|
||||
WHERE user_id = current_user_id
|
||||
AND meeting_id = p_meeting_id
|
||||
AND audio_file_id = p_audio_file_id
|
||||
AND idempotency_key = p_idempotency_key
|
||||
AND kind = 'transcription'
|
||||
FOR UPDATE;
|
||||
IF owned_job.id IS NULL THEN
|
||||
RAISE EXCEPTION 'processing job not found' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
-- A replay of an already committed job must not wipe minutes generated
|
||||
-- from this very transcript after it was committed.
|
||||
first_completion := owned_job.status IS DISTINCT FROM 'succeeded';
|
||||
|
||||
IF first_completion THEN
|
||||
DELETE FROM public.transcripts WHERE meeting_id = p_meeting_id;
|
||||
UPDATE public.audio_files
|
||||
SET meeting_id = NULL
|
||||
WHERE meeting_id = p_meeting_id
|
||||
AND user_id = current_user_id
|
||||
AND id <> p_audio_file_id;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.transcripts (
|
||||
meeting_id, segment_index, timestamp_ms, duration_ms, text, speaker, edited
|
||||
) VALUES (
|
||||
p_meeting_id, 0, 0, LEAST(p_duration_ms, 2147483647)::integer,
|
||||
p_transcript, NULL, false
|
||||
)
|
||||
ON CONFLICT (meeting_id, segment_index) DO UPDATE
|
||||
SET duration_ms = EXCLUDED.duration_ms,
|
||||
text = EXCLUDED.text,
|
||||
speaker = NULL,
|
||||
edited = false,
|
||||
updated_at = now();
|
||||
|
||||
UPDATE public.meetings
|
||||
SET status = 'completed',
|
||||
ended_at = COALESCE(ended_at, now()),
|
||||
duration_ms = p_duration_ms,
|
||||
raw_transcript = p_transcript,
|
||||
edited_transcript = NULL,
|
||||
minutes_markdown = CASE WHEN first_completion THEN NULL ELSE minutes_markdown END,
|
||||
minutes_json = CASE WHEN first_completion THEN NULL ELSE minutes_json END,
|
||||
llm_model = CASE WHEN first_completion THEN NULL ELSE llm_model END,
|
||||
llm_latency_ms = CASE WHEN first_completion THEN NULL ELSE llm_latency_ms END,
|
||||
stt_model = p_provider,
|
||||
stt_latency_ms = p_stt_latency_ms,
|
||||
error_message = NULL,
|
||||
audio_storage_key = owned_audio.storage_key,
|
||||
language = safe_language
|
||||
WHERE id = p_meeting_id AND user_id = current_user_id
|
||||
RETURNING * INTO result;
|
||||
|
||||
UPDATE public.processing_jobs
|
||||
SET status = 'succeeded',
|
||||
progress = 100,
|
||||
error_code = NULL,
|
||||
error_message = NULL,
|
||||
result = jsonb_build_object(
|
||||
'audio_file_id', p_audio_file_id,
|
||||
'language', safe_language,
|
||||
'provider', p_provider,
|
||||
'duration_ms', p_duration_ms
|
||||
),
|
||||
completed_at = now()
|
||||
WHERE id = owned_job.id;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.mobile_begin_meeting_recording(uuid) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.mobile_cancel_meeting_recording(uuid) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.mobile_complete_meeting_processing(
|
||||
uuid, uuid, text, text, text, text, bigint, integer
|
||||
) FROM PUBLIC, anon;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.mobile_begin_meeting_recording(uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.mobile_cancel_meeting_recording(uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.mobile_complete_meeting_processing(
|
||||
uuid, uuid, text, text, text, text, bigint, integer
|
||||
) TO authenticated;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue