neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
Give the Plani a control panel
cf1bcf1 · Gabriel Morell · 2026-09-03 17:29
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 | 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 | 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 | 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
| 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