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.
Preview a Plani, and say what it is driving
7b6b5ac · Gabriel Morell · 2026-09-03 18: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/configuration.ex
+22
−6
| 616 | 616 | ** (Ecto.NoResultsError) |
| 617 | 617 | |
| 618 | 618 | """ |
| 619 | + def get_pythiae!(id), do: Repo.get!(Pythiae, id) | |
| 620 | + | |
| 619 | 621 | alias RoomSanctum.Configuration.Plani |
| 620 | 622 | |
| 621 | 623 | @doc "Every Plani, for the supervisor that keeps a worker per one." |
| 644 | 646 | |
| 645 | 647 | def change_plani(%Plani{} = plani, attrs \\ %{}), do: Plani.changeset(plani, attrs) |
| 646 | 648 | |
| 647 | − def get_pythiae!(id), do: Repo.get!(Pythiae, id) | |
| 648 | − | |
| 649 | 649 | @doc """ |
| 650 | − Every Pythiae that publishes to a given Ankyra. | |
| 650 | + The Pythiae on the other end of something. | |
| 651 | 651 | |
| 652 | − The reverse of the list a Pythiae carries, for the Ankyra end: a client's | |
| 653 | − request arrives on a topic, which identifies the Ankyra, and what has to act | |
| 654 | − on it is whatever publishes there. | |
| 652 | + `:ankyra` is the reverse of the list a Pythiae carries -- a client's request | |
| 653 | + arrives on a topic, and what has to act on it is whatever publishes there. | |
| 654 | + `:plani` is the reverse of `curr_plani`: a Plani is worth nothing on its own, | |
| 655 | + and the first question about one is which board it is driving. | |
| 655 | 656 | """ |
| 657 | + def list_pythiae(:plani, id) when is_binary(id) do | |
| 658 | + case Integer.parse(id) do | |
| 659 | + {n, ""} -> list_pythiae(:plani, n) | |
| 660 | + _ -> [] | |
| 661 | + end | |
| 662 | + end | |
| 663 | + | |
| 664 | + def list_pythiae(:plani, id) do | |
| 665 | + from(p in Pythiae, where: p.curr_plani == ^id) |> Repo.all() | |
| 666 | + end | |
| 667 | + | |
| 668 | + # Every Pythiae that publishes to a given Ankyra: the reverse of the list a | |
| 669 | + # Pythiae carries, for the Ankyra end. A client's request arrives on a topic, | |
| 670 | + # which identifies the Ankyra, and what has to act on it is whatever | |
| 671 | + # publishes there. | |
| 656 | 672 | def list_pythiae(:ankyra, id) when is_binary(id) do |
| 657 | 673 | case Integer.parse(id) do |
| 658 | 674 | {n, ""} -> list_pythiae(:ankyra, n) |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+34
−0
| 21 | 21 | |> assign(:page_title, "Plani") |
| 22 | 22 | |> assign(:plani, Configuration.get_plani!(id)) |
| 23 | 23 | |> assign(:sources, Configuration.list_cfg_sources({:user, socket.assigns.current_user.id})) |
| 24 | + |> assign(:pythiae, Configuration.list_pythiae(:plani, id)) | |
| 24 | 25 | |> look()} |
| 25 | 26 | end |
| 26 | 27 |
| 45 | 46 | def focus(%{anchor: %Geo.Point{coordinates: {lon, lat}}}), do: %{lat: lat, lng: lon} |
| 46 | 47 | def focus(_), do: nil |
| 47 | 48 | |
| 49 | + @doc """ | |
| 50 | + The entries as they would go out on the wire, for the preview. | |
| 51 | + | |
| 52 | + Condensed the way the Pythiae publishes them rather than shown raw: the | |
| 53 | + point of a preview is what a client will draw, and a Plani broken out by | |
| 54 | + stop looks very different on the wire from one that is not. | |
| 55 | + """ | |
| 56 | + def entries(%{data: data}) when is_map(data) and map_size(data) > 0 do | |
| 57 | + data | |
| 58 | + |> Enum.reject(fn {_key, rows} -> rows == [] end) | |
| 59 | + |> Enum.sort_by(fn {{id, _type}, _rows} -> to_string(id) end) | |
| 60 | + end | |
| 61 | + | |
| 62 | + def entries(_state), do: [] | |
| 63 | + | |
| 64 | + @doc "The condensed form of one entry, as the preview components want it." | |
| 65 | + def condensed({id, type}, rows) do | |
| 66 | + condensed = RoomSanctum.Condenser.BasicMQTT.condense_data({id, type}, rows) | |
| 67 | + | |
| 68 | + # The shape the preview components take. Defined here rather than borrowed | |
| 69 | + # from the Pythiae page, which keeps its own private copy of the same | |
| 70 | + # three fields. | |
| 71 | + %{data: condensed, id: id, type: type} | |
| 72 | + end | |
| 73 | + | |
| 74 | + @doc "What to call an entry: the stop or source it was published under." | |
| 75 | + def entry_name(state, id) do | |
| 76 | + case Enum.find(state.queries, &(&1.id == id)) do | |
| 77 | + nil -> to_string(id) | |
| 78 | + query -> query.name | |
| 79 | + end | |
| 80 | + end | |
| 81 | + | |
| 48 | 82 | @doc """ |
| 49 | 83 | The radius, as a ring to draw around the anchor. |
| 50 | 84 |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+65
−0
| 3 | 3 | |
| 4 | 4 | <h1 class="text-3xl font-bold text-accent mx-4 py-2"><%= @plani.name %></h1> |
| 5 | 5 | |
| 6 | +<div class="mx-4"> | |
| 7 | + <%= if @pythiae == [] do %> | |
| 8 | + <p class="text-sm opacity-60"> | |
| 9 | + <i class="fas fa-fw fa-circle-info"></i> | |
| 10 | + No Pythiae is showing this. Set it as one's current Plani and it will be | |
| 11 | + published instead of that Pythiae's vision. | |
| 12 | + </p> | |
| 13 | + <% else %> | |
| 14 | + <p class="text-sm"> | |
| 15 | + Showing on | |
| 16 | + <%= for p <- @pythiae do %> | |
| 17 | + <a href={Routes.pythiae_show_path(@socket, :show, p)} class="btn btn-xs btn-secondary gap-1"> | |
| 18 | + <i class="fas fa-fw fa-tower-broadcast"></i> <%= p.name %> | |
| 19 | + </a> | |
| 20 | + <% end %> | |
| 21 | + </p> | |
| 22 | + <% end %> | |
| 23 | +</div> | |
| 24 | + | |
| 6 | 25 | <div class="mx-4 py-2"> |
| 7 | 26 | <div class="card card-compact w-full bg-base-100 shadow-xl"> |
| 8 | 27 | <div class="card-body"> |
| 29 | 48 | </div> |
| 30 | 49 | </div> |
| 31 | 50 | |
| 51 | +<div class="mx-4 py-2"> | |
| 52 | + <div class="card card-compact w-full bg-base-100 shadow-xl"> | |
| 53 | + <div class="card-body"> | |
| 54 | + <h3 class="card-title"><i class="fas fa-fw fa-eye"></i> What it is publishing</h3> | |
| 55 | + <% entries = RoomSanctumWeb.PlaniLive.Show.entries(@state) %> | |
| 56 | + <%= if entries == [] do %> | |
| 57 | + <p class="opacity-70"> | |
| 58 | + Nothing yet. A Plani asks its sources every half minute, and publishes | |
| 59 | + whatever came back. | |
| 60 | + </p> | |
| 61 | + <% else %> | |
| 62 | + <p class="text-xs opacity-60"> | |
| 63 | + Condensed as it goes on the wire, so this is what a client draws — | |
| 64 | + <%= if @plani.break_out do %> | |
| 65 | + one card per stop. | |
| 66 | + <% else %> | |
| 67 | + one card per source, with the stops near you blended into it. | |
| 68 | + <% end %> | |
| 69 | + </p> | |
| 70 | + <div class="2xl:columns-4 xl:columns-3 lg:columns-2 gap-4 mt-2"> | |
| 71 | + <div :for={{key, rows} <- entries} class="card bg-base-300 shadow-xl mb-2 break-inside-avoid"> | |
| 72 | + <div class="card-body"> | |
| 73 | + <div class="flex gap-2"> | |
| 74 | + <i class={"fas fa-fw fa-xl #{RoomSanctumWeb.IconHelpers.icon(elem(key, 1))}"}></i> | |
| 75 | + <span class="text-secondary font-bold"> | |
| 76 | + <%= RoomSanctumWeb.PlaniLive.Show.entry_name(@state, elem(key, 0)) %> | |
| 77 | + </span> | |
| 78 | + </div> | |
| 79 | + <%= case key do %> | |
| 80 | + <% {_, :gtfs} -> %> | |
| 81 | + <.p_gtfs entries={RoomSanctumWeb.PlaniLive.Show.condensed(key, rows)} /> | |
| 82 | + <% {_, :gbfs} -> %> | |
| 83 | + <.p_gbfs entries={RoomSanctumWeb.PlaniLive.Show.condensed(key, rows)} /> | |
| 84 | + <% {_, :aqi} -> %> | |
| 85 | + <.p_aqi entries={RoomSanctumWeb.PlaniLive.Show.condensed(key, rows)} /> | |
| 86 | + <% _ -> %> | |
| 87 | + <pre class="text-xs"><%= inspect(rows, limit: 4) %></pre> | |
| 88 | + <% end %> | |
| 89 | + </div> | |
| 90 | + </div> | |
| 91 | + </div> | |
| 92 | + <% end %> | |
| 93 | + </div> | |
| 94 | + </div> | |
| 95 | +</div> | |
| 96 | + | |
| 32 | 97 | <div class="mx-4 py-2 flex flex-wrap"> |
| 33 | 98 | <div class="card card-compact w-full md:w-1/2 bg-base-100 shadow-xl m-2"> |
| 34 | 99 | <div class="card-body"> |
modified
apps/room_sanctum/test/room_sanctum_web/live/plani_live_test.exs
+34
−0
| 155 | 155 | end |
| 156 | 156 | end |
| 157 | 157 | |
| 158 | + test "says when nothing is showing it", ctx do | |
| 159 | + plani = plani(ctx) | |
| 160 | + {:ok, _live, html} = live(ctx.conn, "/cfg/plani/#{plani.id}") | |
| 161 | + | |
| 162 | + assert html =~ "No Pythiae is showing this" | |
| 163 | + end | |
| 164 | + | |
| 165 | + test "links back to the Pythiae showing it", ctx do | |
| 166 | + plani = plani(ctx) | |
| 167 | + | |
| 168 | + {:ok, pythiae} = | |
| 169 | + Configuration.create_pythiae(%{ | |
| 170 | + name: "Kitchen", | |
| 171 | + user_id: ctx.user.id, | |
| 172 | + visions: [], | |
| 173 | + ankyra: [], | |
| 174 | + curr_plani: plani.id | |
| 175 | + }) | |
| 176 | + | |
| 177 | + {:ok, _live, html} = live(ctx.conn, "/cfg/plani/#{plani.id}") | |
| 178 | + | |
| 179 | + assert html =~ "Showing on" | |
| 180 | + assert html =~ pythiae.name | |
| 181 | + refute html =~ "No Pythiae is showing this" | |
| 182 | + end | |
| 183 | + | |
| 184 | + test "the preview says so before there is anything to preview", ctx do | |
| 185 | + plani = plani(ctx) | |
| 186 | + {:ok, _live, html} = live(ctx.conn, "/cfg/plani/#{plani.id}") | |
| 187 | + | |
| 188 | + assert html =~ "What it is publishing" | |
| 189 | + assert html =~ "asks its sources every half minute" | |
| 190 | + end | |
| 191 | + | |
| 158 | 192 | test "the map is on the page", ctx do |
| 159 | 193 | plani = plani(ctx) |
| 160 | 194 | {:ok, _live, html} = live(ctx.conn, "/cfg/plani/#{plani.id}") |
Parents: 11bc288