Follow a tint, and show what a Plani is actually asking

951c85d · Gabriel Morell · 2026-09-03 17:38

31 files +238 -36
Message
{commit_body(@commit)}

Files changed

modified ?
+0 −0

Binary file — not rendered.

modified ?
+0 −0

Binary file — not rendered.

modified apps/room_sanctum/lib/room_sanctum/configuration/plani.ex
+39 −0
@@ -28,6 +28,11 @@ defmodule RoomSanctum.Configuration.Plani do
28 28
29 29 # What to look for around the anchor.
30 30 field :sources, {:array, :integer}, default: []
31 +
32 + # Instead of, or as well as, naming them: every source carrying this tint.
33 + # A source tinted later joins without the Plani being edited, which is
34 + # what makes this a shortcut rather than a bulk select.
35 + field :follow_tint, :string
31 36 field :radius, :integer, default: 800
32 37 field :limit, :integer, default: 5
33 38
@@ -44,6 +49,7 @@ defmodule RoomSanctum.Configuration.Plani do
44 49 :ankyra_id,
45 50 :client_id,
46 51 :sources,
52 + :follow_tint,
47 53 :radius,
48 54 :limit
49 55 ])
@@ -52,7 +58,40 @@ defmodule RoomSanctum.Configuration.Plani do
52 58 # couple of kilometres this stops meaning "near me".
53 59 |> validate_number(:radius, greater_than_or_equal_to: 50, less_than_or_equal_to: 3000)
54 60 |> validate_number(:limit, greater_than_or_equal_to: 1, less_than_or_equal_to: 20)
61 + |> validate_tint()
55 62 |> foreign_key_constraint(:user_id)
56 63 |> foreign_key_constraint(:home_foci_id)
57 64 end
65 +
66 + # A tint that is not one of the offered colours renders as an unstyled dot
67 + # rather than failing, which is the kind of wrong that goes unnoticed.
68 + defp validate_tint(changeset) do
69 + validate_change(changeset, :follow_tint, fn :follow_tint, tint ->
70 + if tint in [nil, ""] or RoomSanctum.Tints.valid?(tint) do
71 + []
72 + else
73 + [follow_tint: "is not a tint"]
74 + end
75 + end)
76 + end
77 +
78 + @doc """
79 + Every source this Plani asks: the ones it names, and the ones wearing its
80 + tint.
81 +
82 + A union rather than a choice, so following a tint can be a shortcut to most
83 + of a list with one or two named on top of it.
84 + """
85 + def sources_for(%__MODULE__{} = plani, all_sources) do
86 + tinted =
87 + case plani.follow_tint do
88 + nil -> []
89 + "" -> []
90 + tint -> all_sources |> Enum.filter(&(tint_of(&1) == tint)) |> Enum.map(& &1.id)
91 + end
92 +
93 + (plani.sources ++ tinted) |> Enum.uniq()
94 + end
95 +
96 + defp tint_of(source), do: source.meta && Map.get(source.meta, :tint)
58 97 end
modified apps/room_sanctum/lib/room_sanctum/workers/plani_worker.ex
+10 −1
@@ -103,8 +103,17 @@ defmodule RoomSanctum.Worker.Plani do
103 103 def handle_cast(:query, state) do
104 104 {anchor, anchored_to} = anchor(state.plani)
105 105
106 + # Resolved on every tick rather than held: a source tinted since the last
107 + # one should join without the Plani being edited, which is the whole point
108 + # of following a tint.
109 + sources =
110 + RoomSanctum.Configuration.Plani.sources_for(
111 + state.plani,
112 + Configuration.list_cfg_sources({:user, state.plani.user_id})
113 + )
114 +
106 115 {data, queries} =
107 − state.plani.sources
116 + sources
108 117 |> Enum.reduce({%{}, []}, fn source_id, {data, queries} ->
109 118 case ask(source_id, anchor, state.plani) do
110 119 nil ->
modified apps/room_sanctum/lib/room_sanctum/workers/pythiae_worker.ex
+16 −10
@@ -122,19 +122,25 @@ defmodule RoomSanctum.Worker.Pythiae do
122 122 handle_cast(:refresh_db_cfg, state)
123 123 end
124 124
125 − # What this Pythiae is showing: a Plani, or a vision.
126 − #
127 − # Exclusive, and the Plani wins. A Pythiae pointed at a Plani is asking what
128 − # is near its client; reading its vision as well would answer the same
129 − # question from a fixed place and publish both.
130 − #
131 − # Both workers hand back the same shape, so nothing past this point -- the
132 − # condensers, Ankyra, any client -- can tell which it was.
133 − defp showing(%{curr_plani: plani_id}) when not is_nil(plani_id) do
125 + @doc """
126 + What this Pythiae is showing: a Plani, or a vision.
127 +
128 + Exclusive, and the Plani wins. A Pythiae pointed at a Plani is asking what
129 + is near its client; reading its vision as well would answer the same
130 + question from a fixed place and publish both.
131 +
132 + Both workers hand back the same shape, so nothing past this point -- the
133 + condensers, Ankyra, any client -- can tell which it was.
134 +
135 + Public because the rule has two readers: this worker, which publishes, and
136 + the config page's preview, which should show what is about to be published
137 + rather than what would have been.
138 + """
139 + def showing(%{curr_plani: plani_id}) when not is_nil(plani_id) do
134 140 RoomSanctum.Worker.Plani.get_state(plani_id)
135 141 end
136 142
137 − defp showing(pythiae) do
143 + def showing(pythiae) do
138 144 RoomSanctum.Worker.Vision.get_state(pythiae.curr_vision)
139 145 end
140 146
modified apps/room_sanctum/lib/room_sanctum_web/components/layouts/_action_menu.html.heex
+1 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/form_component.ex
+74 −10

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+12 −1

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+24 −10

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/show.ex
+3 −1

Click to load diff…

added apps/room_sanctum/priv/repo/migrations/20260903150000_add_follow_tint_to_plani.exs
+18 −0

Click to load diff…

modified apps/room_sanctum/test/room_sanctum_web/live/plani_live_test.exs
+41 −3

Click to load diff…

Parents: cf1bcf1