Show the bearing in Plus, and let a Plani switch reads

ce20d2b · Gabriel Morell · 2026-09-03 20:05

13 files +134 -14
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.ex
+26 −4
@@ -11,7 +11,13 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
11 11 # and held there -- so the page asks, rather than waiting to be told.
12 12 if connected?(socket), do: :timer.send_interval(5_000, self(), :look)
13 13
14 − {:ok, socket |> assign(:anchor, nil) |> assign(:state, %{data: %{}, queries: []})}
14 + {:ok,
15 + socket
16 + |> assign(:anchor, nil)
17 + |> assign(:state, %{data: %{}, queries: []})
18 + # Which read of the same data to draw. Basic is what the clients get and
19 + # so the honest default for a page whose job is showing what is published.
20 + |> assign(:view, :basic)}
15 21 end
16 22
17 23 @impl true
@@ -28,6 +34,12 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
28 34 @impl true
29 35 def handle_info(:look, socket), do: {:noreply, look(socket)}
30 36
37 + @impl true
38 + def handle_event("view", %{"view" => "plus"}, socket),
39 + do: {:noreply, assign(socket, :view, :plus)}
40 +
41 + def handle_event("view", _params, socket), do: {:noreply, assign(socket, :view, :basic)}
42 +
31 43 defp look(socket) do
32 44 id = to_string(socket.assigns.plani.id)
33 45
@@ -61,9 +73,19 @@ defmodule RoomSanctumWeb.PlaniLive.Show do
61 73
62 74 def entries(_state), do: []
63 75
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)
76 + @doc """
77 + The condensed form of one entry, as the preview components want it.
78 +
79 + Which condenser depends on the view: Plus keeps each arrival whole where it
80 + has more to say about one, and falls back to Basic's answer for every type
81 + it does not write -- so switching never empties a card.
82 + """
83 + def condensed({id, type}, rows, view \\ :basic) do
84 + condensed =
85 + case view do
86 + :plus -> RoomSanctum.Condenser.PlusMQTT.condense_data({id, type}, rows)
87 + _ -> RoomSanctum.Condenser.BasicMQTT.condense_data({id, type}, rows)
88 + end
67 89
68 90 # The shape the preview components take. Defined here rather than borrowed
69 91 # from the Pythiae page, which keeps its own private copy of the same
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_plani_live/show.html.heex
+36 −10
@@ -51,7 +51,28 @@
51 51 <div class="mx-4 py-2">
52 52 <div class="card card-compact w-full bg-base-100 shadow-xl">
53 53 <div class="card-body">
54 − <h3 class="card-title"><i class="fas fa-fw fa-eye"></i> What it is publishing</h3>
54 + <div class="flex items-center justify-between gap-2 flex-wrap">
55 + <h3 class="card-title"><i class="fas fa-fw fa-eye"></i> What it is publishing</h3>
56 + <%!-- Basic is what the clients actually receive, so it leads and is
57 + the default; Plus is the same data read further, for the types
58 + that carry more than a card has room for. --%>
59 + <div class="join">
60 + <button
61 + class={"btn btn-xs join-item #{if @view == :basic, do: "btn-primary", else: "btn-ghost"}"}
62 + phx-click="view"
63 + phx-value-view="basic"
64 + >
65 + Basic
66 + </button>
67 + <button
68 + class={"btn btn-xs join-item #{if @view == :plus, do: "btn-primary", else: "btn-ghost"}"}
69 + phx-click="view"
70 + phx-value-view="plus"
71 + >
72 + Plus
73 + </button>
74 + </div>
75 + </div>
55 76 <% entries = RoomSanctumWeb.PlaniLive.Show.entries(@state) %>
56 77 <%= if entries == [] do %>
57 78 <p class="opacity-70">
@@ -76,15 +97,20 @@
76 97 <%= RoomSanctumWeb.PlaniLive.Show.entry_name(@state, elem(key, 0)) %>
77 98 </span>
78 99 </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>
100 + <%!-- Dispatched rather than listed here: a case written out on
101 + the page falls through to nothing for every type added
102 + after it, which is how weather and the rest ended up
103 + rendering as `inspect` on this page. --%>
104 + <%= if @view == :plus do %>
105 + <.p_plus
106 + type={elem(key, 1)}
107 + entries={RoomSanctumWeb.PlaniLive.Show.condensed(key, rows, :plus)}
108 + />
109 + <% else %>
110 + <.p_basic
111 + type={elem(key, 1)}
112 + entries={RoomSanctumWeb.PlaniLive.Show.condensed(key, rows)}
113 + />
88 114 <% end %>
89 115 </div>
90 116 </div>
modified apps/room_sanctum/lib/room_sanctum_web/live/live_preview.ex
+45 −0
@@ -434,6 +434,44 @@ use PhoenixHTMLHelpers
434 434 """
435 435 end
436 436
437 + @doc """
438 + The Basic view for whichever type the query is.
439 +
440 + The sibling of `p_plus/1`, and the reason both exist as dispatchers rather
441 + than as a `case` written out at each call site: a page that inlines the list
442 + silently falls through to nothing -- or to `inspect` -- for every type added
443 + after it was written, which is exactly what happened to the Plani page when
444 + weather and the rest learned to travel.
445 + """
446 + attr :entries, :map, required: true
447 + attr :type, :atom, required: true
448 +
449 + def p_basic(assigns) do
450 + ~H"""
451 + <%= case @type do %>
452 + <% :gtfs -> %> <.p_gtfs entries={@entries} />
453 + <% :gbfs -> %> <.p_gbfs entries={@entries} />
454 + <% :tidal -> %> <.p_tidal entries={@entries} />
455 + <% :weather -> %> <.p_weather entries={@entries} />
456 + <% :aqi -> %> <.p_aqi entries={@entries} />
457 + <% :ephem -> %> <.p_ephem entries={@entries} />
458 + <% :calendar -> %> <.p_calendar entries={@entries} />
459 + <% :cronos -> %> <.p_cronos entries={@entries} />
460 + <% :gitlab -> %> <.p_gitlab entries={@entries} />
461 + <% :github -> %> <.p_github entries={@entries} />
462 + <% :drought -> %> <.p_drought entries={@entries} />
463 + <% :pollen -> %> <.p_pollen entries={@entries} />
464 + <% :icarus -> %> <.p_icarus entries={@entries} />
465 + <% :mailbox -> %> <.p_mailbox entries={@entries} />
466 + <% :treasury -> %> <.p_treasury entries={@entries} />
467 + <% :bourse -> %> <.p_bourse entries={@entries} />
468 + <% :packages -> %> <.p_packages entries={@entries} />
469 + <% :const -> %> <.p_const entries={@entries} />
470 + <% _ -> %>
471 + <% end %>
472 + """
473 + end
474 +
437 475 @doc """
438 476 The Plus view for whichever type the query is.
439 477
@@ -514,6 +552,13 @@ use PhoenixHTMLHelpers
514 552 <div class="card-body text-left">
515 553 <h2 class="card-title">
516 554 <p><i class="fa-solid fa-fw fa-bicycle"></i> <%= e.name %> </p>
555 + <%!-- Which way to walk. Only a Plani sends it: a vision's foci is a
556 + fixed place, so there is nothing for a bearing to be relative
557 + to, and it is absent for anything close enough to have no
558 + direction at all. --%>
559 + <span :if={Map.get(e, :dir)} class="badge badge-sm badge-outline gap-1">
560 + <i class="fa-solid fa-location-arrow"></i><%= Map.get(e, :dir) %>
561 + </span>
517 562 </h2>
518 563 <%= if free_bike?(e) do %>
519 564 <p>
modified apps/room_sanctum/test/room_sanctum_web/live/plani_live_test.exs
+27 −0
@@ -216,6 +216,33 @@ defmodule RoomSanctumWeb.PlaniLiveTest do
216 216 assert html =~ "No sources yet"
217 217 end
218 218
219 + test "the preview switches between the Basic and Plus reads", ctx do
220 + plani = plani(ctx)
221 + {:ok, live, html} = live(ctx.conn, "/cfg/plani/#{plani.id}")
222 +
223 + # Basic is what the clients receive, so it is what the page opens on.
224 + assert html =~ "btn-primary"
225 + assert html =~ "Plus"
226 +
227 + plus = live |> element("button[phx-value-view=plus]") |> render_click()
228 + assert plus =~ "Plus"
229 +
230 + back = live |> element("button[phx-value-view=basic]") |> render_click()
231 + assert back =~ "Basic"
232 + end
233 +
234 + test "the bike limit is bounded", ctx do
235 + assert {:error, changeset} =
236 + Configuration.create_plani(%{
237 + name: "Too many",
238 + user_id: ctx.user.id,
239 + home_foci_id: ctx.foci.id,
240 + bike_limit: 500
241 + })
242 +
243 + assert changeset.errors[:bike_limit]
244 + end
245 +
219 246 test "the radius and count are bounded", ctx do
220 247 assert {:error, changeset} =
221 248 Configuration.create_plani(%{

Parents: 0335fca