Skip to content

feat(video): add processing progress tracking and downloadable toggle… - #176

Merged
Apexone11 merged 1 commit into
mainfrom
laptop-branch
Apr 4, 2026
Merged

feat(video): add processing progress tracking and downloadable toggle…#176
Apexone11 merged 1 commit into
mainfrom
laptop-branch

Conversation

@Apexone11

Copy link
Copy Markdown
Owner

… to Video model

  • Added processingStep, processingProgress, and downloadable fields to the Video model in Prisma schema.
  • Implemented database migration to add new columns to the Video table.
  • Enhanced video processing service to update processing progress during transcoding.
  • Created a new VideoProcessingProgress component to display processing status and allow toggling of downloadability.
  • Updated video upload and metadata routes to handle new fields and ensure proper permissions.
  • Adjusted user plan checks to include new subscription statuses and limits.
  • Improved error handling and graceful degradation in various services and controllers.

… to Video model

- Added `processingStep`, `processingProgress`, and `downloadable` fields to the Video model in Prisma schema.
- Implemented database migration to add new columns to the Video table.
- Enhanced video processing service to update processing progress during transcoding.
- Created a new VideoProcessingProgress component to display processing status and allow toggling of downloadability.
- Updated video upload and metadata routes to handle new fields and ensure proper permissions.
- Adjusted user plan checks to include new subscription statuses and limits.
- Improved error handling and graceful degradation in various services and controllers.
Copilot AI review requested due to automatic review settings April 4, 2026 06:16
@Apexone11 Apexone11 self-assigned this Apr 4, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @Apexone11, you have reached your weekly rate limit of 1500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@Apexone11
Apexone11 merged commit fe9684f into main Apr 4, 2026
2 of 6 checks passed
@Apexone11
Apexone11 deleted the laptop-branch branch April 4, 2026 06:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the video pipeline and subscription-aware UX by adding server-side processing progress fields to Video, exposing them via the API, and updating the frontend to show progress + a downloadability toggle. It also updates subscription/plan handling (incl. past_due), adjusts free-tier limits (AI/video), and includes some related feed/user improvements.

Changes:

  • Add processingStep, processingProgress, and downloadable to Video (schema + migration) and update the video processor to persist progress updates.
  • Update frontend video surfaces (uploader + feed) to show processing progress and a “Download” affordance gated by downloadable.
  • Expand “active” subscription status handling (e.g., include past_due) and adjust plan limits/feature text across pricing/settings/AI.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/studyhub-app/src/pages/settings/SubscriptionTab.jsx Derives an “effective plan” using API + session; updates plan display and free video duration text.
frontend/studyhub-app/src/pages/pricing/PricingPage.jsx Uses an effective subscription fallback and treats past_due/trialing as active for Pro gating; updates video upload feature text.
frontend/studyhub-app/src/pages/feed/FeedCard.jsx Adds ProBadge, adds download link + controlsList gating based on video.downloadable.
frontend/studyhub-app/src/pages/dashboard/DashboardPage.jsx Styling/formatting cleanup; moves colors to CSS vars.
frontend/studyhub-app/src/components/video/VideoUploader.jsx Replaces spinner with progress polling component + adds PATCH toggle for downloadable.
backend/src/modules/video/video.service.js Persists processing progress steps during download/probe/transcode/finalize; tweaks ffmpeg preset speed.
backend/src/modules/video/video.routes.js Refactors upload chunk handler formatting; adds PATCH /api/video/:id; includes new fields in formatVideoResponse.
backend/src/modules/users/users.controller.js Formatting cleanup; fixes follow suggestions query to use userFollow.
backend/src/modules/studyGroups/studyGroups.controller.js Adds private-group limit enforcement when changing privacy from public → private/invite_only.
backend/src/modules/sheets/sheets.fork.controller.js Counts forks toward monthly upload quota for free users.
backend/src/modules/payments/payments.service.js Treats incomplete subscriptions as free until confirmed; refines inactive handling.
backend/src/modules/payments/payments.routes.js Adds /subscription/debug endpoint returning raw subscription info.
backend/src/modules/payments/payments.constants.js Lowers free-tier AI daily messages constant.
backend/src/modules/feed/feed.discovery.controller.js Switches fork counts to forkChildren and wraps blocked-id fetch in try/catch.
backend/src/modules/ai/ai.service.js Adds donor daily-limit path (DB lookup); formatting changes.
backend/src/modules/ai/ai.constants.js Adds donor daily limit constant.
backend/src/lib/userBadges.js Treats past_due as active for plan enrichment; formatting cleanup.
backend/src/lib/getUserPlan.js Centralizes active statuses (active, trialing, past_due).
backend/prisma/schema.prisma Adds new Video fields for progress + downloadability.
backend/prisma/migrations/20260404000001_add_video_processing_progress/migration.sql Migration to add the new Video columns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +242 to +252
// ── GET /subscription/debug — Raw DB record for debugging ────────────────
router.get('/subscription/debug', paymentReadLimiter, requireAuth, async (req, res) => {
try {
const raw = await prisma.subscription.findUnique({ where: { userId: req.user.userId } })
const planFromEnv = {
STRIPE_PRICE_ID_PRO: process.env.STRIPE_PRICE_ID_PRO ? 'set' : 'MISSING',
STRIPE_PRICE_ID_PRO_YEARLY: process.env.STRIPE_PRICE_ID_PRO_YEARLY ? 'set' : 'MISSING',
}
res.json({ raw: raw || null, envStatus: planFromEnv, userId: req.user.userId })
} catch (error) {
res.status(500).json({ error: error.message })

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

/subscription/debug returns the full Subscription row (including Stripe customer/subscription/price IDs) to any authenticated user. This leaks sensitive billing identifiers and should be removed or gated (e.g., only in non-production and/or admin-only) and return a redacted shape if it must exist.

Suggested change
// ── GET /subscription/debug — Raw DB record for debugging ────────────────
router.get('/subscription/debug', paymentReadLimiter, requireAuth, async (req, res) => {
try {
const raw = await prisma.subscription.findUnique({ where: { userId: req.user.userId } })
const planFromEnv = {
STRIPE_PRICE_ID_PRO: process.env.STRIPE_PRICE_ID_PRO ? 'set' : 'MISSING',
STRIPE_PRICE_ID_PRO_YEARLY: process.env.STRIPE_PRICE_ID_PRO_YEARLY ? 'set' : 'MISSING',
}
res.json({ raw: raw || null, envStatus: planFromEnv, userId: req.user.userId })
} catch (error) {
res.status(500).json({ error: error.message })
// ── GET /subscription/debug — Redacted debug info (non-production only) ──
router.get('/subscription/debug', paymentReadLimiter, requireAuth, async (req, res) => {
try {
if (process.env.NODE_ENV === 'production') {
return sendError(res, 404, 'Not found.', ERROR_CODES.NOT_FOUND)
}
const raw = await prisma.subscription.findUnique({ where: { userId: req.user.userId } })
const planFromEnv = {
STRIPE_PRICE_ID_PRO: process.env.STRIPE_PRICE_ID_PRO ? 'set' : 'MISSING',
STRIPE_PRICE_ID_PRO_YEARLY: process.env.STRIPE_PRICE_ID_PRO_YEARLY ? 'set' : 'MISSING',
}
const redacted = raw
? {
id: raw.id,
exists: true,
status: raw.status ?? null,
plan: raw.plan ?? null,
interval: raw.interval ?? null,
cancelAtPeriodEnd: raw.cancelAtPeriodEnd ?? null,
currentPeriodEnd: raw.currentPeriodEnd ?? null,
createdAt: raw.createdAt ?? null,
updatedAt: raw.updatedAt ?? null,
}
: null
res.json({ subscription: redacted, envStatus: planFromEnv })
} catch (error) {
captureError(error, { context: 'payments.subscriptionDebug' })
log.error({ err: error }, 'Failed to get subscription debug info')
sendError(res, 500, 'Failed to retrieve subscription debug info.', ERROR_CODES.INTERNAL)

Copilot uses AI. Check for mistakes.
Comment on lines 134 to 145
// Check active subscription
try {
const sub = await prisma.subscription.findUnique({
where: { userId: req.user.userId },
select: { plan: true, status: true },
})
if (sub && sub.status === 'active') {
userPlan = sub.plan
}
} catch (e) {
} catch {
// Graceful degradation: treat as free on error
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Plan detection here still treats only status === 'active' as Pro; elsewhere in this PR you treat trialing/past_due as active. This mismatch can incorrectly apply free-tier upload limits to trialing/past_due users. Consider using the shared getUserPlan() helper or checking all active statuses consistently.

Copilot uses AI. Check for mistakes.
Comment on lines 347 to +356
// Determine user plan (mirror of logic in routes)
try {
const sub = await prisma.subscription.findUnique({
where: { userId: video.userId },
select: { plan: true, status: true },
})
if (sub && sub.status === 'active') {
userPlan = sub.plan
}
} catch (e) {
} catch {

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Same as upload/init: duration validation only treats status === 'active' as Pro, but other parts of the codebase now consider trialing/past_due active. This can incorrectly fail/limit processing for trialing/past_due users; prefer the shared getUserPlan() helper or ACTIVE_STATUSES.includes(sub.status) here too.

Copilot uses AI. Check for mistakes.
if (req.body.title !== undefined) updates.title = String(req.body.title).slice(0, 200)
if (req.body.description !== undefined)
updates.description = String(req.body.description).slice(0, 2000)
if (req.body.downloadable !== undefined) updates.downloadable = Boolean(req.body.downloadable)

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

updates.downloadable = Boolean(req.body.downloadable) will coerce non-boolean values unexpectedly (e.g. the string "false" becomes true). Validate that req.body.downloadable is a boolean (or explicitly parse accepted string values) and reject invalid inputs with 400.

Suggested change
if (req.body.downloadable !== undefined) updates.downloadable = Boolean(req.body.downloadable)
if (req.body.downloadable !== undefined) {
if (typeof req.body.downloadable === 'boolean') {
updates.downloadable = req.body.downloadable
} else if (req.body.downloadable === 'true') {
updates.downloadable = true
} else if (req.body.downloadable === 'false') {
updates.downloadable = false
} else {
return res.status(400).json({ error: 'Invalid downloadable value.' })
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 173 to 180
<video
src={streamUrl}
poster={thumbnailUrl || undefined}
controls
playsInline
preload="metadata"
controlsList={video.downloadable === false ? 'nodownload' : undefined}
style={{ width: '100%', display: 'block', maxHeight: 500 }}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This UI relies on video.downloadable coming from the feed API, but the backend feed formatter currently only selects/returns thumbnailR2Key, variants, hlsManifestR2Key, etc. Without adding downloadable to the feed video payload, controlsList and the download gating will never activate.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +216
{video.downloadable !== false && streamUrl && (
<a
href={streamUrl}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

The "Download" link uses the same signed /api/video/:id/stream URL used for playback. If downloadable is meant to be enforced, this should ideally call a dedicated download endpoint that checks video.downloadable (and possibly sets appropriate headers), while /stream remains for playback.

Suggested change
{video.downloadable !== false && streamUrl && (
<a
href={streamUrl}
{video.downloadable !== false && video.id && (
<a
href={`${API}/api/video/${video.id}/download`}

Copilot uses AI. Check for mistakes.
return
}
if (data.processingStep) setStep(data.processingStep)
if (data.processingProgress) setPct(data.processingProgress)

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Progress polling ignores legitimate 0 values because it only updates when data.processingProgress is truthy. Use an explicit null/undefined check (e.g. data.processingProgress != null) so the UI can reflect 0% and other falsy values correctly.

Suggested change
if (data.processingProgress) setPct(data.processingProgress)
if (data.processingProgress != null) setPct(data.processingProgress)

Copilot uses AI. Check for mistakes.
Comment on lines 370 to 375
.map((sheet) => {
const ageHours = (now - new Date(sheet.createdAt).getTime()) / (1000 * 60 * 60)
const recencyBoost = Math.max(0, 1 - ageHours / DISCOVERY_RECENCY_DECAY_HOURS)
const score = (sheet.stars || 0) * 3 + (sheet._count.forks || 0) * 5 + recencyBoost * 10
const score =
(sheet.stars || 0) * 3 + (sheet._count.forkChildren || 0) * 5 + recencyBoost * 10
return { ...sheet, _score: score }

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This controller switches scoring to use _count.forkChildren, but later in the same pipeline the response mapping still reads _count.forks for forkCount (so it will always be 0). Update the mapper to use _count.forkChildren consistently with the query selection.

Copilot uses AI. Check for mistakes.
Comment on lines 390 to 395
const score =
(sheet.stars || 0) * 3 +
(sheet._count.comments || 0) * 2 +
(sheet._count.forks || 0) * 5 +
(sheet._count.forkChildren || 0) * 5 +
recencyBoost * 10
return { ...sheet, _score: score }

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Same consistency issue for the trending score path: _count.forkChildren is used in scoring, but downstream mapping still uses _count.forks when producing forkCount. Align the response mapping with the selected _count field.

Copilot uses AI. Check for mistakes.
Apexone11 added a commit that referenced this pull request Aug 2, 2026
Clears 3 advisories (2 high) that landed after round 3.
**Lockfile-only** — no manifest changes.

| Alert | Package | Advisory floor | Now at |
|---|---|---|---|
| #179, #182 (high) | postcss | 8.5.18 | **8.5.25** (all 3 lockfiles) |
| #180 (medium) | tar | 7.5.21 | **7.5.22** (root + frontend) |

## Not included: react-router (deliberate)
The 2 remaining high alerts (#176, #177) are `react-router`, and the fix
is **8.3.0 — a major bump from 7.18.1**. CLAUDE.md lists React Router
among the majors that require an explicit founder approval, so it is
held out of this PR. It touches every route in the app and deserves its
own PR with a full route smoke pass. Dependabot has it open as
#450/#451.

Also open and awaiting the same call: #446 recharts 2→3, #447
@vitejs/plugin-react 5→6, #448 @testing-library/jest-dom 6→7, #449
rollup-plugin-visualizer 6→7 — all majors.

## Validation
- Backend: lint ✅ · build ✅ · tests ✅ **3541 passed**
- Frontend: lint ✅ 0 errors · build ✅ · **906 passed**
- Release-log entry added (CI gate)
- `playwright-smoke` remains the known-red baseline (red on main since
2026-06-02)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Summary by Sourcery

Update dependency lockfiles to address recent security advisories for
postcss and tar, and document the changes in the release log.

Enhancements:
- Record security round 4 dependency updates and remaining react-router
advisories in the v2.3.0 release log entry.

Chores:
- Upgrade postcss to 8.5.25 and tar to 7.5.22 across all lockfiles to
clear three new security advisories.
Apexone11 added a commit that referenced this pull request Aug 2, 2026
…, jest-dom 7, visualizer 7 (#457)

Clears the **last 2 open high-severity alerts** (#176, #177 —
react-router). Supersedes Dependabot #446#451.

## react-router 7.18.1 → 8.3.0 (the breaking one)

**v8 removes the `react-router-dom` package entirely.** In v7 everything
DOM-specific had already collapsed into `react-router`;
`react-router-dom` was kept only as a v6-compat convenience, and v8
drops it.

- Swapped all **225 importing files** (src + tests, including
`vi.mock`/`vi.importActual` targets) to `react-router`.
- Every symbol this app uses is exported from `react-router` in v8:
`BrowserRouter`, `MemoryRouter`, `Routes`, `Route`, `Link`, `NavLink`,
`Navigate`, `useNavigate`, `useLocation`, `useParams`,
`useSearchParams`, `useBlocker`.
- The app never used `RouterProvider`/`HydratedRouter` (those move to
`react-router/dom`), so there are no other import-path changes.

**v8 raises its Node floor to 22.22.0**, so:
- frontend `engines.node` → `>=22.22.0`
- every workflow's `setup-node` → **22** (was 20)
- added a root **`.node-version`** so Cloudflare Pages builds on 22
rather than defaulting to an older runtime and tripping the engine check
— this was the one silent deploy risk in the upgrade.

**Breaking changes that do NOT apply here** (checked against the v8.0.0
changelog): ESM-only publish, middleware always-on +
`RouterContextProvider`, `meta` `data`→`loaderData` rename,
`hasErrorBoundary` removal, and the `future.v8_*` flag removals all
affect data-router/framework mode. This app is declarative
`BrowserRouter` with no loaders, actions, or meta.

## Other majors
| Package | From → To |
|---|---|
| recharts | 2.15.4 → **3.10.1** |
| @vitejs/plugin-react | 5.1.1 → **6.0.4** |
| @testing-library/jest-dom | 6.9.1 → **7.0.0** |
| rollup-plugin-visualizer | 6.0.11 → **7.0.1** |

## Validation
- Backend: lint ✅ · build ✅ · tests ✅ **3541 passed**
- Frontend: lint ✅ **0 errors** · build ✅ (charts + editor chunks build
fine on recharts 3 / plugin-react 6) · **906 passed, zero failures**
- All three lockfiles regenerated together from a root install;
`react-router-dom` is gone from every lockfile and `react-router@8.3.0`
is the only entry
- Release-log entry added (CI gate)

After this merges, **Dependabot alerts should be at 0**.
`playwright-smoke` remains the known-red baseline (red on main since
2026-06-02).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

## Summary by Sourcery

Upgrade routing and frontend tooling dependencies and align Node version
requirements across the app and CI.

Bug Fixes:
- Resolve outstanding high-severity dependency alerts related to
react-router.

Enhancements:
- Migrate all frontend and test imports from react-router-dom to
react-router v8.
- Update recharts, @vitejs/plugin-react, @testing-library/jest-dom, and
rollup-plugin-visualizer to their latest major versions.

Build:
- Raise frontend Node engine requirement to >=22.22.0 and add a root
.node-version file for consistent runtime selection.

CI:
- Switch all GitHub Actions workflows to use Node 22 for backend,
frontend, mobile, nightly, and integrity jobs.

Documentation:
- Add release-log entry documenting the security-focused dependency
upgrades and Node version change.

Tests:
- Adjust routing-related tests and mocks to target react-router instead
of react-router-dom while keeping coverage intact.

Chores:
- Regenerate root and workspace lockfiles to reflect the new dependency
versions and removal of react-router-dom.
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.

2 participants