fix: Harden Alpine.js components against silent failures
Fetches proceeded on error responses — reloading the page or
updating state as if the request succeeded. The escape-key handler
on date inputs reset to the server-rendered value, not the last
successful save.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index daa1a6f..dd1ad9b 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -43,9 +43,6 @@ module Views
integrity: "sha384-oO6wSYxEDXeZSOcEf28Yv/b18PqYxmhTbhc9Qfn8PSQJxv82nH/6Awq3eCof6VcA",
crossorigin: "anonymous")
script(src: asset_path("/js/app.js"), defer: true)
- script(src: "https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.0/dist/cdn.min.js",
- integrity: "sha384-6WOLkykwLb3YWzXZ6lAq+GI0p3V+enUm9jY6yIXGpIriiAUOSF5dgNJLoSSNam4j",
- crossorigin: "anonymous", defer: true)
script(src: "https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js",
integrity: "sha384-LXWjKwDZz29o7TduNe+r/UxaolHh5FsSvy2W7bDHSZ8jJeGgDeuNnsDNHoxpSgDi",
crossorigin: "anonymous", defer: true)
diff --git a/lib/ketchup/views/series/show.rb b/lib/ketchup/views/series/show.rb
index c0c495f..8af2139 100644
--- a/lib/ketchup/views/series/show.rb
+++ b/lib/ketchup/views/series/show.rb
@@ -156,7 +156,7 @@ module Views
"x-ref": "dateInput",
"x-on:blur": "save()",
"x-on:keydown.enter": "$el.blur()",
- "x-on:keydown.escape": "completedDate = '#{completed_date}'; editingDate = false"
+ "x-on:keydown.escape": "cancel()"
)
span(
class: "task-history-add-note",
diff --git a/lib/ketchup/views/user/show.rb b/lib/ketchup/views/user/show.rb
index 564d06d..e09c7f5 100644
--- a/lib/ketchup/views/user/show.rb
+++ b/lib/ketchup/views/user/show.rb
@@ -31,6 +31,14 @@ module Views
) do
plain "Edit"
end
+ button(
+ class: "section-edit-btn section-edit-btn--cancel",
+ "x-show": "editing",
+ "x-cloak": true,
+ "x-on:click": "editing = false; location.reload()"
+ ) do
+ plain "Cancel"
+ end
button(
class: "section-edit-btn",
"x-show": "editing",
diff --git a/public/js/app.js b/public/js/app.js
index 47f4c48..ea30e47 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -107,7 +107,7 @@ function initPanelEditors(container) {
ta.addEventListener("blur", () => {
const note = editor.getValue().trim()
if (note === (initialNote || "").trim()) return
- saveSeriesField(seriesId, "note", note).then(() => location.reload())
+ saveSeriesField(seriesId, "note", note).then((r) => { if (r.ok) location.reload() })
})
container.addEventListener("start-editing", () => {
@@ -120,7 +120,7 @@ function initPanelEditors(container) {
container.addEventListener("stop-editing", () => {
const note = editor.getValue().trim()
if (note !== (initialNote || "").trim()) {
- saveSeriesField(seriesId, "note", note).then(() => location.reload())
+ saveSeriesField(seriesId, "note", note).then((r) => { if (r.ok) location.reload() })
return
}
ta.style.pointerEvents = "none"
@@ -141,7 +141,7 @@ document.addEventListener("alpine:init", () => {
method: "PATCH",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `interval_count=${encodeURIComponent(this.count)}&interval_unit=${encodeURIComponent(this.unit)}`,
- }).then(() => location.reload())
+ }).then((r) => { if (r.ok) location.reload() })
},
}))
@@ -158,7 +158,7 @@ document.addEventListener("alpine:init", () => {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ due_date: this.dueDate }),
- }).then(() => location.reload())
+ }).then((r) => { if (r.ok) location.reload() })
},
}))
@@ -171,6 +171,13 @@ document.addEventListener("alpine:init", () => {
if (this.hasNote) this._initEditor()
},
+ destroy() {
+ if (this._editor) {
+ this._editor.destroy()
+ this._editor = null
+ }
+ },
+
edit() {
this.editing = true
this.$nextTick(() => this._initEditor())
@@ -211,10 +218,12 @@ document.addEventListener("alpine:init", () => {
return
}
- this._patchTask({ note })
- el.dataset.value = note
- this.hasNote = !!note
- this.editing = false
+ this._patchTask({ note }).then((r) => {
+ if (!r.ok) return
+ el.dataset.value = note
+ this.hasNote = !!note
+ this.editing = false
+ })
})
}
},
@@ -224,6 +233,11 @@ document.addEventListener("alpine:init", () => {
editingDate: false,
completedDate: initialDate,
+ cancel() {
+ this.completedDate = initialDate
+ this.editingDate = false
+ },
+
save() {
if (this.completedDate === initialDate) {
this.editingDate = false
@@ -233,7 +247,7 @@ document.addEventListener("alpine:init", () => {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed_at: this.completedDate }),
- }).then(() => location.reload())
+ }).then((r) => { if (r.ok) location.reload() })
},
}))