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);
}
}