From 9dc202ce03cf8b49af97d4fca4a599eb102bf285 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Wed, 15 Apr 2026 11:15:00 +0100 Subject: [PATCH] fix(webapp): log recording upload failures and restore 6s clips --- WebApp/src/lib/app/controller.js | 124 ++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 9 deletions(-) diff --git a/WebApp/src/lib/app/controller.js b/WebApp/src/lib/app/controller.js index 77ce1f6..837db1b 100644 --- a/WebApp/src/lib/app/controller.js +++ b/WebApp/src/lib/app/controller.js @@ -34,7 +34,7 @@ const MOTION_DETECTION_PROFILES = { releaseThreshold: 0.05, consecutiveTriggerFrames: 3, cooldownMs: 12000, - minimumEventMs: 9000 + minimumEventMs: 6000 }, balanced: { profile: 'balanced', @@ -46,7 +46,7 @@ const MOTION_DETECTION_PROFILES = { releaseThreshold: 0.04, consecutiveTriggerFrames: 3, cooldownMs: 9000, - minimumEventMs: 8000 + minimumEventMs: 6000 }, responsive: { profile: 'responsive', @@ -58,7 +58,7 @@ const MOTION_DETECTION_PROFILES = { releaseThreshold: 0.035, consecutiveTriggerFrames: 2, cooldownMs: 7000, - minimumEventMs: 7000 + minimumEventMs: 6000 } }; @@ -1478,6 +1478,24 @@ const startOfferToClient = async (streamSessionId, requesterDeviceId) => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const summarizeFetchFailure = async (response) => { + if (!response) { + return 'no response returned'; + } + + const parts = [`status ${response.status}`]; + try { + const bodyText = (await response.text()).trim(); + if (bodyText) { + parts.push(bodyText.slice(0, 240)); + } + } catch { + // Ignore body parsing failures for diagnostics. + } + + return parts.join(' ยท '); +}; + const finalizeRecordingForStream = async (streamSessionId, captureResult) => { const currentDevice = getAppState().device; if (!currentDevice?.id) { @@ -1496,6 +1514,8 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => { if (!captureResult?.blob || captureResult.blob.size === 0) { throw new Error('No captured video blob to upload'); } + + addActivity('Recording', `Preparing upload for stream ${streamSessionId}`); const compressedBlob = await compressRecordingBlob(captureResult.blob); const uploadMeta = await api.request('/videos/upload-url', { @@ -1507,6 +1527,26 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => { recordingId: recording.id }) }); + const uploadOrigin = (() => { + try { + return new URL(uploadMeta.uploadUrl).origin; + } catch { + return 'invalid upload URL'; + } + })(); + console.info('[recording.upload] upload URL issued', { + recordingId: recording.id, + streamSessionId, + objectKey: uploadMeta.objectKey, + bucket: uploadMeta.bucket, + uploadOrigin, + blobSize: compressedBlob.size, + blobType: compressedBlob.type || 'video/webm' + }); + addActivity( + 'Recording', + `Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}` + ); const uploadResponse = await fetch(uploadMeta.uploadUrl, { method: 'PUT', @@ -1515,9 +1555,19 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => { }); if (!uploadResponse.ok) { - throw new Error(`Upload failed with status ${uploadResponse.status}`); + throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`); } + console.info('[recording.upload] object uploaded', { + recordingId: recording.id, + streamSessionId, + objectKey: uploadMeta.objectKey, + bucket: uploadMeta.bucket, + status: uploadResponse.status, + sizeBytes: compressedBlob.size + }); + addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`); + await api.events.finalizeRecording(recording.id, { objectKey: uploadMeta.objectKey, bucket: uploadMeta.bucket, @@ -1525,14 +1575,29 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => { sizeBytes: compressedBlob.size }); + console.info('[recording.upload] recording finalized', { + recordingId: recording.id, + streamSessionId, + objectKey: uploadMeta.objectKey, + durationSeconds: captureResult.durationSeconds, + sizeBytes: compressedBlob.size + }); addActivity('Recording', 'Recording uploaded and finalized'); return true; } catch (error) { - console.error('Recording upload failed, falling back to simulated key', error); + console.error('[recording.upload] stream upload failed, falling back to simulated key', { + recordingId: recording.id, + streamSessionId, + error: error instanceof Error ? error.message : String(error) + }); + addActivity( + 'Recording', + `Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}` + ); const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`; await api.events.finalizeRecording(recording.id, { objectKey: fallbackObjectKey, - durationSeconds: captureResult?.durationSeconds ?? 15, + durationSeconds: captureResult?.durationSeconds ?? 6, sizeBytes: captureResult?.blob?.size ?? 5000000 }); addActivity('Recording', 'Upload failed; finalized with simulator fallback'); @@ -1560,6 +1625,7 @@ const uploadStandaloneMotionRecording = async (captureResult) => { } try { + addActivity('Recording', 'Preparing standalone motion clip upload'); const compressedBlob = await compressRecordingBlob(captureResult.blob); const uploadMeta = await api.request('/videos/upload-url', { method: 'POST', @@ -1570,6 +1636,23 @@ const uploadStandaloneMotionRecording = async (captureResult) => { eventId: lastMotionEventId }) }); + const uploadOrigin = (() => { + try { + return new URL(uploadMeta.uploadUrl).origin; + } catch { + return 'invalid upload URL'; + } + })(); + console.info('[recording.upload] standalone upload URL issued', { + eventId: lastMotionEventId, + recordingId: uploadMeta.video?.id, + objectKey: uploadMeta.objectKey, + bucket: uploadMeta.bucket, + uploadOrigin, + blobSize: compressedBlob.size, + blobType: compressedBlob.type || 'video/webm' + }); + addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`); const uploadResponse = await fetch(uploadMeta.uploadUrl, { method: 'PUT', @@ -1578,9 +1661,19 @@ const uploadStandaloneMotionRecording = async (captureResult) => { }); if (!uploadResponse.ok) { - throw new Error(`Upload failed with status ${uploadResponse.status}`); + throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`); } + console.info('[recording.upload] standalone object uploaded', { + eventId: lastMotionEventId, + recordingId: uploadMeta.video?.id, + objectKey: uploadMeta.objectKey, + bucket: uploadMeta.bucket, + status: uploadResponse.status, + sizeBytes: compressedBlob.size + }); + addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`); + await api.events.finalizeRecording(uploadMeta.video.id, { objectKey: uploadMeta.objectKey, bucket: uploadMeta.bucket, @@ -1588,11 +1681,24 @@ const uploadStandaloneMotionRecording = async (captureResult) => { sizeBytes: compressedBlob.size }); + console.info('[recording.upload] standalone recording finalized', { + eventId: lastMotionEventId, + recordingId: uploadMeta.video?.id, + objectKey: uploadMeta.objectKey, + durationSeconds: captureResult.durationSeconds, + sizeBytes: compressedBlob.size + }); addActivity('Recording', `Motion clip uploaded (${uploadMeta.objectKey})`); return true; } catch (error) { - console.error('Standalone motion upload failed', error); - addActivity('Recording', 'Standalone motion upload failed'); + console.error('[recording.upload] standalone motion upload failed', { + eventId: lastMotionEventId, + error: error instanceof Error ? error.message : String(error) + }); + addActivity( + 'Recording', + `Standalone motion upload failed: ${error instanceof Error ? error.message : 'unknown error'}` + ); return false; } };