Remove series detail panel
The series show page replaced the slide-over panel for viewing series
details. The panel route, views, and tests are now dead code.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/lib/ketchup/views/series_detail.rb b/lib/ketchup/views/series_detail.rb
deleted file mode 100644
index cbc8bd2..0000000
--- a/lib/ketchup/views/series_detail.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-module Views
- class SeriesDetail < Phlex::HTML
- def initialize(series:, csrf: nil)
- @series = series
- @csrf = csrf
- end
-
- def view_template
- active_task = @series.active_task
- div(class: "panel-inner", "x-data": "{ editing: false }") do
- div(class: "panel-header") do
- a(href: "/", class: "panel-close", "aria-label": "Close") { "←" }
- div(class: "panel-actions") do
- button(
- class: "panel-action",
- "x-show": "!editing",
- "x-on:click": "editing = true; $dispatch('start-editing')"
- ) { "Edit" }
- button(
- class: "panel-action",
- "x-show": "editing",
- "x-on:click": "editing = false; $dispatch('stop-editing')"
- ) { "Done" }
- end
- end
-
- div(class: "panel-body") do
- div(
- id: "series-note-detail",
- class: "series-note",
- "data-value": @series.note || "",
- "data-series-id": @series.id.to_s
- )
-
- dl(class: "detail-fields") do
- dt { "Repeat every" }
- dd("x-show": "!editing") do
- plain interval_text(@series.interval_count, @series.interval_unit)
- end
- dd(
- class: "detail-edit-interval",
- "x-show": "editing",
- "x-cloak": true,
- "x-data": "intervalEditor(#{@series.id}, #{@series.interval_count}, '#{@series.interval_unit}')",
- ) do
- input(
- type: "number",
- class: "detail-input detail-input-count",
- min: 1,
- "x-model.number": "count",
- "x-on:change": "save()"
- )
- select(
- class: "detail-input detail-input-unit",
- "x-model": "unit",
- "x-on:change": "save()"
- ) do
- INTERVAL_OPTIONS.each { |val, label| option(value: val) { label } }
- end
- end
-
- if active_task
- dt { "Due date" }
- dd(
- "x-show": "!editing",
- "x-text": "new Date('#{active_task[:due_date]}T00:00').toLocaleDateString()"
- ) { active_task[:due_date].to_s }
- dd(
- "x-show": "editing",
- "x-cloak": true,
- "x-data": "dueDateEditor(#{@series.id}, '#{active_task[:due_date]}')"
- ) do
- input(
- type: "date",
- class: "detail-input detail-input-date",
- "x-model": "dueDate",
- "x-on:change": "save()"
- )
- end
-
- if active_task.urgency > 0
- dt { "Urgency" }
- dd { "#{format("%.1f", active_task.urgency)}×" }
- end
- end
- end
-
- unless @series.completed_tasks.empty?
- div(class: "task-history") do
- h3 { "History" }
- ul do
- @series.completed_tasks.each do |ct|
- li(
- class: "task-history-item",
- "x-data": "historyNote(#{@series.id}, #{ct[:id]}, #{ct[:note] ? "true" : "false"})"
- ) do
- div(class: "task-history-row") do
- span(class: "task-history-check") { "✓" }
- span(class: "task-history-date") { ct[:completed_at].strftime("%Y-%m-%d") }
- span(
- class: "task-history-add-note",
- "x-show": "!hasNote && !editing",
- "x-on:click": "edit()"
- ) { "add a note..." }
- end
- div(
- class: "task-history-note-editor",
- "data-value": ct[:note] || "",
- "x-show": "hasNote || editing",
- "x-ref": "editor"
- )
- end
- end
- end
- end
- end
- end
- end
- end
-
- private
-
- def interval_text(count, unit)
- "#{count} #{count == 1 ? unit : "#{unit}s"}"
- end
- end
-end
diff --git a/lib/ketchup/views/series_panel.rb b/lib/ketchup/views/series_panel.rb
deleted file mode 100644
index 0d21d45..0000000
--- a/lib/ketchup/views/series_panel.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# 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 4c43e7c..5c75eae 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -10,7 +10,6 @@ require_relative "views/calendar"
require_relative "views/agenda"
require_relative "views/series/new"
require_relative "views/series/show"
-require_relative "views/series_panel"
require_relative "views/user_panel"
class Web < Roda
@@ -130,10 +129,6 @@ 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::Series::Show.new(series: @series, current_user: @user, csrf: method(:csrf_token)).call
diff --git a/test/test_web.rb b/test/test_web.rb
index 817ed49..0ef34ae 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -272,23 +272,6 @@ class TestWeb < Minitest::Test
assert_includes last_response.body, "2 weeks"
end
- def test_get_series_panel_shows_completed_history
- create_series(note: "Call Mom", interval_unit: "week", interval_count: "1",
- first_due_date: "2026-03-01")
-
- task = DB[:tasks].first
- series = DB[:series].first
- csrf_post "/series/#{series[:id]}/tasks/#{task[:id]}/complete", {}, auth_headers
-
- completed_task = DB[:tasks].first(id: task[:id])
- patch "/series/#{series[:id]}/tasks/#{completed_task[:id]}/note", { note: "Left a message" }, auth_headers
-
- get "/series/#{series[:id]}/panel", {}, auth_headers
- assert last_response.ok?
- assert_includes last_response.body, "Left a message"
- assert_includes last_response.body, "task-history"
- end
-
def test_get_series_requires_own_series
create_series(
note: "Alice task", interval_unit: "day", interval_count: "1",
@@ -494,31 +477,6 @@ 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_layout_includes_footer
get "/", {}, auth_headers
assert_includes last_response.body, "site-footer"