Use OverType for markdown note editing
Plain textareas gave no formatting feedback while typing.
OverType overlays rendered markdown on the textarea so bold,
italic, and lists are visible as you write.

Seed data now includes markdown in completion notes and ensures
both overdue and upcoming series get completed task history.

Assisted-by: Claude Opus 4.6 via Claude Code
change vxtymrryrxnlunuyqtlwvvvxoylrspoy
commit 250d2cf8047be8863945cd480b3cbe767e0e54ea
author Alpha Chen <alpha@kejadlen.dev>
date
parent wlowqnsy
diff --git a/Rakefile b/Rakefile
index 9f5c1b6..0ab2a5d 100644
--- a/Rakefile
+++ b/Rakefile
@@ -77,13 +77,16 @@ task :seed do
   # Add completed task history to some series
   completion_notes = [
     "Done, no issues",
-    "Rescheduled from last week",
-    "Took longer than expected",
-    "Had to call back twice",
-    "All good",
+    "Rescheduled from **last week**",
+    "Took longer than expected — **2 hours** instead of 1",
+    "Had to call back *twice*",
+    "All good\n\n- Changed filter\n- Reset thermostat",
   ]
 
-  all_series.sample(5).each do |s|
+  overdue, upcoming = all_series.partition { |s| s.active_task.due_date < Date.today }
+  with_history = overdue.sample([3, overdue.length].min) + upcoming.sample([3, upcoming.length].min)
+
+  with_history.each do |s|
     rand(1..4).times do |i|
       oldest_task = s.active_task
       note = rand < 0.5 ? completion_notes.sample : nil
diff --git a/lib/ketchup/views/home.rb b/lib/ketchup/views/home.rb
index 6f7b922..5523dcc 100644
--- a/lib/ketchup/views/home.rb
+++ b/lib/ketchup/views/home.rb
@@ -92,12 +92,12 @@ module Views
                           p(class: "task-history-note", "x-text": "ct.note")
                         end
                         template("x-if": "$store.sidebar.editingNoteId === ct.id") do
-                          div(class: "task-history-edit", "x-on:click.stop": "") do
-                            textarea(
-                              rows: 2,
-                              "x-model": "$store.sidebar.editingNoteText",
-                              "x-ref": "noteInput"
-                            )
+                          div(
+                            class: "task-history-edit",
+                            "x-on:click.stop": "",
+                            "x-init": "$store.sidebar.initNoteEditor($el.querySelector('.note-editor'))"
+                          ) do
+                            div(class: "note-editor overtype-wrap")
                             div(class: "task-history-edit-actions") do
                               button(type: "button", "x-on:click.stop": "$store.sidebar.saveNote(ct.id)") { "Save" }
                               button(type: "button", class: "btn-cancel", "x-on:click.stop": "$store.sidebar.cancelNote()") { "Cancel" }
@@ -114,7 +114,7 @@ module Views
             form(method: "post", action: "/series", "x-show": "$store.sidebar.mode === 'form'") do
               div(class: "field") do
                 label(for: "note") { "Note" }
-                textarea(id: "note", name: "note", rows: 2, required: true)
+                div(id: "series-note-editor", class: "overtype-wrap")
               end
 
               div(class: "field") do
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index c8342f5..9fc3d20 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -19,6 +19,7 @@ module Views
           link(rel: "stylesheet", href: "/css/reset.css")
           link(rel: "stylesheet", href: "/css/utopia.css")
           link(rel: "stylesheet", href: "/css/app.css")
+          script(src: "https://unpkg.com/overtype/dist/overtype.min.js")
           script(src: "/js/app.js", defer: true)
           script(src: "https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js", defer: true)
         end
diff --git a/public/css/app.css b/public/css/app.css
index cc4c99b..dbaa6cd 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -292,11 +292,8 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   margin-top: var(--space-3xs);
 }
 
-.task-history-edit textarea {
-  width: 100%;
+.overtype-wrap {
   font-size: var(--step--2);
-  padding: var(--space-3xs);
-  box-sizing: border-box;
 }
 
 .task-history-edit-actions {
diff --git a/public/js/app.js b/public/js/app.js
index 6c1066a..609a79d 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -38,6 +38,7 @@ document.addEventListener("alpine:init", () => {
     completedTasks: [],
     editingNoteId: null,
     editingNoteText: "",
+    _noteEditor: null,
 
     init() {
       const seriesId = sessionStorage.getItem("showSeries")
@@ -91,6 +92,9 @@ document.addEventListener("alpine:init", () => {
     },
 
     saveNote(taskId) {
+      if (this._noteEditor) {
+        this.editingNoteText = this._noteEditor.getValue()
+      }
       const note = this.editingNoteText.trim()
       fetch(`/tasks/${taskId}/note`, {
         method: "PATCH",
@@ -101,12 +105,23 @@ document.addEventListener("alpine:init", () => {
         .then(() => {
           const ct = this.completedTasks.find((t) => t.id === taskId)
           if (ct) ct.note = note || null
+          this._noteEditor = null
           this.editingNoteId = null
           this.editingNoteText = ""
         })
     },
 
+    initNoteEditor(el) {
+      if (!el) return
+      const [editor] = new OverType(el, {
+        value: this.editingNoteText,
+        placeholder: "Add a note...",
+      })
+      this._noteEditor = editor
+    },
+
     cancelNote() {
+      this._noteEditor = null
       this.editingNoteId = null
       this.editingNoteText = ""
     },
@@ -121,6 +136,11 @@ document.addEventListener("alpine:init", () => {
     },
   })
 
+  new OverType("#series-note-editor", {
+    placeholder: "What needs doing...",
+    textareaProps: { name: "note", required: true },
+  })
+
   Alpine.data("upcoming", () => ({
     showEmpty: localStorage.getItem("upcoming-show-empty") !== "false",
 
diff --git a/test/test_web.rb b/test/test_web.rb
index dc34ffe..bc3fb54 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -22,7 +22,7 @@ class TestWeb < Minitest::Test
     get "/", {}, tailscale_headers
     assert last_response.ok?
     assert_includes last_response.body, 'method="post" action="/series"'
-    assert_includes last_response.body, 'name="note"'
+    assert_includes last_response.body, 'id="series-note-editor"'
     assert_includes last_response.body, 'name="interval_count"'
     assert_includes last_response.body, 'name="interval_unit"'
     assert_includes last_response.body, 'name="first_due_date"'