groups: check submitted members against ids alone
Both group saves read every card in the book, photos and all, to learn
which ids exist. Store#contact_ids reads the ids.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNVLyiMrZJ7sZctEYq6WCM
change
commit bec0ad630f0de8104bc547a361cf657e8cce054b
author Claude <noreply@anthropic.com>
date
parent 8fa9392b
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 2bba8f3..1487fa2 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -246,6 +246,13 @@ module ProTacts
       }
     end
 
+    # Every contact's id, in #contacts' order, for a caller that only
+    # asks which cards exist: no card is read to answer.
+    #: () -> Array[String]
+    def contact_ids
+      cards.order(:id).select_map(:id).map(&:to_s)
+    end
+
     # Every contact paired with its card's updated_at, newest first — the
     # read behind any "recently updated" surface. Same shape and cost as
     # #contacts, ordered by the column SQLite already stamps rather than
diff --git a/lib/pro_tacts/web/groups.rb b/lib/pro_tacts/web/groups.rb
index 369b4c8..e013526 100644
--- a/lib/pro_tacts/web/groups.rb
+++ b/lib/pro_tacts/web/groups.rb
@@ -121,7 +121,7 @@ module ProTacts
       submitted = r.params["members"]
       members =
         if submitted.is_a?(Array)
-          submitted.map(&:to_s) & store.contacts.map(&:id)
+          submitted.map(&:to_s) & store.contact_ids
         else
           group.members
         end #: Array[String]
@@ -156,7 +156,7 @@ module ProTacts
       group = store.group(id)
       return if group.nil?
 
-      known = store.contacts.map(&:id)
+      known = store.contact_ids
       checked = ids_in(r.params["members"]) & known
       was = ids_in(r.params["was"]) & known
       store.edit_members(id, join: checked - was, leave: was - checked)