Skip to content
39 changes: 22 additions & 17 deletions js/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const STORAGE_SAVE_KEY = "launchdesk-v1-items";
const STORAGE_LOAD_KEY = "launchdesk-items-v1"; // Intentional bug: this key should match STORAGE_SAVE_KEY.
const STORAGE_LOAD_KEY = "launchdesk-v1-items";

const demoChecks = [
{
Expand Down Expand Up @@ -91,7 +91,7 @@ const activityLog = document.getElementById("activityLog");
let checks = loadChecks();
let currentView = checks;

form.addEventListener("submit", (event) => handleAddChek(event)); // Intentional bug: misspelled function name.
form.addEventListener("submit", handleAddCheck);
searchInput.addEventListener("input", applyFilters);
statusFilter.addEventListener("change", applyFilters);
priorityFilter.addEventListener("change", applyFilters);
Expand Down Expand Up @@ -132,8 +132,7 @@ function handleAddCheck(event) {
const owner = ownerInput.value.trim() || "Unassigned";
const dueDate = dueDateInput.value || new Date().toISOString().slice(0, 10);

if (!title && !category) {
// Intentional bug: validation should stop when either required field is missing.
if (!title || !category) {
formMessage.textContent =
"Please enter a check title and choose a category.";
return;
Expand Down Expand Up @@ -164,12 +163,15 @@ function applyFilters() {
const selectedPriority = priorityFilter.value;

let filtered = checks.filter((check) =>
check.owner.toLowerCase().includes(searchTerm),
); // Intentional bug: search should include title, category, priority, status, and owner.
[check.title, check.category, check.priority, check.status, check.owner]
.join(" ")
.toLowerCase()
.includes(searchTerm),
);

if (selectedStatus !== "All") {
filtered = filtered.filter((check) => check.priority === selectedStatus);
} // Intentional bug: status filter compares against priority.
filtered = filtered.filter((check) => check.status === selectedStatus);
}

if (selectedPriority !== "All") {
filtered = filtered.filter((check) => check.priority === selectedPriority);
Expand All @@ -191,7 +193,7 @@ function renderRows(list) {

const rows = list.map((check) => {
const priorityClass = `priority-${check.priority.toLowerCase()}`;
const statusClass = `status-${check.status.toLowerCase()}`; // Intentional bug: "In Progress" needs a slug class.
const statusClass = `status-${check.status.toLowerCase().replace(" ", "-")}`;

return `
<tr>
Expand Down Expand Up @@ -231,11 +233,14 @@ function renderRows(list) {

function updateMetrics() {
const total = checks.length;
const fixed = checks.filter((check) => check.status === "Complete").length; // Intentional bug: valid fixed status is "Fixed".
const fixed = checks.filter((check) => check.status === "Fixed").length;
const criticalOpen = checks.filter(
(check) => check.priority === "Critical" && check.status !== "Fixed",
).length;
const dueSoon = checks.filter((check) => daysUntil(check.dueDate) > 7).length; // Intentional bug: this should count items due within 7 days.
const dueSoon = checks.filter((check) => {
const days = daysUntil(check.dueDate);
return days >= 0 && days <= 7;
}).length;
Comment on lines +240 to +243
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In ECMAScript, does new Date("2026-05-10")parse as UTC or local time, and what is the recommended way to avoid off-by-one day errors when computing day differences againstnew Date()?

💡 Result:

  1. new Date("2026-05-10") is parsed as UTC midnight (2026-05-10T00:00:00Z), not local time. [1] So the Date object represents an instant at the start of the UTC day; when you display it using local-time getters/formatters (e.g., toString(), getFullYear(), etc.), it may appear as the previous day in time zones west of UTC (off-by-one in the display layer). 2) Recommended way to avoid off-by-one day errors when computing day differences vs new Date() For “calendar day” differences, don’t subtract raw timestamps coming from mixed origins/normalizations. Instead, normalize both sides to a consistent day boundary in the domain you care about (UTC for global/backend logic, or a specific IANA time zone via a library/Temporal). If you want UTC-based day differences using ISO date-only strings, parse them as UTC and then compute using UTC midnight: const start = new Date('2026-05-10T00:00:00Z'); const end = new Date; // Normalize end to UTC midnight too end.setUTCHours(0, 0, 0, 0); const msPerDay = 24 * 60 * 60 * 1000; const diffDays = Math.round((end - start) / msPerDay); Why this helps: Date math is based on instants (timestamps), while “day differences” are usually about calendar days; UTC avoids DST-length day problems because UTC days are always 86,400,000 ms, and normalization prevents partial-day values from producing 0.99/1.01 day rounding errors. [2][3] If you instead need “local calendar day” differences, normalize both dates using local midnight (via setHours(0,0,0,0)), or better, use an explicit time zone library rather than relying on environment locale/timezone rules. Sources: [1] MDN Date page explains: when timezone offset is absent, date-only forms are interpreted as a UTC time (with a historical spec compatibility note). [3] ECMAScript spec defines time values relative to UTC days and 86,400,000 ms per day boundaries. [2] Synyx article explains DST/day-length issues and that UTC avoids those problems for “how many days” calculations.

Citations:


🏁 Script executed:

git ls-files js/app.js

Repository: codezelaca/fs-se-debugging-bugs

Length of output: 85


🏁 Script executed:

if [ -f js/app.js ]; then wc -l js/app.js; fi

Repository: codezelaca/fs-se-debugging-bugs

Length of output: 89


🏁 Script executed:

if [ -f js/app.js ]; then sed -n '235,250p' js/app.js; fi

Repository: codezelaca/fs-se-debugging-bugs

Length of output: 701


🏁 Script executed:

rg "function daysUntil|const daysUntil" js/app.js -A 6

Repository: codezelaca/fs-se-debugging-bugs

Length of output: 276


Normalize day math before dueSoon bucketing.

The daysUntil() function compares new Date(dateValue) (which parses ISO strings like "2026-05-10" as UTC midnight) against new Date() (which uses local time). This timezone mismatch causes incorrect day-difference calculations, producing off-by-one counts near the 0 and 7-day boundaries depending on the local timezone and time of day.

Normalize both dates to local midnight before computing the difference:

Suggested fix
 function daysUntil(dateValue) {
-  const today = new Date();
-  const target = new Date(dateValue);
-  const difference = target.getTime() - today.getTime();
-  return Math.ceil(difference / 86400000);
+  if (!dateValue) return Number.POSITIVE_INFINITY;
+
+  const now = new Date();
+  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+  const [year, month, day] = String(dateValue).split("-").map(Number);
+  const target = new Date(year, month - 1, day);
+
+  return Math.floor((target.getTime() - today.getTime()) / 86400000);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@js/app.js` around lines 240 - 243, The day-difference calculation used for
the dueSoon bucket is off-by-one because daysUntil compares a parsed date (UTC
midnight) with new Date() (local time); update the daysUntil implementation so
it normalizes both the target date and "today" to local midnight (e.g.,
setHours(0,0,0,0) or equivalent) before computing the difference, then keep
using that corrected daysUntil when computing dueSoon in the expression that
filters checks.

const score = total === 0 ? 0 : Math.round((fixed / total) * 100);

totalCount.textContent = total;
Expand All @@ -247,13 +252,13 @@ function updateMetrics() {
}

function handleTableClick(event) {
const deleteButton = event.target.closest("[data-delete-id]"); // Intentional bug: button uses data-remove-id.
const deleteButton = event.target.closest("[data-remove-id]");

if (!deleteButton) {
return;
}

const id = Number(deleteButton.dataset.deleteId);
const id = Number(deleteButton.dataset.removeId);
const removed = checks.find((check) => check.id === id);
checks = checks.filter((check) => check.id !== id);
saveChecks();
Expand All @@ -276,16 +281,16 @@ function handleStatusChange(event) {
}

check.status = statusSelect.value;
renderRows(currentView);
saveChecks();
applyFilters();
logActivity(`Changed "${check.title}" to ${check.status}.`);
// Intentional bug: status changes should save, update filters, and refresh metrics.
}

async function resetDemoData() {
formMessage.textContent = "";

try {
const response = await fetch("data/launch-seed.json"); // Intentional bug: real file is data/launch-checks.json.
const response = await fetch("data/launch-checks.json");

if (!response.ok) {
throw new Error(`Demo data request failed with ${response.status}`);
Expand All @@ -312,7 +317,7 @@ function exportCsv() {
"Due Date",
];
const rows = currentView.map((check) => [
check.name, // Intentional bug: property should be check.title.
check.title,
check.category,
check.priority,
check.status,
Expand Down