Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
104 changes: 62 additions & 42 deletions sources/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getUTCDayOfYear,
formatCountdown,
parseDuration,
calcCycleNumber,
calcResetCount,
makeInfoLineItem,
calcTaskTimes,
makeSectionStats,
Expand All @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`;
Expand All @@ -339,10 +338,10 @@ export function displayOtherTaskCountdown(task) {
const diff = taskTimes.thisCycleLeaveTimestamp - now.getTime();
resetTimer.innerHTML = `(Available for <span class="tooltip" title="${new Date(taskTimes.thisCycleLeaveTimestamp).toString()}">${formatCountdown(diff)}</span>)`;

// 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
Expand All @@ -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);
Expand Down Expand Up @@ -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}`);
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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...)";
Expand Down Expand Up @@ -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);

Expand All @@ -781,6 +805,9 @@ function showScheduleAction(task, period, cycleIndex, isAvailable) {
header += "</tr>";
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
Expand Down Expand Up @@ -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&nbsp;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&nbsp;Cycle";
if (!isAvailable) {prefix = "Next&nbsp;Cycle";}
period = parseDuration(task.period);
if (!isAvailable) { prefix = "Next&nbsp;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];
Expand All @@ -879,7 +899,7 @@ function makeInfoLine(task, appendTo) {
showSchedule.type = "button";
showSchedule.classList.add("more-info-btn");
showSchedule.innerHTML = "Show&nbsp;Schedule";
showSchedule.addEventListener("click", showScheduleAction(task, period, cycleIndex, isAvailable));
showSchedule.addEventListener("click", showScheduleAction(task, cycleIndex, isAvailable));
currentCycle.appendChild(showSchedule);

taskInfoExpanderContent.appendChild(currentCycle);
Expand Down
14 changes: 9 additions & 5 deletions sources/js/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion sources/tests/cycles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions sources/tests/tasks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down