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.
Show the bearing in Plus, and let a Plani switch reads
ce20d2b · Gabriel Morell · 2026-09-03 20:05
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