Unify seed data between rake seed and snapshot capture
Snapshots built data through browser form automation while rake seed
generated random data. Both now share one deterministic dataset in
Ketchup::Seed::DATA, so snapshots show realistic content without
brittle click-through sequences.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/Rakefile b/Rakefile
index 11471a8..85d90aa 100644
--- a/Rakefile
+++ b/Rakefile
@@ -30,71 +30,8 @@ task :seed do
user = User.first || abort("No users yet — visit the app first to create one")
- notes = [
- "Call Mom",
- "Water the plants\n\nCheck soil moisture *before* watering",
- "Clean the kitchen",
- "Back up laptop\n\n- Time Machine to external drive\n- Sync cloud photos\n- Verify **offsite** backup",
- "Review finances",
- "Dentist appointment\n\n**Dr. Chen**, 10am\n555-0142 to reschedule",
- "Oil change",
- "Haircut",
- "Replace HVAC filter\n\nSize: **20x25x1**",
- "Check smoke detectors",
- ]
-
- max_count = {
- "day" => 14, "week" => 4, "month" => 6, "quarter" => 2, "year" => 2
- }
- overdue_spread = {
- "day" => 7, "week" => 21, "month" => 60, "quarter" => 120, "year" => 180
- }
- completion_notes = [
- "Done, no issues",
- "Rescheduled from **last week**",
- "Took longer than expected — **2 hours** instead of 1",
- "Had to call back *twice*",
- "All good\n\n- Changed filter\n- Reset thermostat",
- ]
-
- series_data = notes.map do |note|
- unit = max_count.keys.sample
- count = rand(1..max_count.fetch(unit))
- spread = overdue_spread.fetch(unit)
- due_date = Date.today + rand(-spread..spread)
-
- interval_days = case unit
- when "day" then count
- when "week" then 7 * count
- when "month" then 30 * count
- when "quarter" then 91 * count
- when "year" then 365 * count
- end
-
- history = if [true, false].sample
- rand(1..4).times.map do |i|
- past_date = due_date - (interval_days * (i + 1))
- {
- due_date: past_date,
- completed_at: past_date.to_time + rand(0..3) * 86400,
- note: rand < 0.5 ? completion_notes.sample : nil
- }
- end
- else
- []
- end
-
- {
- note: note,
- interval_unit: unit,
- interval_count: count,
- due_date: due_date,
- history: history
- }
- end
-
- Seed.call(user: user, series: series_data)
- puts "Seeded #{notes.length} series for #{user.name} (#{user.login})"
+ Ketchup::Seed.call(user: user, series: Ketchup::Seed::DATA)
+ puts "Seeded #{Ketchup::Seed::DATA.length} series for #{user.name} (#{user.login})"
end
namespace :snapshots do
diff --git a/lib/ketchup/seed.rb b/lib/ketchup/seed.rb
index 9bdc6a6..98c3518 100644
--- a/lib/ketchup/seed.rb
+++ b/lib/ketchup/seed.rb
@@ -2,24 +2,128 @@
require_relative "models"
-module Seed
- def self.call(user:, series:)
- series.each do |s|
- created = Series.create_with_first_task(
- user: user,
- note: s[:note],
- interval_unit: s[:interval_unit],
- interval_count: s[:interval_count],
- first_due_date: s[:due_date]
- )
+module Ketchup
+ module Seed
+ DATA = [
+ # Overdue — high urgency (short interval, many days past)
+ {
+ note: "Water the plants\n\nCheck soil moisture *before* watering",
+ interval_unit: "day",
+ interval_count: 3,
+ due_date: Date.today - 6,
+ history: [
+ { due_date: Date.today - 9, completed_at: (Date.today - 9).to_time, note: "Done, no issues" },
+ { due_date: Date.today - 12, completed_at: (Date.today - 11).to_time, note: nil },
+ ]
+ },
+ # Overdue — moderate urgency
+ {
+ note: "Clean the kitchen",
+ interval_unit: "week",
+ interval_count: 1,
+ due_date: Date.today - 4,
+ history: []
+ },
+ # Overdue — low urgency (long interval, few days past)
+ {
+ note: "Review finances",
+ interval_unit: "month",
+ interval_count: 1,
+ due_date: Date.today - 3,
+ history: [
+ { due_date: Date.today - 33, completed_at: (Date.today - 32).to_time, note: "All good\n\n- Checked statements\n- Updated budget" },
+ ]
+ },
+ # Overdue — quarterly, just past due
+ {
+ note: "Dentist appointment\n\n**Dr. Chen**, 10am\n555-0142 to reschedule",
+ interval_unit: "quarter",
+ interval_count: 1,
+ due_date: Date.today - 1,
+ history: [
+ { due_date: Date.today - 92, completed_at: (Date.today - 92).to_time, note: "Rescheduled from **last week**" },
+ { due_date: Date.today - 183, completed_at: (Date.today - 182).to_time, note: nil },
+ ]
+ },
+ # Upcoming — soon
+ {
+ note: "Call Mom\n\nAsk about *weekend plans*\n- Bring **birthday cake**\n- Check flight times",
+ interval_unit: "week",
+ interval_count: 2,
+ due_date: Date.today + 2,
+ history: [
+ { due_date: Date.today - 12, completed_at: (Date.today - 12).to_time, note: "Had to call back *twice*" },
+ { due_date: Date.today - 26, completed_at: (Date.today - 25).to_time, note: nil },
+ ]
+ },
+ # Upcoming — next week
+ {
+ note: "Oil change",
+ interval_unit: "month",
+ interval_count: 3,
+ due_date: Date.today + 8,
+ history: []
+ },
+ # Upcoming — a couple weeks out
+ {
+ note: "Replace HVAC filter\n\nSize: **20x25x1**",
+ interval_unit: "quarter",
+ interval_count: 1,
+ due_date: Date.today + 18,
+ history: [
+ { due_date: Date.today - 73, completed_at: (Date.today - 73).to_time, note: "All good\n\n- Changed filter\n- Reset thermostat" },
+ ]
+ },
+ # Upcoming — far out
+ {
+ note: "Back up laptop\n\n- Time Machine to external drive\n- Sync cloud photos\n- Verify **offsite** backup",
+ interval_unit: "month",
+ interval_count: 1,
+ due_date: Date.today + 24,
+ history: [
+ { due_date: Date.today - 6, completed_at: (Date.today - 6).to_time, note: "Took longer than expected — **2 hours** instead of 1" },
+ { due_date: Date.today - 36, completed_at: (Date.today - 35).to_time, note: nil },
+ ]
+ },
+ # Completed history only — daily
+ {
+ note: "Check smoke detectors",
+ interval_unit: "year",
+ interval_count: 1,
+ due_date: Date.today + 140,
+ history: [
+ { due_date: Date.today - 225, completed_at: (Date.today - 225).to_time, note: "Replaced batteries in **hallway** unit" },
+ { due_date: Date.today - 590, completed_at: (Date.today - 589).to_time, note: nil },
+ ]
+ },
+ # Simple upcoming with no history
+ {
+ note: "Haircut",
+ interval_unit: "month",
+ interval_count: 2,
+ due_date: Date.today + 45,
+ history: []
+ },
+ ].freeze
- s[:history].each do |h|
- Task.create(
- series_id: created.id,
- due_date: h[:due_date],
- completed_at: h[:completed_at],
- note: h[:note]
+ def self.call(user:, series:)
+ series.each do |s|
+ created = Series.create_with_first_task(
+ user: user,
+ note: s[:note],
+ interval_unit: s[:interval_unit],
+ interval_count: s[:interval_count],
+ first_due_date: s[:due_date]
)
+
+ s[:history].each do |h|
+ Task.create(
+ series_id: created.id,
+ due_date: h[:due_date],
+ completed_at: h[:completed_at],
+ note: h[:note]
+ )
+ end
end
end
end
diff --git a/lib/ketchup/snapshots.rb b/lib/ketchup/snapshots.rb
index 24243dd..8291251 100644
--- a/lib/ketchup/snapshots.rb
+++ b/lib/ketchup/snapshots.rb
@@ -9,6 +9,7 @@ require "ferrum"
require "puma"
require "puma/configuration"
+require_relative "seed"
require_relative "web"
module Ketchup
@@ -42,51 +43,36 @@ module Ketchup
private
def run_capture
- # Empty dashboard
- snap("empty-dashboard") do
+ # 1. Dashboard — populated overdue + upcoming columns
+ snap("dashboard") do
goto @base
wait_for(".home")
end
- # New series form filled with markdown, before creating
+ # 2. Series detail — pick one with completed history
+ series_with_history = Series.first(
+ id: Task.exclude(completed_at: nil).select(:series_id)
+ ) || Series.first
+
+ snap("series-detail") do
+ goto "#{@base}/series/#{series_with_history.id}"
+ wait_for("#series-note-detail")
+ end
+
+ # 3. New series form with markdown, snap sidebar only
goto @base
wait_for("#new-series-form")
fill_new_series(
- note: "Call Mom\n\nAsk about *weekend plans*\n- Bring **birthday cake**\n- Check flight times",
+ note: "Call the vet\n\nAsk about *vaccination schedule*\n- Bring **shot records**\n- Check flea meds",
interval_count: 2,
interval_unit: "week"
)
snap("new-series-editing", selector: ".column-aside")
- # Submit and capture series detail
- @browser.at_css("#create-series-btn").click
- wait_for("#series-note-detail")
- snap("series-detail")
-
- # Create more series for a populated dashboard
- [
- { note: "Water the plants", interval_count: 3, interval_unit: "day" },
- { note: "Clean the kitchen", interval_count: 1, interval_unit: "week" },
- { note: "Review finances", interval_count: 1, interval_unit: "month" },
- { note: "Dentist appointment", interval_count: 1, interval_unit: "quarter" },
- ].each do |series|
- goto @base
- wait_for("#new-series-form")
- fill_new_series(**series)
- @browser.at_css("#create-series-btn").click
- wait_for("#series-note-detail")
- end
-
- snap("dashboard") do
- goto @base
- wait_for(".home")
- end
-
- # Complete a task
+ # 4. Complete a task, snap the resulting detail page
goto @base
wait_for(".complete-btn").click
wait_for(".task-history")
-
snap("after-complete")
entries = @names.map { |name| { name: name } }
@@ -112,6 +98,8 @@ module Ketchup
url = "http://127.0.0.1:#{launcher.connected_ports.first}"
user = User.find_or_create(login: "snapshot@example.com") { |u| u.name = "Snapshot User" }
+ Ketchup::Seed.call(user: user, series: Ketchup::Seed::DATA)
+
browser.headers.set(
"Tailscale-User-Login" => user.login,
"Tailscale-User-Name" => user.name
diff --git a/test/test_seed.rb b/test/test_seed.rb
index 1942311..49a8bbd 100644
--- a/test/test_seed.rb
+++ b/test/test_seed.rb
@@ -25,7 +25,7 @@ class TestSeed < Minitest::Test
}
]
- Seed.call(user: user, series: series_data)
+ Ketchup::Seed.call(user: user, series: series_data)
assert_equal 1, Series.count
assert_equal 1, Task.count
@@ -51,7 +51,7 @@ class TestSeed < Minitest::Test
}
]
- Seed.call(user: user, series: series_data)
+ Ketchup::Seed.call(user: user, series: series_data)
assert_equal 3, Task.count
assert_equal 2, Task.exclude(completed_at: nil).count