feat(map): auto-advance cursor, full forecast timeline, panel spacing
Three UI fixes: 1. MapLive now ticks every 60s and, when the selected_time is still the "Now" slot, advances to the newer latest-past-or-now hour as the clock crosses into it. The URL is patched in place so shared links stay accurate without polluting history. Manually picking a specific past/future hour turns tracking off. 2. point_forecast/3 now reads its valid_times list straight from the on-disk .ntms files (same source the main-map timeline uses), then consults the cache per-hour for a fast score lookup. The old cache- or-store branch could leave the click-to-detail sparkline at 13 hours while the bottom timeline showed 14+; both now line up. 3. Point-detail panel moves from bottom-14 → bottom-24 on desktop so its lower edge clears the forecast-timeline bar (+ max-height tuned to match), fixing the overlap visible in screenshots.
This commit is contained in:
parent
d3a1e93587
commit
ffb14cb64f
4 changed files with 233 additions and 37 deletions
|
|
@ -355,40 +355,31 @@ defmodule Microwaveprop.Propagation do
|
||||||
{snapped_lat, snapped_lon} = snap_to_grid(lat, lon)
|
{snapped_lat, snapped_lon} = snap_to_grid(lat, lon)
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
case ScoreCache.valid_times(band_mhz) do
|
# Use the on-disk .ntms file list as the authoritative timeline so
|
||||||
[] -> point_forecast_from_store(band_mhz, snapped_lat, snapped_lon, now)
|
# the chart never falls behind the main-map timeline (which also
|
||||||
cached -> point_forecast_from_cache(band_mhz, snapped_lat, snapped_lon, now, cached)
|
# reads the disk). The cache is still consulted per-hour for a
|
||||||
end
|
# fast score lookup; a miss falls through to the file.
|
||||||
|
band_mhz
|
||||||
|
|> ScoresFile.list_valid_times()
|
||||||
|
|> forecast_window(now)
|
||||||
|
|> Enum.map(&point_forecast_entry(band_mhz, &1, snapped_lat, snapped_lon))
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp point_forecast_from_store(band_mhz, lat, lon, now) do
|
defp point_forecast_entry(band_mhz, valid_time, lat, lon) do
|
||||||
band_mhz
|
case ScoreCache.fetch_point(band_mhz, valid_time, lat, lon) do
|
||||||
|> ScoresFile.list_valid_times()
|
{:ok, score} ->
|
||||||
|> forecast_window(now)
|
%{valid_time: valid_time, score: score}
|
||||||
|> Enum.map(&point_forecast_entry(&1, band_mhz, lat, lon))
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp point_forecast_entry(valid_time, band_mhz, lat, lon) do
|
:miss ->
|
||||||
case ScoresFile.read_point(band_mhz, valid_time, lat, lon) do
|
case ScoresFile.read_point(band_mhz, valid_time, lat, lon) do
|
||||||
nil -> nil
|
nil -> nil
|
||||||
score -> %{valid_time: valid_time, score: score}
|
score -> %{valid_time: valid_time, score: score}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp point_forecast_from_cache(band_mhz, lat, lon, now, cached_times) do
|
|
||||||
cached_times
|
|
||||||
|> forecast_window(now)
|
|
||||||
|> Enum.map(fn t ->
|
|
||||||
case ScoreCache.fetch_point(band_mhz, t, lat, lon) do
|
|
||||||
{:ok, score} -> %{valid_time: t, score: score}
|
|
||||||
:miss -> nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Select the set of valid_times the forecast chart should render.
|
# Select the set of valid_times the forecast chart should render.
|
||||||
# Mirrors `available_valid_times`: keep everything from one hour
|
# Mirrors `available_valid_times`: keep everything from one hour
|
||||||
# before now onward so the most recent analysis hour (typically
|
# before now onward so the most recent analysis hour (typically
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,18 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
# visit for an in-progress hourly run, long enough to be cheap.
|
# visit for an in-progress hourly run, long enough to be cheap.
|
||||||
@pipeline_status_refresh_ms 15_000
|
@pipeline_status_refresh_ms 15_000
|
||||||
|
|
||||||
|
# How often to check whether the wall clock has ticked into a new
|
||||||
|
# forecast hour. 60s is frequent enough to advance within a minute
|
||||||
|
# of top-of-hour and cheap enough to run for every connected map.
|
||||||
|
@advance_now_cursor_ms 60_000
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(params, session, socket) do
|
def mount(params, session, socket) do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
|
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
|
||||||
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline")
|
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline")
|
||||||
Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
||||||
|
Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms)
|
||||||
end
|
end
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
|
|
@ -81,6 +87,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
initial_scores_json: Jason.encode!(initial_scores),
|
initial_scores_json: Jason.encode!(initial_scores),
|
||||||
valid_times: valid_times,
|
valid_times: valid_times,
|
||||||
selected_time: selected_time,
|
selected_time: selected_time,
|
||||||
|
tracking_now?: tracking_now?(selected_time, valid_times, DateTime.utc_now()),
|
||||||
bounds: bounds,
|
bounds: bounds,
|
||||||
initial_center: center,
|
initial_center: center,
|
||||||
initial_zoom: zoom,
|
initial_zoom: zoom,
|
||||||
|
|
@ -200,6 +207,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(selected_band: band, valid_times: valid_times, selected_time: selected_time)
|
|> assign(selected_band: band, valid_times: valid_times, selected_time: selected_time)
|
||||||
|
|> assign(:tracking_now?, tracking_now?(selected_time, valid_times, DateTime.utc_now()))
|
||||||
|> LiveStash.stash_assigns([:selected_band, :selected_time])
|
|> LiveStash.stash_assigns([:selected_band, :selected_time])
|
||||||
|> push_event("update_scores", %{scores: scores})
|
|> push_event("update_scores", %{scores: scores})
|
||||||
|> push_event("update_band_info", %{band_info: band_info(band)})
|
|> push_event("update_band_info", %{band_info: band_info(band)})
|
||||||
|
|
@ -217,6 +225,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:selected_time, time)
|
|> assign(:selected_time, time)
|
||||||
|
|> assign(:tracking_now?, tracking_now?(time, socket.assigns.valid_times, DateTime.utc_now()))
|
||||||
|> push_event("update_scores", %{scores: scores})
|
|> push_event("update_scores", %{scores: scores})
|
||||||
|> patch_map_url()
|
|> patch_map_url()
|
||||||
|
|
||||||
|
|
@ -233,7 +242,13 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
def handle_event("set_selected_time", %{"time" => time_str}, socket) do
|
def handle_event("set_selected_time", %{"time" => time_str}, socket) do
|
||||||
case DateTime.from_iso8601(time_str) do
|
case DateTime.from_iso8601(time_str) do
|
||||||
{:ok, time, _} ->
|
{:ok, time, _} ->
|
||||||
{:noreply, socket |> assign(:selected_time, time) |> patch_map_url()}
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:selected_time, time)
|
||||||
|
|> assign(:tracking_now?, tracking_now?(time, socket.assigns.valid_times, DateTime.utc_now()))
|
||||||
|
|> patch_map_url()
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -378,6 +393,32 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info(:advance_now_cursor, socket) do
|
||||||
|
Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms)
|
||||||
|
|
||||||
|
if socket.assigns.tracking_now? do
|
||||||
|
new_time = closest_to_now(socket.assigns.valid_times)
|
||||||
|
|
||||||
|
if new_time && socket.assigns.selected_time &&
|
||||||
|
DateTime.after?(new_time, socket.assigns.selected_time) do
|
||||||
|
scores = Propagation.scores_at(socket.assigns.selected_band, new_time, socket.assigns.bounds)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:selected_time, new_time)
|
||||||
|
|> push_event("update_scores", %{scores: scores})
|
||||||
|
|> push_timeline()
|
||||||
|
|> patch_map_url(replace: true)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info(:refresh_pipeline_status, socket) do
|
def handle_info(:refresh_pipeline_status, socket) do
|
||||||
Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
||||||
|
|
||||||
|
|
@ -563,6 +604,22 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
# True when the map's current selected_time is still the "Now" slot,
|
||||||
|
# i.e. the user hasn't scrubbed to a specific past/future hour. Used
|
||||||
|
# by :advance_now_cursor to decide whether to auto-follow the wall
|
||||||
|
# clock when it ticks into the next hour.
|
||||||
|
@spec tracking_now?(DateTime.t() | nil, [DateTime.t()], DateTime.t()) :: boolean()
|
||||||
|
def tracking_now?(nil, _times, _now), do: false
|
||||||
|
def tracking_now?(_selected, [], _now), do: false
|
||||||
|
|
||||||
|
def tracking_now?(%DateTime{} = selected, times, %DateTime{} = now) do
|
||||||
|
case cursor_for_now(times, now) do
|
||||||
|
nil -> false
|
||||||
|
cursor -> DateTime.compare(selected, cursor) == :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp score_range_km(score, config) do
|
defp score_range_km(score, config) do
|
||||||
cond do
|
cond do
|
||||||
score >= 80 -> config.exceptional_range_km
|
score >= 80 -> config.exceptional_range_km
|
||||||
|
|
@ -830,7 +887,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
class={[
|
class={[
|
||||||
"bg-neutral text-neutral-content shadow-lg border border-base-300 overflow-hidden overflow-y-auto z-[1001]",
|
"bg-neutral text-neutral-content shadow-lg border border-base-300 overflow-hidden overflow-y-auto z-[1001]",
|
||||||
"fixed bottom-0 left-0 right-0 max-h-[50vh] rounded-t-2xl",
|
"fixed bottom-0 left-0 right-0 max-h-[50vh] rounded-t-2xl",
|
||||||
"md:absolute md:top-auto md:right-auto md:bottom-14 md:left-3 md:max-h-[70vh] md:w-80 md:rounded-box"
|
"md:absolute md:top-auto md:right-auto md:bottom-24 md:left-3 md:max-h-[calc(100vh-8rem)] md:w-80 md:rounded-box"
|
||||||
]}
|
]}
|
||||||
style="display:none;"
|
style="display:none;"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -341,12 +341,24 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "point_forecast/3 with cache" do
|
describe "point_forecast/3 with cache" do
|
||||||
|
# Write a score to both disk and cache. Production always writes
|
||||||
|
# the store first, then warms the cache — nothing produces a
|
||||||
|
# cache-only record, so the tests mirror that invariant.
|
||||||
|
defp seed_point(band_mhz, valid_time, lat, lon, score) do
|
||||||
|
Propagation.replace_scores(
|
||||||
|
[%{lat: lat, lon: lon, valid_time: valid_time, band_mhz: band_mhz, score: score, factors: nil}],
|
||||||
|
valid_time
|
||||||
|
)
|
||||||
|
|
||||||
|
ScoreCache.put(band_mhz, valid_time, [%{lat: lat, lon: lon, score: score}])
|
||||||
|
end
|
||||||
|
|
||||||
test "returns forecast timeline from cached scores when warm" do
|
test "returns forecast timeline from cached scores when warm" do
|
||||||
t1 = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
t1 = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
||||||
t2 = DateTime.add(t1, 3600, :second)
|
t2 = DateTime.add(t1, 3600, :second)
|
||||||
|
|
||||||
ScoreCache.put(10_000, t1, [%{lat: 32.875, lon: -97.0, score: 70}])
|
seed_point(10_000, t1, 32.875, -97.0, 70)
|
||||||
ScoreCache.put(10_000, t2, [%{lat: 32.875, lon: -97.0, score: 75}])
|
seed_point(10_000, t2, 32.875, -97.0, 75)
|
||||||
|
|
||||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||||
|
|
||||||
|
|
@ -363,9 +375,9 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
recent = DateTime.utc_now() |> DateTime.add(-1800, :second) |> DateTime.truncate(:second)
|
||||||
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.truncate(:second)
|
||||||
|
|
||||||
ScoreCache.put(10_000, stale, [%{lat: 32.875, lon: -97.0, score: 40}])
|
seed_point(10_000, stale, 32.875, -97.0, 40)
|
||||||
ScoreCache.put(10_000, recent, [%{lat: 32.875, lon: -97.0, score: 60}])
|
seed_point(10_000, recent, 32.875, -97.0, 60)
|
||||||
ScoreCache.put(10_000, future, [%{lat: 32.875, lon: -97.0, score: 80}])
|
seed_point(10_000, future, 32.875, -97.0, 80)
|
||||||
|
|
||||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||||
|
|
||||||
|
|
@ -382,8 +394,8 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second)
|
||||||
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second)
|
||||||
|
|
||||||
ScoreCache.put(10_000, older, [%{lat: 32.875, lon: -97.0, score: 30}])
|
seed_point(10_000, older, 32.875, -97.0, 30)
|
||||||
ScoreCache.put(10_000, newer, [%{lat: 32.875, lon: -97.0, score: 45}])
|
seed_point(10_000, newer, 32.875, -97.0, 45)
|
||||||
|
|
||||||
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||||
assert [%{valid_time: ^newer, score: 45}] = result
|
assert [%{valid_time: ^newer, score: 45}] = result
|
||||||
|
|
@ -391,7 +403,7 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
|
|
||||||
test "snaps coordinates to the nearest grid point" do
|
test "snaps coordinates to the nearest grid point" do
|
||||||
valid_time = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
valid_time = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second)
|
||||||
ScoreCache.put(10_000, valid_time, [%{lat: 32.875, lon: -97.0, score: 65}])
|
seed_point(10_000, valid_time, 32.875, -97.0, 65)
|
||||||
|
|
||||||
# Off-grid query snaps to 32.875, -97.0
|
# Off-grid query snaps to 32.875, -97.0
|
||||||
result = Propagation.point_forecast(10_000, 32.88, -96.98)
|
result = Propagation.point_forecast(10_000, 32.88, -96.98)
|
||||||
|
|
@ -403,6 +415,34 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "point_forecast/3 authoritative timeline" do
|
||||||
|
test "includes hours present on disk but not yet in the cache" do
|
||||||
|
# Cache has the two earliest valid_times, but a newer forecast
|
||||||
|
# hour has just landed on disk. The forecast chart should include
|
||||||
|
# that third hour so it matches the main-map timeline.
|
||||||
|
t1 = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0})
|
||||||
|
t2 = DateTime.add(t1, 3600, :second)
|
||||||
|
t3 = DateTime.add(t1, 2 * 3600, :second)
|
||||||
|
|
||||||
|
for {t, score} <- [{t1, 50}, {t2, 60}, {t3, 70}] do
|
||||||
|
Propagation.replace_scores(
|
||||||
|
[%{lat: 32.875, lon: -97.0, valid_time: t, band_mhz: 10_000, score: score, factors: nil}],
|
||||||
|
t
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
ScoreCache.clear()
|
||||||
|
ScoreCache.put(10_000, t1, [%{lat: 32.875, lon: -97.0, score: 50}])
|
||||||
|
ScoreCache.put(10_000, t2, [%{lat: 32.875, lon: -97.0, score: 60}])
|
||||||
|
|
||||||
|
result = Propagation.point_forecast(10_000, 32.875, -97.0)
|
||||||
|
times = Enum.map(result, & &1.valid_time)
|
||||||
|
|
||||||
|
assert t3 in times
|
||||||
|
assert length(result) == 3
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "point_forecast/3 from store" do
|
describe "point_forecast/3 from store" do
|
||||||
test "includes recent past valid_times and falls back to the latest when stale" do
|
test "includes recent past valid_times and falls back to the latest when stale" do
|
||||||
# Three .ntms files on disk: one 2h old (dropped by cutoff),
|
# Three .ntms files on disk: one 2h old (dropped by cutoff),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
||||||
|
|
||||||
alias Microwaveprop.Propagation
|
alias Microwaveprop.Propagation
|
||||||
alias Microwaveprop.Propagation.ScoreCache
|
alias Microwaveprop.Propagation.ScoreCache
|
||||||
|
alias Phoenix.LiveView.Utils
|
||||||
|
|
||||||
describe "mount" do
|
describe "mount" do
|
||||||
test "renders full-page map", %{conn: conn} do
|
test "renders full-page map", %{conn: conn} do
|
||||||
|
|
@ -224,6 +225,113 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "tracking_now?/2" do
|
||||||
|
alias MicrowavepropWeb.MapLive
|
||||||
|
|
||||||
|
test "true when selected time equals the latest at-or-before-now slot" do
|
||||||
|
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z], ~U[2026-04-18 20:00:00Z]]
|
||||||
|
now = ~U[2026-04-18 19:35:00Z]
|
||||||
|
# closest_to_now at 19:35 with these slots is 19:00.
|
||||||
|
assert MapLive.tracking_now?(~U[2026-04-18 19:00:00Z], times, now) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "false when selected time is an earlier historical slot" do
|
||||||
|
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z], ~U[2026-04-18 20:00:00Z]]
|
||||||
|
now = ~U[2026-04-18 19:35:00Z]
|
||||||
|
assert MapLive.tracking_now?(~U[2026-04-18 18:00:00Z], times, now) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "false when selected time is a future forecast slot" do
|
||||||
|
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z], ~U[2026-04-18 20:00:00Z]]
|
||||||
|
now = ~U[2026-04-18 19:35:00Z]
|
||||||
|
assert MapLive.tracking_now?(~U[2026-04-18 20:00:00Z], times, now) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "false when valid_times is empty" do
|
||||||
|
assert MapLive.tracking_now?(~U[2026-04-18 19:00:00Z], [], ~U[2026-04-18 19:00:00Z]) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "false when selected_time is nil" do
|
||||||
|
times = [~U[2026-04-18 19:00:00Z]]
|
||||||
|
assert MapLive.tracking_now?(nil, times, ~U[2026-04-18 19:00:00Z]) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "advance_now_cursor handler" do
|
||||||
|
setup do
|
||||||
|
ScoreCache.clear()
|
||||||
|
on_exit(fn -> ScoreCache.clear() end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "advances selected_time to the newly-past hour when tracking now", %{conn: conn} do
|
||||||
|
# Seed two adjacent hours so there's somewhere to advance to. The
|
||||||
|
# later of the two is the one we want the cursor to snap to once
|
||||||
|
# the clock ticks past it.
|
||||||
|
previous_hour =
|
||||||
|
DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0}) |> DateTime.add(-1, :hour)
|
||||||
|
|
||||||
|
current_hour = DateTime.add(previous_hour, 3600, :second)
|
||||||
|
|
||||||
|
for t <- [previous_hour, current_hour] do
|
||||||
|
Propagation.replace_scores(
|
||||||
|
[%{lat: 33.0, lon: -97.0, valid_time: t, band_mhz: 10_000, score: 50, factors: nil}],
|
||||||
|
t
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
# Force the LV onto the previous hour and mark it as tracking_now?
|
||||||
|
# so the tick handler has a clear advance opportunity.
|
||||||
|
:sys.replace_state(lv.pid, fn state ->
|
||||||
|
socket =
|
||||||
|
state.socket
|
||||||
|
|> Utils.assign(:selected_time, previous_hour)
|
||||||
|
|> Utils.assign(:tracking_now?, true)
|
||||||
|
|
||||||
|
%{state | socket: socket}
|
||||||
|
end)
|
||||||
|
|
||||||
|
send(lv.pid, :advance_now_cursor)
|
||||||
|
_ = render(lv)
|
||||||
|
|
||||||
|
assigns = :sys.get_state(lv.pid).socket.assigns
|
||||||
|
assert assigns.selected_time == current_hour
|
||||||
|
end
|
||||||
|
|
||||||
|
test "leaves selected_time alone when the user is on a specific hour", %{conn: conn} do
|
||||||
|
previous_hour =
|
||||||
|
DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0}) |> DateTime.add(-1, :hour)
|
||||||
|
|
||||||
|
current_hour = DateTime.add(previous_hour, 3600, :second)
|
||||||
|
|
||||||
|
for t <- [previous_hour, current_hour] do
|
||||||
|
Propagation.replace_scores(
|
||||||
|
[%{lat: 33.0, lon: -97.0, valid_time: t, band_mhz: 10_000, score: 50, factors: nil}],
|
||||||
|
t
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
:sys.replace_state(lv.pid, fn state ->
|
||||||
|
socket =
|
||||||
|
state.socket
|
||||||
|
|> Utils.assign(:selected_time, previous_hour)
|
||||||
|
|> Utils.assign(:tracking_now?, false)
|
||||||
|
|
||||||
|
%{state | socket: socket}
|
||||||
|
end)
|
||||||
|
|
||||||
|
send(lv.pid, :advance_now_cursor)
|
||||||
|
_ = render(lv)
|
||||||
|
|
||||||
|
assigns = :sys.get_state(lv.pid).socket.assigns
|
||||||
|
assert assigns.selected_time == previous_hour
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "format_data_timestamp/1" do
|
describe "format_data_timestamp/1" do
|
||||||
alias MicrowavepropWeb.MapLive
|
alias MicrowavepropWeb.MapLive
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue