From 2c88fba1df260c6180dc22f72a98dca783b3a01a Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 3 May 2026 14:08:49 -0500 Subject: [PATCH] feat(rover-planning): multi-band missions + smart reconcile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mission now carries bands_mhz ({:array, :integer}) — operator picks one or more bands as multi-checkboxes. enqueue_paths_for builds the cross product (rover x station x band) and persists each tuple as its own Path row keyed by (mission_id, rover_location_id, station_id, band_mhz). The path-profile worker reads band_mhz from the path itself (legacy single-band jobs without band_mhz in args still resolve to their unique row). replace_mission_paths/1 is now a thin alias for reconcile_mission_paths/1 which diffs desired vs actual: stale tuples (old band that the user unchecked, station they removed, rover-site they deleted) get dropped, new tuples become :pending and enqueue, surviving :complete rows are left in place — no more wholesale destruction of already-computed paths on every edit. The show table gains a Band column, and band_label() in the mission summary becomes bands_label() (joins the list with commas). --- lib/microwaveprop/rover_planning.ex | 96 +++++++++++++------ lib/microwaveprop/rover_planning/mission.ex | 69 ++++++++++++- lib/microwaveprop/rover_planning/path.ex | 7 +- .../workers/rover_path_profile_worker.ex | 26 +++-- .../live/rover_planning_live/form.ex | 55 +++++++++-- .../live/rover_planning_live/show.ex | 26 ++++- ...03190225_add_multi_band_rover_planning.exs | 67 +++++++++++++ test/microwaveprop/rover_planning_test.exs | 43 +++++++++ .../live/rover_planning_live_test.exs | 4 +- 9 files changed, 342 insertions(+), 51 deletions(-) create mode 100644 priv/repo/migrations/20260503190225_add_multi_band_rover_planning.exs diff --git a/lib/microwaveprop/rover_planning.ex b/lib/microwaveprop/rover_planning.ex index 9ab3cebe..dfc70f37 100644 --- a/lib/microwaveprop/rover_planning.ex +++ b/lib/microwaveprop/rover_planning.ex @@ -112,16 +112,46 @@ defmodule Microwaveprop.RoverPlanning do end @doc """ - Wipes the existing path matrix for a mission and re-enqueues all - pairings. Called after a mission edit when the station list or - `only_known_good` toggle could have shifted the matrix. + Reconciles the path matrix for a mission against the current set of + (rover_location × station × band) tuples. Deletes paths for tuples + that no longer apply, leaves :complete paths in place when they're + still relevant, and enqueues new pending paths for any tuple missing + from the table. Called after a mission edit, or after the user + adds/removes a rover site. + + Aliased as `replace_mission_paths/1` for backwards compatibility + with callers that expected the previous destructive behaviour; + internally both go through the same reconcile path. """ - @spec replace_mission_paths(Mission.t()) :: :ok - def replace_mission_paths(%Mission{} = mission) do - Repo.delete_all(from(p in Path, where: p.mission_id == ^mission.id)) - enqueue_paths_for(mission) + @spec reconcile_mission_paths(Mission.t()) :: :ok + def reconcile_mission_paths(%Mission{} = mission) do + desired = MapSet.new(desired_tuples(mission)) + + actual_paths = + Repo.all(from(p in Path, where: p.mission_id == ^mission.id)) + + actual = MapSet.new(actual_paths, &{&1.rover_location_id, &1.station_id, &1.band_mhz}) + + stale_ids = + actual_paths + |> Enum.reject(&MapSet.member?(desired, {&1.rover_location_id, &1.station_id, &1.band_mhz})) + |> Enum.map(& &1.id) + + if stale_ids != [] do + Repo.delete_all(from(p in Path, where: p.id in ^stale_ids)) + end + + desired + |> MapSet.difference(actual) + |> Enum.to_list() + |> insert_pending_paths_and_jobs(mission) + + :ok end + @spec replace_mission_paths(Mission.t()) :: :ok + defdelegate replace_mission_paths(mission), to: __MODULE__, as: :reconcile_mission_paths + @doc """ Walks every mission and re-enqueues any (rover_location × station) pair whose `Path` is missing or not `:complete`. Existing complete @@ -143,17 +173,22 @@ defmodule Microwaveprop.RoverPlanning do end defp enqueue_paths_for(%Mission{} = mission) do + mission + |> desired_tuples() + |> insert_pending_paths_and_jobs(mission) + end + + # Cross product of (rover_location_id, station_id, band_mhz) for the + # mission's current scope. The reconcile + initial-create paths both + # go through this same source of truth. + @spec desired_tuples(Mission.t()) :: [{Ecto.UUID.t(), Ecto.UUID.t(), integer()}] + defp desired_tuples(%Mission{} = mission) do rovers = candidate_rover_locations(mission) stations = mission.stations || [] + bands = Mission.bands(mission) - pairs = - for rover <- rovers, station <- stations do - %{rover: rover, station: station} - end - - case pairs do - [] -> :ok - _ -> insert_pending_paths_and_jobs(mission, pairs) + for rover <- rovers, station <- stations, band <- bands do + {rover.id, station.id, band} end end @@ -171,34 +206,41 @@ defmodule Microwaveprop.RoverPlanning do Repo.all(from(l in Location, order_by: [desc: l.inserted_at])) end - defp insert_pending_paths_and_jobs(mission, pairs) do + defp insert_pending_paths_and_jobs([], _mission), do: :ok + + defp insert_pending_paths_and_jobs(tuples, mission) when is_list(tuples) do {:ok, _} = - pairs - |> Enum.reduce(Multi.new(), fn %{rover: r, station: s}, multi -> + tuples + |> Enum.reduce(Multi.new(), fn {rover_id, station_id, band_mhz}, multi -> attrs = %{ mission_id: mission.id, - rover_location_id: r.id, - station_id: s.id, + rover_location_id: rover_id, + station_id: station_id, + band_mhz: band_mhz, status: :pending } - Multi.insert(multi, {:path, r.id, s.id}, Path.changeset(%Path{}, attrs), + Multi.insert( + multi, + {:path, rover_id, station_id, band_mhz}, + Path.changeset(%Path{}, attrs), on_conflict: :nothing, - conflict_target: [:mission_id, :rover_location_id, :station_id] + conflict_target: [:mission_id, :rover_location_id, :station_id, :band_mhz] ) end) |> Repo.transaction() - enqueue_jobs(mission, pairs) + enqueue_jobs(tuples, mission) :ok end - defp enqueue_jobs(mission, pairs) do - for %{rover: r, station: s} <- pairs do + defp enqueue_jobs(tuples, mission) do + for {rover_id, station_id, band_mhz} <- tuples do %{ "mission_id" => mission.id, - "rover_location_id" => r.id, - "station_id" => s.id + "rover_location_id" => rover_id, + "station_id" => station_id, + "band_mhz" => band_mhz } |> RoverPathProfileWorker.new() |> Oban.insert() diff --git a/lib/microwaveprop/rover_planning/mission.ex b/lib/microwaveprop/rover_planning/mission.ex index a2c025b0..6d09c789 100644 --- a/lib/microwaveprop/rover_planning/mission.ex +++ b/lib/microwaveprop/rover_planning/mission.ex @@ -21,6 +21,7 @@ defmodule Microwaveprop.RoverPlanning.Mission do schema "rover_missions" do field :name, :string field :band_mhz, :integer, default: 10_000 + field :bands_mhz, {:array, :integer}, default: [] field :only_known_good, :boolean, default: true field :rover_height_ft, :float, default: 8.0 field :station_height_ft, :float, default: 30.0 @@ -38,26 +39,92 @@ defmodule Microwaveprop.RoverPlanning.Mission do @castable_fields [ :name, :band_mhz, + :bands_mhz, :only_known_good, :rover_height_ft, :station_height_ft, :notes ] + @doc """ + Returns the bands the mission's path matrix should iterate over. + Prefers `bands_mhz` (canonical, possibly empty) and falls back to + `[band_mhz]` for legacy rows. Always returns a non-empty list when + the mission is valid. + """ + @spec bands(t()) :: [integer()] + def bands(%__MODULE__{bands_mhz: list}) when is_list(list) and list != [], do: Enum.uniq(list) + + def bands(%__MODULE__{band_mhz: primary}) when is_integer(primary), do: [primary] + def bands(_), do: [] + @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def changeset(mission, attrs) do mission - |> cast(attrs, @castable_fields) + |> cast(prepare_bands_attr(attrs), @castable_fields) + |> normalize_bands() |> cast_assoc(:stations, with: &Station.changeset/2, required: true) |> validate_required([:name, :band_mhz]) |> validate_length(:name, min: 1, max: 100) |> validate_number(:band_mhz, greater_than: 0) + |> validate_bands_mhz() |> validate_number(:rover_height_ft, greater_than_or_equal_to: 0.0) |> validate_number(:station_height_ft, greater_than_or_equal_to: 0.0) |> validate_length(:notes, max: 4000) |> validate_at_least_one_station() end + # The multi-checkbox form sends a hidden empty-string sentinel so + # the key always exists even when nothing is checked. Drop those + # sentinels here so Ecto's `{:array, :integer}` cast doesn't choke. + defp prepare_bands_attr(%{"bands_mhz" => list} = attrs) when is_list(list) do + cleaned = + list + |> Enum.reject(fn v -> v == "" or v == nil end) + |> Enum.uniq() + + Map.put(attrs, "bands_mhz", cleaned) + end + + defp prepare_bands_attr(attrs), do: attrs + + # Treats `bands_mhz` as the canonical multi-band list. When the + # form submits it (multi-checkbox), use that. When only `band_mhz` + # is present (legacy single-band form path), seed bands_mhz from it. + # Also keeps `band_mhz` in sync as the "primary band" so older + # readers stay correct. + defp normalize_bands(changeset) do + bands = get_change(changeset, :bands_mhz) || get_field(changeset, :bands_mhz) + primary = get_change(changeset, :band_mhz) || get_field(changeset, :band_mhz) + + cond do + is_list(bands) and bands != [] -> + cleaned = bands |> Enum.uniq() |> Enum.sort() + + changeset + |> put_change(:bands_mhz, cleaned) + |> put_change(:band_mhz, hd(cleaned)) + + is_integer(primary) and primary > 0 -> + put_change(changeset, :bands_mhz, [primary]) + + true -> + changeset + end + end + + defp validate_bands_mhz(changeset) do + case get_field(changeset, :bands_mhz) do + list when is_list(list) and list != [] -> + if Enum.all?(list, &(is_integer(&1) and &1 > 0)), + do: changeset, + else: add_error(changeset, :bands_mhz, "must be a list of positive integers") + + _ -> + add_error(changeset, :bands_mhz, "must include at least one band") + end + end + defp validate_at_least_one_station(changeset) do case fetch_field(changeset, :stations) do {_source, list} when is_list(list) and list != [] -> diff --git a/lib/microwaveprop/rover_planning/path.ex b/lib/microwaveprop/rover_planning/path.ex index 7ee149d1..46e8e054 100644 --- a/lib/microwaveprop/rover_planning/path.ex +++ b/lib/microwaveprop/rover_planning/path.ex @@ -18,6 +18,7 @@ defmodule Microwaveprop.RoverPlanning.Path do @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "rover_mission_paths" do + field :band_mhz, :integer field :status, Ecto.Enum, values: @statuses, default: :pending field :result, :map field :error, :string @@ -39,6 +40,7 @@ defmodule Microwaveprop.RoverPlanning.Path do def changeset(path, attrs) do path |> cast(attrs, [ + :band_mhz, :status, :result, :error, @@ -47,9 +49,10 @@ defmodule Microwaveprop.RoverPlanning.Path do :rover_location_id, :station_id ]) - |> validate_required([:mission_id, :rover_location_id, :station_id, :status]) + |> validate_required([:mission_id, :rover_location_id, :station_id, :band_mhz, :status]) |> validate_inclusion(:status, @statuses) - |> unique_constraint([:mission_id, :rover_location_id, :station_id], + |> validate_number(:band_mhz, greater_than: 0) + |> unique_constraint([:mission_id, :rover_location_id, :station_id, :band_mhz], name: :rover_mission_paths_unique ) end diff --git a/lib/microwaveprop/workers/rover_path_profile_worker.ex b/lib/microwaveprop/workers/rover_path_profile_worker.ex index 8f1028cc..1a5433c5 100644 --- a/lib/microwaveprop/workers/rover_path_profile_worker.ex +++ b/lib/microwaveprop/workers/rover_path_profile_worker.ex @@ -44,10 +44,8 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do end @impl Oban.Worker - def perform(%Oban.Job{ - args: %{"mission_id" => mission_id, "rover_location_id" => rover_location_id, "station_id" => station_id} - }) do - case load_path(mission_id, rover_location_id, station_id) do + def perform(%Oban.Job{args: args}) do + case load_path(args) do nil -> :ok @@ -59,21 +57,33 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do end end - defp load_path(mission_id, rover_location_id, station_id) do + defp load_path( + %{"mission_id" => mission_id, "rover_location_id" => rover_location_id, "station_id" => station_id} = args + ) do + band_mhz = Map.get(args, "band_mhz") + Path |> where([p], p.mission_id == ^mission_id) |> where([p], p.rover_location_id == ^rover_location_id) |> where([p], p.station_id == ^station_id) + |> maybe_filter_band(band_mhz) |> preload([:rover_location, :station, :mission]) |> Repo.one() end + # Legacy queued jobs (pre-multi-band) didn't include band_mhz in + # their args. The single Path row that existed for that tuple is the + # only candidate, so loading without the band filter is unambiguous. + defp maybe_filter_band(query, nil), do: query + defp maybe_filter_band(query, band) when is_integer(band), do: where(query, [p], p.band_mhz == ^band) + defp run(%Path{rover_location: rover, station: station, mission: mission} = path) do mark_status(path, :computing) + band_mhz = path.band_mhz || mission.band_mhz rover_height_m = (mission.rover_height_ft || 8.0) * 0.3048 station_height_m = (mission.station_height_ft || 30.0) * 0.3048 - freq_ghz = mission.band_mhz / 1000 + freq_ghz = band_mhz / 1000 dist_km = Geo.haversine_km(rover.lat, rover.lon, station.lat, station.lon) bearing = Geo.bearing_deg(rover.lat, rover.lon, station.lat, station.lon) @@ -94,10 +104,10 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do midlat = (rover.lat + station.lat) / 2 midlon = (rover.lon + station.lon) / 2 - propagation_score = lookup_propagation_score(mission.band_mhz, midlat, midlon) + propagation_score = lookup_propagation_score(band_mhz, midlat, midlon) loss_budget = - compute_loss_budget(dist_km, mission.band_mhz, freq_ghz, analysis.diffraction_db) + compute_loss_budget(dist_km, band_mhz, freq_ghz, analysis.diffraction_db) store_complete(path, profile, analysis, dist_km, bearing, propagation_score, loss_budget) diff --git a/lib/microwaveprop_web/live/rover_planning_live/form.ex b/lib/microwaveprop_web/live/rover_planning_live/form.ex index e1a85710..8c5496e4 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/form.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/form.ex @@ -181,6 +181,19 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do defp can_modify?(%User{id: id}, %Mission{user_id: id}) when not is_nil(id), do: true defp can_modify?(_, _), do: false + # Multi-checkbox values come from the form as a list of strings (or + # ints, depending on whether the changeset has cast them yet). Tolerate + # both, falling back to "not checked" for nil / empty bands lists. + defp band_checked?(values, target) when is_list(values) do + Enum.any?(values, fn + v when is_integer(v) -> v == target + v when is_binary(v) -> v == Integer.to_string(target) + _ -> false + end) + end + + defp band_checked?(_, _), do: false + @impl true def render(assigns) do ~H""" @@ -201,13 +214,41 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do > <.input field={@form[:name]} type="text" label="Mission name" placeholder="DFW August rove" /> -
- <.input - field={@form[:band_mhz]} - type="select" - label="Band" - options={@band_options} - /> +
+ + <%!-- + Hidden zero-value field so an unchecked-everywhere submit + still parses as an empty list (Phoenix drops the key + otherwise). Mission.changeset rejects an empty bands_mhz + with a friendly error. + --%> + +
+ +
+

+ {Phoenix.HTML.html_escape(Enum.map_join(@form[:bands_mhz].errors, ", ", &elem(&1, 0)))} +

+
+ +
<.input field={@form[:rover_height_ft]} type="number" diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index 72a6d2d0..3dabd22e 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -161,6 +161,14 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp band_label(mhz), do: "#{mhz} MHz" + defp bands_label(%Mission{} = mission) do + case Mission.bands(mission) do + [] -> "—" + [single] -> band_label(single) + list -> Enum.map_join(list, ", ", &band_label/1) + end + end + defp status_badge(:complete) do assigns = %{} ~H|Complete| @@ -244,11 +252,16 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do end defp group_to_pair({_id, [%{rover_location: location} | _] = group}) do - {location, Enum.sort_by(group, &station_position/1)} + {location, Enum.sort_by(group, &sort_key/1)} end defp group_to_pair({_id, []}), do: {nil, []} + # Sort within each rover-location group by station position (so all + # bands for a station cluster together) then ascending band — gives + # 902 → 1296 → 2304 → … in each station block. + defp sort_key(path), do: {station_position(path), path.band_mhz || 0} + defp station_position(%{station: %{position: pos}}) when is_integer(pos), do: pos defp station_position(_), do: 0 @@ -326,16 +339,17 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp path_url(_mission, %{rover_location: nil}), do: nil defp path_url(_mission, %{station: nil}), do: nil - defp path_url(%Mission{} = mission, %{rover_location: %{lat: lat, lon: lon}, station: station}) + defp path_url(%Mission{} = mission, %{rover_location: %{lat: lat, lon: lon}, station: station} = path) when is_number(lat) and is_number(lon) do src = Maidenhead.from_latlon(lat, lon, 10) dst = station_endpoint(station) + band = path.band_mhz || mission.band_mhz query = URI.encode_query(%{ "source" => src, "destination" => dst, - "band" => Integer.to_string(mission.band_mhz), + "band" => Integer.to_string(band), "src_height_ft" => Float.to_string(mission.rover_height_ft * 1.0), "dst_height_ft" => Float.to_string(mission.station_height_ft * 1.0) }) @@ -356,7 +370,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do <.header> {@mission.name} <:subtitle> - {band_label(@mission.band_mhz)} · {length(@mission.stations)} station(s) · + {bands_label(@mission)} · {length(@mission.stations)} station(s) · rover {@mission.rover_height_ft}ft / station {@mission.station_height_ft}ft <:actions> @@ -520,6 +534,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do Station + Band Status Distance Loss @@ -541,6 +556,9 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
{primary}
{secondary}
+ + {band_label(path.band_mhz || @mission.band_mhz)} + {status_badge(path.status)} {if path.result, do: format_distance(path.result["distance_km"]), else: "—"} diff --git a/priv/repo/migrations/20260503190225_add_multi_band_rover_planning.exs b/priv/repo/migrations/20260503190225_add_multi_band_rover_planning.exs new file mode 100644 index 00000000..f73e9be4 --- /dev/null +++ b/priv/repo/migrations/20260503190225_add_multi_band_rover_planning.exs @@ -0,0 +1,67 @@ +defmodule Microwaveprop.Repo.Migrations.AddMultiBandRoverPlanning do + use Ecto.Migration + + def up do + # Mission carries the SET of bands the operator wants to evaluate. + # `band_mhz` stays as a denormalized "primary band" for any legacy + # readers; new code reads `bands_mhz`. + alter table(:rover_missions) do + add :bands_mhz, {:array, :integer}, default: [], null: false + end + + execute( + "UPDATE rover_missions SET bands_mhz = ARRAY[band_mhz] WHERE coalesce(array_length(bands_mhz, 1), 0) = 0" + ) + + # Each computed path is now scoped to a specific band — the worker + # uses this for its loss budget + propagation-score lookup. + alter table(:rover_mission_paths) do + add :band_mhz, :integer + end + + execute(""" + UPDATE rover_mission_paths p + SET band_mhz = m.band_mhz + FROM rover_missions m + WHERE p.mission_id = m.id AND p.band_mhz IS NULL + """) + + alter table(:rover_mission_paths) do + modify :band_mhz, :integer, null: false + end + + drop_if_exists( + unique_index(:rover_mission_paths, [:mission_id, :rover_location_id, :station_id], + name: :rover_mission_paths_unique + ) + ) + + create unique_index( + :rover_mission_paths, + [:mission_id, :rover_location_id, :station_id, :band_mhz], + name: :rover_mission_paths_unique + ) + end + + def down do + drop_if_exists( + unique_index( + :rover_mission_paths, + [:mission_id, :rover_location_id, :station_id, :band_mhz], + name: :rover_mission_paths_unique + ) + ) + + create unique_index(:rover_mission_paths, [:mission_id, :rover_location_id, :station_id], + name: :rover_mission_paths_unique + ) + + alter table(:rover_mission_paths) do + remove :band_mhz + end + + alter table(:rover_missions) do + remove :bands_mhz + end + end +end diff --git a/test/microwaveprop/rover_planning_test.exs b/test/microwaveprop/rover_planning_test.exs index 1069c587..d79b7b50 100644 --- a/test/microwaveprop/rover_planning_test.exs +++ b/test/microwaveprop/rover_planning_test.exs @@ -148,6 +148,49 @@ defmodule Microwaveprop.RoverPlanningTest do assert path.status == :complete end + test "multi-band mission inserts one path per (rover, station, band)" do + user = AccountsFixtures.user_fixture() + _loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good}) + + attrs = + valid_attrs(%{ + "bands_mhz" => [10_000, 24_000, 47_000], + # Form may still include band_mhz (legacy primary band); normalize + # should overwrite it from bands_mhz in any case. + "band_mhz" => 10_000 + }) + + assert {:ok, mission} = RoverPlanning.create_mission(user, attrs) + assert mission.bands_mhz == [10_000, 24_000, 47_000] + + paths = RoverPlanning.list_paths(mission) + assert length(paths) == 3 + assert Enum.sort(Enum.map(paths, & &1.band_mhz)) == [10_000, 24_000, 47_000] + end + + test "reconcile drops paths whose band is no longer requested" do + user = AccountsFixtures.user_fixture() + _loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good}) + + attrs = valid_attrs(%{"bands_mhz" => [10_000, 24_000]}) + assert {:ok, mission} = RoverPlanning.create_mission(user, attrs) + assert length(RoverPlanning.list_paths(mission)) == 2 + + # User narrows the mission to a single band — reconcile should + # remove the now-unused 24 GHz row, keep the 10 GHz row. + mission = + mission + |> Mission.changeset(%{"bands_mhz" => [10_000]}) + |> Repo.update!() + |> Repo.preload([:user, :stations], force: true) + + :ok = RoverPlanning.replace_mission_paths(mission) + + paths = RoverPlanning.list_paths(mission) + assert length(paths) == 1 + assert hd(paths).band_mhz == 10_000 + end + test "with only_known_good=false, all rover-locations get a path entry" do user = AccountsFixtures.user_fixture() _ideal = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good}) diff --git a/test/microwaveprop_web/live/rover_planning_live_test.exs b/test/microwaveprop_web/live/rover_planning_live_test.exs index 432622ab..5526bf3b 100644 --- a/test/microwaveprop_web/live/rover_planning_live_test.exs +++ b/test/microwaveprop_web/live/rover_planning_live_test.exs @@ -590,7 +590,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do |> form("#mission-form", mission: %{ name: "Some name", - band_mhz: "10000", + bands_mhz: ["10000"], rover_height_ft: "8.0", station_height_ft: "30.0", only_known_good: "true", @@ -653,7 +653,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do |> form("#mission-form", mission: %{ name: "From form", - band_mhz: "10000", + bands_mhz: ["10000"], only_known_good: "true", rover_height_ft: "8.0", station_height_ft: "30.0",