Skip to content

Conversation

@oscartbeaumont
Copy link
Contributor

@oscartbeaumont oscartbeaumont commented Aug 18, 2025

  • Fix schema
  • Don't do refetching of the upload progress usless we are actively uploading
  • Pretty UI implemented
  • Rewrite logic to EffectTS
  • Only show loading when required (hook up to data fetching)
  • Fix progress polling logic
  • Hook up video player to progress data (instead of just doing the hardcoded 3 retries)
  • Backdrop (the progress circle can be hard to read). Is this a problem in practice?
  • Handle "error" state with some UI
  • Remove upload speed limiter
  • Put upload progress onto reupload button
  • Upload progress working with a web upload too
  • Upload button in web - prevent navigating off the page with unsaved changes.
  • Show upload progress on the main Cap dashboard page
  • Cleanup code

NOTE: This PR requires database migrations: https://app.planetscale.com/cap/cap-production/deploy-requests/36

Summary by CodeRabbit

  • New Features

    • Real-time upload progress streams via channels and a new ProgressCircle UI; progress shown across desktop flows (Export, Share, Settings, Recordings) and web (dashboard cards, embed/share players).
    • Backend progress endpoint and tracking added; lists and APIs now expose hasActiveUpload; players show progress overlays and failure alerts; controls hide while uploading. Tab-close protection during uploads; desktop export defaults to 30fps.
  • Chores

    • Replaced prior event-based upload surface with channel-based progress streaming.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

Replace event-based upload progress with Tauri Channel streaming on desktop; add server-side upload progress tracking (new video_uploads table and POST /progress); surface polling-based upload progress in web UI players and dashboard; plus small clone/conditional refactors in rendering and camera code.

Changes

Cohort / File(s) Summary
Desktop: Tauri progress plumbing
apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/src/upload.rs, apps/desktop/src-tauri/src/recording.rs
Replace tauri event emissions with Channel<UploadProgress>; remove tauri_specta::Event derive; import Channel; add UploadProgressUpdater and CreateErrorResponse; update upload_exported_video / upload_video signatures to accept a progress channel (Option<Channel<UploadProgress>>); emit progress via channel.send(...) and updater; propagate None where appropriate.
Desktop: Frontend Channel wiring
apps/desktop/src/utils/tauri.ts, apps/desktop/src/routes/editor/ExportDialog.tsx, apps/desktop/src/routes/editor/ShareButton.tsx, apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx, apps/desktop/src/routes/recordings-overlay.tsx
uploadExportedVideo now accepts a channel and includes it in invoke payload; exported uploadProgress event removed; UI components create Channel<UploadProgress>, update UI progress from channel callbacks, and pass channel into commands; remove event unlisten cleanup.
Web backend: progress API & lifecycle
apps/web/app/api/desktop/[...route]/video.ts, apps/web/app/api/upload/[...route]/multipart.ts
Add POST /progress endpoint to upsert/delete video_uploads rows with updatedAt gating and ownership checks; create/delete video_uploads rows on video create/delete and delete on multipart complete; switch pro-check to userIsPro.
Database schema
packages/database/schema.ts
Add exported videoUploads table (video_uploads) with videoId, uploaded, total, startedAt, updatedAt.
Web service & RPCs / Domain
packages/web-backend/src/Videos/index.ts, packages/web-backend/src/Videos/VideosRpcs.ts, packages/web-domain/src/Video.ts, packages/web-domain/src/Errors.ts
Add Videos.getUploadProgress(videoId) and RPC GetUploadProgress; add UploadProgress domain type/class; extend InternalError discriminants to include "unknown"; adjust RPC error mappings.
Web UI: players & overlays
apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx, apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx, apps/web/app/s/[videoId]/_components/video/media-player.tsx, apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx, apps/web/app/s/[videoId]/_components/ShareVideo.tsx
Forward videoId prop to players; integrate useUploadProgress(videoId) to show ProgressCircle overlay or failure UI; hide controls when uploading/failed; add retry-on-complete reload behavior.
Web UI: progress hook & component
apps/web/app/s/[videoId]/_components/ProgressCircle.tsx
Add useUploadProgress(videoId, enabled) hook with adaptive polling and RPC; add ProgressCircle SVG UI component.
Web dashboard & data joins
apps/web/actions/videos/get-user-videos.ts, apps/web/app/(org)/dashboard/caps/page.tsx, apps/web/app/(org)/dashboard/caps/Caps.tsx, apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx, apps/web/lib/folder.ts
Left-join video_uploads to compute hasActiveUpload; map to boolean in results; add hasActiveUpload to Caps types; CapCard shows overlay/progress when active.
Uploader: browser progress reporting
apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
Track XHR upload progress, maintain debounced uploadState, POST progress to /api/desktop/video/progress, prevent unload during active upload, and finalize state on completion.
UI library: ProgressCircle
packages/ui-solid/src/ProgressCircle.tsx, packages/ui-solid/src/index.tsx
New accessible ProgressCircle Solid component (variants, sizing); re-exported from package root.
Rendering / camera small refactors
crates/recording/src/feeds/camera.rs, crates/rendering-skia/src/layers/background.rs, crates/rendering/src/lib.rs, crates/rendering/src/scene.rs
Remove unnecessary clone() where types are Copy, simplify conditional expressions, and rename a parameter; behavior preserved.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Desktop UI
  participant Tauri as Tauri (invoke)
  participant Uploader as Desktop uploader (Rust)
  participant API as Web API /api/desktop/video/progress

  User->>UI: Start export & upload
  UI->>UI: create Channel<UploadProgress>(onProgress)
  UI->>Tauri: invoke upload_exported_video(path, mode, channel)
  Tauri->>Uploader: upload_video(..., Some(channel.clone()))
  loop chunked read & upload
    Uploader->>UI: channel.send({progress})
    Uploader->>API: POST /progress {videoId, uploaded, total, updatedAt}
  end
  Uploader-->>Tauri: Result<UploadedVideo>
  Tauri-->>UI: UploadResult
  UI->>UI: Update UI (progress complete)
Loading
sequenceDiagram
  autonumber
  actor Viewer
  participant Page as Web Player Page
  participant RPC as GetUploadProgress RPC
  participant DB as video_uploads

  Viewer->>Page: open video page
  loop adaptive poll (1s..30s)
    Page->>RPC: GetUploadProgress(videoId)
    RPC->>DB: SELECT uploaded,total,updatedAt
    DB-->>RPC: row or null
    alt uploading
      RPC-->>Page: Some{uploaded,total,updatedAt}
      Page->>Page: show ProgressCircle, hide controls
    else none or stale
      RPC-->>Page: None
      Page->>Page: normal UI / show failure
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Brendonovich

Poem

A rabbit taps the upload drum, bytes hop in a row,
Channels whisper progress true, through Rust and Tauri glow.
Tables note each nibble's pace, the UI learns to show,
Circles fill and players wait—then play the final show.
Hop on, little pipelines—carrots for the flow! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Upload Progress in UI" is concise, focused, and accurately describes the primary change in the PR—adding upload progress visibility and the associated progress-reporting wiring—so a teammate scanning history will understand the main intent at a glance.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch upload-progress

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/desktop/src-tauri/src/lib.rs (2)

1037-1040: Consider deriving Clone for better performance and simpler logic.

The UploadProgress struct could benefit from deriving Clone to avoid potential issues with move semantics when passing to channels.

-#[derive(Serialize, Type, Debug, Clone)]
+#[derive(Serialize, Type, Debug, Clone)]
pub struct UploadProgress {
    progress: f64,
}

1078-1078: Consider error handling for channel send operations.

While .ok() is used to ignore send errors (which is reasonable when the receiver might disconnect), consider logging these failures for debugging purposes in production environments.

-channel.send(UploadProgress { progress: 0.0 }).ok();
+if let Err(e) = channel.send(UploadProgress { progress: 0.0 }) {
+    trace!("Failed to send upload progress: {}", e);
+}

Also applies to: 1117-1117, 1122-1122

apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx (1)

250-263: Consider extracting the channel callback for better testability.

The progress calculation and channel callback could be extracted into a separate function for improved testability and reusability.

+const createProgressChannel = (setProgress: (value: number) => void) => {
+  return new Channel<UploadProgress>((progress) =>
+    setProgress(Math.round(progress.progress * 100))
+  );
+};

const [progress, setProgress] = createSignal(0);
const reupload = createMutation(() => ({
  mutationFn: async () => {
    setProgress(0);
    return await commands.uploadExportedVideo(
      props.recording.path,
      "Reupload",
-     new Channel<UploadProgress>((progress) =>
-       setProgress(Math.round(progress.progress * 100)),
-     ),
+     createProgressChannel(setProgress),
    );
  },
  onSettled: () => setProgress(0),
}));
apps/desktop/src/routes/editor/ShareButton.tsx (1)

56-65: Consider handling potential Channel errors.

While the channel callback is straightforward, consider adding error boundaries or try-catch blocks around the state updates to prevent potential crashes.

const uploadChannel = new Channel<UploadProgress>((progress) => {
  console.log("Upload progress:", progress);
- setUploadState(
-   produce((state) => {
-     if (state.type !== "uploading") return;
-     state.progress = Math.round(progress.progress * 100);
-   }),
- );
+ try {
+   setUploadState(
+     produce((state) => {
+       if (state.type !== "uploading") return;
+       state.progress = Math.round(progress.progress * 100);
+     }),
+   );
+ } catch (error) {
+   console.error("Error updating upload progress:", error);
+ }
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0de6d and a3050b3.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • apps/desktop/src-tauri/src/lib.rs (6 hunks)
  • apps/desktop/src-tauri/src/recording.rs (1 hunks)
  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx (4 hunks)
  • apps/desktop/src/routes/editor/ShareButton.tsx (2 hunks)
  • apps/desktop/src/utils/tauri.ts (3 hunks)
  • apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx (2 hunks)
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx (2 hunks)
  • crates/recording/src/feeds/camera.rs (1 hunks)
  • crates/rendering/src/lib.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • crates/recording/src/feeds/camera.rs
  • apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • crates/rendering/src/lib.rs
  • apps/desktop/src-tauri/src/recording.rs
  • apps/desktop/src/utils/tauri.ts
🧰 Additional context used
📓 Path-based instructions (7)
apps/desktop/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/**/*.{ts,tsx}: In the desktop app, rely on unplugin-icons auto-imports; do not manually import icon modules
Use generated tauri_specta commands/events (commands, events) in the desktop frontend; listen to generated events directly
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
  • apps/desktop/src/routes/editor/ShareButton.tsx
{apps/desktop,packages/ui-solid}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Component naming (Solid): components in PascalCase; hooks/utilities in camelCase starting with 'use' where applicable

Files:

  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
  • apps/desktop/src/routes/editor/ShareButton.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
  • apps/desktop/src/routes/editor/ShareButton.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
  • apps/desktop/src/routes/editor/ShareButton.tsx
apps/desktop/src-tauri/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

Use tauri_specta on the Rust side for IPC (typed commands/events) and emit events via generated types

Files:

  • apps/desktop/src-tauri/src/lib.rs
{crates,apps/cli,apps/desktop/src-tauri}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{crates,apps/cli,apps/desktop/src-tauri}/**/*.rs: Rust code must be formatted with rustfmt and adhere to workspace clippy lints
Rust module files should use snake_case names

Files:

  • apps/desktop/src-tauri/src/lib.rs
{crates,apps/cli,apps/desktop/src-tauri}/**/{src,tests}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Rust tests should live in src (inline/unit) or tests (integration) directories

Files:

  • apps/desktop/src-tauri/src/lib.rs
🧠 Learnings (2)
📚 Learning: 2025-08-25T10:58:06.142Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-25T10:58:06.142Z
Learning: Applies to apps/desktop/**/*.{ts,tsx} : Use tanstack/solid-query for server state in the desktop app

Applied to files:

  • apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx
📚 Learning: 2025-08-25T10:58:06.142Z
Learnt from: CR
PR: CapSoftware/Cap#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-25T10:58:06.142Z
Learning: Applies to apps/desktop/src-tauri/**/*.rs : Use tauri_specta on the Rust side for IPC (typed commands/events) and emit events via generated types

Applied to files:

  • apps/desktop/src-tauri/src/lib.rs
🧬 Code graph analysis (3)
apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx (2)
apps/desktop/src/utils/tauri.ts (2)
  • commands (7-269)
  • UploadProgress (453-453)
packages/ui-solid/src/ProgressCircle.tsx (1)
  • ProgressCircle (55-112)
apps/desktop/src-tauri/src/lib.rs (1)
apps/desktop/src/utils/tauri.ts (1)
  • UploadProgress (453-453)
apps/desktop/src/routes/editor/ShareButton.tsx (4)
apps/desktop/src/utils/tauri.ts (3)
  • UploadProgress (453-453)
  • exportVideo (62-64)
  • commands (7-269)
apps/desktop/src/utils/export.ts (1)
  • exportVideo (4-13)
apps/desktop/src/routes/editor/Header.tsx (1)
  • RESOLUTION_OPTIONS (34-38)
apps/desktop/src/routes/editor/context.ts (1)
  • meta (308-310)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
apps/desktop/src-tauri/src/lib.rs (3)

74-74: LGTM! Correct import for Channel-based progress.

The addition of ipc::Channel import aligns with the transition from event-based to channel-based progress reporting, following the Tauri best practices.


1052-1056: LGTM! Clean channel integration for progress reporting.

The function signature properly integrates the Channel<UploadProgress> parameter, enabling real-time progress streaming to the frontend.


2314-2320: LGTM! Cleaner conditional logic.

The refactored conditional using if *focused && let Ok(window_id) = window_id && window_id.activates_dock() is more idiomatic and improves readability.

apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx (2)

1-1: LGTM! Proper imports for channel-based progress.

The imports are correctly updated to include ProgressCircle, Channel, and UploadProgress type for the new progress reporting mechanism.

Also applies to: 9-9, 25-30


273-278: LGTM! Clean progress UI implementation.

The conditional rendering properly shows either the ProgressCircle with actual progress or the rotation icon, providing clear visual feedback to users.

apps/desktop/src/routes/editor/ShareButton.tsx (3)

4-4: LGTM! Proper Channel import for progress tracking.

The imports correctly include Channel from Tauri API and the UploadProgress type.

Also applies to: 11-11


73-93: LGTM! Clean export video integration with progress callback.

The export video call properly integrates with the progress callback for rendering state updates.


112-117: LGTM! Proper error handling for upload results.

The error handling correctly checks for different failure states and provides appropriate user feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
apps/web/app/api/upload/[...route]/multipart.ts (1)

279-296: Don’t gate progress cleanup on rowsAffected; always derive videoId from fileKey and verify ownership explicitly.

  • Using body.videoId risks mismatching the DB row you update/delete vs. the S3 fileKey. Always trust videoIdFromFileKey or reject mismatches.
  • Deleting video_uploads only when rowsAffected > 0 is brittle: if no metadata fields change (or values are identical), your update can be a no-op and the progress row won’t be cleared, leaving the UI stuck.
  • Fix: derive videoId from fileKey, optionally reject mismatch if body.videoId is present, explicitly check ownership via a SELECT, then delete progress unconditionally for owned rows. Optionally coerce numeric fields to numbers before update.

Apply this diff within the current block:

-          const videoId = "videoId" in body ? body.videoId : videoIdFromFileKey;
-          if (videoId) {
-            const result = await db()
-              .update(videos)
-              .set({
-                duration: updateIfDefined(body.durationInSecs, videos.duration),
-                width: updateIfDefined(body.width, videos.width),
-                height: updateIfDefined(body.height, videos.height),
-                fps: updateIfDefined(body.fps, videos.fps),
-              })
-              .where(and(eq(videos.id, videoId), eq(videos.ownerId, user.id)));
-
-            // This proves authentication
-            if (result.rowsAffected > 0)
-              await db()
-                .delete(videoUploads)
-                .where(eq(videoUploads.videoId, videoId));
-          }
+          // Source of truth: derive from fileKey to prevent mismatch with body.videoId
+          const videoId = videoIdFromFileKey;
+          if ("videoId" in body && body.videoId !== videoIdFromFileKey) {
+            console.warn(
+              `videoId mismatch: body.videoId=${body.videoId} fileKey=${fileKey}`,
+            );
+            return c.json({ error: "videoId mismatch" }, 400);
+          }
+          if (videoId) {
+            // Prove ownership explicitly
+            const owned = await db()
+              .select({ id: videos.id })
+              .from(videos)
+              .where(and(eq(videos.id, videoId), eq(videos.ownerId, user.id)))
+              .limit(1);
+            if (!owned.length) {
+              console.warn(
+                `Refusing to update/delete progress for unowned videoId=${videoId}`,
+              );
+            } else {
+              // Optional: coerce numeric fields if present
+              const duration =
+                body.durationInSecs !== undefined
+                  ? Number(body.durationInSecs)
+                  : undefined;
+              const width =
+                body.width !== undefined ? Number(body.width) : undefined;
+              const height =
+                body.height !== undefined ? Number(body.height) : undefined;
+              const fps = body.fps !== undefined ? Number(body.fps) : undefined;
+
+              // Update only when any metadata is provided
+              if (
+                duration !== undefined ||
+                width !== undefined ||
+                height !== undefined ||
+                fps !== undefined
+              ) {
+                await db()
+                  .update(videos)
+                  .set({
+                    duration: updateIfDefined(duration, videos.duration),
+                    width: updateIfDefined(width, videos.width),
+                    height: updateIfDefined(height, videos.height),
+                    fps: updateIfDefined(fps, videos.fps),
+                  })
+                  .where(eq(videos.id, videoId));
+              }
+              // Always clear progress for owned video after complete
+              await db()
+                .delete(videoUploads)
+                .where(eq(videoUploads.videoId, videoId));
+            }
+          }
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

461-484: Make overlay non-blocking to preserve navigation

The overlay's absolute positioning with z-50 blocks the underlying Link from functioning. Add pointer-events-none to allow click-through while keeping the visual overlay.

- <div className="flex absolute inset-0 z-50 justify-center items-center bg-black rounded-t-xl">
+ <div className="flex absolute inset-0 z-50 justify-center items-center bg-black/70 rounded-t-xl pointer-events-none">

Consider also making the overlay slightly translucent (bg-black/70) so users can still see the thumbnail beneath.

apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx (1)

337-344: Use locally captured values for final progress update

The xhr.onload handler reads from uploadState which may be stale due to React state batching. Small uploads might not trigger onprogress, leaving the state uninitialized.

Store the required values (videoId, file size) before the upload starts and use them in onload:

+ const finalVideoId = uploadId;
+ const finalTotal = optimizedBlob.size;
  await new Promise<void>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    // ... existing code ...
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
-       sendProgressUpdate(uploadId, uploadState.total, uploadState.total);
+       sendProgressUpdate(finalVideoId, finalTotal, finalTotal);
        resolve();
      } else {
        reject(new Error(`Upload failed with status ${xhr.status}`));
      }
    };
apps/web/app/api/desktop/[...route]/video.ts (1)

294-301: Add check for existing row before insert to prevent race condition

The current pattern could fail with a primary key constraint violation if another process inserts a row between the UPDATE and INSERT operations.

Check for existence before inserting:

  if (result.rowsAffected === 0) {
-   const result2 = await db().insert(videoUploads).values({
-     videoId,
-     uploaded,
-     total,
-     updatedAt,
-   });
+   // Check if row exists (might have newer updatedAt)
+   const [existing] = await db()
+     .select({ videoId: videoUploads.videoId })
+     .from(videoUploads)
+     .where(eq(videoUploads.videoId, videoId));
+   
+   if (!existing) {
+     // Only insert if row doesn't exist
+     await db().insert(videoUploads).values({
+       videoId,
+       uploaded,
+       total,
+       updatedAt,
+     });
+   }
+   // If row exists with newer updatedAt, silently ignore
  }

Alternatively, use an upsert pattern with ON DUPLICATE KEY UPDATE if your database supports it.

🧹 Nitpick comments (2)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (1)

167-167: Coerce hasActiveUpload to boolean to avoid unintended polling

The optional hasActiveUpload?: boolean can be undefined, which will pass through to useUploadProgress and may trigger unintended behavior. Explicitly convert to boolean to ensure predictable polling.

- const uploadProgress = useUploadProgress(cap.id, cap.hasActiveUpload);
+ const uploadProgress = useUploadProgress(cap.id, Boolean(cap.hasActiveUpload));
apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx (1)

451-498: Potential memory leak from effect dependencies

The effect cleanup function references uploadState.pendingTask from the closure, but this value might be stale when cleanup runs. This could lead to timers not being properly cleared.

Store the timer reference in a ref to ensure proper cleanup:

+ const pendingTaskRef = useRef<ReturnType<typeof setTimeout>>();
  useEffect(() => {
    if (!uploadState.videoId || uploadState.uploaded === 0 || !isUploading)
      return;
    
-   // Clear any existing pending task
-   if (uploadState.pendingTask) clearTimeout(uploadState.pendingTask);
+   // Clear any existing pending task
+   if (pendingTaskRef.current) {
+     clearTimeout(pendingTaskRef.current);
+     pendingTaskRef.current = undefined;
+   }
    
    const shouldSendImmediately = uploadState.uploaded >= uploadState.total;
    
    if (shouldSendImmediately) {
      // Send completion update immediately and clear state
      sendProgressUpdate(
        uploadState.videoId,
        uploadState.uploaded,
        uploadState.total,
      );
-     setUploadState((prev) => ({
-       ...prev,
-       pendingTask: undefined,
-     }));
    } else {
      // Schedule delayed update (after 2 seconds)
      const newPendingTask = setTimeout(() => {
        if (uploadState.videoId) {
          sendProgressUpdate(
            uploadState.videoId,
            uploadState.uploaded,
            uploadState.total,
          );
        }
      }, 2000);
-     setUploadState((prev) => ({
-       ...prev,
-       pendingTask: newPendingTask,
-     }));
+     pendingTaskRef.current = newPendingTask;
    }
    
    return () => {
-     if (uploadState.pendingTask) clearTimeout(uploadState.pendingTask);
+     if (pendingTaskRef.current) {
+       clearTimeout(pendingTaskRef.current);
+       pendingTaskRef.current = undefined;
+     }
    };
  }, [
    uploadState.videoId,
    uploadState.uploaded,
    uploadState.total,
    isUploading,
  ]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3050b3 and c210134.

📒 Files selected for processing (6)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx (4 hunks)
  • apps/web/app/api/desktop/[...route]/video.ts (5 hunks)
  • apps/web/app/api/upload/[...route]/multipart.ts (2 hunks)
  • apps/web/app/s/[videoId]/_components/ProgressCircle.tsx (1 hunks)
  • packages/web-domain/src/Video.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/s/[videoId]/_components/ProgressCircle.tsx
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

When HTTP routes are necessary, implement them under app/api/*, configure CORS correctly, and set precise revalidation

Files:

  • apps/web/app/api/upload/[...route]/multipart.ts
  • apps/web/app/api/desktop/[...route]/video.ts
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration

Files:

  • apps/web/app/api/upload/[...route]/multipart.ts
  • apps/web/app/api/desktop/[...route]/video.ts
  • apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
{apps/web,packages/ui}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'

Files:

  • apps/web/app/api/upload/[...route]/multipart.ts
  • apps/web/app/api/desktop/[...route]/video.ts
  • apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/web/app/api/upload/[...route]/multipart.ts
  • packages/web-domain/src/Video.ts
  • apps/web/app/api/desktop/[...route]/video.ts
  • apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
🧬 Code graph analysis (4)
apps/web/app/api/upload/[...route]/multipart.ts (2)
packages/database/index.ts (2)
  • db (30-35)
  • updateIfDefined (38-39)
packages/database/schema.ts (2)
  • videos (238-284)
  • videoUploads (644-650)
packages/web-domain/src/Video.ts (3)
packages/web-domain/src/Folder.ts (1)
  • NotFoundError (13-16)
packages/web-domain/src/Errors.ts (1)
  • InternalError (3-6)
packages/web-domain/src/Policy.ts (1)
  • PolicyDeniedError (18-21)
apps/web/app/api/desktop/[...route]/video.ts (4)
packages/utils/src/constants/plans.ts (1)
  • userIsPro (21-45)
packages/database/index.ts (1)
  • db (30-35)
packages/database/schema.ts (2)
  • videoUploads (644-650)
  • videos (238-284)
packages/web-domain/src/Video.ts (1)
  • Video (13-35)
apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (2)
apps/web/app/s/[videoId]/_components/ProgressCircle.tsx (1)
  • useUploadProgress (25-65)
apps/web/components/VideoThumbnail.tsx (1)
  • VideoThumbnail (46-136)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
apps/web/app/api/upload/[...route]/multipart.ts (1)

2-3: LGTM on schema imports.

Importing videoUploads here is appropriate for cleanup after completion.

packages/web-domain/src/Video.ts (1)

37-44: LGTM! Well-structured upload progress type with proper constraints

The UploadProgress class properly constrains uploaded and total as non-negative integers, matching the database schema and preventing invalid states.

apps/web/app/api/desktop/[...route]/video.ts (1)

269-278: LGTM! Ownership verification properly implemented

The ownership check correctly destructures the first element from the array result and validates both existence and ownership before proceeding.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/s/[videoId]/page.tsx (1)

103-113: Declare hasActiveUpload?: boolean on VideoWithOrganization and the nested video type

The fetched data includes hasActiveUpload (see apps/web/app/s/[videoId]/page.tsx:297 and the object at ~line 664) but the type at ~lines 103–113 omits it — add hasActiveUpload?: boolean to VideoWithOrganization and to the nested video type so consumers (ShareVideo, HLSVideoPlayer, CapVideoPlayer, etc.) do not get TS errors.

♻️ Duplicate comments (1)
apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx (1)

280-286: Loading overlay condition inverted; spinner won’t show during normal load.

- className={clsx(
-   "flex absolute inset-0 z-10 justify-center items-center bg-black transition-opacity duration-300",
-   isUploading || videoLoaded || !isUploadFailed
-     ? "opacity-0 pointer-events-none"
-     : "opacity-100",
- )}
+ className={clsx(
+   "flex absolute inset-0 z-10 justify-center items-center bg-black transition-opacity duration-300",
+   !isUploading && !videoLoaded && !isUploadFailed
+     ? "opacity-100"
+     : "opacity-0 pointer-events-none",
+ )}
🧹 Nitpick comments (1)
apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (1)

307-313: Duplicate ‘loadedmetadata’ listener registration.

handleLoadedMetadataWithTracks is added twice; causes duplicate cue handling and inconsistent state.

- video.addEventListener("loadedmetadata", handleLoadedMetadataWithTracks);
- video.addEventListener("load", handleLoad);
- video.addEventListener("play", handlePlay);
- video.addEventListener("error", handleError as EventListener);
- video.addEventListener("loadedmetadata", handleLoadedMetadataWithTracks);
+ video.addEventListener("loadedmetadata", handleLoadedMetadataWithTracks);
+ video.addEventListener("load", handleLoad);
+ video.addEventListener("play", handlePlay);
+ video.addEventListener("error", handleError as EventListener);

Ensure cleanup removes it once (already handled).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c210134 and 2451afc.

📒 Files selected for processing (16)
  • apps/desktop/src-tauri/src/recording.rs (3 hunks)
  • apps/desktop/src-tauri/src/upload.rs (9 hunks)
  • apps/web/actions/videos/get-user-videos.ts (2 hunks)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (4 hunks)
  • apps/web/app/(org)/dashboard/caps/page.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx (1 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx (3 hunks)
  • apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx (2 hunks)
  • apps/web/app/embed/[videoId]/page.tsx (3 hunks)
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (4 hunks)
  • apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx (3 hunks)
  • apps/web/app/s/[videoId]/_components/ProgressCircle.tsx (1 hunks)
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx (2 hunks)
  • apps/web/app/s/[videoId]/page.tsx (4 hunks)
  • apps/web/lib/folder.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/desktop/src-tauri/src/recording.rs
  • apps/web/app/s/[videoId]/_components/ShareVideo.tsx
  • apps/web/app/s/[videoId]/_components/ProgressCircle.tsx
  • apps/web/app/embed/[videoId]/_components/EmbedVideo.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/actions/videos/get-user-videos.ts
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/desktop/src-tauri/src/upload.rs
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration

Files:

  • apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • apps/web/app/embed/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/lib/folder.ts
  • apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx
{apps/web,packages/ui}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'

Files:

  • apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • apps/web/app/embed/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/lib/folder.ts
  • apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • apps/web/app/embed/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/lib/folder.ts
  • apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx
  • apps/web/app/embed/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx
  • apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx
🧬 Code graph analysis (5)
apps/web/app/embed/[videoId]/page.tsx (1)
packages/database/schema.ts (3)
  • videos (238-284)
  • sharedVideos (286-306)
  • videoUploads (644-650)
apps/web/app/s/[videoId]/page.tsx (1)
packages/database/schema.ts (4)
  • videos (238-284)
  • sharedVideos (286-306)
  • users (46-94)
  • videoUploads (644-650)
apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (3)
packages/web-domain/src/Video.ts (3)
  • Video (13-35)
  • VideoId (9-9)
  • VideoId (10-10)
apps/web/app/s/[videoId]/_components/ProgressCircle.tsx (1)
  • useUploadProgress (25-62)
apps/web/app/s/[videoId]/_components/video/media-player.tsx (32)
  • MediaPlayerVideo (3114-3114)
  • MediaPlayerVideo (3138-3138)
  • MediaPlayerLoading (3118-3118)
  • MediaPlayerLoading (3142-3142)
  • MediaPlayerError (3119-3119)
  • MediaPlayerError (3144-3144)
  • MediaPlayerVolumeIndicator (3120-3120)
  • MediaPlayerVolumeIndicator (3143-3143)
  • MediaPlayerControls (3116-3116)
  • MediaPlayerControls (3140-3140)
  • MediaPlayerControlsOverlay (3117-3117)
  • MediaPlayerControlsOverlay (3141-3141)
  • MediaPlayerSeek (3124-3124)
  • MediaPlayerSeek (3148-3148)
  • MediaPlayerPlay (3121-3121)
  • MediaPlayerPlay (3145-3145)
  • MediaPlayerSeekBackward (3122-3122)
  • MediaPlayerSeekBackward (3146-3146)
  • MediaPlayerSeekForward (3123-3123)
  • MediaPlayerSeekForward (3147-3147)
  • MediaPlayerVolume (3125-3125)
  • MediaPlayerVolume (3149-3149)
  • MediaPlayerTime (3126-3126)
  • MediaPlayerTime (3150-3150)
  • MediaPlayerCaptions (3131-3131)
  • MediaPlayerCaptions (3155-3155)
  • MediaPlayerSettings (3133-3133)
  • MediaPlayerSettings (3157-3157)
  • MediaPlayerPiP (3130-3130)
  • MediaPlayerPiP (3154-3154)
  • MediaPlayerFullscreen (3129-3129)
  • MediaPlayerFullscreen (3153-3153)
apps/web/lib/folder.ts (1)
packages/database/schema.ts (2)
  • videos (238-284)
  • videoUploads (644-650)
apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx (2)
packages/web-domain/src/Video.ts (3)
  • Video (13-35)
  • VideoId (9-9)
  • VideoId (10-10)
apps/web/app/s/[videoId]/_components/ProgressCircle.tsx (1)
  • useUploadProgress (25-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx (1)

35-36: Type shape extension LGTM.

SharedVideoData now includes hasActiveUpload; no logic change here. Ensure the server data guarantees this field to avoid undefined where not intended.

apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx (1)

389-399: Progress gating depends on server flag; verify upstream fixes.

useUploadProgress(videoId, hasActiveUpload || false) will be disabled if hasActiveUpload is inverted upstream. After fixing queries, this component is correct.

apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx (1)

17-18: Approve — downstream typing includes hasActiveUpload.

CapCard's cap type declares hasActiveUpload: boolean | undefined in apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx, so passing it through from SharedCapCard is safe.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/app/embed/[videoId]/page.tsx (1)

150-150: Fix inverted hasPassword predicate (use IS NOT NULL).

This currently returns true when no password exists.

Apply:

-          hasPassword: sql`${videos.password} IS NULL`.mapWith(Boolean),
+          hasPassword: sql`${videos.password} IS NOT NULL`.mapWith(Boolean),
🧹 Nitpick comments (2)
apps/web/app/embed/[videoId]/page.tsx (2)

160-161: Add a LIMIT to avoid extra rows from sharedVideos left join.

shared_videos can be one-to-many; destructuring [video] will take the first arbitrary row. Add a limit to reduce scan and ensure deterministic single row.

   .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId))
   .where(eq(videos.id, videoId)),
+  .limit(1),

203-204: Narrow the type of hasActiveUpload to boolean.

SQL IS NOT NULL maps to a definite boolean; no undefined.

-    hasActiveUpload: boolean | undefined;
+    hasActiveUpload: boolean;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2451afc and a4a9185.

📒 Files selected for processing (6)
  • apps/web/actions/videos/get-user-videos.ts (2 hunks)
  • apps/web/app/(org)/dashboard/caps/page.tsx (2 hunks)
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx (3 hunks)
  • apps/web/app/embed/[videoId]/page.tsx (3 hunks)
  • apps/web/app/s/[videoId]/page.tsx (4 hunks)
  • apps/web/lib/folder.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/actions/videos/get-user-videos.ts
  • apps/web/app/(org)/dashboard/spaces/[spaceId]/page.tsx
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/lib/folder.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration

Files:

  • apps/web/app/embed/[videoId]/page.tsx
{apps/web,packages/ui}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'

Files:

  • apps/web/app/embed/[videoId]/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/web/app/embed/[videoId]/page.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/web/app/embed/[videoId]/page.tsx
🧬 Code graph analysis (1)
apps/web/app/embed/[videoId]/page.tsx (1)
packages/database/schema.ts (3)
  • videos (238-284)
  • sharedVideos (286-306)
  • videoUploads (644-650)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Analyze (rust)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
🔇 Additional comments (2)
apps/web/app/embed/[videoId]/page.tsx (2)

9-10: Import of videoUploads looks correct.

Matches schema and enables the join below.


154-156: hasActiveUpload mapping is correct.

Using IS NOT NULL with .mapWith(Boolean) is the right approach here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/app/(org)/dashboard/caps/page.tsx (1)

176-179: Aggregate hasActiveUpload to be GROUP BY–safe (or use EXISTS).

Using ${videoUploads.videoId} IS NOT NULL in a grouped query is non-aggregated and can error under only_full_group_by or yield nondeterministic values due to row multiplication from other LEFT JOINs. Replace with an aggregate or a scalar EXISTS subquery.

Minimal fix (aggregate; keeps the LEFT JOIN):

-      hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith(
-        Boolean,
-      ),
+      hasActiveUpload: sql<number>`IF(COUNT(DISTINCT ${videoUploads.videoId}) > 0, 1, 0)`.mapWith(Boolean),

Alternative (preferred; removes need to join video_uploads and avoids row blow-up):

-    .leftJoin(videoUploads, eq(videos.id, videoUploads.videoId))
+    // no join needed for hasActiveUpload when using EXISTS
-      hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith(
-        Boolean,
-      ),
+      hasActiveUpload: sql<boolean>`
+        EXISTS(
+          SELECT 1
+          FROM video_uploads vu
+          WHERE vu.video_id = ${videos.id}
+        )
+      `.mapWith(Boolean),

Also applies to: 186-195

🧹 Nitpick comments (2)
apps/web/app/embed/[videoId]/page.tsx (2)

203-204: Tighten type: make hasActiveUpload non-optional.

This field is always selected; removing undefined simplifies consumers.

-    hasActiveUpload: boolean | undefined;
+    hasActiveUpload: boolean;

150-150: Optional cleanup: drop unused hasPassword from this select if it’s not consumed.

If downstream doesn’t use it here, omit to save bytes and reduce coupling.

-          hasPassword: sql`${videos.password} IS NOT NULL`.mapWith(Boolean),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4a9185 and e470b08.

📒 Files selected for processing (5)
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx (5 hunks)
  • apps/web/app/(org)/dashboard/caps/page.tsx (2 hunks)
  • apps/web/app/embed/[videoId]/page.tsx (3 hunks)
  • apps/web/app/s/[videoId]/page.tsx (4 hunks)
  • apps/web/lib/folder.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/lib/folder.ts
  • apps/web/app/s/[videoId]/page.tsx
  • apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration

Files:

  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/embed/[videoId]/page.tsx
{apps/web,packages/ui}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'

Files:

  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/embed/[videoId]/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/embed/[videoId]/page.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/web/app/(org)/dashboard/caps/page.tsx
  • apps/web/app/embed/[videoId]/page.tsx
🧬 Code graph analysis (2)
apps/web/app/(org)/dashboard/caps/page.tsx (1)
packages/database/schema.ts (2)
  • videos (238-284)
  • videoUploads (644-650)
apps/web/app/embed/[videoId]/page.tsx (1)
packages/database/schema.ts (3)
  • videos (238-284)
  • sharedVideos (286-306)
  • videoUploads (644-650)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (1)
apps/web/app/embed/[videoId]/page.tsx (1)

150-156: LGTM on password/upload flags.

Predicates now correctly use IS NOT NULL, and the LEFT JOIN on video_uploads matches. No issues.

Also applies to: 160-161

Comment on lines +250 to 263
const [progress, setProgress] = createSignal(0);
const reupload = createMutation(() => ({
mutationFn: () => {
return commands.uploadExportedVideo(
mutationFn: async () => {
setProgress(0);
return await commands.uploadExportedVideo(
props.recording.path,
"Reupload",
new Channel<UploadProgress>((progress) =>
setProgress(Math.round(progress.progress * 100)),
),
);
},
onSettled: () => setProgress(0),
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but this could be refactored to use an event bus and the signal moved into the progress component, that gets recreated and initialized at 0 when reupload.isPending

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upload progress/feedback in UI

4 participants