Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR refactors the task management system to improve performance through pagination and optimized aggregation queries. The changes separate task overview data (status counts and tasks grouped by name) from detailed task lists, enabling more efficient data loading for large task sets.
Changes:
- Introduced a new
getTaskOverviewbackend action that uses MongoDB aggregation to efficiently compute status counts and task summaries by name - Refactored
getTasksto support pagination with skip/limit parameters and return document counts - Added date range filtering and pagination UI to the task-by-name page with a fixed bottom navigation bar
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/actions/Task/getTaskOverview.js | New action that aggregates task overview data (status counts and tasks by name) using MongoDB's $facet |
| backend/actions/Task/getTasks.js | Refactored to add pagination support with skip/limit parameters and return total document count |
| backend/actions/Task/index.js | Exports the new getTaskOverview action |
| frontend/src/api.js | Adds getTaskOverview API method for both Lambda and non-Lambda configurations |
| frontend/src/tasks/tasks.js | Refactored to use getTaskOverview for status counts, removed 'All Time' filter, fixed date range handling |
| frontend/src/task-by-name/task-by-name.js | Added pagination logic, date range filtering, and query parameter handling |
| frontend/src/task-by-name/task-by-name.html | Added pagination UI with page controls and per-page size selector |
| frontend/src/tasks/task-details/task-details.js | Added showBackButton prop to conditionally display back button |
| frontend/src/tasks/task-details/task-details.html | Made back button conditional based on showBackButton prop |
| frontend/src/task-single/task-single.js | Fixed indentation (formatting only) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const y = now.getUTCFullYear(); | ||
| const m = now.getUTCMonth(); | ||
| start = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0)); | ||
| end = new Date(Date.UTC(y, m, 0, 23, 59, 59, 999)); |
There was a problem hiding this comment.
Since you use $lt shouldn't this just be new Date(Date.UTC(y, m, 0, 0, 0, 0, 0)) ?
There was a problem hiding this comment.
Well don't we want to include the day?
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Define all possible statuses | ||
| const allStatuses = ['pending', 'in_progress', 'succeeded', 'failed', 'cancelled', 'unknown']; | ||
| const skip = Math.max(0, Number(params.skip) || 0); | ||
| const limit = Math.min(MAX_LIMIT, Math.max(0, Number(params.limit) || 100)); |
There was a problem hiding this comment.
limit parsing uses Number(params.limit) || 100, which treats an explicit limit: 0 as falsy and silently replaces it with 100. If callers want limit: 0 (e.g. fetch counts only), this will return documents unexpectedly. Consider using a nullish check / Number.isFinite() instead of || so that 0 is preserved.
| const limit = Math.min(MAX_LIMIT, Math.max(0, Number(params.limit) || 100)); | |
| const rawLimit = Number(params.limit); | |
| const limit = Math.min( | |
| MAX_LIMIT, | |
| Math.max(0, Number.isFinite(rawLimit) ? rawLimit : 100) | |
| ); |
| skip: { $type: Number, $default: 0 }, | ||
| limit: { $type: Number, $default: 100 } |
There was a problem hiding this comment.
This file is the only place in backend/actions that uses Archetype number types as $type: Number. Elsewhere the codebase consistently uses $type: 'number' for numeric params. To avoid inconsistent behavior (and potential Archetype type parsing differences), align skip/limit to the same $type: 'number' pattern used in other actions.
| skip: { $type: Number, $default: 0 }, | |
| limit: { $type: Number, $default: 100 } | |
| skip: { $type: 'number', $default: 0 }, | |
| limit: { $type: 'number', $default: 100 } |
| $group: { | ||
| _id: '$name', | ||
| totalCount: { $sum: 1 }, | ||
| lastRun: { $max: '$scheduledAt' }, | ||
| pending: { $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } }, | ||
| succeeded: { $sum: { $cond: [{ $eq: ['$status', 'succeeded'] }, 1, 0] } }, | ||
| failed: { $sum: { $cond: [{ $eq: ['$status', 'failed'] }, 1, 0] } }, | ||
| cancelled: { $sum: { $cond: [{ $eq: ['$status', 'cancelled'] }, 1, 0] } } | ||
| } | ||
| }, | ||
| { | ||
| $project: { | ||
| _id: 0, | ||
| name: '$_id', | ||
| totalCount: 1, | ||
| lastRun: 1, | ||
| statusCounts: { | ||
| pending: '$pending', | ||
| succeeded: '$succeeded', | ||
| failed: '$failed', | ||
| cancelled: '$cancelled' | ||
| } | ||
| } |
There was a problem hiding this comment.
tasksByName computes totalCount as $sum: 1 across all matched tasks, but the per-name statusCounts only counts 4 statuses (pending/succeeded/failed/cancelled). If any tasks are in_progress or unknown, totalCount will exceed the sum of the displayed counts (the same mismatch called out earlier for the UI). Consider either (a) adding in_progress/unknown (or an “other”) count fields in the $group + statusCounts projection, or (b) changing the default match to only include OVERVIEW_STATUSES when no explicit status filter is provided so totals and statusCounts stay consistent.
| end = new Date(); | ||
| end.setHours(23, 59, 59, 999); | ||
| break; | ||
| case 'yesterday': | ||
| start = new Date(now); | ||
| start.setDate(start.getDate() - 1); | ||
| start.setHours(0, 0, 0, 0); | ||
| end = new Date(start); | ||
| end.setHours(23, 59, 59, 999); | ||
| break; | ||
| case 'thisWeek': | ||
| start = new Date(now.getTime() - (7 * 86400000)); | ||
| start.setHours(0, 0, 0, 0); | ||
| end = new Date(); | ||
| end.setHours(23, 59, 59, 999); | ||
| break; | ||
| case 'lastWeek': | ||
| start = new Date(now.getTime() - (14 * 86400000)); | ||
| start.setHours(0, 0, 0, 0); | ||
| end = new Date(now.getTime() - (7 * 86400000)); | ||
| end.setHours(23, 59, 59, 999); | ||
| break; | ||
| case 'thisMonth': { | ||
| const y = now.getUTCFullYear(); | ||
| const m = now.getUTCMonth(); | ||
| start = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0)); | ||
| end = new Date(Date.UTC(y, m + 1, 0, 23, 59, 59, 999)); | ||
| break; | ||
| } | ||
| case 'lastMonth': { | ||
| const y = now.getUTCFullYear(); | ||
| const m = now.getUTCMonth(); | ||
| start = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0)); | ||
| end = new Date(Date.UTC(y, m, 0, 23, 59, 59, 999)); |
There was a problem hiding this comment.
getDateRangeForRange() sets end to 23:59:59.999 for day/week ranges, but the backend queries use { scheduledAt: { $lt: end } }. That makes the last millisecond of the intended range exclusive, and mixes an inclusive-style end with an exclusive $lt query. To make the boundary unambiguous, consider returning an exclusive end (start of the next day / next week range) when the backend uses $lt, or switch the backend to $lte when using 23:59:59.999 style ends.
| end = new Date(); | |
| end.setHours(23, 59, 59, 999); | |
| break; | |
| case 'yesterday': | |
| start = new Date(now); | |
| start.setDate(start.getDate() - 1); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(start); | |
| end.setHours(23, 59, 59, 999); | |
| break; | |
| case 'thisWeek': | |
| start = new Date(now.getTime() - (7 * 86400000)); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(); | |
| end.setHours(23, 59, 59, 999); | |
| break; | |
| case 'lastWeek': | |
| start = new Date(now.getTime() - (14 * 86400000)); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(now.getTime() - (7 * 86400000)); | |
| end.setHours(23, 59, 59, 999); | |
| break; | |
| case 'thisMonth': { | |
| const y = now.getUTCFullYear(); | |
| const m = now.getUTCMonth(); | |
| start = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0)); | |
| end = new Date(Date.UTC(y, m + 1, 0, 23, 59, 59, 999)); | |
| break; | |
| } | |
| case 'lastMonth': { | |
| const y = now.getUTCFullYear(); | |
| const m = now.getUTCMonth(); | |
| start = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0)); | |
| end = new Date(Date.UTC(y, m, 0, 23, 59, 59, 999)); | |
| end = new Date(start); | |
| end.setDate(end.getDate() + 1); | |
| end.setHours(0, 0, 0, 0); | |
| break; | |
| case 'yesterday': | |
| start = new Date(now); | |
| start.setDate(start.getDate() - 1); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(start); | |
| end.setDate(end.getDate() + 1); | |
| end.setHours(0, 0, 0, 0); | |
| break; | |
| case 'thisWeek': | |
| start = new Date(now.getTime() - (7 * 86400000)); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(); | |
| end.setDate(end.getDate() + 1); | |
| end.setHours(0, 0, 0, 0); | |
| break; | |
| case 'lastWeek': | |
| start = new Date(now.getTime() - (14 * 86400000)); | |
| start.setHours(0, 0, 0, 0); | |
| end = new Date(now.getTime() - (7 * 86400000)); | |
| end.setHours(0, 0, 0, 0); | |
| break; | |
| case 'thisMonth': { | |
| const y = now.getUTCFullYear(); | |
| const m = now.getUTCMonth(); | |
| start = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0)); | |
| end = new Date(Date.UTC(y, m + 1, 1, 0, 0, 0, 0)); | |
| break; | |
| } | |
| case 'lastMonth': { | |
| const y = now.getUTCFullYear(); | |
| const m = now.getUTCMonth(); | |
| start = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0)); | |
| end = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0)); |
| const res = await actions.Task.getTasks({ limit: 10000 }); | ||
| assert.strictEqual(Array.isArray(res.tasks), true); | ||
| assert.strictEqual(res.tasks.length, 0); | ||
| assert.strictEqual(res.numDocs, 0); |
There was a problem hiding this comment.
The test caps limit at MAX_LIMIT (2000) doesn’t actually verify the cap: it calls getTasks({ limit: 10000 }) with an empty collection and only asserts tasks.length === 0. To cover the behavior, insert >2000 tasks and assert that tasks.length is 2000 (and numDocs reflects the full matching count).
| const res = await actions.Task.getTasks({ limit: 10000 }); | |
| assert.strictEqual(Array.isArray(res.tasks), true); | |
| assert.strictEqual(res.tasks.length, 0); | |
| assert.strictEqual(res.numDocs, 0); | |
| const totalTasks = 2100; | |
| const docs = []; | |
| const now = new Date(); | |
| for (let i = 0; i < totalTasks; i++) { | |
| docs.push({ name: `task-${i}`, status: 'pending', scheduledAt: now }); | |
| } | |
| await Task.insertMany(docs); | |
| const res = await actions.Task.getTasks({ limit: 10000 }); | |
| assert.strictEqual(Array.isArray(res.tasks), true); | |
| assert.strictEqual(res.tasks.length, 2000); | |
| assert.strictEqual(res.numDocs, totalTasks); |
No description provided.