From 6a548a31b127cec6fd396710f4603187b8b1fa3b Mon Sep 17 00:00:00 2001 From: NiloCK Date: Fri, 16 May 2025 11:08:55 -0300 Subject: [PATCH 1/2] fixes for heatmap data munging --- packages/common-ui/src/components/HeatMap.vue | 69 +++++++++++++++++-- .../common-ui/src/components/StudySession.vue | 2 +- packages/db/src/impl/pouch/userDB.ts | 52 ++++++++++---- 3 files changed, 103 insertions(+), 20 deletions(-) diff --git a/packages/common-ui/src/components/HeatMap.vue b/packages/common-ui/src/components/HeatMap.vue index 3e05cb606..b813a179b 100644 --- a/packages/common-ui/src/components/HeatMap.vue +++ b/packages/common-ui/src/components/HeatMap.vue @@ -89,7 +89,10 @@ export default defineComponent({ return 7 * (this.cellSize + this.cellMargin); }, effectiveActivityRecords(): ActivityRecord[] { - return this.localActivityRecords.length > 0 ? this.localActivityRecords : this.activityRecords; + const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0; + const records = useLocal ? this.localActivityRecords : this.activityRecords || []; + console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop'); + return records; }, }, @@ -107,12 +110,28 @@ export default defineComponent({ if (this.activityRecordsGetter) { try { this.isLoading = true; - this.localActivityRecords = await this.activityRecordsGetter(); + console.log('Fetching activity records using getter...'); + // Ensure the getter is called safely with proper error handling + const result = await this.activityRecordsGetter(); + + if (Array.isArray(result)) { + this.localActivityRecords = result; + console.log('Received activity records:', this.localActivityRecords.length); + // Process the loaded records + this.processRecords(); + this.createWeeksData(); + } else { + console.error('Activity records getter did not return an array:', result); + this.localActivityRecords = []; + } } catch (error) { console.error('Error fetching activity records:', error); + this.localActivityRecords = []; } finally { this.isLoading = false; } + } else { + console.log('No activityRecordsGetter provided, using direct activityRecords prop'); } }, @@ -123,16 +142,45 @@ export default defineComponent({ }, processRecords() { - const records = this.effectiveActivityRecords; + const records = this.effectiveActivityRecords || []; console.log(`Processing ${records.length} records`); const data: { [key: string]: number } = {}; + if (records.length === 0) { + console.log('No records to process'); + this.heatmapData = data; + return; + } + records.forEach((record) => { - const date = moment(record.timeStamp).format('YYYY-MM-DD'); - data[date] = (data[date] || 0) + 1; + if (!record || typeof record !== 'object') { + console.warn('Invalid record:', record); + return; + } + + if (!record.timeStamp) { + console.warn('Record missing timeStamp:', record); + return; + } + + // Make sure timeStamp is properly handled + let date; + try { + // Try to parse the timestamp + const m = moment(record.timeStamp); + if (m.isValid()) { + date = m.format('YYYY-MM-DD'); + data[date] = (data[date] || 0) + 1; + } else { + console.warn('Invalid date from record:', record); + } + } catch (e) { + console.error('Error processing record date:', e, record); + } }); + console.log('Processed heatmap data:', Object.keys(data).length, 'unique dates'); this.heatmapData = data; }, @@ -140,18 +188,21 @@ export default defineComponent({ // Reset weeks and max count this.weeks = []; this.maxInRange = 0; - + const end = moment(); const start = end.clone().subtract(52, 'weeks'); const day = start.clone().startOf('week'); + + console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD')); while (day.isSameOrBefore(end)) { const weekData: DayData[] = []; for (let i = 0; i < 7; i++) { const date = day.format('YYYY-MM-DD'); + const count = this.heatmapData[date] || 0; const dayData: DayData = { date, - count: this.heatmapData[date] || 0, + count, }; weekData.push(dayData); if (dayData.count > this.maxInRange) { @@ -162,6 +213,10 @@ export default defineComponent({ } this.weeks.push(weekData); } + + console.log('Weeks data created, maxInRange:', this.maxInRange); + console.log('First week sample:', this.weeks[0]); + console.log('Last week sample:', this.weeks[this.weeks.length - 1]); }, getColor(count: number): string { diff --git a/packages/common-ui/src/components/StudySession.vue b/packages/common-ui/src/components/StudySession.vue index b2d6dc1cf..dc27b6f9f 100644 --- a/packages/common-ui/src/components/StudySession.vue +++ b/packages/common-ui/src/components/StudySession.vue @@ -19,7 +19,7 @@ Start another study session, or try adding some new content to challenge yourself and others!

- +
diff --git a/packages/db/src/impl/pouch/userDB.ts b/packages/db/src/impl/pouch/userDB.ts index 5d16cbb48..6f8fff630 100644 --- a/packages/db/src/impl/pouch/userDB.ts +++ b/packages/db/src/impl/pouch/userDB.ts @@ -245,20 +245,48 @@ Currently logged-in as ${this._username}.` } public async getActivityRecords(): Promise { - const hist = await this.getHistory(); - - const allRecords: ActivityRecord[] = []; - for (let i = 0; i < hist.length; i++) { - if (hist[i] && hist[i]!.records) { - hist[i]!.records.forEach((record: CardRecord) => { - allRecords.push({ - timeStamp: record.timeStamp.toString(), - }); - }); + try { + const hist = await this.getHistory(); + + const allRecords: ActivityRecord[] = []; + if (!Array.isArray(hist)) { + console.error('getHistory did not return an array:', hist); + return allRecords; + } + + for (let i = 0; i < hist.length; i++) { + try { + if (hist[i] && Array.isArray(hist[i]!.records)) { + hist[i]!.records.forEach((record: CardRecord) => { + try { + // Convert Moment objects to ISO string format for consistency + const timeStamp = record.timeStamp && typeof record.timeStamp.isValid === 'function' && record.timeStamp.isValid() + ? record.timeStamp.toISOString() + : new Date().toISOString(); + + allRecords.push({ + timeStamp, + courseID: record.courseID || 'unknown', + cardID: record.cardID || 'unknown', + timeSpent: record.timeSpent || 0, + type: 'card_view' + }); + } catch (err) { + console.error('Error processing record:', err, record); + } + }); + } + } catch (err) { + console.error('Error processing history item:', err, hist[i]); + } } - } - return allRecords; + console.log(`Found ${allRecords.length} activity records`); + return allRecords; + } catch (err) { + console.error('Error in getActivityRecords:', err); + return []; + } } private async getReviewstoDate(targetDate: Moment, course_id?: string) { From b126bf6c23540eaf53cae22b13ed7cde1375b44d Mon Sep 17 00:00:00 2001 From: NiloCK Date: Fri, 16 May 2025 11:14:01 -0300 Subject: [PATCH 2/2] fix timestamp parsing --- packages/common-ui/src/components/HeatMap.vue | 118 ++++++++++++++---- packages/db/src/impl/pouch/userDB.ts | 51 ++++++-- 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/packages/common-ui/src/components/HeatMap.vue b/packages/common-ui/src/components/HeatMap.vue index b813a179b..a5e7bc773 100644 --- a/packages/common-ui/src/components/HeatMap.vue +++ b/packages/common-ui/src/components/HeatMap.vue @@ -111,12 +111,27 @@ export default defineComponent({ try { this.isLoading = true; console.log('Fetching activity records using getter...'); + // Ensure the getter is called safely with proper error handling - const result = await this.activityRecordsGetter(); + let result = await this.activityRecordsGetter(); + // Handle the result - ensure it's an array of activity records if (Array.isArray(result)) { - this.localActivityRecords = result; - console.log('Received activity records:', this.localActivityRecords.length); + // Filter out records with invalid timestamps before processing + this.localActivityRecords = result.filter(record => { + if (!record || !record.timeStamp) return false; + + // Basic validation check for timestamps + try { + const m = moment(record.timeStamp); + return m.isValid() && m.year() > 2000 && m.year() < 2100; + } catch (e) { + return false; + } + }); + + console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`); + // Process the loaded records this.processRecords(); this.createWeeksData(); @@ -153,34 +168,70 @@ export default defineComponent({ return; } - records.forEach((record) => { - if (!record || typeof record !== 'object') { - console.warn('Invalid record:', record); - return; - } + // Sample logging of a few records to understand structure without flooding console + const uniqueDates = new Set(); + const dateDistribution: Record = {}; + let validCount = 0; + let invalidCount = 0; + + for (let i = 0; i < records.length; i++) { + const record = records[i]; - if (!record.timeStamp) { - console.warn('Record missing timeStamp:', record); - return; + if (!record || typeof record !== 'object' || !record.timeStamp) { + invalidCount++; + continue; } - // Make sure timeStamp is properly handled - let date; try { - // Try to parse the timestamp - const m = moment(record.timeStamp); - if (m.isValid()) { - date = m.format('YYYY-MM-DD'); - data[date] = (data[date] || 0) + 1; + // Attempt to normalize the timestamp + let normalizedDate: string; + + if (typeof record.timeStamp === 'string') { + // For ISO strings, parse directly with moment + normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD'); + } else if (typeof record.timeStamp === 'number') { + // For numeric timestamps, use Date constructor then moment + normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD'); + } else if (typeof record.timeStamp === 'object') { + // For objects (like Moment), try toString() or direct parsing + if (typeof record.timeStamp.format === 'function') { + // It's likely a Moment object + normalizedDate = record.timeStamp.format('YYYY-MM-DD'); + } else if (record.timeStamp instanceof Date) { + normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD'); + } else { + // Try to parse it as a string representation + normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD'); + } + } else { + // Unhandled type + invalidCount++; + continue; + } + + // Verify the date is valid before using it + if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) { + data[normalizedDate] = (data[normalizedDate] || 0) + 1; + uniqueDates.add(normalizedDate); + + // Track distribution by month for debugging + const month = normalizedDate.substring(0, 7); // YYYY-MM + dateDistribution[month] = (dateDistribution[month] || 0) + 1; + + validCount++; } else { - console.warn('Invalid date from record:', record); + invalidCount++; } } catch (e) { - console.error('Error processing record date:', e, record); + invalidCount++; } - }); + } + + // Log summary statistics + console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`); + console.log(`Found ${uniqueDates.size} unique dates`); + console.log('Date distribution by month:', dateDistribution); - console.log('Processed heatmap data:', Object.keys(data).length, 'unique dates'); this.heatmapData = data; }, @@ -194,7 +245,17 @@ export default defineComponent({ const day = start.clone().startOf('week'); console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD')); + + // Ensure we have data to display + if (Object.keys(this.heatmapData).length === 0) { + console.log('No heatmap data available to display'); + } + // For debugging, log some sample dates from the heatmap data + const sampleDates = Object.keys(this.heatmapData).slice(0, 5); + console.log('Sample dates in heatmap data:', sampleDates); + + // Build the week data structure while (day.isSameOrBefore(end)) { const weekData: DayData[] = []; for (let i = 0; i < 7; i++) { @@ -215,8 +276,17 @@ export default defineComponent({ } console.log('Weeks data created, maxInRange:', this.maxInRange); - console.log('First week sample:', this.weeks[0]); - console.log('Last week sample:', this.weeks[this.weeks.length - 1]); + + // Calculate summary stats for display + let totalDaysWithActivity = 0; + let totalActivity = 0; + + Object.values(this.heatmapData).forEach(count => { + totalDaysWithActivity++; + totalActivity += count; + }); + + console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`); }, getColor(count: number): string { diff --git a/packages/db/src/impl/pouch/userDB.ts b/packages/db/src/impl/pouch/userDB.ts index 6f8fff630..0fdc97b53 100644 --- a/packages/db/src/impl/pouch/userDB.ts +++ b/packages/db/src/impl/pouch/userDB.ts @@ -254,16 +254,53 @@ Currently logged-in as ${this._username}.` return allRecords; } + // Sample the first few records to understand structure + let sampleCount = 0; + for (let i = 0; i < hist.length; i++) { try { if (hist[i] && Array.isArray(hist[i]!.records)) { hist[i]!.records.forEach((record: CardRecord) => { try { - // Convert Moment objects to ISO string format for consistency - const timeStamp = record.timeStamp && typeof record.timeStamp.isValid === 'function' && record.timeStamp.isValid() - ? record.timeStamp.toISOString() - : new Date().toISOString(); - + // Skip this record if timeStamp is missing + if (!record.timeStamp) { + return; + } + + let timeStamp; + + // Handle different timestamp formats + if (typeof record.timeStamp === 'object') { + // It's likely a Moment object + if (typeof record.timeStamp.toDate === 'function') { + // It's definitely a Moment object + timeStamp = record.timeStamp.toISOString(); + } else if (record.timeStamp instanceof Date) { + // It's a Date object + timeStamp = record.timeStamp.toISOString(); + } else { + // Log a sample of unknown object types, but don't flood console + if (sampleCount < 3) { + console.warn('Unknown timestamp object type:', record.timeStamp); + sampleCount++; + } + return; + } + } else if (typeof record.timeStamp === 'string') { + // It's already a string, but make sure it's a valid date + const date = new Date(record.timeStamp); + if (isNaN(date.getTime())) { + return; // Invalid date string + } + timeStamp = record.timeStamp; + } else if (typeof record.timeStamp === 'number') { + // Assume it's a Unix timestamp (milliseconds since epoch) + timeStamp = new Date(record.timeStamp).toISOString(); + } else { + // Unknown type, skip + return; + } + allRecords.push({ timeStamp, courseID: record.courseID || 'unknown', @@ -272,12 +309,12 @@ Currently logged-in as ${this._username}.` type: 'card_view' }); } catch (err) { - console.error('Error processing record:', err, record); + // Silently skip problematic records to avoid flooding logs } }); } } catch (err) { - console.error('Error processing history item:', err, hist[i]); + console.error('Error processing history item:', err); } }