diff --git a/lib/microwaveprop_web/live/rover_locations_live/map.ex b/lib/microwaveprop_web/live/rover_locations_live/map.ex index f8f6955a..fec2dd0e 100644 --- a/lib/microwaveprop_web/live/rover_locations_live/map.ex +++ b/lib/microwaveprop_web/live/rover_locations_live/map.ex @@ -7,17 +7,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Map do import Ecto.Query + alias Microwaveprop.Cache alias Microwaveprop.Radio.Maidenhead alias Microwaveprop.Repo alias Microwaveprop.Rover.Location + @cache_key {__MODULE__, :points} + @cache_ttl_ms 30_000 + @impl true def mount(_params, _session, socket) do - points = - Location - |> where([l], l.status == :good) - |> Repo.all() - |> Enum.map(&point_payload/1) + points = cached_points() {:ok, assign(socket, @@ -27,6 +27,15 @@ defmodule MicrowavepropWeb.RoverLocationsLive.Map do )} end + defp cached_points do + Cache.fetch_or_store(@cache_key, @cache_ttl_ms, fn -> + Location + |> where([l], l.status == :good) + |> Repo.all() + |> Enum.map(&point_payload/1) + end) + end + defp point_payload(%Location{} = loc) do %{ id: loc.id, diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index 634c660f..a64c543e 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -51,7 +51,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do {:noreply, assign(socket, rover_site_input: input, - rover_site_input_matches: find_grid_matches(input) + rover_site_input_matches: find_grid_matches(input, socket.assigns.rover_sites) )} end @@ -112,20 +112,14 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp authenticated(_), do: {:error, :unauthenticated} - # Resolve the user-typed input and surface any existing rover-locations - # that share its 6-char Maidenhead grid (sub-grid resolution ≈ 5 km). - # Cheap enough for ~hundreds of locations: we map them in memory rather - # than maintaining a stored grid column. - defp find_grid_matches(input) when is_binary(input) do + defp find_grid_matches(input, locations) when is_binary(input) do trimmed = String.trim(input) case trimmed != "" && LocationResolver.resolve(trimmed) do {:ok, %{lat: lat, lon: lon}} when is_number(lat) and is_number(lon) -> target = Maidenhead.from_latlon(lat, lon, 6) - Location - |> Repo.all() - |> Enum.filter(fn loc -> + Enum.filter(locations, fn loc -> Maidenhead.from_latlon(loc.lat, loc.lon, 6) == target end) @@ -134,7 +128,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do end end - defp find_grid_matches(_), do: [] + defp find_grid_matches(_, _), do: [] defp can_modify?(%{current_scope: %{user: %User{is_admin: true}}}, _), do: true diff --git a/lib/microwaveprop_web/live/status_live.ex b/lib/microwaveprop_web/live/status_live.ex index 312887b0..f4f964f6 100644 --- a/lib/microwaveprop_web/live/status_live.ex +++ b/lib/microwaveprop_web/live/status_live.ex @@ -10,6 +10,9 @@ defmodule MicrowavepropWeb.StatusLive do alias Microwaveprop.Repo alias Microwaveprop.Weather.NarrClient + @all_stats_cache_key {__MODULE__, :all_stats} + @stats_cache_ttl_ms 2_000 + @impl true def mount(_params, _session, socket) do _ = @@ -19,24 +22,20 @@ defmodule MicrowavepropWeb.StatusLive do # Rust prop-grid-rs emits NOTIFY propagation_ready on completion — # PropagationNotifyListener fans it out as `propagation:updated` so # the grid_tasks panel transitions from "running" → "done" without - # a manual refresh. + # a single refresh. Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated") end - stats = fetch_stats() - unprocessed = count_unprocessed() - db_stats = fetch_db_stats() - grid_tasks = fetch_grid_tasks_stats() - hrrr_point_tasks = fetch_hrrr_point_tasks_stats() + all = fetch_all_stats() {:ok, assign(socket, page_title: "Status", - stats: stats, - unprocessed: unprocessed, - db_stats: db_stats, - grid_tasks: grid_tasks, - hrrr_point_tasks: hrrr_point_tasks, + stats: all.stats, + unprocessed: all.unprocessed, + db_stats: all.db_stats, + grid_tasks: all.grid_tasks, + hrrr_point_tasks: all.hrrr_point_tasks, refresh_timer: nil )} end @@ -55,25 +54,36 @@ defmodule MicrowavepropWeb.StatusLive do end def handle_info(:refresh_stats, socket) do + all = fetch_all_stats() + {:noreply, assign(socket, - stats: fetch_stats(), - unprocessed: count_unprocessed(), - db_stats: fetch_db_stats(), - grid_tasks: fetch_grid_tasks_stats(), - hrrr_point_tasks: fetch_hrrr_point_tasks_stats(), + stats: all.stats, + unprocessed: all.unprocessed, + db_stats: all.db_stats, + grid_tasks: all.grid_tasks, + hrrr_point_tasks: all.hrrr_point_tasks, refresh_timer: nil )} end + defp fetch_all_stats do + Cache.fetch_or_store(@all_stats_cache_key, @stats_cache_ttl_ms, fn -> + %{ + stats: fetch_stats(), + unprocessed: count_unprocessed(), + db_stats: fetch_db_stats(), + grid_tasks: fetch_grid_tasks_stats(), + hrrr_point_tasks: fetch_hrrr_point_tasks_stats() + } + end) + end + defp schedule_refresh(%{assigns: %{refresh_timer: ref}} = socket) when is_reference(ref) do - # Already scheduled, skip socket end defp schedule_refresh(socket) do - # 2s debounce — bulk enrichment runs produce hundreds of status_changed - # events per second, and each stats refresh fires 8+ count queries. ref = Process.send_after(self(), :refresh_stats, 2_000) assign(socket, refresh_timer: ref) end