Skip to content
Open
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
80 changes: 56 additions & 24 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 All @@ -103,6 +103,7 @@ exportButton.addEventListener("click", exportCsv);
renderApp();
logActivity("Demo data loaded. Start by testing the checklist workflows.");

// Loads the checks from localStorage or uses demo checks if no saved data is found.
function loadChecks() {
const saved = localStorage.getItem(STORAGE_LOAD_KEY);

Expand All @@ -111,17 +112,20 @@ function loadChecks() {
}

try {
return JSON.parse(saved);
const parsed = JSON.parse(saved);
return Array.isArray(parsed) ? parsed : [...demoChecks]
Comment on lines +115 to +116
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 | 🟠 Major | ⚡ Quick win

Validate checklist items before assigning them to checks.

Array.isArray(parsed) is too weak here, and resetDemoData() accepts any JSON payload. The render path later assumes every item has string priority/status/dueDate fields, so malformed localStorage or a bad demo file can still take down the app on the next render.

Suggested hardening
+function cloneDemoChecks() {
+  return demoChecks.map((check) => ({ ...check }))
+}
+
+function isValidCheck(check) {
+  return (
+    check &&
+    Number.isFinite(check.id) &&
+    typeof check.title === "string" &&
+    typeof check.category === "string" &&
+    typeof check.priority === "string" &&
+    typeof check.status === "string" &&
+    typeof check.owner === "string" &&
+    typeof check.dueDate === "string" &&
+    typeof check.notes === "string"
+  )
+}
+
+function normalizeChecks(value) {
+  return Array.isArray(value) && value.every(isValidCheck)
+    ? value
+    : cloneDemoChecks()
+}
+
 function loadChecks() {
   const saved = localStorage.getItem(STORAGE_LOAD_KEY);
@@
   try {
     const parsed = JSON.parse(saved);
-    return Array.isArray(parsed) ? parsed : [...demoChecks]
+    return normalizeChecks(parsed)
   } catch (error) {
     console.warn("Could not parse saved launch checks.", error);
-    return [...demoChecks];
+    return cloneDemoChecks();
   }
 }
@@
-    checks = await response.json();
+    checks = normalizeChecks(await response.json());

Also applies to: 316-318

🤖 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 115 - 116, The code assigns JSON-parsed data to
checks using only Array.isArray(parsed) which is unsafe; update the logic where
parsed is used (the block returning parsed vs [...demoChecks], and the similar
block at lines ~316-318) to validate each item is an object and has string
fields "priority", "status", and "dueDate" (and any other required checklist
properties) before accepting it—e.g., filter or map parsed to include only items
that pass typeof checks and required key presence, and if validation fails for
the array, fall back to demoChecks or call resetDemoData(); reference the parsed
variable and the resetDemoData handling so malformed localStorage/demo payloads
never get assigned to checks.

} catch (error) {
console.warn("Could not parse saved launch checks.", error);
return [...demoChecks];
}
}

// Saves the current list of checks to localStorage.
function saveChecks() {
localStorage.setItem(STORAGE_SAVE_KEY, JSON.stringify(checks));
}

// Handles adding a new check to the list.
function handleAddCheck(event) {
event.preventDefault();

Expand All @@ -132,8 +136,7 @@ function handleAddCheck(event) {
const owner = ownerInput.value.trim() || "Unassigned";
const dueDate = dueDateInput.value || new Date().toISOString().slice(0, 10);
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 | 🟠 Major | ⚡ Quick win

Handle date-only values in local time instead of UTC.

Line 137 uses toISOString(), and Lines 375-392 parse YYYY-MM-DD with new Date(...). In US time zones that shifts calendar dates, so due dates can render a day early, dueSoon can be off by one, and blank due dates can default to the wrong day around midnight.

Suggested fix
+function toLocalDateString(date = new Date()) {
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, "0")
+  const day = String(date.getDate()).padStart(2, "0")
+  return `${year}-${month}-${day}`
+}
+
+function parseLocalDate(dateValue) {
+  if (!dateValue) {
+    return null
+  }
+
+  const [year, month, day] = dateValue.split("-").map(Number)
+  return new Date(year, month - 1, day)
+}
+
 function handleAddCheck(event) {
@@
-  const dueDate = dueDateInput.value || new Date().toISOString().slice(0, 10);
+  const dueDate = dueDateInput.value || toLocalDateString();
@@
 function daysUntil(dateValue) {
-  const today = new Date();
-  const target = new Date(dateValue);
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+
+  const target = parseLocalDate(dateValue);
+  if (!target || Number.isNaN(target.getTime())) {
+    return Number.POSITIVE_INFINITY;
+  }
+
   const difference = target.getTime() - today.getTime();
   return Math.ceil(difference / 86400000);
 }
@@
 function formatDate(dateValue) {
   if (!dateValue) {
     return "No date";
   }
 
+  const parsed = parseLocalDate(dateValue);
+  if (!parsed || Number.isNaN(parsed.getTime())) {
+    return "Invalid date";
+  }
+
   return new Intl.DateTimeFormat("en", {
     month: "short",
     day: "numeric",
     year: "numeric",
-  }).format(new Date(dateValue));
+  }).format(parsed);
 }

Also applies to: 252-255, 375-392

🤖 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` at line 137, The code uses toISOString() and new
Date('YYYY-MM-DD') which treat/produce UTC dates and cause off-by-one local-date
errors; change places that create or parse date-only values (e.g., dueDate
default via dueDateInput.value / dueDate, the parser used in the block around
lines 375-392, and any dueSoon comparisons) to construct dates in local time by
splitting the YYYY-MM-DD string into year/month/day and using new Date(year,
month-1, day) (and when defaulting build today's YYYY-MM-DD from new Date()
local values), and ensure any comparisons normalize to local-midnight Date
objects so due dates and dueSoon logic use local calendar days instead of UTC.


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 All @@ -158,18 +161,28 @@ function handleAddCheck(event) {
logActivity(`Added "${newCheck.title}" to the launch checklist.`);
}

// Applies filters to the checks list based on search term and status/priority filters.
function applyFilters() {
const searchTerm = searchInput.value.trim().toLowerCase();
const selectedStatus = statusFilter.value;
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.
let filtered = checks.filter((check) => {
if (!searchTerm) return true;

const term = searchTerm;
return (
check.title?.toLowerCase().includes(term) ||
check.category?.toLowerCase().includes(term) ||
check.priority?.toLowerCase().includes(term) ||
check.status?.toLowerCase().includes(term) ||
check.owner?.toLowerCase().includes(term)
);
});

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 +204,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 All @@ -210,12 +223,12 @@ function renderRows(list) {
<span class="row-actions">
<select data-status-id="${check.id}" aria-label="Update status for ${escapeHtml(check.title)}">
${["Pending", "In Progress", "Fixed", "Blocked"]
.map(
(status) => `
.map(
(status) => `
<option value="${status}" ${status === check.status ? "selected" : ""}>${status}</option>
`,
)
.join("")}
)
.join("")}
</select>
<button class="icon-button" type="button" data-remove-id="${check.id}" title="Delete check">
x
Expand All @@ -229,13 +242,17 @@ function renderRows(list) {
checkRows.innerHTML = rows.join("");
}

// Updates all dashboard metrics: total, fixed, critical open, due soon and score.
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;
const score = total === 0 ? 0 : Math.round((fixed / total) * 100);

totalCount.textContent = total;
Expand All @@ -246,21 +263,23 @@ function updateMetrics() {
scoreBar.style.width = `${score}%`;
}

// Handles table clicks for delete and status updates
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();
applyFilters();
logActivity(`Deleted "${removed?.title || "launch check"}".`);
}

// Handles status dropdown changes and persists the update.
function handleStatusChange(event) {
const statusSelect = event.target.closest("[data-status-id]");

Expand All @@ -276,22 +295,30 @@ function handleStatusChange(event) {
}

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

}

// Resets the demo data and persists the update.
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}`);
}
const data = await response.json();

if (!Array.isArray(data)) {
throw new Error("Demo data is not an array");
}

checks = await response.json();
checks = data;
saveChecks();
applyFilters();
logActivity("Demo checklist reloaded from JSON.");
Expand All @@ -302,6 +329,7 @@ async function resetDemoData() {
}
}

// Exports the current view as a CSV file.
function exportCsv() {
const header = [
"Title",
Expand All @@ -312,7 +340,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 All @@ -337,6 +365,7 @@ function exportCsv() {
logActivity("Exported the current checklist view.");
}

// Logs activity messages to the activity log.
function logActivity(message) {
const item = document.createElement("li");
item.textContent = `${new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - ${message}`;
Expand All @@ -347,13 +376,15 @@ function logActivity(message) {
}
}

// Returns the number of days between the given date and today.
function daysUntil(dateValue) {
const today = new Date();
const target = new Date(dateValue);
const difference = target.getTime() - today.getTime();
return Math.ceil(difference / 86400000);
}

// Formats a date value for display.
function formatDate(dateValue) {
if (!dateValue) {
return "No date";
Expand All @@ -366,6 +397,7 @@ function formatDate(dateValue) {
}).format(new Date(dateValue));
}

// Escapes HTML special characters to prevent XSS attacks.
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
Expand Down