Add completion stats (streak, on-time rate) to series panel
Assisted-by: Claude Opus 4.6 via Claude Code
change yoyslvlpvsnxvvopzvzmtymrvlzptzvm
commit 9a0a0920afefc7dc368a65d86c6406a83b8510b9
author Alpha Chen <alpha@kejadlen.dev>
date
parent oyloywun
diff --git a/lib/ketchup/models.rb b/lib/ketchup/models.rb
index f731e69..bc66adb 100644
--- a/lib/ketchup/models.rb
+++ b/lib/ketchup/models.rb
@@ -48,6 +48,20 @@ class Series < Sequel::Model
       .all
   end
 
+  def completion_stats
+    completed = completed_tasks
+    return { streak: 0, on_time_pct: 100, total: 0 } if completed.empty?
+
+    streak = 0
+    on_time = 0
+    completed.each_with_index do |t, i|
+      on_time += 1 if t[:completed_at].to_date <= t[:due_date]
+      streak += 1 if i == streak && t[:completed_at].to_date <= t[:due_date]
+    end
+
+    { streak: streak, on_time_pct: (on_time * 100.0 / completed.size).round, total: completed.size }
+  end
+
   def self.create_with_first_task(user:, note:, interval_unit:, interval_count:, first_due_date:)
     DB.transaction do
       series = create(
diff --git a/lib/ketchup/views/series_detail.rb b/lib/ketchup/views/series_detail.rb
index cbc8bd2..648d1c2 100644
--- a/lib/ketchup/views/series_detail.rb
+++ b/lib/ketchup/views/series_detail.rb
@@ -90,6 +90,16 @@ module Views
           end
 
           unless @series.completed_tasks.empty?
+            stats = @series.completion_stats
+            div(class: "panel-stats") do
+              dl(class: "detail-fields") do
+                dt { "Streak" }
+                dd { stats[:streak].to_s }
+                dt { "On-time" }
+                dd { "#{stats[:on_time_pct]}%" }
+              end
+            end
+
             div(class: "task-history") do
               h3 { "History" }
               ul do
diff --git a/public/css/app.css b/public/css/app.css
index 476118a..df0d2a0 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -550,6 +550,12 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   width: 100%;
 }
 
+.panel-stats {
+  margin-block-start: var(--space-m);
+  padding-block-start: var(--space-s);
+  border-block-start: 1px solid #f0f0f0;
+}
+
 /* -------------------- */
 /* Task history */
 /* -------------------- */
diff --git a/test/test_web.rb b/test/test_web.rb
index 1b73b3f..0429233 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -637,6 +637,18 @@ class TestWeb < Minitest::Test
     assert_includes last_response.body, "Haircut"
   end
 
+  def test_series_panel_shows_stats
+    create_series(note: "Call Mom", interval_unit: "week", interval_count: "1",
+                  first_due_date: (Date.today - 7).to_s)
+
+    series = DB[:series].first
+    task = DB[:tasks].first
+    csrf_post "/series/#{series[:id]}/tasks/#{task[:id]}/complete", {}, auth_headers
+
+    get "/series/#{series[:id]}/panel", {}, auth_headers
+    assert_includes last_response.body, "panel-stats"
+  end
+
   private
 
   def csrf_post(path, params = {}, headers = auth_headers)