Add /series/:id/panel endpoint for fragment fetching
Assisted-by: Claude Opus 4.6 via Claude Code
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", {