Seed sample lists in demo.ru
The dev server and the Fly apps start from an in-memory database, so
every restart and every preview opened on an empty home page. demo.ru
now gives the stub login three sample lists (one with a few results
already recorded) when that user has none, which leaves a database file
with lists of its own alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTqTY6SoR6G484RLi6fgMc
change
commit bed5f5dbdee242e89d1e8f0608e1cced325a5d01
author Claude <noreply@anthropic.com>
date
parent 3351e3bb
diff --git a/AGENTS.md b/AGENTS.md
index fbc079e..8b267c1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,7 @@ Rank King is a webapp for ranking items along multiple axes through pairwise com
 
 ```
 config.ru          — Rack entrypoint (expects the proxy's Remote-User header)
-demo.ru            — config.ru plus StubLogin, for dev and the Fly apps
+demo.ru            — config.ru plus StubLogin and sample lists, for dev and the Fly apps
 Gemfile            — dependencies
 Rakefile           — rake tasks (test, dev)
 db/migrate/        — Sequel migrations
@@ -25,6 +25,7 @@ lib/rank_king/     — app code
   db.rb            — Sequel connection + migration runner
   models.rb        — Sequel models (User, List, Item, Axis, Result)
   proxy_auth.rb    — reads the login from Remote-User
+  seed.rb          — sample lists for the stub login (demo.ru)
   stub_login.rb    — writes a fixed login where there is no proxy (demo.ru)
   web.rb           — Roda routes
   views/           — Phlex view components
diff --git a/demo.ru b/demo.ru
index db3bf51..f4023a5 100644
--- a/demo.ru
+++ b/demo.ru
@@ -1,14 +1,18 @@
 # frozen_string_literal: true
 
 # The demo entrypoint: config.ru plus a login written the way the proxy would
-# write it. The dev server and the Fly apps serve from here; a deployment
-# behind the proxy runs config.ru alone.
+# write it, and a few sample lists for that login, since the database here
+# starts empty. The dev server and the Fly apps serve from here; a
+# deployment behind the proxy runs config.ru alone.
 
 require "pathname"
 
 $LOAD_PATH.unshift(Pathname(__dir__).join("lib").to_s)
 
+require "rank_king/seed"
 require "rank_king/stub_login"
 
+RankKing::Seed.call(RankKing::StubLogin::LOGIN)
+
 use RankKing::StubLogin
 run Rack::Builder.parse_file(Pathname(__dir__).join("config.ru").to_s)
diff --git a/fly.toml b/fly.toml
index ffa4d2d..f8e8383 100644
--- a/fly.toml
+++ b/fly.toml
@@ -2,10 +2,10 @@
 # deploy job in .github/workflows/ci.yml) and the per-PR previews (the
 # review-app action in .github/workflows/preview.yml, which overrides
 # the app name with the PR's). DATABASE_URL defaults to :memory:, so
-# every deploy starts empty. Both run demo.ru rather than the image's
-# config.ru: nothing writes the Remote-User header in front of them, so
-# StubLogin stands in a login, and the one demo user's lists are served
-# to anyone who opens the URL.
+# every deploy starts with only the sample lists demo.ru seeds. Both run
+# demo.ru rather than the image's config.ru: nothing writes the
+# Remote-User header in front of them, so StubLogin stands in a login,
+# and the one demo user's lists are served to anyone who opens the URL.
 #
 # The app name is Fly's, not GitHub's, so it has to be unique across all
 # of Fly and a taken one fails the deploy. ci.yml spells it twice more,
diff --git a/lib/rank_king/seed.rb b/lib/rank_king/seed.rb
new file mode 100644
index 0000000..30ca254
--- /dev/null
+++ b/lib/rank_king/seed.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require_relative "models"
+
+module RankKing
+  # Sample lists for the dev server and the Fly apps, whose database
+  # starts empty (DATABASE_URL defaults to :memory:). Only demo.ru seeds,
+  # and only a user with no lists, so restarting against a database file
+  # leaves what is already in it alone.
+  module Seed
+    # Each list's items and axes, and a few results already recorded on
+    # it as [axis, winner, loser], so its ratings are not all equal.
+    LISTS = {
+      "Pizza toppings" => {
+        items: ["Pepperoni", "Mushroom", "Pineapple", "Sausage", "Olives", "Basil"],
+        axes: ["Taste", "Value"],
+        results: [
+          ["Taste", "Pepperoni", "Pineapple"],
+          ["Taste", "Basil", "Olives"],
+          ["Taste", "Sausage", "Mushroom"],
+          ["Value", "Mushroom", "Sausage"],
+          ["Value", "Olives", "Pepperoni"],
+        ],
+      },
+      "Board games" => {
+        items: ["Catan", "Ticket to Ride", "Azul", "Wingspan", "Codenames", "Carcassonne"],
+        axes: ["Fun", "Replayability", "Ease of teaching"],
+        results: [],
+      },
+      "Weekend hikes" => {
+        items: ["Mission Peak", "Mount Tam", "Point Reyes", "Muir Woods"],
+        axes: [],
+        results: [],
+      },
+    } #: Hash[String, { items: Array[String], axes: Array[String], results: Array[[String, String, String]] }]
+
+    #: (String login) -> void
+    def self.call(login)
+      user = User.find_or_create(login: login)
+      return if List.where(user_id: user.id).first
+
+      DB.transaction do
+        LISTS.each do |name, list|
+          create_list(user, name, **list)
+        end
+      end
+    end
+
+    #: (User user, String name, items: Array[String], axes: Array[String], results: Array[[String, String, String]]) -> void
+    def self.create_list(user, name, items:, axes:, results:)
+      l = List.create(name: name, user_id: user.id)
+      items_by_name = items.to_h do |n|
+        [n, Item.create(list_id: l.id, name: n)] #: [String, Item]
+      end
+      axes_by_name = axes.to_h do |n|
+        [n, Axis.create(list_id: l.id, name: n)] #: [String, Axis]
+      end
+      results.each do |axis, winner, loser|
+        Result.create(
+          axis_id: axes_by_name.fetch(axis).id,
+          winner_id: items_by_name.fetch(winner).id,
+          loser_id: items_by_name.fetch(loser).id,
+        )
+      end
+    end
+  end
+end
diff --git a/test/test_seed.rb b/test/test_seed.rb
new file mode 100644
index 0000000..1a0da08
--- /dev/null
+++ b/test/test_seed.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+require "minitest/autorun"
+
+require_relative "../lib/rank_king/seed"
+
+class TestSeed < Minitest::Test
+  LOGIN = "alpha@example.com"
+
+  def setup
+    RankKing::DB[:results].delete
+    RankKing::DB[:axes].delete
+    RankKing::DB[:items].delete
+    RankKing::DB[:lists].delete
+    RankKing::DB[:users].delete
+  end
+
+  def test_seeds_lists_for_the_user
+    RankKing::Seed.call(LOGIN)
+
+    user = RankKing::User.first(login: LOGIN)
+    names = RankKing::List.where(user_id: user.id).order(:id).map(&:name)
+    assert_equal RankKing::Seed::LISTS.keys, names
+    refute_empty RankKing::DB[:results].all
+  end
+
+  def test_leaves_a_user_with_lists_alone
+    RankKing::Seed.call(LOGIN)
+    RankKing::Seed.call(LOGIN)
+
+    assert_equal RankKing::Seed::LISTS.size, RankKing::DB[:lists].count
+  end
+end