Skip to content

Revamp/stats#499

Merged
lui7henrique merged 17 commits intomainfrom
revamp/stats
Mar 1, 2026
Merged

Revamp/stats#499
lui7henrique merged 17 commits intomainfrom
revamp/stats

Conversation

@lui7henrique
Copy link
Member

@lui7henrique lui7henrique commented Feb 27, 2026

Describe your changes

Issue ticket number and link

Checklist before requesting a review

  • I have performed a self-review of my code
  • If it's an essential feature, I've tested it thoroughly.
  • Do we need to implement analytics?
  • Will this be part of a product update? If yes, please write one phrase about this update.

Note

Medium Risk
Adds new period-filtered stats queries and a new timeline endpoint, plus significant iOS stats UI refactor; correctness depends on date-range filtering, cache key/TTL behavior, and additional DB aggregation for percentile calculations.

Overview
Adds period-scoped user stats across the backend by introducing a period query param (including YYYY-MM) with periodToDateRange, and threading startDate/endDate filtering through user items, episodes, and best-reviews DB queries.

Extends stats caching to include period in cache keys and uses a shorter TTL for period-scoped keys; also invalidates user stats caches on review and episode create/update/delete.

Expands total-hours stats to return additional insights (daily/monthly breakdown adjustments, peak time + hourly distribution, daily activity, and an all-time percentile rank), and adds a new GET /user/:id/stats-timeline endpoint that paginates month sections (hours + top genre + top review).

Revamps the iOS Stats tab to consume the new period/timeline APIs: adds period selector + timeline UI, richer genre/review detail screens, share card rendering, new cache keyed by userId+period, and localized strings/theme tweaks to support the redesigned stats experience.

Written by Cursor Bugbot for commit 15bb0f5. This will update automatically on new commits. Configure here.

lui7henrique and others added 16 commits February 18, 2026 13:11
- Simplify Stats page to card-based UI with period filtering (month/last_month/year/all)
- Replace expandable sections with NavigationLink cards to detail pages
- Add custom profile-style headers (circular back button, scroll-aware title) to all detail views
- Fix intermittent data: genres now include series with episodes watched in period
- Fix stale cache: invalidate stats on episode create/delete and review create/update/delete
- Reduce cache TTL to 10min for period-scoped stats (month/last_month)
- Add posterPath to genre response schema so Fastify doesn't strip it
- Add statsCardBackground color with proper light/dark mode contrast
- Shared statsPoster helper: 70% width, standard 16px corner radius, optional rating badge
- Add singular localization strings (favoriteGenre, bestReview) across 7 languages

Co-authored-by: Cursor <cursoragent@cursor.com>
- Introduced a new MonthSection model to manage monthly statistics, improving data organization and display.
- Updated ProfileStatsView to support a timeline mode, allowing users to view stats by month with enhanced visual elements.
- Added new localization strings for timeline and comparison metrics, improving user experience across multiple languages.
- Refactored various components to streamline data handling and improve code readability, including the removal of unused methods and properties.

These changes aim to provide users with a more engaging and informative stats experience, aligning with modern UI practices.
- Enhanced the layout of StatsShareCardView to utilize a new card size and updated gradient background for better visual appeal.
- Adjusted text sizes and spacing for improved readability and consistency across different screen sizes.
- Refactored the rendering logic in ProfileStatsView to accommodate the new card dimensions, ensuring high-quality image generation for sharing.
- Improved accessibility by refining color contrasts and text legibility.

These changes aim to create a more engaging and visually appealing user experience in the stats section.
- Fix floating scrubber: use absolute drag offset from start index
  instead of cumulative reset, ensuring reliable month navigation
- Localize month names in scrubber and share card using app language
- Pre-download poster images before rendering share card snapshot
- Use PlotistLogo asset in share card footer instead of text label
- Darken share card gradient background for better contrast
- Stories format (9:16) share card with genre and review posters
- Lighten statsCardBackground in light theme for subtler contrast
- Keep cards at 50% width when only one of genre/review exists

Co-authored-by: Cursor <cursoragent@cursor.com>
- Added checks for empty genre lists to conditionally display UI elements in topGenreCard.
- Updated layout and styling in ProfileStatsView to enhance readability and visual appeal.
- Introduced new text elements for period labels in various views for better context.
- Refactored monthContentView and highlightCards functions to streamline logic and improve performance.
- Adjusted padding and frame settings for better alignment and responsiveness.

These changes aim to create a more engaging and user-friendly experience in the stats section.
…profiles

- Added a check to only show the share button when viewing one's own profile, enhancing user experience and preventing unnecessary options for other profiles.
- Maintained existing styling and functionality for the share button to ensure consistency across the UI.

These changes aim to streamline the user interface and improve the relevance of available actions in the ProfileStatsView.
- Introduced a new service to fetch user stats timeline, allowing for paginated monthly sections of viewing data.
- Updated ProfileStatsView and related components to display timeline data, enhancing user engagement with visual summaries of total hours, top genres, and reviews.
- Added new query schema and response structure for timeline data, improving data handling and user experience.
- Refactored existing components for better organization and readability, ensuring a cohesive integration of the new timeline feature.

These changes aim to provide users with a more comprehensive and engaging overview of their viewing habits over time.
- Added new `getUserStatsTimelineService` to retrieve user statistics in a paginated monthly format, providing insights into total hours watched, top genres, and reviews.
- Updated `ProfileStatsView` to support timeline mode, displaying monthly sections with improved UI components for better user engagement.
- Introduced new query schema for timeline requests and corresponding response schema to structure the data effectively.
- Refactored existing services to integrate episode data into total hours calculations, ensuring accurate statistics across different viewing periods.

These changes aim to enrich the user experience by offering a more detailed and organized view of user statistics over time.
- Changed cache key for user watched genres from 'watched-genres-v2' to 'watched-genres-v5' for improved versioning.
- Introduced a new `GenreItem` type to encapsulate genre-related data, including `tmdbId`, `mediaType`, and `posterPath`.
- Refactored genre data structures to include `posterPaths` and `items`, allowing for more detailed genre information in responses.
- Updated response schema to accommodate new fields, enhancing the data returned for user watched genres.

These changes aim to provide a richer and more organized representation of user genre statistics, improving overall user experience.
…proved response structure

- Updated the cache key for user total hours from 'total-hours-v2' to 'total-hours-v6' for better versioning.
- Introduced new metrics including peak time slot, hourly distribution, daily activity, and user percentile rank to provide a more comprehensive view of user engagement.
- Enhanced the response schema to include these new metrics, improving the data returned for user total hours.
- Added new utility functions to compute daily activity and peak viewing times, enriching the analytics capabilities of the service.

These changes aim to deliver a more detailed and insightful user statistics experience, enhancing overall engagement and understanding of viewing habits.
…sualization

- Updated the task identifier for loading detail data to include dynamic parameters, ensuring accurate data fetching based on user activity.
- Refined the logic for determining when to fetch data, focusing on daily activity and hourly distribution metrics.
- Introduced a new method to calculate the peak day of the week from daily activity, enhancing the analytics capabilities.
- Replaced the peak summary row with a more detailed peak visualization that includes bars representing hourly distribution, improving user engagement and understanding of viewing patterns.

These changes aim to provide a more comprehensive and visually appealing representation of user viewing habits in the TimeWatchedDetailView.
Replace the segmented picker (timeline vs general) with a native Menu
dropdown that lets users select a specific month or "Geral" (lifetime).
Defaults to the current month. Each period loads data on-demand and
caches it for instant switching.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ced user experience

- Introduced PeriodGenresView to display user favorite genres with loading state and genre details.
- Added PeriodReviewsView to showcase best reviews for a selected period, including loading state and review details.
- Implemented shared components for consistent UI, including detail headers and genre item displays.
- Enhanced ProfileStatsSections to integrate new views, improving overall user engagement and data presentation.

These additions aim to provide users with a richer and more interactive experience when exploring their viewing habits and preferences.
…iew, and TimeWatchedDetailView

- Replaced the previous scrolling detection mechanism with a more efficient approach using scroll offset tracking.
- Updated PeriodGenresView and PeriodReviewsView to utilize a new scroll offset reader for improved user experience.
- Enhanced TimeWatchedDetailView with similar scroll offset functionality, ensuring consistent behavior across views.
- Adjusted ProfileStatsView to set the default selected period to "all" and removed unnecessary animations for smoother transitions.

These changes aim to enhance the responsiveness and interactivity of the user interface when navigating through different views.
- Removed unoptimized image loading in the Next.js configuration for improved performance.
- Added structured data components including WebsiteJsonLd, OrganizationJsonLd, MovieJsonLd, and BreadcrumbJsonLd to enhance SEO.
- Implemented HtmlLangSetter component to dynamically set the document language based on user preferences.
- Updated metadata generation in various pages to include language alternates for better search engine indexing.
- Refactored existing pages to utilize the new buildLanguageAlternates utility for consistent SEO practices.

These changes aim to improve the application's visibility and indexing in search engines, enhancing overall user experience.
…experience

- Changed the import statement in next-env.d.ts to use double quotes for consistency.
- Added a CSS rule to set the font size of input, select, and textarea elements to 16px on mobile devices, improving usability.

These changes aim to ensure a more consistent code style and enhance the user experience on mobile devices.
@vercel
Copy link

vercel bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
plotwist Ready Ready Preview, Comment Mar 1, 2026 6:59pm

Request Review


enum API {
static let baseURL = Env.apiBaseURL
static let baseURL = "http://localhost:3333"
Copy link

Choose a reason for hiding this comment

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

Hardcoded localhost URL in production iOS app

High Severity

The API base URL was changed from Env.apiBaseURL (which reads from the environment/Info.plist with a production fallback) to a hardcoded "http://localhost:3333". This will cause the iOS app to fail to connect to any backend in non-local environments, breaking all API functionality in production builds.

Fix in Cursor Fix in Web

return computeDailyBreakdown(movieData, episodes, period)
}

const monthCount = period === 'year' ? 12 : 12
Copy link

Choose a reason for hiding this comment

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

Dead conditional always evaluates to same value

Medium Severity

const monthCount = period === 'year' ? 12 : 12 is a no-op ternary — both branches return 12. The old code used 6 months (for (let i = 5; i >= 0; i--)). The else branch at line 128 uses monthCount to generate the month slots for the 'all' period, so the intended distinction between periods is lost. This looks like an incomplete refactor where the non-'year' value was meant to differ (likely 6 to preserve the original behavior).

Fix in Cursor Fix in Web

eq(schema.reviews.userId, userId),
sql`${schema.reviews.seasonNumber} IS NULL`,
sql`${schema.reviews.episodeNumber} IS NULL`,
]
Copy link

Choose a reason for hiding this comment

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

Best reviews filter for rating=5 silently removed

Medium Severity

selectBestReviews previously filtered reviews with eq(schema.reviews.rating, 5), ensuring only 5-star reviews were returned. This filter was removed entirely without replacement. Now the query returns all reviews (including low-rated ones) ordered by rating descending. While the ordering mitigates the worst case, this fundamentally changes the semantics of "best reviews" and may surface mediocre reviews for users with few high ratings.

Fix in Cursor Fix in Web

) {
const { id } = reviewParamsSchema.parse(request.params)
const body = updateReviewBodySchema.parse(request.body)

const result = await updateReviewService({ ...body, id })

await invalidateUserStatsCache(redis, request.user.id)
Copy link

Choose a reason for hiding this comment

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

Crash accessing request.user.id on unprotected route

High Severity

The newly added invalidateUserStatsCache(redis, request.user.id) in updateReviewController accesses request.user, but the PUT /review/by/:id route definition lacks onRequest: [verifyJwt] middleware. Without JWT verification, request.user is undefined at runtime, so .id throws a TypeError and returns a 500. The old controller never touched request.user, so this crash path is new.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

day,
hours: Math.round(hours * 10) / 10,
}))
}
Copy link

Choose a reason for hiding this comment

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

YYYY-MM period not handled in daily activity computation

Medium Severity

computeAllDailyActivity and computeMonthlyHours don't handle YYYY-MM period strings (e.g., "2024-03"). These fall through to the else branch intended for 'all', causing computeAllDailyActivity to generate daily entries from the earliest date in the (already-filtered) data all the way to today, and computeMonthlyHours to generate 12 months of mostly-empty data. This produces bloated responses for period-scoped timeline requests.

Additional Locations (1)

Fix in Cursor Fix in Web

const parts = ['user-stats', userId, statType]
if (language) parts.push(language)
if (period && period !== 'all') parts.push(period)
return parts.join(':')
Copy link

Choose a reason for hiding this comment

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

Cache key collision between period-scoped and language-scoped stats

Medium Severity

getUserStatsCacheKey appends language and period as positional parts without labels, so a key with language=undefined, period="en-US" would collide with language="en-US", period="all". For example, watched-cast uses language=undefined and period="en-US" (a YYYY-MM looking value aside), while other stat types use language="en-US" and period="all". Since period="all" is filtered out, collisions between unlabeled positional segments are possible.

Fix in Cursor Fix in Web


currentYM = getPreviousYearMonth(currentYM)
scanned++
}
Copy link

Choose a reason for hiding this comment

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

Timeline N+1 sequential calls creates severe latency

High Severity

getUserStatsTimelineService calls three heavy services (getUserTotalHoursService, getUserWatchedGenresService, getUserBestReviewsService) inside a while loop that can iterate up to 24 times. Each service itself does DB queries and external TMDB API calls in batches. On a cold cache, a single timeline request could trigger dozens of DB queries and hundreds of TMDB calls sequentially, leading to extreme response times.

Fix in Cursor Fix in Web

period: currentMonth,
totalHours: hoursResponse.totalHours,
watchedGenres: genres,
itemsStatus: status,
Copy link

Choose a reason for hiding this comment

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

Prefetch omits movie/series hours from cache storage

Medium Severity

The prefetch statsCache.set call omits movieHours, seriesHours, and monthlyHours parameters, so they default to 0, 0, and []. When loadPeriod later reads from this cache, it populates the MonthSection with these zeroed-out values, causing the movie/series hour breakdown and monthly chart to appear empty despite the API having returned valid data in hoursResponse.

Fix in Cursor Fix in Web

@lui7henrique lui7henrique merged commit c61c841 into main Mar 1, 2026
3 of 5 checks passed
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.

1 participant