The download progress toast sits in the bottom-right corner on top of the
comment composer, blocking the voice-recording button and the comment box for
the whole duration of a download.
Render it through toast.custom so it can be collapsed to a small pill (percent
+ spinner) and expanded again while the download keeps running. The minimized
choice sticks for the rest of the session. The sonner <li> is click-through, so
only the panel itself covers the controls underneath.
Also dismiss the panel on failure — it had duration: Infinity and used to stay
on screen forever after an error.
- move resolveR2PlaybackUrl into lib/video-upload-validation.ts so the compare
view and the main video page cannot drift apart
- validate the resolved URL with isPlayableVideoUrl before it reaches <video src>
- add a per-player cooldown so a follower that cannot keep up is not seeked
every second, which would stutter rather than correct
A Stripe customer can own several subscriptions. Two defects let that happen
and corrupt the user's billing state:
1. Checkout allowed a fresh subscription whenever the user was not ACTIVE/
TRIALING, so a PAST_DUE user started a brand-new subscription (Stripe
Checkout always creates one) instead of recovering the existing one.
Add hasRecoverableSubscription() (ACTIVE/TRIALING/PAST_DUE/UNPAID/
INCOMPLETE); block checkout and route these users to the billing portal
('Update Payment Method') both in the API guard and the settings UI.
2. Subscription webhooks trusted the event's single subscription, so an old
subscription's deletion could clobber a newer active one (marking the user
CANCELED / No access). Every subscription event now re-derives state from
the full set of the customer's Stripe subscriptions via
syncStripeCustomerSubscriptions() + selectAuthoritativeSubscription(),
making the sync order-independent and self-healing.
The compare page predates the r2 upload provider: r2 versions fell
through to a URL-safety check that throws on app-relative upload URLs,
so their panels rendered nothing and registered no player — the shared
controls drove an empty list and nothing played.
- add an R2Panel mapping a plain video element over the app upload
route to the shared adapter, using the same playback-url resolution
as the main video page
- make play/pause state detection work without the YouTube API loaded
(numeric fallback), so bunny/r2-only comparisons can pause
- re-sync panels that drift more than 350ms from the source player
once per second so playback stays aligned, not just starts aligned
Verified against the running app: both versions play in lockstep
(0.000s measured drift), pause together, and seek together.
Co-Authored-By: Claude Fable 5 <[email protected]>
Bunny/cross-origin downloads are fetched into a blob before saving, which
on large files or slow connections looked stuck (spinner only). Stream the
body through a counting transform and show real byte progress in a toast:
per-file percent for single downloads and file N/M + percent for bulk.
- Progress is measured from Content-Length + received bytes (not estimated).
- The blob is assembled by the browser from the stream (can be disk-backed),
so we don't accumulate chunks in the JS heap.
- Only the blob path shows a toast; same-origin (R2/S3/MinIO) and the >10 GB
fallback use the browser's native download UI.
Project/selected downloads now include only videos by default. Add an
"Include assets" checkbox toggle to both download dropdowns (default off)
that adds b-rolls and other attached assets to the download when enabled.
- buildProjectDownloadManifest gains an includeAssets option (default false).
- Download route reads ?assets=1 and passes it through.
Add a "Move to project" action in the video card dropdown and the
selection-mode toolbar. Videos (with their versions, comments, assets and
video-scoped share links) can be moved into another project in the same
workspace.
- New GET/POST /api/projects/[projectId]/videos/move: GET lists manageable
destination projects in the workspace; POST performs the move.
- Requires canEdit on both source and destination; same-workspace only.
- Move runs in an interactive transaction that re-asserts source ownership
atomically (updateMany guarded by projectId) to avoid a TOCTOU race, and
returns 409 on conflict. GET is rate-limited ('api').
- Accept source=auto in the version download route (was 400 Bad Request),
so bulk/project downloads of Bunny videos no longer fail.
- Bulk/project downloads now request the original (uncompressed) Bunny file
so quality never drops (was source=auto which could fall back to compressed).
- Project/selected downloads default to the latest version of each video and
add a separate "All versions" option in the download dropdowns.
Comment content was stored with newlines intact but rendered inside <p>
elements with default white-space, collapsing line breaks into single
spaces. Add whitespace-pre-wrap (and break-words) to the comment/reply
render wrappers in the comments pane and the compare-versions view.
"Select all" previously selected every video across every page, which is
too easy to trigger by accident when the user only meant the videos
visible on the current page. Scope select/deselect to the current page's
videos and relabel the button to "Select page"/"Deselect page" when the
project spans multiple pages.
When every video on the current page was bulk-deleted, router.refresh()
re-queried the same out-of-range page and rendered "No videos yet" even
though earlier pages still had videos. Clamp to the last valid page based
on the remaining video count, falling back to refresh in place.
Add a "Download project" / "Download selected" flow that builds a
server-side manifest of downloadable media, plus a selection mode with
bulk delete for project videos.
Gate viewer downloads behind a new project allowDownloads setting
(default off, opt-in). Admins can always download; enabling on a public
project allows anonymous visitors to download. Enforce the setting on
every download surface (manifest, version, asset, watch, video routes)
via canDownloadProjectMedia.
Add rate limits for the manifest endpoint, host allowlisting for direct
download URLs, and configurable file/byte caps.
Closes#16Closes#19
Add multi-file drag-and-drop queues for project videos and the assets pane, and route asset video uploads through S3/R2 when direct Bunny uploads are disabled.
- Added support for self-hosted S3 video uploads with new environment variables: OPENFRAME_ENABLE_S3_VIDEO_UPLOADS and OPENFRAME_MAX_VIDEO_UPLOAD_BYTES.
- Updated .env.example and .env.docker.example to reflect new configuration options.
- Enhanced Content Security Policy to include origins for S3-compatible storage.
- Updated dependencies for AWS SDK to support new features.
- Refactored upload logic to accommodate both Bunny and S3 upload providers.
- Updated documentation to clarify the usage of direct uploads and S3 configurations.
- Closes#11
- Added storage quota enforcement for audio and image uploads in the respective routes.
- Introduced reservation system to manage concurrent uploads and prevent quota overages.
- Enhanced comment creation to account for audio and image attachment sizes against user quotas.
- Created new UploadReservation model to track in-flight upload reservations.
- Backfilled existing video assets with size information from R2.
- Added progress component for UI feedback during uploads.
- Updated API responses to include reservation IDs for better quota management.
- Adjusted error handling to return appropriate storage limit exceeded messages.
- Moved registration logic to a new client component for better separation of concerns.
- Integrated invite code requirement based on feature flags in the registration process.
- Enhanced dashboard functionality to conditionally enable Bunny uploads based on feature flags.
- Updated various components and API routes to check for Bunny uploads and Stripe billing feature flags.
- Added new feature flag utilities for managing feature toggles in the application.
- Added billing-related fields to the User model in the database.
- Implemented functions for managing billing access, including trial periods and subscription statuses.
- Created new billing utility functions for Stripe integration.
- Updated onboarding page to include billing overview and workspace creation eligibility.
- Enhanced route access checks to require billing access for certain actions.
- Implemented cleanup scripts for expired billing workspaces and associated media.
- Updated header component to conditionally show app navigation based on billing access.
- Added new migrations for billing-related database changes.
Replace per-frame React state updates with ref-based tracking and direct DOM
mutation for the progress bar, playhead, and timecode. React state is now
throttled to ~4 commits/sec (~250ms), eliminating unnecessary re-renders on
every animation frame during playback.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Add `onboardingCompletedAt` field to User model + migration
- Add AUDIO/R2_AUDIO enum values to schema
- Gate dashboard behind onboarding check; redirect to /onboarding if not completed
- Add /onboarding page + layout with 5-step wizard (welcome, workspace, project, video info, notifications)
- Add POST /api/onboarding/complete endpoint to mark onboarding done
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>