Give ownerless lists to the first user
Lists from before users existed go to the first user to sign in, and
user_id is required from here on, so lists are private without
exception. Deploy only after that first sign-in: the migration fails if
there are ownerless lists and no users.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
change
commit 0026221734260958c57be3098631997c7f7bd212
author Claude <noreply@anthropic.com>
date
parent e8989696
diff --git a/AGENTS.md b/AGENTS.md
index 8de48bb..789910c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,7 +85,7 @@ Tests set `DATABASE_URL=:memory:` in `test/test_helper.rb`.
 - Views are Phlex components under `lib/rank_king/views/`
 - CSRF protection is on for all POST routes
 - Every request needs a `Remote-User` header or it gets a 401. That is safe only because a proxy (Tailscale identity) writes the header. `StubLogin` fills it in for `demo.ru` (dev server, Fly apps) and nowhere else: never in `config.ru`, and never in front of real data.
-- Lists are private to their owner (404 for anyone else); lists from before users existed have no owner and are visible to everyone
+- Lists are private to their owner (404 for anyone else)
 - Config is a frozen `Data.define` value object
 - Types are inline RBS comments (`#:` before a method, `# @rbs` for the
   rest), checked by `rake steep`. `sig/` holds only what that syntax
diff --git a/db/migrate/003_assign_list_owners.rb b/db/migrate/003_assign_list_owners.rb
new file mode 100644
index 0000000..75212f2
--- /dev/null
+++ b/db/migrate/003_assign_list_owners.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Lists from before users existed go to the first user, the one who signed in
+# when the user migration was first deployed, and every list has an owner
+# from here on. Run it only after that first sign-in: with no users yet, it
+# fails rather than leave lists without one.
+Sequel.migration do
+  up do
+    unowned = from(:lists).where(user_id: nil)
+    unless unowned.empty?
+      first_user = from(:users).min(:id)
+      raise "no users to own the #{unowned.count} lists without an owner" unless first_user
+
+      unowned.update(user_id: first_user)
+    end
+
+    alter_table(:lists) do
+      set_column_not_null :user_id
+    end
+  end
+
+  down do
+    alter_table(:lists) do
+      set_column_allow_null :user_id
+    end
+  end
+end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 48fda45..104d57d 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -24,7 +24,7 @@ module RankKing
       user = User.for_login(ProxyAuth.login(r.env))
 
       r.root do
-        lists = List.where(user_id: [user.id, nil]).order(:id).all
+        lists = List.where(user_id: user.id).order(:id).all
         Views::Home.new(lists: lists, csrf: method(:csrf_token)).call
       end
 
@@ -55,9 +55,8 @@ module RankKing
         end
 
         r.on Integer do |list_id|
-          # Lists are private to their owner; ones from before users
-          # existed have none and stay visible to everyone.
-          @list = List.where(id: list_id, user_id: [user.id, nil]).first
+          # Lists are private to their owner.
+          @list = List.where(id: list_id, user_id: user.id).first
           r.halt 404 unless @list
 
           r.is do
diff --git a/sig/rank_king/models.rbs b/sig/rank_king/models.rbs
index 7497348..cf17012 100644
--- a/sig/rank_king/models.rbs
+++ b/sig/rank_king/models.rbs
@@ -9,8 +9,8 @@ module RankKing
   end
 
   class List < Sequel::Model
-    def user_id: () -> Integer?
-    def user: () -> User?
+    def user_id: () -> Integer
+    def user: () -> User
     def name: () -> String
     def items: () -> Array[Item]
     def axes: () -> Array[Axis]