Give the Plani a control panel

cf1bcf1 · Gabriel Morell · 2026-09-03 17:29

26 files +572 -10
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/workers/ankyra_worker.ex
+12 −7
@@ -74,13 +74,18 @@ defmodule RoomSanctum.Worker.Ankyra do
74 74 signal to fall back to a home foci rather than an error.
75 75 """
76 76 def position(name, client_id) do
77 − "ankyra#{name}"
78 − |> via_tuple()
79 − |> GenServer.call({:position, client_id}, 5_000)
80 − catch
81 − # A worker that is not up yet, or a node on its way down. Not knowing
82 − # where somebody is is a normal answer here.
83 − :exit, _ -> nil
77 + try do
78 + "ankyra#{name}"
79 + |> via_tuple()
80 + |> GenServer.call({:position, client_id}, 5_000)
81 + rescue
82 + # A registry that is not up raises from the lookup rather than exiting.
83 + ArgumentError -> nil
84 + catch
85 + # A worker that is not up yet, or a node on its way down. Not knowing
86 + # where somebody is is a normal answer here.
87 + :exit, _ -> nil
88 + end
84 89 end
85 90
86 91 def meta_check(name) do
modified apps/room_sanctum/lib/room_sanctum/workers/plani_worker.ex
+36 −3
@@ -60,9 +60,38 @@ defmodule RoomSanctum.Worker.Plani do
60 60 the two it is reading.
61 61 """
62 62 def get_state(name) do
63 − "plani#{name}" |> via_tuple() |> GenServer.call(:return_state, 15_000)
64 − catch
65 − :exit, _ -> %{data: %{}, queries: []}
63 + try do
64 + "plani#{name}" |> via_tuple() |> GenServer.call(:return_state, 15_000)
65 + rescue
66 + ArgumentError -> empty()
67 + catch
68 + :exit, _ -> empty()
69 + end
70 + end
71 +
72 + # A Plani whose worker is not up yet: it has nothing to say, and saying so
73 + # is better than a Pythiae crashing on the way out of a deploy.
74 + defp empty, do: %{data: %{}, queries: []}
75 +
76 + @doc """
77 + Where this Plani currently thinks it is, and how it knows.
78 +
79 + `:client` means somebody reported inside the window; `:home` means nobody
80 + has and the home foci is answering. Worth showing on a page, because a Plani
81 + answering from home looks exactly like a Plani answering from a client that
82 + happens to be at home.
83 + """
84 + def where(name) do
85 + try do
86 + "plani#{name}" |> via_tuple() |> GenServer.call(:where, 5_000)
87 + rescue
88 + # A registry that is not up raises from the lookup before any call
89 + # happens, where a dead process would have exited -- a node still
90 + # booting, or a test. Not knowing where a Plani is is a normal answer.
91 + ArgumentError -> nil
92 + catch
93 + :exit, _ -> nil
94 + end
66 95 end
67 96
68 97 def handle_cast(:refresh_db_cfg, state) do
@@ -89,6 +118,10 @@ defmodule RoomSanctum.Worker.Plani do
89 118 {:noreply, %{state | data: data, queries: Enum.reverse(queries), anchor: anchor, anchored_to: anchored_to}}
90 119 end
91 120
121 + def handle_call(:where, _from, state) do
122 + {:reply, %{anchor: state.anchor, anchored_to: state.anchored_to}, state}
123 + end
124 +
92 125 def handle_call(:return_state, _from, state) do
93 126 {:reply, %{data: state.data, queries: state.queries}, state}
94 127 end
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/form_component.ex
+146 −0
@@ -0,0 +1,146 @@
1 +defmodule RoomSanctumWeb.PlaniLive.FormComponent do
2 + use RoomSanctumWeb, :live_component
3 +
4 + alias RoomSanctum.Configuration
5 +
6 + @impl true
7 + def render(assigns) do
8 + ~H"""
9 + <div>
10 + <.header>
11 + <%= @title %>
12 + <:subtitle>A vision whose anchor moves with a client</:subtitle>
13 + </.header>
14 +
15 + <.simple_form
16 + for={@form}
17 + id="plani-form"
18 + phx-target={@myself}
19 + phx-change="validate"
20 + phx-submit="save"
21 + >
22 + <.input field={@form[:name]} type="text" label="Name" />
23 +
24 + <div class="divider">Where it is</div>
25 +
26 + <.input
27 + field={@form[:home_foci_id]}
28 + type="select"
29 + label="Home foci"
30 + options={@cfg_foci_sel}
31 + />
32 + <p class="text-xs text-base-content/60">
33 + Where it falls back to. A Plani answers from here until a client reports,
34 + and returns here when one stops — nothing about a position is written down,
35 + so this is what it knows when it knows nothing else.
36 + </p>
37 +
38 + <.input
39 + field={@form[:ankyra_id]}
40 + type="select"
41 + label="Ankyra to follow"
42 + options={[{"— none, stay at home —", nil}] ++ @cfg_ankyra_sel}
43 + />
44 + <.input field={@form[:client_id]} type="text" label="Client id" />
45 + <p class="text-xs text-base-content/60">
46 + Which client on that Ankyra to follow. An Ankyra can carry several — a
47 + phone and a panel on a wall — and only one of them travels. Leave blank
48 + to follow whichever reported most recently.
49 + </p>
50 +
51 + <div class="divider">What to look for</div>
52 +
53 + <.input
54 + field={@form[:sources]}
55 + type="select"
56 + multiple={true}
57 + label="Sources"
58 + options={@cfg_sources_sel}
59 + />
60 + <p class="text-xs text-base-content/60">
61 + Sources, not queries: a Plani asks what is near it rather than about a
62 + place named in advance. Only sources with something located in them can
63 + answer — transit stops, loose bikes, air quality monitors.
64 + </p>
65 +
66 + <.input field={@form[:radius]} type="number" label="Radius (metres, 50-3000)" />
67 + <.input field={@form[:limit]} type="number" label="How many of each (1-20)" />
68 +
69 + <:actions>
70 + <.button phx-disable-with="Saving...">Save Plani</.button>
71 + </:actions>
72 + </.simple_form>
73 + </div>
74 + """
75 + end
76 +
77 + @impl true
78 + def update(%{plani: plani} = assigns, socket) do
79 + uid = assigns.current_user.id
80 + focis = Configuration.list_focis({:user, uid})
81 + sources = Configuration.list_cfg_sources({:user, uid})
82 + ankyra = RoomSanctum.Accounts.list_users_rabbit({:user, uid})
83 +
84 + {:ok,
85 + socket
86 + |> assign(assigns)
87 + |> assign_form(Configuration.change_plani(plani))
88 + |> assign(:cfg_foci_sel, Enum.map(focis, fn x -> {x.name, x.id} end))
89 + |> assign(:cfg_sources_sel, Enum.map(sources, fn x -> {x.name, x.id} end))
90 + |> assign(:cfg_ankyra_sel, Enum.map(ankyra, fn x -> {x.topic, x.id} end))}
91 + end
92 +
93 + @impl true
94 + def handle_event("validate", %{"plani" => params}, socket) do
95 + changeset =
96 + socket.assigns.plani
97 + |> Configuration.change_plani(with_user(params, socket))
98 + |> Map.put(:action, :validate)
99 +
100 + {:noreply, assign_form(socket, changeset)}
101 + end
102 +
103 + def handle_event("save", %{"plani" => params}, socket) do
104 + save(socket, socket.assigns.action, with_user(params, socket))
105 + end
106 +
107 + defp save(socket, :edit, params) do
108 + case Configuration.update_plani(socket.assigns.plani, params) do
109 + {:ok, plani} ->
110 + notify_parent({:saved, plani})
111 +
112 + {:noreply,
113 + socket
114 + |> put_flash(:info, "Plani updated")
115 + |> push_patch(to: socket.assigns.patch)}
116 +
117 + {:error, %Ecto.Changeset{} = changeset} ->
118 + {:noreply, assign_form(socket, changeset)}
119 + end
120 + end
121 +
122 + defp save(socket, :new, params) do
123 + case Configuration.create_plani(params) do
124 + {:ok, plani} ->
125 + notify_parent({:saved, plani})
126 +
127 + {:noreply,
128 + socket
129 + |> put_flash(:info, "Plani created")
130 + |> push_patch(to: socket.assigns.patch)}
131 +
132 + {:error, %Ecto.Changeset{} = changeset} ->
133 + {:noreply, assign_form(socket, changeset)}
134 + end
135 + end
136 +
137 + # The form does not offer an owner, so it is put back on the way through
138 + # rather than trusted from the browser.
139 + defp with_user(params, socket), do: Map.put(params, "user_id", socket.assigns.current_user.id)
140 +
141 + defp assign_form(socket, %Ecto.Changeset{} = changeset) do
142 + assign(socket, :form, to_form(changeset))
143 + end
144 +
145 + defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
146 +end
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/index.ex
+47 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/index.html.heex
+55 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+60 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+65 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/show.ex
+15 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/show.html.heex
+44 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/router.ex
+5 −0

Click to load diff…

added apps/room_sanctum/test/room_sanctum_web/live/plani_live_test.exs
+87 −0

Click to load diff…

Parents: 1830332