Preview a Plani, and say what it is driving

7b6b5ac · Gabriel Morell · 2026-09-03 18:10

17 files +155 -6
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,6 +616,8 @@ defmodule RoomSanctum.Configuration do
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,15 +646,29 @@ defmodule RoomSanctum.Configuration do
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,6 +21,7 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
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,6 +46,39 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
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,6 +3,25 @@
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,6 +48,52 @@
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,6 +155,40 @@ defmodule RoomSanctumWeb.PlaniLiveTest do
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