Add flash and undo for archive
Matches the existing completion pattern so archiving is reversible.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index 94090bc..24853d3 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -96,12 +96,31 @@ module Ketchup
r.on Integer do |series_id|
@series = @user.series_dataset.where(id: series_id).sole
- r.post "archive" do
- DB.transaction do
- @series.active_task&.destroy
- @series.update(archived_at: Time.now)
+ r.on "archive" do
+ r.post do
+ DB.transaction do
+ @series.active_task&.destroy
+ @series.update(archived_at: Time.now)
+ end
+
+ note_title = @series.note.lines.first&.strip || @series.note
+ archive_path = "/series/#{series_id}/archive"
+ session["flash"] = { "message" => "Archived \u201c#{note_title}\u201d", "undo_path" => archive_path }
+ r.redirect "/"
+ end
+
+ r.delete do
+ DB.transaction do
+ @series.update(archived_at: nil)
+ unless @series.active_task
+ last_completed = @series.completed_tasks.first
+ due_date = last_completed ? @series.next_due_date(last_completed[:completed_at].to_date) : Date.today
+ Task.create(series_id: @series.id, due_date: due_date)
+ end
+ end
+ response.status = 204
+ ""
end
- r.redirect "/"
end
r.is do
diff --git a/test/test_web.rb b/test/test_web.rb
index 4b05911..2fb2238 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -741,6 +741,8 @@ class TestWeb < Minitest::Test
series = Ketchup::DB[:series].first
archive_series(series[:id])
+ # First request shows the flash; second request clears it.
+ get "/", {}, auth_headers
get "/", {}, auth_headers
refute_includes last_response.body, "Call Mom"
end
@@ -783,6 +785,52 @@ class TestWeb < Minitest::Test
assert_includes [403, 404], last_response.status
end
+ def test_archive_shows_flash_with_undo
+ create_series(note: "Call Mom", interval_unit: "week", interval_count: "2",
+ first_due_date: (Date.today - 3).to_s)
+
+ series = Ketchup::DB[:series].first
+ archive_series(series[:id])
+
+ get "/", {}, auth_headers
+ body = last_response.body
+ assert_includes body, "Archived"
+ assert_includes body, "Call Mom"
+ assert_includes body, "data-undo-path=\"/series/#{series[:id]}/archive\""
+ end
+
+ def test_unarchive_restores_series
+ create_series(note: "Call Mom", interval_unit: "week", interval_count: "2",
+ first_due_date: (Date.today - 3).to_s)
+
+ series = Ketchup::DB[:series].first
+ archive_series(series[:id])
+
+ refute_nil Ketchup::DB[:series].first(id: series[:id])[:archived_at]
+ assert_nil Ketchup::DB[:tasks].first(series_id: series[:id], completed_at: nil)
+
+ delete "/series/#{series[:id]}/archive", {}, auth_headers
+ assert_equal 204, last_response.status
+
+ assert_nil Ketchup::DB[:series].first(id: series[:id])[:archived_at]
+ active_task = Ketchup::DB[:tasks].first(series_id: series[:id], completed_at: nil)
+ refute_nil active_task
+ end
+
+ def test_unarchive_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 = Ketchup::DB[:series].first
+ archive_series(series[:id], login: "alice@example.com")
+
+ delete "/series/#{series[:id]}/archive", {}, auth_headers(login: "bob@example.com")
+ assert_includes [403, 404], last_response.status
+ end
+
def test_series_detail_has_archive_button
create_series(note: "Call Mom", interval_unit: "week", interval_count: "2",
first_due_date: "2026-03-01")