musora-content-services 2.113.0 → 2.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.115.0](https://github.com/railroadmedia/musora-content-services/compare/v2.114.0...v2.115.0) (2026-01-09)
6
+
7
+
8
+ ### Features
9
+
10
+ * Use Get requests for Sanity if query under character limit ([#694](https://github.com/railroadmedia/musora-content-services/issues/694)) ([da5c384](https://github.com/railroadmedia/musora-content-services/commit/da5c384e7f538cf7c72a3d8f742787f24a4ed570))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **BEH-878:** correct brand song type names ([#695](https://github.com/railroadmedia/musora-content-services/issues/695)) ([2b1cde9](https://github.com/railroadmedia/musora-content-services/commit/2b1cde9978dd744129bd906aff3a9bb03e01b04e))
16
+
17
+ ## [2.114.0](https://github.com/railroadmedia/musora-content-services/compare/v2.113.0...v2.114.0) (2026-01-08)
18
+
19
+
20
+ ### Features
21
+
22
+ * extra data for method card ([#691](https://github.com/railroadmedia/musora-content-services/issues/691)) ([36b034c](https://github.com/railroadmedia/musora-content-services/commit/36b034c83b86f0ff825dfc00af6a6b97b793c4c6))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **T3PS-1347:** Update getUserWeeklyStats to include query for past 60 days for streak calculation, retain original contract of the function ([d798acf](https://github.com/railroadmedia/musora-content-services/commit/d798acf2a01f25551746030722a41ccb81030ed9))
28
+ * **T3PS:** Cleanup ([07f057f](https://github.com/railroadmedia/musora-content-services/commit/07f057f867c8a3931ae6cbf5ecca9ae471f23d00))
29
+
5
30
  ## [2.113.0](https://github.com/railroadmedia/musora-content-services/compare/v2.112.2...v2.113.0) (2026-01-08)
6
31
 
7
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.113.0",
3
+ "version": "2.115.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,6 +1,8 @@
1
1
  // Metadata is taken from the 'common' element and then merged with the <brand> metadata.
2
2
  // Brand values are prioritized and will override the same property in the 'common' element.
3
3
 
4
+ import {ALWAYS_VISIBLE_TABS} from "./services/sanity.js";
5
+
4
6
  const PROGRESS_NAMES = ['All', 'In Progress', 'Completed', 'Not Started']
5
7
  const DIFFICULTY_STRINGS = ['Introductory', 'Beginner', 'Intermediate', 'Advanced', 'Expert']
6
8
 
@@ -68,7 +70,9 @@ export class Tabs {
68
70
  static Artists = { name: 'Artists', short_name: 'ARTISTS', is_group_by: true, value: 'artist' }
69
71
  static Songs = { name: 'Songs', short_name: 'Songs', value: '' }
70
72
  static Tutorials = { name: 'Tutorials', short_name: 'Tutorials', value: 'type,tutorials', cardType: 'big' }
71
- static Transcriptions = { name: 'Transcriptions', short_name: 'Transcriptions', value: 'type,transcription', cardType: 'small' }
73
+ static Transcriptions = { name: 'Transcriptions', short_name: 'Transcriptions', value: 'type,transcriptions', cardType: 'small' }
74
+ static SheetMusic = { name: 'Sheet Music', short_name: 'Sheet Music', value: 'type,transcriptions', cardType: 'small' }
75
+ static Tabs = { name: 'Tabs', short_name: 'Tabs', value: 'type,transcriptions', cardType: 'small' }
72
76
  static PlayAlongs = { name: 'Play-Alongs', short_name: 'Play-Alongs', value:'type,play along', cardType: 'small' }
73
77
  static JamTracks = { name: 'Jam Tracks', short_name: 'Jam Tracks', value:'type,jam-track', cardType: 'small' }
74
78
  static RecentAll = { name: 'All', short_name: 'All' }
@@ -236,6 +240,7 @@ export function processMetadata(brand, type, withFilters = false) {
236
240
  brandMetaData = { ...commonMetaData, ...brandMetaData }
237
241
  if (type === 'songs' && contentMetadata[brand]?.['songs-types']) {
238
242
  brandMetaData['filterOptions']['type'] = contentMetadata[brand]['songs-types']
243
+ brandMetaData.tabs = mapSongTabNames(brandMetaData)
239
244
  }
240
245
  if (Object.keys(brandMetaData).length === 0) {
241
246
  return null
@@ -262,6 +267,26 @@ export function processMetadata(brand, type, withFilters = false) {
262
267
  return processedData
263
268
  }
264
269
 
270
+ function mapSongTabNames(brandMetaData) {
271
+ brandMetaData.tabs.forEach((tab, index) => {
272
+ if (ALWAYS_VISIBLE_TABS.some(visibleTab => visibleTab.name === tab)) {
273
+ return;
274
+ }
275
+
276
+ const targetName = brandMetaData['filterOptions']['type'][index - 1];
277
+
278
+ // Find the matching Tab by name
279
+ const matchingTab = Object.values(Tabs).find(
280
+ tabObj => tabObj.name === targetName
281
+ );
282
+
283
+ if (matchingTab) {
284
+ brandMetaData.tabs[index] = matchingTab;
285
+ }
286
+ });
287
+ return brandMetaData.tabs;
288
+ }
289
+
265
290
  /**
266
291
  * Defines the filter types for each key
267
292
  */
@@ -339,3 +364,7 @@ function transformFilters(filterOptions) {
339
364
  export function capitalizeFirstLetter(string) {
340
365
  return string.charAt(0).toUpperCase() + string.slice(1)
341
366
  }
367
+
368
+ export function getSongType(brand) {
369
+ return contentMetadata[brand]?.['songs-types'][1]
370
+ }
package/src/index.d.ts CHANGED
@@ -300,6 +300,7 @@ import {
300
300
  fetchTopLevelParentId,
301
301
  fetchUpcomingEvents,
302
302
  getSanityDate,
303
+ getSongTypesFor,
303
304
  getSortOrder,
304
305
  jumpToContinueContent
305
306
  } from './services/sanity.js';
@@ -617,6 +618,7 @@ declare module 'musora-content-services' {
617
618
  getResumeTimeSecondsByIdsAndCollections,
618
619
  getSanityDate,
619
620
  getScheduleContentRows,
621
+ getSongTypesFor,
620
622
  getSortOrder,
621
623
  getStartedOrCompletedProgressOnly,
622
624
  getTabResults,
package/src/index.js CHANGED
@@ -304,6 +304,7 @@ import {
304
304
  fetchTopLevelParentId,
305
305
  fetchUpcomingEvents,
306
306
  getSanityDate,
307
+ getSongTypesFor,
307
308
  getSortOrder,
308
309
  jumpToContinueContent
309
310
  } from './services/sanity.js';
@@ -616,6 +617,7 @@ export {
616
617
  getResumeTimeSecondsByIdsAndCollections,
617
618
  getSanityDate,
618
619
  getScheduleContentRows,
620
+ getSongTypesFor,
619
621
  getSortOrder,
620
622
  getStartedOrCompletedProgressOnly,
621
623
  getTabResults,
@@ -94,7 +94,10 @@ export async function getTabResults(brand, pageName, tabName, {
94
94
  const filteredSelectedFilters = selectedFilters.filter(f => !f.startsWith('progress,'));
95
95
 
96
96
  // Prepare included fields
97
- const mergedIncludedFields = [...filteredSelectedFilters, `tab,${tabName.toLowerCase()}`];
97
+ const tabValue = Object.values(Tabs).find(
98
+ tabObj => tabObj.name === tabName
99
+ ).value
100
+ const mergedIncludedFields = [...filteredSelectedFilters, tabValue];
98
101
 
99
102
  // Fetch data
100
103
  let results
@@ -112,11 +112,12 @@ export async function getMethodCard(brand) {
112
112
  }
113
113
 
114
114
  return {
115
- id: 1,
115
+ id: learningPath?.id,
116
116
  type: COLLECTION_TYPE.LEARNING_PATH,
117
117
  progressType: 'method',
118
118
  header: 'Method',
119
119
  body: learningPath,
120
+ content: learningPath, // FE uses this field for cards, MA uses `body`
120
121
  cta: {
121
122
  text: ctaText,
122
123
  action: action,
@@ -33,7 +33,7 @@ import {
33
33
  SONG_TYPES_WITH_CHILDREN,
34
34
  } from '../contentTypeConfig.js'
35
35
  import { fetchSimilarItems, recommendations } from './recommendations.js'
36
- import { processMetadata } from '../contentMetaData.js'
36
+ import {getSongType, processMetadata, Tabs} from '../contentMetaData.js'
37
37
  import { GET } from '../infrastructure/http/HttpClient.ts'
38
38
 
39
39
  import { globalConfig } from './config.js'
@@ -53,9 +53,9 @@ const excludeFromGeneratedIndex = ['fetchRelatedByLicense']
53
53
  /**
54
54
  * Song/Lesson tabs that are always visible.
55
55
  *
56
- * @type {string[]}
56
+ * @type {object[]}
57
57
  */
58
- const ALWAYS_VISIBLE_TABS = ['For You', 'Explore All'];
58
+ export const ALWAYS_VISIBLE_TABS = [Tabs.ForYou, Tabs.ExploreAll];
59
59
 
60
60
  /**
61
61
  * Mapping from tab names to their underlying Sanity content types.
@@ -69,9 +69,11 @@ const TAB_TO_CONTENT_TYPES = {
69
69
  'Entertainment': entertainmentLessonTypes,
70
70
  'Tutorials': tutorialsLessonTypes,
71
71
  'Transcriptions': transcriptionsLessonTypes,
72
+ 'Sheet Music': transcriptionsLessonTypes,
73
+ 'Tabs': transcriptionsLessonTypes,
72
74
  'Play-Alongs': playAlongLessonTypes,
73
75
  'Jam Tracks': jamTrackLessonTypes,
74
- };
76
+ }
75
77
 
76
78
  /**
77
79
  * Fetch a song by its document ID from Sanity.
@@ -1492,22 +1494,34 @@ export async function fetchSanity(
1492
1494
  if (!checkSanityConfig(globalConfig)) {
1493
1495
  return null
1494
1496
  }
1495
-
1496
1497
  const perspective = globalConfig.sanityConfig.perspective ?? 'published'
1497
1498
  const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api'
1498
- const url = `https://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
1499
+ const baseUrl = `https://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?perspective=${perspective}`
1499
1500
  const headers = {
1500
1501
  'Content-Type': 'application/json',
1501
1502
  }
1502
-
1503
1503
  try {
1504
- const method = 'post'
1505
- const options = {
1506
- method,
1507
- headers,
1508
- body: JSON.stringify({ query: query }),
1504
+ const encodedQuery = encodeURIComponent(query)
1505
+ const fullGetUrl = `${baseUrl}&query=${encodedQuery}`
1506
+ const useGet = fullGetUrl.length < 8000
1507
+
1508
+ let url, method, options
1509
+ if (useGet) {
1510
+ url = fullGetUrl
1511
+ method = 'GET'
1512
+ options = {
1513
+ method,
1514
+ headers,
1515
+ }
1516
+ } else {
1517
+ url = baseUrl
1518
+ method = 'POST'
1519
+ options = {
1520
+ method,
1521
+ headers,
1522
+ body: JSON.stringify({ query }),
1523
+ }
1509
1524
  }
1510
-
1511
1525
  const adapter = getPermissionsAdapter()
1512
1526
  let promisesResult = await Promise.all([
1513
1527
  fetch(url, options),
@@ -1647,8 +1661,8 @@ export async function fetchShowsData(brand) {
1647
1661
  */
1648
1662
  export async function fetchMetadata(brand, type, options = {}) {
1649
1663
  // Handle backward compatibility - type was previously the 3rd param (boolean)
1650
- const withFilters = typeof options === 'boolean' ? options : true;
1651
- const skipTabFiltering = options.skipTabFiltering || false;
1664
+ const withFilters = typeof options === 'boolean' ? options : true
1665
+ const skipTabFiltering = options.skipTabFiltering || false
1652
1666
  let processedData = processMetadata(brand, type, withFilters)
1653
1667
 
1654
1668
  if (processedData?.onlyAvailableTabs === true) {
@@ -1659,23 +1673,20 @@ export async function fetchMetadata(brand, type, options = {}) {
1659
1673
  if ((type === 'lessons' || type === 'songs') && !skipTabFiltering) {
1660
1674
  try {
1661
1675
  // Single API call to get all content type counts
1662
- const contentTypeCounts = await fetchContentTypeCounts(brand, type);
1676
+ const contentTypeCounts = await fetchContentTypeCounts(brand, type)
1663
1677
 
1664
1678
  // Filter tabs based on counts
1665
- processedData.tabs = filterTabsByContentCounts(
1666
- processedData.tabs,
1667
- contentTypeCounts
1668
- );
1679
+ processedData.tabs = filterTabsByContentCounts(processedData.tabs, contentTypeCounts)
1669
1680
 
1670
1681
  // Filter Type options based on counts
1671
1682
  if (processedData.filters) {
1672
1683
  processedData.filters = filterTypeOptionsByContentCounts(
1673
1684
  processedData.filters,
1674
1685
  contentTypeCounts
1675
- );
1686
+ )
1676
1687
  }
1677
1688
  } catch (error) {
1678
- console.error('Error fetching content type counts, using all tabs/filters:', error);
1689
+ console.error('Error fetching content type counts, using all tabs/filters:', error)
1679
1690
  // Fail open - show all tabs and filters
1680
1691
  }
1681
1692
  }
@@ -2188,7 +2199,7 @@ export async function fetchBrandsByContentIds(contentIds) {
2188
2199
  * // Returns: ['lesson', 'quick-tips', 'course', 'guided-course', ...]
2189
2200
  */
2190
2201
  function getAllContentTypesForPage(pageName) {
2191
- return filterTypes[pageName] || [];
2202
+ return filterTypes[pageName] || []
2192
2203
  }
2193
2204
 
2194
2205
  /**
@@ -2205,16 +2216,14 @@ function getAllContentTypesForPage(pageName) {
2205
2216
  * // Returns: { 'guided-course': 45, 'skill-pack': 12, 'special': 8 }
2206
2217
  */
2207
2218
  export async function fetchContentTypeCounts(brand, pageName) {
2208
- const allContentTypes = getAllContentTypesForPage(pageName);
2219
+ const allContentTypes = getAllContentTypesForPage(pageName)
2209
2220
 
2210
2221
  if (allContentTypes.length === 0) {
2211
- return {};
2222
+ return {}
2212
2223
  }
2213
2224
 
2214
2225
  // Build array of type objects for GROQ query
2215
- const typesString = allContentTypes
2216
- .map(type => `{"type": "${type}"}`)
2217
- .join(', ');
2226
+ const typesString = allContentTypes.map((type) => `{"type": "${type}"}`).join(', ')
2218
2227
 
2219
2228
  const query = `{
2220
2229
  "typeCounts": [${typesString}]{
@@ -2225,19 +2234,19 @@ export async function fetchContentTypeCounts(brand, pageName) {
2225
2234
  && status == "published"
2226
2235
  ])
2227
2236
  }[count > 0]
2228
- }`;
2237
+ }`
2229
2238
 
2230
- const results = await fetchSanity(query, true, { processNeedAccess: false });
2239
+ const results = await fetchSanity(query, true, { processNeedAccess: false })
2231
2240
 
2232
2241
  // Convert array to object for easier lookup: { 'guided-course': 45, ... }
2233
- const countsMap = {};
2242
+ const countsMap = {}
2234
2243
  if (results.typeCounts) {
2235
- results.typeCounts.forEach(item => {
2236
- countsMap[item.type] = item.count;
2237
- });
2244
+ results.typeCounts.forEach((item) => {
2245
+ countsMap[item.type] = item.count
2246
+ })
2238
2247
  }
2239
2248
 
2240
- return countsMap;
2249
+ return countsMap
2241
2250
  }
2242
2251
 
2243
2252
  /**
@@ -2250,21 +2259,21 @@ export async function fetchContentTypeCounts(brand, pageName) {
2250
2259
  */
2251
2260
  function filterTabsByContentCounts(tabs, contentTypeCounts) {
2252
2261
  return tabs.filter(tab => {
2253
- if (ALWAYS_VISIBLE_TABS.includes(tab.name)) {
2262
+ if (ALWAYS_VISIBLE_TABS.some(visibleTab => visibleTab.name === tab.name)) {
2254
2263
  return true;
2255
2264
  }
2256
2265
 
2257
- const tabContentTypes = TAB_TO_CONTENT_TYPES[tab.name] || [];
2266
+ const tabContentTypes = TAB_TO_CONTENT_TYPES[tab.name] || []
2258
2267
 
2259
2268
  if (tabContentTypes.length === 0) {
2260
2269
  // Unknown tab - show it to be safe
2261
- console.warn(`Unknown tab "${tab.name}" - showing by default`);
2262
- return true;
2270
+ console.warn(`Unknown tab "${tab.name}" - showing by default`)
2271
+ return true
2263
2272
  }
2264
2273
 
2265
2274
  // Tab has content if ANY of its content types have count > 0
2266
- return tabContentTypes.some(type => contentTypeCounts[type] > 0);
2267
- });
2275
+ return tabContentTypes.some((type) => contentTypeCounts[type] > 0)
2276
+ })
2268
2277
  }
2269
2278
 
2270
2279
  /**
@@ -2277,60 +2286,64 @@ function filterTabsByContentCounts(tabs, contentTypeCounts) {
2277
2286
  * @returns {Array} - Filtered filter groups
2278
2287
  */
2279
2288
  function filterTypeOptionsByContentCounts(filters, contentTypeCounts) {
2280
- return filters.map(filter => {
2281
- // Only process Type filter
2282
- if (filter.key !== 'type') {
2283
- return filter;
2284
- }
2289
+ return filters
2290
+ .map((filter) => {
2291
+ // Only process Type filter
2292
+ if (filter.key !== 'type') {
2293
+ return filter
2294
+ }
2285
2295
 
2286
- const filteredItems = filter.items.map(item => {
2287
- // For hierarchical filters (parent with children)
2288
- if (item.isParent && item.items) {
2289
- // Filter children based on their content types
2290
- const availableChildren = item.items.filter(child => {
2291
- const childTypes = getContentTypesForFilterName(child.name);
2296
+ const filteredItems = filter.items
2297
+ .map((item) => {
2298
+ // For hierarchical filters (parent with children)
2299
+ if (item.isParent && item.items) {
2300
+ // Filter children based on their content types
2301
+ const availableChildren = item.items.filter((child) => {
2302
+ const childTypes = getContentTypesForFilterName(child.name)
2303
+
2304
+ if (!childTypes || childTypes.length === 0) {
2305
+ console.warn(`Unknown filter child "${child.name}" - showing by default`)
2306
+ return true
2307
+ }
2308
+
2309
+ // Child has content if ANY of its types have count > 0
2310
+ return childTypes.some((type) => contentTypeCounts[type] > 0)
2311
+ })
2292
2312
 
2293
- if (!childTypes || childTypes.length === 0) {
2294
- console.warn(`Unknown filter child "${child.name}" - showing by default`);
2295
- return true;
2313
+ // Keep parent only if it has available children
2314
+ if (availableChildren.length > 0) {
2315
+ // Return NEW object to avoid mutation
2316
+ return { ...item, items: availableChildren }
2317
+ }
2318
+ return null
2296
2319
  }
2297
2320
 
2298
- // Child has content if ANY of its types have count > 0
2299
- return childTypes.some(type => contentTypeCounts[type] > 0);
2300
- });
2321
+ // For flat items (no children)
2322
+ const itemTypes = getContentTypesForFilterName(item.name)
2301
2323
 
2302
- // Keep parent only if it has available children
2303
- if (availableChildren.length > 0) {
2304
- // Return NEW object to avoid mutation
2305
- return { ...item, items: availableChildren };
2306
- }
2307
- return null;
2308
- }
2324
+ if (!itemTypes || itemTypes.length === 0) {
2325
+ console.warn(`Unknown filter item "${item.name}" - showing by default`)
2326
+ return item
2327
+ }
2309
2328
 
2310
- // For flat items (no children)
2311
- const itemTypes = getContentTypesForFilterName(item.name);
2329
+ // Item has content if ANY of its types have count > 0
2330
+ const hasContent = itemTypes.some((type) => contentTypeCounts[type] > 0)
2331
+ return hasContent ? item : null
2332
+ })
2333
+ .filter(Boolean) // Remove nulls
2312
2334
 
2313
- if (!itemTypes || itemTypes.length === 0) {
2314
- console.warn(`Unknown filter item "${item.name}" - showing by default`);
2315
- return item;
2335
+ // Return new filter object with filtered items
2336
+ return {
2337
+ ...filter,
2338
+ items: filteredItems,
2316
2339
  }
2317
-
2318
- // Item has content if ANY of its types have count > 0
2319
- const hasContent = itemTypes.some(type => contentTypeCounts[type] > 0);
2320
- return hasContent ? item : null;
2321
- }).filter(Boolean); // Remove nulls
2322
-
2323
- // Return new filter object with filtered items
2324
- return {
2325
- ...filter,
2326
- items: filteredItems
2327
- };
2328
- }).filter(filter => {
2329
- if (filter.key === 'type' && filter.items.length === 0) {
2330
- return false;
2331
- }
2332
- return true;
2333
- });
2340
+ })
2341
+ .filter((filter) => {
2342
+ if (filter.key === 'type' && filter.items.length === 0) {
2343
+ return false
2344
+ }
2345
+ return true
2346
+ })
2334
2347
  }
2335
2348
 
2336
2349
  /**
@@ -2340,25 +2353,30 @@ function filterTypeOptionsByContentCounts(filters, contentTypeCounts) {
2340
2353
  */
2341
2354
  function getContentTypesForFilterName(displayName) {
2342
2355
  const displayNameToKey = {
2343
- 'Lessons': 'lessons',
2356
+ Lessons: 'lessons',
2344
2357
  'Practice Alongs': 'practice alongs',
2345
2358
  'Live Archives': 'live archives',
2346
2359
  'Student Archives': 'student archives',
2347
- 'Courses': 'courses',
2360
+ Courses: 'courses',
2348
2361
  'Guided Courses': 'guided courses',
2349
2362
  'Tiered Courses': 'tiered courses',
2350
- 'Specials': 'specials',
2351
- 'Documentaries': 'documentaries',
2352
- 'Shows': 'shows',
2363
+ Specials: 'specials',
2364
+ Documentaries: 'documentaries',
2365
+ Shows: 'shows',
2353
2366
  'Skill Packs': 'skill packs',
2354
- 'Tutorials': 'tutorials',
2355
- 'Transcriptions': 'transcriptions',
2367
+ Tutorials: 'tutorials',
2368
+ Transcriptions: 'transcriptions',
2356
2369
  'Sheet Music': 'sheet music',
2357
- 'Tabs': 'tabs',
2370
+ Tabs: 'tabs',
2358
2371
  'Play-Alongs': 'play-alongs',
2359
2372
  'Jam Tracks': 'jam tracks',
2360
- };
2373
+ }
2374
+
2375
+ const mappingKey = displayNameToKey[displayName]
2376
+ return mappingKey ? lessonTypesMapping[mappingKey] : undefined
2377
+ }
2361
2378
 
2362
- const mappingKey = displayNameToKey[displayName];
2363
- return mappingKey ? lessonTypesMapping[mappingKey] : undefined;
2379
+ // this is so we can export the inner function from mcs
2380
+ export function getSongTypesFor(brand) {
2381
+ return getSongType(brand)
2364
2382
  }
@@ -96,12 +96,24 @@ export async function getUserWeeklyStats() {
96
96
  const today = dayjs()
97
97
  const startOfWeek = getMonday(today, timeZone)
98
98
  const weekDays = Array.from({ length: 7 }, (_, i) => startOfWeek.add(i, 'day').format('YYYY-MM-DD'))
99
-
100
- const practices = await getOwnPractices(
99
+ // Query THIS WEEK's practices for display
100
+ const weekPractices = await getOwnPractices(
101
101
  Q.where('date', Q.oneOf(weekDays)),
102
102
  Q.sortBy('date', 'desc')
103
103
  )
104
- const practiceDaysSet = new Set(Object.keys(practices))
104
+
105
+ // Query LAST 60 DAYS for streak calculation (balances accuracy vs performance)
106
+ // This captures:
107
+ // - Current active streaks up to 60 days
108
+ // - Recent breaks (to show "restart" message)
109
+ // - Sufficient context for accurate weekly streak calculation
110
+ const sixtyDaysAgo = today.subtract(60, 'days').format('YYYY-MM-DD')
111
+ const recentPractices = await getOwnPractices(
112
+ Q.where('date', Q.gte(sixtyDaysAgo)),
113
+ Q.sortBy('date', 'desc')
114
+ )
115
+
116
+ const practiceDaysSet = new Set(Object.keys(weekPractices))
105
117
  let dailyStats = []
106
118
  for (let i = 0; i < 7; i++) {
107
119
  const day = startOfWeek.add(i, 'day')
@@ -119,9 +131,8 @@ export async function getUserWeeklyStats() {
119
131
  })
120
132
  }
121
133
 
122
- let { streakMessage } = getStreaksAndMessage(practices)
123
-
124
- return { data: { dailyActiveStats: dailyStats, streakMessage, practices } }
134
+ let { streakMessage } = getStreaksAndMessage(recentPractices)
135
+ return { data: { dailyActiveStats: dailyStats, streakMessage, practices: weekPractices } }
125
136
  }
126
137
 
127
138
  /**