diff --git a/package.json b/package.json index c7d0be7..dfebfca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "task-checklist", - "version": "5.3", + "version": "5.3.1", "description": "A Warframe Task Checklist to track daily, weekly, and other tasks. Built with HTML, CSS, and vanilla JavaScript, processed with Vite.", "type": "module", "scripts": { diff --git a/sources/js/app.js b/sources/js/app.js index 67a0e97..9fc0a0d 100644 --- a/sources/js/app.js +++ b/sources/js/app.js @@ -14,7 +14,7 @@ import { getUTCDayOfYear, formatCountdown, parseDuration, - calcCycleNumber, + calcResetCount, makeInfoLineItem, calcTaskTimes, makeSectionStats, @@ -34,7 +34,7 @@ const dailyBackgroundImageIds = [ 'bg-image-4', // Add more IDs if you add more background image divs in HTML ]; -export const APP_VERSION = "5.3"; +export const APP_VERSION = "5.3.1"; const GIT_COMMIT_HASH_LONG = import.meta.env.VITE_GIT_COMMIT_HASH; const GIT_COMMIT_HASH = GIT_COMMIT_HASH_LONG.slice(0,7); const WARFRAME_VERSION = "43.0.8"; @@ -149,9 +149,7 @@ function prepTasks() { task.section = task.id.split("_")[0]; if (["daily", "weekly"].includes(task.section)) { - if (task.ref) { // alternate ref tasks need a period for countdown and reset to work correctly - task.period = (task.section === "daily") ? "1d" : "7d"; - } + task.period = (task.section === "daily") ? "1d" : "7d"; } if (task.id in moreInfo) { @@ -331,6 +329,7 @@ export function displayOtherTaskCountdown(task) { const now = new Date(); const taskTimes = calcTaskTimes(task, now); + const resetCount = calcResetCount(task, now); if (task.duration) { // intermittently available task (e.g., Baro) const leaveNotifId = `${task.id}/departure`; @@ -339,10 +338,10 @@ export function displayOtherTaskCountdown(task) { const diff = taskTimes.thisCycleLeaveTimestamp - now.getTime(); resetTimer.innerHTML = `(Available for ${formatCountdown(diff)})`; - // Leaving soon notification (Arrival notification is handled in runAutoResets, the same as always available tasks) - if (diff < C.MILLISECONDS_PER_HOUR && checklistData.notificationPreferences[task.id] && checklistData.notificationsSent[leaveNotifId] !== cycleNumber) { + // Leaving soon notification (Arrival notification is handled in otherTaskReset, the same as always available tasks) + if (diff < C.MILLISECONDS_PER_HOUR && checklistData.notificationPreferences[task.id] && checklistData.notificationsSent[leaveNotifId] !== resetCount) { showNotification(`${task.title} Leaving Soon!`, `Approximately ${Math.round(diff / C.MILLISECONDS_PER_MINUTE)} minutes remaining.`); - checklistData.notificationsSent[leaveNotifId] = cycleNumber; + checklistData.notificationsSent[leaveNotifId] = resetCount; saveData(false); } } else { // task not available @@ -354,7 +353,39 @@ export function displayOtherTaskCountdown(task) { } } +/** + * Time travel (adjusting the system clock) may be used for debugging purposes, which leaves behind some annoying + * artifacts in the save data. This cleans them up. + */ +function fixTimeTravel() { + const now = new Date(); + + for (const prop of ["lastSaved", "lastDailyReset", "lastWeeklyReset"]) { + if (new Date(checklistData[prop]) > now) { + checklistData[prop] = null; + console.warn(`fixed time travel of ${prop}`); + } + } + + for (const taskId in checklistData.lastTaskResetTimes) { + if (checklistData.lastTaskResetTimes[taskId] > now.getTime()) { + checklistData.lastTaskResetTimes[taskId] = 0; + console.warn(`fixed time travel of ${taskId} reset`); + } + } + + for (const notifId in checklistData.notificationsSent) { + const taskId = notifId.split("/")[0]; + const resetCount = calcResetCount(getTaskById(taskId), now); + if (checklistData.notificationsSent[notifId] > resetCount) { + delete checklistData.notificationsSent[notifId]; + console.warn(`fixed time travel of ${notifId} notification`); + } + } +} + function runAutoResets() { + fixTimeTravel(); const now = new Date(); const nowUTCTimestamp = now.getTime(); const todayUTCString = getUTCDateString(now); @@ -388,32 +419,27 @@ function runAutoResets() { } function otherTaskReset(task) { - if (["daily", "weekly"].includes(task.section) && !task.ref) { // daily and weekly tasks with an alternate ref are handled as "other" tasks + if (["daily", "weekly"].includes(task.section) && !task.ref) { // daily and weekly tasks without an alternate ref are not handled as "other" tasks return false; - } else if (!task.period) { - console.error(`[${task.id}] other_* tasks MUST specify a "period"`); - return false; - } - if (!task.ref) { - console.warn(`[${task.id}] other_* tasks SHOULD specify a "ref". The default ref of 0 ("1970-01-01T00:00:00Z") will be used otherwise.`) } let didReset = false; const now = new Date(); - const cycleNumber = calcCycleNumber(task, now); + const resetCount = calcResetCount(task, now); const lastResetTime = checklistData.lastTaskResetTimes[task.id] || 0; - const lastResetCycleNumber = calcCycleNumber(task, lastResetTime); + const lastResetCount = calcResetCount(task, lastResetTime); - if (checklistData.progress[task.id] && !calcTaskTimes(task, now).isAvailable) { + if (!calcTaskTimes(task, now).isAvailable && (checklistData.progress[task.id] || !document.getElementById(task.id)?.indeterminate)) { // Uncheck unavailable task checklistData.progress[task.id] = false; + console.log(`${task.id} is now unavailalbe`); didReset = true; } - if (cycleNumber > lastResetCycleNumber) { + if (resetCount > lastResetCount) { // Reset task - if (checklistData.progress[task.id] || checklistData.skippedTasks[task.id]) { + if (checklistData.progress[task.id] || checklistData.skippedTasks[task.id] || task.duration) { checklistData.progress[task.id] = false; checklistData.skippedTasks[task.id] = false; console.log(`Resetting task: ${task.id}`); @@ -423,14 +449,14 @@ function otherTaskReset(task) { // Delete old notification record const notifSent = checklistData.notificationsSent[task.id]; - if (notifSent && notifSent !== cycleNumber) { + if (notifSent && notifSent !== resetCount) { delete checklistData.notificationsSent[task.id]; } // Send and record new notification - if (checklistData.notificationPreferences[task.id] && checklistData.notificationsSent[task.id] !== cycleNumber) { + if (checklistData.notificationPreferences[task.id] && checklistData.notificationsSent[task.id] !== resetCount) { showNotification(`${task.title} has reset!`, "Vendor stock may have updated."); - checklistData.notificationsSent[task.id] = cycleNumber; + checklistData.notificationsSent[task.id] = resetCount; saveData(false); } } @@ -524,7 +550,7 @@ function createChecklistItem(task) { } // Reset Timer - if (task.period) { + if (task.ref) { const resetTimer = document.createElement("span"); resetTimer.classList.add("other-countdown"); resetTimer.textContent = "(Loading...)"; @@ -763,9 +789,7 @@ function taskDialogHeaderSetup(task, dialog) { } } -function showScheduleAction(task, period, cycleIndex, isAvailable) { - const cycleCount = cycles[task.id].columns[0].order.length; - +function showScheduleAction(task, cycleIndex, isAvailable) { return () => { taskDialogHeaderSetup(task, scheduleDialog); @@ -781,6 +805,9 @@ function showScheduleAction(task, period, cycleIndex, isAvailable) { header += ""; thead.innerHTML += header; + const cycleCount = cycles[task.id].columns[0].order.length; + const period = parseDuration(task.period); + const now = new Date(); const ref = new Date(cycles[task.id].ref); const cyclesSinceRef = Math.floor((now.getTime() - ref.getTime()) / period) + (isAvailable ? 0 : 1); // add 1 if unavailable to skip the current cycle for unavailable intermittent tasks @@ -841,26 +868,19 @@ function makeInfoLine(task, appendTo) { const ref = new Date(cycles[task.id].ref); const cycleCount = cycles[task.id].columns[0].order.length; - let prefix, period, cycleIndex; + let prefix; const isAvailable = calcTaskTimes(task, now).isAvailable; - if (task.id.startsWith("weekly_")) { + if (task.section === "weekly") { prefix = "This Week"; - period = 7 * C.MILLISECONDS_PER_DAY; - if (ref.getUTCDay() !== 1) { - console.warn(`${task.id} cycle ref ${cycles[task.id].ref} is not a Monday`); - } - } - else if (task.id.startsWith("daily_")) { + } else if (task.section === "daily") { prefix = "Today"; - period = C.MILLISECONDS_PER_DAY; - } - else { + } else { prefix = "Current Cycle"; - if (!isAvailable) {prefix = "Next Cycle";} - period = parseDuration(task.period); + if (!isAvailable) { prefix = "Next Cycle"; } } - cycleIndex = modulo(Math.floor((now.getTime() - ref.getTime()) / period), cycleCount); + const resetCount = calcResetCount(task, now, ref); // calcResetCount handles DST + let cycleIndex = modulo(resetCount, cycleCount); if (!isAvailable) { cycleIndex = modulo(cycleIndex + 1, cycleCount); } console.log(`${task.id} cycleIndex ${cycleIndex}`); const cycleData = cycles[task.id].columns[0].order[cycleIndex]; @@ -879,7 +899,7 @@ function makeInfoLine(task, appendTo) { showSchedule.type = "button"; showSchedule.classList.add("more-info-btn"); showSchedule.innerHTML = "Show Schedule"; - showSchedule.addEventListener("click", showScheduleAction(task, period, cycleIndex, isAvailable)); + showSchedule.addEventListener("click", showScheduleAction(task, cycleIndex, isAvailable)); currentCycle.appendChild(showSchedule); taskInfoExpanderContent.appendChild(currentCycle); diff --git a/sources/js/functions.js b/sources/js/functions.js index 61e510d..7b8bddb 100644 --- a/sources/js/functions.js +++ b/sources/js/functions.js @@ -171,10 +171,14 @@ export function isDst(date, timezone) { return currentOffset === dstOffset; } -/** calculates the cycleNumber (number of resets since the reference time) of the given task at the given time (a Date object or timestamp) */ -export function calcCycleNumber(task, time) { +/** + * Calculates the reset count (number of resets since the reference time) of the given task + * at the given time (a Date object or timestamp). + * Uses the task definition ref by default, if it has one. If you need to use the cycle ref, pass it as `altRef`. +*/ +export function calcResetCount(task, time, altRef = undefined) { time = new Date(time); - const ref = new Date(task.ref || 0); + const ref = new Date(altRef || task.ref || 0); const period = parseDuration(task.period); let diff = time.getTime() - ref.getTime(); if (task.observesDst && isDst(time, C.SERVER_TIMEZONE)) { @@ -196,8 +200,8 @@ export function calcTaskTimes(task, date) { date = new Date(date); const ref = new Date(task.ref || 0); const period = parseDuration(task.period); - const cycleNumber = calcCycleNumber(task, date); - const prevResetTimestamp = ref.getTime() + (cycleNumber * period); + const resetCount = calcResetCount(task, date); + const prevResetTimestamp = ref.getTime() + (resetCount * period); let nextResetTimestamp = prevResetTimestamp + period; let thisCycleLeaveTimestamp = prevResetTimestamp + parseDuration(task.duration); diff --git a/sources/tests/cycles.schema.json b/sources/tests/cycles.schema.json index 5c12a1f..0d4dca7 100644 --- a/sources/tests/cycles.schema.json +++ b/sources/tests/cycles.schema.json @@ -54,7 +54,7 @@ "type": "string" }, "iconFilter": { - "description": "set to `true` to convert the `icon` to monochrome black or white depending on the theme. Otherwise, it will be used in color as-is. Note the opposite name and default bahaviour to `tasks.json`", + "description": "set to `true` to convert the `icon` to monochrome black or white depending on the theme. Otherwise, it will be used in color as-is. Note the opposite name and default behaviour to `tasks.json`", "type": "boolean", "default": false } diff --git a/sources/tests/tasks.test.js b/sources/tests/tasks.test.js index 9988ade..6446031 100644 --- a/sources/tests/tasks.test.js +++ b/sources/tests/tasks.test.js @@ -69,6 +69,28 @@ describe("valildate task definitions", () => { }); }); + describe("check cycle refs", () => { + test.for(cycle_keys_for_test)("%s", ([task_id]) => { + const section = task_id.split("_")[0]; + const ref_text = cycles[task_id].ref; + const ref = new Date(ref_text); + + const task_def = flatTasks.find((t) => t.id === task_id); + if (task_def.ref) { // cycle ref for alternate ref task must match task's alternate ref + expect(ref, `cycle ref ${ref_text} does not equal task ref ${task_def.ref}`).toEqual(new Date(task_def.ref)); + } else if (["daily", "weekly"].includes(section)) { // cycle for standard ref tasks must match standard resets + if (section === "weekly") { + expect(ref.getUTCDay(), `cycle ref ${ref_text} is not a Monday`).toEqual(1); + } + + expect( + [ref.getUTCHours(), ref.getUTCMinutes(), ref.getUTCSeconds(), ref.getUTCMilliseconds()], + `cycle ref ${ref_text} is not midnight`, + ).toEqual([0, 0, 0, 0]); + } + }); + }); + describe("validate moreInfo", () => { test.for(Object.keys(moreInfo).map((i) => [i]))("%s", ([task_id]) => { expect(task_ids, "moreInfo keys must be task ids from `tasks.json`").toContain(task_id);