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
change mluxmynpqorprunmzxplppmruslvtsqq
commit 0a1fdff85f881aee6c8bb08685704231cc6dd302
author Alpha Chen <alpha@kejadlen.dev>
date
parent qxqtkrsm
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() })
     },
   }))