Add /series/:id/panel endpoint for fragment fetching
Assisted-by: Claude Opus 4.6 via Claude Code
change sklmvkuyonkkpktstlkpqonyqwollpmm
commit da6b32e773b4a8151c035a2db471ba34c75ab627
author Alpha Chen <alpha@kejadlen.dev>
date
parent qlsvlrvw
diff --git a/lib/ketchup/views/series_panel.rb b/lib/ketchup/views/series_panel.rb
new file mode 100644
index 0000000..0d21d45
--- /dev/null
+++ b/lib/ketchup/views/series_panel.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "phlex"
+
+require_relative "series_detail"
+
+module Views
+  class SeriesPanel < Phlex::HTML
+    def initialize(series:, csrf: nil)
+      @series = series
+      @csrf = csrf
+    end
+
+    def view_template
+      render SeriesDetail.new(series: @series, csrf: @csrf)
+    end
+  end
+end
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index 00bf1da..71ad83f 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -6,6 +6,7 @@ require "roda"
 require_relative "models"
 require_relative "views/dashboard"
 require_relative "views/series/new"
+require_relative "views/series_panel"
 
 class Web < Roda
   plugin :halt
@@ -92,6 +93,10 @@ class Web < Roda
       r.on Integer do |series_id|
         @series = @user.series_dataset.where(id: series_id).sole
 
+        r.get "panel" do
+          Views::SeriesPanel.new(series: @series, csrf: method(:csrf_token)).call
+        end
+
         r.is do
           r.get do
             Views::Dashboard.new(current_user: @user, series: @series, csrf: method(:csrf_token)).call
diff --git a/test/test_web.rb b/test/test_web.rb
index 260b0df..50c5e4a 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -494,6 +494,31 @@ class TestWeb < Minitest::Test
     assert_equal 404, last_response.status
   end
 
+  def test_get_series_panel_returns_fragment
+    create_series(note: "Call Mom", interval_unit: "week", interval_count: "2",
+                  first_due_date: "2026-03-01")
+
+    series = DB[:series].first
+    get "/series/#{series[:id]}/panel", {}, auth_headers
+    assert last_response.ok?
+    assert_includes last_response.body, "Call Mom"
+    assert_includes last_response.body, "panel-inner"
+    # Fragment — no <html>, no Layout wrapper
+    refute_includes last_response.body, "<!DOCTYPE"
+  end
+
+  def test_get_series_panel_requires_own_series
+    create_series(
+      note: "Alice task", interval_unit: "day", interval_count: "1",
+      first_due_date: "2026-03-01",
+      headers: auth_headers(login: "alice@example.com")
+    )
+
+    series = DB[:series].first
+    get "/series/#{series[:id]}/panel", {}, auth_headers(login: "bob@example.com")
+    assert_equal 404, last_response.status
+  end
+
   def test_csrf_rejects_post_without_token
     get "/", {}, auth_headers  # establish session
     post "/series", {