-
Notifications
You must be signed in to change notification settings - Fork 7
fix: resolve all 12 debugging bugs in launch checklist app #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6bb08de
e2022e6
7eed6cf
5c6f098
8cfba28
c920d65
876f4d3
9987682
8e86f9e
dc9b6b9
7059d7a
f454f57
cb387f8
47c207c
399090d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 = [ | ||
| { | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -111,17 +112,20 @@ function loadChecks() { | |
| } | ||
|
|
||
| try { | ||
| return JSON.parse(saved); | ||
| const parsed = JSON.parse(saved); | ||
| return Array.isArray(parsed) ? parsed : [...demoChecks] | ||
| } 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(); | ||
|
|
||
|
|
@@ -132,8 +136,7 @@ function handleAddCheck(event) { | |
| const owner = ownerInput.value.trim() || "Unassigned"; | ||
| const dueDate = dueDateInput.value || new Date().toISOString().slice(0, 10); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle date-only values in local time instead of UTC. Line 137 uses 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 |
||
|
|
||
| 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; | ||
|
|
@@ -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); | ||
|
|
@@ -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> | ||
|
|
@@ -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 | ||
|
|
@@ -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; | ||
|
|
@@ -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]"); | ||
|
|
||
|
|
@@ -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."); | ||
|
|
@@ -302,6 +329,7 @@ async function resetDemoData() { | |
| } | ||
| } | ||
|
|
||
| // Exports the current view as a CSV file. | ||
| function exportCsv() { | ||
| const header = [ | ||
| "Title", | ||
|
|
@@ -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, | ||
|
|
@@ -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}`; | ||
|
|
@@ -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"; | ||
|
|
@@ -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("&", "&") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate checklist items before assigning them to
checks.Array.isArray(parsed)is too weak here, andresetDemoData()accepts any JSON payload. The render path later assumes every item has stringpriority/status/dueDatefields, so malformed localStorage or a bad demo file can still take down the app on the next render.Suggested hardening
Also applies to: 316-318
🤖 Prompt for AI Agents