feat(rover): full-bleed layout matching /map and /weather; remove Mode selector
This commit is contained in:
parent
7c506a6453
commit
c13a5f53be
3 changed files with 210 additions and 252 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule MicrowavepropWeb.RoverLive do
|
defmodule MicrowavepropWeb.RoverLive do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Rover-planning page: enter the fixed stations you want to work, pick a
|
Rover-planning page: enter the fixed stations you want to work, pick a
|
||||||
band/mode/forecast hour, and the map paints a smoothed propagation
|
band and forecast hour, and the map paints a smoothed propagation
|
||||||
quality heatmap with the top 5 ranked drive-to candidates pinned in the
|
quality heatmap with the top 5 ranked drive-to candidates pinned in the
|
||||||
bottom strip.
|
bottom strip.
|
||||||
"""
|
"""
|
||||||
|
|
@ -10,7 +10,6 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
|
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
alias Microwaveprop.Propagation
|
alias Microwaveprop.Propagation
|
||||||
alias Microwaveprop.Propagation.ModeThresholds
|
|
||||||
alias Microwaveprop.Radio.Maidenhead
|
alias Microwaveprop.Radio.Maidenhead
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
alias Microwaveprop.Rover
|
alias Microwaveprop.Rover
|
||||||
|
|
@ -119,10 +118,6 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
{:noreply, assign(socket, band: band)}
|
{:noreply, assign(socket, band: band)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("select_mode", %{"mode" => mode_str}, socket) do
|
|
||||||
{:noreply, assign(socket, mode: parse_mode(mode_str, socket.assigns.mode))}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("set_forecast_hour", %{"value" => v}, socket) do
|
def handle_event("set_forecast_hour", %{"value" => v}, socket) do
|
||||||
hour = v |> parse_int(@default_forecast_hour) |> clamp(0, 18)
|
hour = v |> parse_int(@default_forecast_hour) |> clamp(0, 18)
|
||||||
{:noreply, assign(socket, forecast_hour: hour)}
|
{:noreply, assign(socket, forecast_hour: hour)}
|
||||||
|
|
@ -471,28 +466,12 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_mode(mode_str, fallback) do
|
|
||||||
case mode_str do
|
|
||||||
"ssb" -> :ssb
|
|
||||||
"cw" -> :cw
|
|
||||||
"q65_60a" -> :q65_60a
|
|
||||||
"q65_120b" -> :q65_120b
|
|
||||||
_ -> fallback
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp clamp(v, lo, hi), do: v |> max(lo) |> min(hi)
|
defp clamp(v, lo, hi), do: v |> max(lo) |> min(hi)
|
||||||
|
|
||||||
defp band_label(band) do
|
defp band_label(band) do
|
||||||
Enum.find_value(@bands, "#{band} MHz", fn b -> if b.value == band, do: b.label end)
|
Enum.find_value(@bands, "#{band} MHz", fn b -> if b.value == band, do: b.label end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mode_label(mode) do
|
|
||||||
Enum.find_value(ModeThresholds.modes(), "#{mode}", fn {atom, label} ->
|
|
||||||
if atom == mode, do: label
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp station_id(%{id: id}), do: to_string(id)
|
defp station_id(%{id: id}), do: to_string(id)
|
||||||
defp station_id(_), do: nil
|
defp station_id(_), do: nil
|
||||||
|
|
||||||
|
|
@ -501,244 +480,223 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-full">
|
<div class="flex flex-col w-screen h-screen overflow-hidden">
|
||||||
<div class="flex flex-col h-[calc(100vh-3.5rem)] w-full">
|
<header class="flex items-center justify-between h-11 px-4 bg-base-100 border-b border-base-300 shrink-0">
|
||||||
<header class="flex items-center justify-between h-11 px-4 bg-base-100 border-b border-base-300 shrink-0">
|
<div class="flex flex-col leading-tight">
|
||||||
<div class="flex flex-col leading-tight">
|
<span class="font-semibold text-sm">Rover planning</span>
|
||||||
<span class="font-semibold text-sm">Rover planning</span>
|
<span class="text-xs text-base-content/60">
|
||||||
<span class="text-xs text-base-content/60">
|
{band_label(@band)} · Forecast +{@forecast_hour}h
|
||||||
{band_label(@band)} · {mode_label(@mode)} · Forecast +{@forecast_hour}h
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
phx-click="calculate"
|
||||||
phx-click="calculate"
|
class="btn btn-primary btn-sm"
|
||||||
class="btn btn-primary btn-sm"
|
disabled={@scoring_loading}
|
||||||
disabled={@scoring_loading}
|
>
|
||||||
|
<span :if={@scoring_loading} class="loading loading-spinner loading-xs"></span>
|
||||||
|
{if @scoring_loading, do: "Calculating…", else: "Calculate"}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-1 min-h-0">
|
||||||
|
<div class="relative flex-1 min-w-0">
|
||||||
|
<div
|
||||||
|
id="rover-map"
|
||||||
|
phx-hook="RoverMap"
|
||||||
|
phx-update="ignore"
|
||||||
|
data-home-lat={@home.lat}
|
||||||
|
data-home-lon={@home.lon}
|
||||||
|
data-drive-radius-km={@drive_radius_km}
|
||||||
|
data-stations={Jason.encode!(stations_for_compute(@fixed_stations))}
|
||||||
|
class={[
|
||||||
|
"absolute inset-0 z-0 transition-opacity",
|
||||||
|
@scoring_loading && "opacity-60"
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<span :if={@scoring_loading} class="loading loading-spinner loading-xs"></span>
|
</div>
|
||||||
{if @scoring_loading, do: "Calculating…", else: "Calculate"}
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="flex flex-1 min-h-0">
|
<div
|
||||||
<div class="relative flex-1 min-w-0">
|
:if={@top_candidates != []}
|
||||||
<div
|
class="absolute bottom-0 inset-x-0 h-[110px] bg-base-100/90 backdrop-blur border-t border-base-300 z-10 overflow-x-auto"
|
||||||
id="rover-map"
|
>
|
||||||
phx-hook="RoverMap"
|
<div class="carousel carousel-start gap-3 p-3 h-full">
|
||||||
phx-update="ignore"
|
<div
|
||||||
data-home-lat={@home.lat}
|
:for={c <- @top_candidates}
|
||||||
data-home-lon={@home.lon}
|
class="carousel-item w-[280px] card bg-base-200 shadow-sm cursor-pointer"
|
||||||
data-drive-radius-km={@drive_radius_km}
|
phx-click="select_candidate"
|
||||||
data-stations={Jason.encode!(stations_for_compute(@fixed_stations))}
|
phx-value-grid={c.grid}
|
||||||
class={[
|
>
|
||||||
"absolute inset-0 z-0 transition-opacity",
|
<div class="card-body p-3">
|
||||||
@scoring_loading && "opacity-60"
|
<div class="flex items-center justify-between">
|
||||||
]}
|
<span class="font-mono font-semibold">{c.grid}</span>
|
||||||
>
|
<span
|
||||||
</div>
|
class="badge badge-sm font-mono"
|
||||||
|
style={"background-color: #{c.tier_color}; color: #000"}
|
||||||
<div
|
>
|
||||||
:if={@top_candidates != []}
|
{Float.round(c.score, 1)} dB
|
||||||
class="absolute bottom-0 inset-x-0 h-[110px] bg-base-100/90 backdrop-blur border-t border-base-300 z-10 overflow-x-auto"
|
</span>
|
||||||
>
|
</div>
|
||||||
<div class="carousel carousel-start gap-3 p-3 h-full">
|
<div class="text-xs opacity-70">
|
||||||
<div
|
{round(c.distance_km)} km {c.bearing_compass} of home
|
||||||
:for={c <- @top_candidates}
|
</div>
|
||||||
class="carousel-item w-[280px] card bg-base-200 shadow-sm cursor-pointer"
|
<div class="text-xs opacity-70">
|
||||||
phx-click="select_candidate"
|
{c.elev_m} m · {round(c.drive_min)} min drive
|
||||||
phx-value-grid={c.grid}
|
|
||||||
>
|
|
||||||
<div class="card-body p-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="font-mono font-semibold">{c.grid}</span>
|
|
||||||
<span
|
|
||||||
class="badge badge-sm font-mono"
|
|
||||||
style={"background-color: #{c.tier_color}; color: #000"}
|
|
||||||
>
|
|
||||||
{Float.round(c.score, 1)} dB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs opacity-70">
|
|
||||||
{round(c.distance_km)} km {c.bearing_compass} of home
|
|
||||||
</div>
|
|
||||||
<div class="text-xs opacity-70">
|
|
||||||
{c.elev_m} m · {round(c.drive_min)} min drive
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-[122px] left-4 z-20 bg-base-100/90 rounded-box shadow p-2 text-xs flex flex-col gap-1">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="w-3 h-3 rounded-sm" style="background-color: #16a34a; opacity: 0.5">
|
|
||||||
</span>
|
|
||||||
<span>Excellent ≥10 dB</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="w-3 h-3 rounded-sm" style="background-color: #eab308; opacity: 0.5">
|
|
||||||
</span>
|
|
||||||
<span>Good 3..10 dB</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="w-3 h-3 rounded-sm" style="background-color: #f97316; opacity: 0.5">
|
|
||||||
</span>
|
|
||||||
<span>Marginal 0..3 dB</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 pt-1 border-t border-base-300/50">
|
|
||||||
<span class="w-4 border-t-2 border-dashed border-sky-500"></span>
|
|
||||||
<span>Drive radius</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside
|
<div class="absolute bottom-[122px] left-4 z-20 bg-base-100/90 rounded-box shadow p-2 text-xs flex flex-col gap-1">
|
||||||
id="rover-sidebar"
|
<div class="flex items-center gap-2">
|
||||||
data-theme="dark"
|
<span class="w-3 h-3 rounded-sm" style="background-color: #16a34a; opacity: 0.5"></span>
|
||||||
class="hidden md:flex flex-col w-72 shrink-0 h-full bg-neutral text-neutral-content border-l border-base-300 overflow-y-auto"
|
<span>Excellent ≥10 dB</span>
|
||||||
>
|
|
||||||
<.sidebar_section title="HOME">
|
|
||||||
<.home_form home={@home} error={@home_form_error} persisted?={@persisted?} />
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="FIXED STATIONS">
|
|
||||||
<ul class="menu menu-sm p-0 gap-0.5">
|
|
||||||
<li :for={s <- @fixed_stations}>
|
|
||||||
<label class="cursor-pointer flex items-center gap-2 py-1">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-xs"
|
|
||||||
checked={s.selected}
|
|
||||||
phx-click="toggle_station"
|
|
||||||
phx-value-id={station_id(s)}
|
|
||||||
/>
|
|
||||||
<span class="font-mono text-xs flex-1">{s.callsign}</span>
|
|
||||||
<span :if={s.grid} class="opacity-60 text-[11px]">{s.grid}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100"
|
|
||||||
phx-click="delete_station"
|
|
||||||
phx-value-id={station_id(s)}
|
|
||||||
title="Remove"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<form phx-submit="add_station" class="flex gap-1 mt-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="callsign"
|
|
||||||
placeholder="Call"
|
|
||||||
class="input input-bordered input-xs flex-1"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="grid"
|
|
||||||
placeholder="Grid"
|
|
||||||
class="input input-bordered input-xs w-20"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<button type="submit" class="btn btn-xs btn-primary">+</button>
|
|
||||||
</form>
|
|
||||||
<p :if={@station_form_error} class="text-error text-[11px] mt-1">
|
|
||||||
{@station_form_error}
|
|
||||||
</p>
|
|
||||||
<p :if={!@persisted?} class="text-[11px] opacity-50 mt-2">
|
|
||||||
Sign in to save your station list.
|
|
||||||
</p>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="FORECAST HOUR">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="18"
|
|
||||||
step="1"
|
|
||||||
value={@forecast_hour}
|
|
||||||
phx-change="set_forecast_hour"
|
|
||||||
phx-debounce="200"
|
|
||||||
name="value"
|
|
||||||
class="range range-xs range-primary"
|
|
||||||
/>
|
|
||||||
<div class="text-xs opacity-70 mt-1">+{@forecast_hour}h</div>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="BAND">
|
|
||||||
<div class="join w-full">
|
|
||||||
<button
|
|
||||||
:for={b <- @bands}
|
|
||||||
type="button"
|
|
||||||
phx-click="select_band"
|
|
||||||
phx-value-band={b.value}
|
|
||||||
class={[
|
|
||||||
"btn btn-sm join-item flex-1",
|
|
||||||
@band == b.value && "btn-primary"
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{b.label |> String.replace(" GHz", "G")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="MODE">
|
|
||||||
<select
|
|
||||||
name="mode"
|
|
||||||
phx-change="select_mode"
|
|
||||||
class="select select-sm select-bordered w-full"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
:for={{atom, label} <- ModeThresholds.modes()}
|
|
||||||
value={Atom.to_string(atom)}
|
|
||||||
selected={atom == @mode}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="MAX DRIVE TIME">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="30"
|
|
||||||
max="240"
|
|
||||||
step="15"
|
|
||||||
value={@max_drive_min}
|
|
||||||
phx-change="set_max_drive_time"
|
|
||||||
phx-debounce="200"
|
|
||||||
name="value"
|
|
||||||
class="range range-xs range-primary"
|
|
||||||
/>
|
|
||||||
<div class="text-xs opacity-70 mt-1">
|
|
||||||
{Float.round(@max_drive_min / 60, 1)}h from {@home.label}
|
|
||||||
</div>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<.sidebar_section title="MIN ELEV GAIN">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="500"
|
|
||||||
step="25"
|
|
||||||
value={@min_elev_gain}
|
|
||||||
phx-change="set_min_elev_gain"
|
|
||||||
phx-debounce="200"
|
|
||||||
name="value"
|
|
||||||
class="range range-xs range-primary"
|
|
||||||
/>
|
|
||||||
<div class="text-xs opacity-70 mt-1">{@min_elev_gain} m above home</div>
|
|
||||||
</.sidebar_section>
|
|
||||||
|
|
||||||
<div class="mt-auto p-3 text-[11px] text-neutral-content/50 border-t border-base-300/50">
|
|
||||||
Tip: click any cell to see per-station predicted SNR.
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-3 h-3 rounded-sm" style="background-color: #eab308; opacity: 0.5"></span>
|
||||||
|
<span>Good 3..10 dB</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-3 h-3 rounded-sm" style="background-color: #f97316; opacity: 0.5"></span>
|
||||||
|
<span>Marginal 0..3 dB</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pt-1 border-t border-base-300/50">
|
||||||
|
<span class="w-4 border-t-2 border-dashed border-sky-500"></span>
|
||||||
|
<span>Drive radius</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
id="rover-sidebar"
|
||||||
|
data-theme="dark"
|
||||||
|
class="hidden md:flex flex-col w-72 shrink-0 h-full bg-neutral text-neutral-content border-l border-base-300 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<.sidebar_section title="HOME">
|
||||||
|
<.home_form home={@home} error={@home_form_error} persisted?={@persisted?} />
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<.sidebar_section title="FIXED STATIONS">
|
||||||
|
<ul class="menu menu-sm p-0 gap-0.5">
|
||||||
|
<li :for={s <- @fixed_stations}>
|
||||||
|
<label class="cursor-pointer flex items-center gap-2 py-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-xs"
|
||||||
|
checked={s.selected}
|
||||||
|
phx-click="toggle_station"
|
||||||
|
phx-value-id={station_id(s)}
|
||||||
|
/>
|
||||||
|
<span class="font-mono text-xs flex-1">{s.callsign}</span>
|
||||||
|
<span :if={s.grid} class="opacity-60 text-[11px]">{s.grid}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100"
|
||||||
|
phx-click="delete_station"
|
||||||
|
phx-value-id={station_id(s)}
|
||||||
|
title="Remove"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form phx-submit="add_station" class="flex gap-1 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="callsign"
|
||||||
|
placeholder="Call"
|
||||||
|
class="input input-bordered input-xs flex-1"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="grid"
|
||||||
|
placeholder="Grid"
|
||||||
|
class="input input-bordered input-xs w-20"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-xs btn-primary">+</button>
|
||||||
|
</form>
|
||||||
|
<p :if={@station_form_error} class="text-error text-[11px] mt-1">
|
||||||
|
{@station_form_error}
|
||||||
|
</p>
|
||||||
|
<p :if={!@persisted?} class="text-[11px] opacity-50 mt-2">
|
||||||
|
Sign in to save your station list.
|
||||||
|
</p>
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<.sidebar_section title="FORECAST HOUR">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="18"
|
||||||
|
step="1"
|
||||||
|
value={@forecast_hour}
|
||||||
|
phx-change="set_forecast_hour"
|
||||||
|
phx-debounce="200"
|
||||||
|
name="value"
|
||||||
|
class="range range-xs range-primary"
|
||||||
|
/>
|
||||||
|
<div class="text-xs opacity-70 mt-1">+{@forecast_hour}h</div>
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<.sidebar_section title="BAND">
|
||||||
|
<div class="join w-full">
|
||||||
|
<button
|
||||||
|
:for={b <- @bands}
|
||||||
|
type="button"
|
||||||
|
phx-click="select_band"
|
||||||
|
phx-value-band={b.value}
|
||||||
|
class={[
|
||||||
|
"btn btn-sm join-item flex-1",
|
||||||
|
@band == b.value && "btn-primary"
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{b.label |> String.replace(" GHz", "G")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<.sidebar_section title="MAX DRIVE TIME">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="30"
|
||||||
|
max="240"
|
||||||
|
step="15"
|
||||||
|
value={@max_drive_min}
|
||||||
|
phx-change="set_max_drive_time"
|
||||||
|
phx-debounce="200"
|
||||||
|
name="value"
|
||||||
|
class="range range-xs range-primary"
|
||||||
|
/>
|
||||||
|
<div class="text-xs opacity-70 mt-1">
|
||||||
|
{Float.round(@max_drive_min / 60, 1)}h from {@home.label}
|
||||||
|
</div>
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<.sidebar_section title="MIN ELEV GAIN">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="500"
|
||||||
|
step="25"
|
||||||
|
value={@min_elev_gain}
|
||||||
|
phx-change="set_min_elev_gain"
|
||||||
|
phx-debounce="200"
|
||||||
|
name="value"
|
||||||
|
class="range range-xs range-primary"
|
||||||
|
/>
|
||||||
|
<div class="text-xs opacity-70 mt-1">{@min_elev_gain} m above home</div>
|
||||||
|
</.sidebar_section>
|
||||||
|
|
||||||
|
<div class="mt-auto p-3 text-[11px] text-neutral-content/50 border-t border-base-300/50">
|
||||||
|
Tip: click any cell to see per-station predicted SNR.
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</Layouts.app>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ defmodule Microwaveprop.Rover.ComputeTest do
|
||||||
|
|
||||||
assert is_list(result.cells)
|
assert is_list(result.cells)
|
||||||
assert length(result.top_candidates) <= 5
|
assert length(result.top_candidates) <= 5
|
||||||
assert length(result.top_candidates) > 0
|
refute result.top_candidates == []
|
||||||
|
|
||||||
# Sorted by score desc.
|
# Sorted by score desc.
|
||||||
scores = Enum.map(result.top_candidates, & &1.score)
|
scores = Enum.map(result.top_candidates, & &1.score)
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ defmodule MicrowavepropWeb.RoverLiveTest do
|
||||||
assert html =~ "Calculate"
|
assert html =~ "Calculate"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "shows the right-docked sidebar with the six configured sections", %{conn: conn} do
|
test "shows the right-docked sidebar with the configured sections", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, ~p"/rover")
|
{:ok, _view, html} = live(conn, ~p"/rover")
|
||||||
assert html =~ ~s(id="rover-sidebar")
|
assert html =~ ~s(id="rover-sidebar")
|
||||||
assert html =~ "FIXED STATIONS"
|
assert html =~ "FIXED STATIONS"
|
||||||
assert html =~ "FORECAST HOUR"
|
assert html =~ "FORECAST HOUR"
|
||||||
assert html =~ "BAND"
|
assert html =~ "BAND"
|
||||||
assert html =~ "MODE"
|
|
||||||
assert html =~ "MAX DRIVE TIME"
|
assert html =~ "MAX DRIVE TIME"
|
||||||
assert html =~ "MIN ELEV GAIN"
|
assert html =~ "MIN ELEV GAIN"
|
||||||
|
refute html =~ ">MODE<"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays the three default NTMS stations", %{conn: conn} do
|
test "displays the three default NTMS stations", %{conn: conn} do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue