diff --git a/lib/microwaveprop/accounts.ex b/lib/microwaveprop/accounts.ex index 0dd1d222..7d5c25b0 100644 --- a/lib/microwaveprop/accounts.ex +++ b/lib/microwaveprop/accounts.ex @@ -210,7 +210,7 @@ defmodule Microwaveprop.Accounts do def sudo_mode?(user, minutes \\ -20) def sudo_mode?(%User{authenticated_at: ts}, minutes) when is_struct(ts, DateTime) do - DateTime.after?(ts, DateTime.add(DateTime.utc_now(), minutes, :minute)) + DateTime.after?(ts, DateTime.shift(DateTime.utc_now(), minute: minutes)) end def sudo_mode?(_user, _minutes), do: false diff --git a/lib/microwaveprop/backtest.ex b/lib/microwaveprop/backtest.ex index 38e72aa3..56243e56 100644 --- a/lib/microwaveprop/backtest.ex +++ b/lib/microwaveprop/backtest.ex @@ -405,7 +405,7 @@ defmodule Microwaveprop.Backtest do contact = Enum.random(usable) {lat, lon} = pos_to_latlon(contact.pos1) offset_seconds = :rand.uniform(60 * 24 * 3600) - 30 * 24 * 3600 - time = DateTime.add(contact.qso_timestamp, offset_seconds, :second) + time = DateTime.shift(contact.qso_timestamp, second: offset_seconds) {lat, lon, time} end end diff --git a/lib/microwaveprop/backtest/features.ex b/lib/microwaveprop/backtest/features.ex index d433d199..5a90a3a2 100644 --- a/lib/microwaveprop/backtest/features.ex +++ b/lib/microwaveprop/backtest/features.ex @@ -306,8 +306,8 @@ defmodule Microwaveprop.Backtest.Features do def nexrad_texture(lat, lon, valid_time) do dlat = 0.1 dlon = 0.1 - time_start = DateTime.add(valid_time, -900, :second) - time_end = DateTime.add(valid_time, 900, :second) + time_start = DateTime.shift(valid_time, minute: -15) + time_end = DateTime.shift(valid_time, minute: 15) NexradObservation |> where( @@ -336,8 +336,8 @@ defmodule Microwaveprop.Backtest.Features do defp find_nearest_native(lat, lon, valid_time) do dlat = 0.07 dlon = 0.07 - time_start = DateTime.add(valid_time, -3600, :second) - time_end = DateTime.add(valid_time, 3600, :second) + time_start = DateTime.shift(valid_time, hour: -1) + time_end = DateTime.shift(valid_time, hour: 1) HrrrNativeProfile |> where( diff --git a/lib/microwaveprop/commercial.ex b/lib/microwaveprop/commercial.ex index c0c363ce..629ce898 100644 --- a/lib/microwaveprop/commercial.ex +++ b/lib/microwaveprop/commercial.ex @@ -64,8 +64,8 @@ defmodule Microwaveprop.Commercial do baseline_days = Keyword.get(opts, :baseline_days, @default_baseline_days) current_window = Keyword.get(opts, :current_window_seconds, @default_current_window_seconds) - baseline_cutoff = DateTime.add(valid_time, -baseline_days * 24 * 3600, :second) - current_cutoff = DateTime.add(valid_time, -current_window, :second) + baseline_cutoff = DateTime.shift(valid_time, day: -baseline_days) + current_cutoff = DateTime.shift(valid_time, second: -current_window) enabled_links() |> Enum.map(fn link -> {link, link_endpoint(link)} end) diff --git a/lib/microwaveprop/commercial/poll_worker.ex b/lib/microwaveprop/commercial/poll_worker.ex index 59ce06d6..f550905b 100644 --- a/lib/microwaveprop/commercial/poll_worker.ex +++ b/lib/microwaveprop/commercial/poll_worker.ex @@ -65,7 +65,7 @@ defmodule Microwaveprop.Commercial.PollWorker do |> Enum.uniq() now = DateTime.utc_now() - start_dt = DateTime.add(now, -3600, :second) + start_dt = DateTime.shift(now, hour: -1) stations |> Task.async_stream(&fetch_station_weather(&1, start_dt, now), diff --git a/lib/microwaveprop/moon.ex b/lib/microwaveprop/moon.ex index 9591c76b..8e91b875 100644 --- a/lib/microwaveprop/moon.ex +++ b/lib/microwaveprop/moon.ex @@ -171,8 +171,8 @@ defmodule Microwaveprop.Moon do """ @spec radial_velocity_km_s(DateTime.t(), float(), float()) :: float() def radial_velocity_km_s(%DateTime{} = dt, lat_deg, lon_deg) do - before = DateTime.add(dt, -1, :second) - later = DateTime.add(dt, 1, :second) + before = DateTime.shift(dt, second: -1) + later = DateTime.shift(dt, second: 1) d_before = observer_position(before, lat_deg, lon_deg).distance_km d_later = observer_position(later, lat_deg, lon_deg).distance_km diff --git a/lib/microwaveprop/propagation.ex b/lib/microwaveprop/propagation.ex index e23556a1..60525872 100644 --- a/lib/microwaveprop/propagation.ex +++ b/lib/microwaveprop/propagation.ex @@ -234,7 +234,7 @@ defmodule Microwaveprop.Propagation do """ @spec prune_old_scores() :: :ok def prune_old_scores do - cutoff = DateTime.add(DateTime.utc_now(), -3, :hour) + cutoff = DateTime.shift(DateTime.utc_now(), hour: -3) file_deleted = ScoresFile.prune_older_than(cutoff) profiles_deleted = ProfilesFile.prune_older_than(cutoff) scalar_deleted = ScalarFile.prune_older_than(cutoff) @@ -348,8 +348,8 @@ defmodule Microwaveprop.Propagation do @spec hot_cache_window() :: {DateTime.t(), DateTime.t()} def hot_cache_window do now = DateTime.utc_now() - past = DateTime.add(now, -3600, :second) - future = DateTime.add(now, @hrrr_forecast_horizon_hours * 3600, :second) + past = DateTime.shift(now, hour: -1) + future = DateTime.shift(now, hour: @hrrr_forecast_horizon_hours) {past, future} end @@ -556,8 +556,8 @@ defmodule Microwaveprop.Propagation do defp forecast_window([], _now), do: [] defp forecast_window(times, now) do - past_cutoff = DateTime.add(now, -3600, :second) - future_cutoff = DateTime.add(now, @hrrr_forecast_horizon_hours * 3600, :second) + past_cutoff = DateTime.shift(now, hour: -1) + future_cutoff = DateTime.shift(now, hour: @hrrr_forecast_horizon_hours) filter_or_latest(times, past_cutoff, future_cutoff) end @@ -687,7 +687,7 @@ defmodule Microwaveprop.Propagation do end defp latest_profile_time_within_lookback(%DateTime{} = valid_time) do - lookback_cutoff = DateTime.add(valid_time, -@fallback_profile_lookback_hours * 3600, :second) + lookback_cutoff = DateTime.shift(valid_time, hour: -@fallback_profile_lookback_hours) ProfilesFile.list_valid_times() |> Enum.filter(fn t -> diff --git a/lib/microwaveprop/propagation/grid_task_enqueuer.ex b/lib/microwaveprop/propagation/grid_task_enqueuer.ex index 958a3cfe..b1c38125 100644 --- a/lib/microwaveprop/propagation/grid_task_enqueuer.ex +++ b/lib/microwaveprop/propagation/grid_task_enqueuer.ex @@ -104,7 +104,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: fh, - valid_time: DateTime.add(run_time, fh * 3600, :second), + valid_time: DateTime.shift(run_time, hour: fh), status: "queued", attempt: 0, kind: "forecast", @@ -137,7 +137,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: fh, - valid_time: DateTime.add(run_time, fh * 3600, :second), + valid_time: DateTime.shift(run_time, hour: fh), status: "queued", attempt: 0, kind: "forecast", @@ -166,7 +166,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do """ @spec reclaim_stale_running(pos_integer()) :: %{requeued: non_neg_integer(), failed: non_neg_integer()} def reclaim_stale_running(cutoff_seconds \\ @stale_running_cutoff_seconds) do - cutoff = DateTime.utc_now() |> DateTime.add(-cutoff_seconds, :second) |> DateTime.truncate(:second) + cutoff = DateTime.utc_now() |> DateTime.shift(second: -cutoff_seconds) |> DateTime.truncate(:second) now = DateTime.truncate(DateTime.utc_now(), :second) {failed_n, requeued_n} = @@ -237,7 +237,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do id: Ecto.UUID.bingenerate(), run_time: run_time, forecast_hour: fh, - valid_time: DateTime.add(run_time, fh * 3600, :second), + valid_time: DateTime.shift(run_time, hour: fh), status: "queued", attempt: 0, kind: "forecast", diff --git a/lib/microwaveprop/pskr/calibration_sampler.ex b/lib/microwaveprop/pskr/calibration_sampler.ex index 6e216e46..26164d73 100644 --- a/lib/microwaveprop/pskr/calibration_sampler.ex +++ b/lib/microwaveprop/pskr/calibration_sampler.ex @@ -130,8 +130,8 @@ defmodule Microwaveprop.Pskr.CalibrationSampler do # ── HRRR feature lookup ───────────────────────────────────────── defp build_hrrr_index(hour_utc) do - time_start = DateTime.add(hour_utc, -@hrrr_lookahead_seconds, :second) - time_end = DateTime.add(hour_utc, @hrrr_lookahead_seconds, :second) + time_start = DateTime.shift(hour_utc, second: -@hrrr_lookahead_seconds) + time_end = DateTime.shift(hour_utc, second: @hrrr_lookahead_seconds) Repo.all( from(h in HrrrProfile, diff --git a/lib/microwaveprop/qrz.ex b/lib/microwaveprop/qrz.ex index e4868ef5..442f648d 100644 --- a/lib/microwaveprop/qrz.ex +++ b/lib/microwaveprop/qrz.ex @@ -35,7 +35,7 @@ defmodule Microwaveprop.Qrz do defp get_fresh_cache(callsign) do ttl_hours = config()[:cache_ttl_hours] || 168 - cutoff = DateTime.add(DateTime.utc_now(), -ttl_hours, :hour) + cutoff = DateTime.shift(DateTime.utc_now(), hour: -ttl_hours) Callsign |> where([c], c.callsign == ^callsign and c.updated_at > ^cutoff) diff --git a/lib/microwaveprop/radio/adif_import.ex b/lib/microwaveprop/radio/adif_import.ex index 62234cb0..51986a8a 100644 --- a/lib/microwaveprop/radio/adif_import.ex +++ b/lib/microwaveprop/radio/adif_import.ex @@ -322,8 +322,8 @@ defmodule Microwaveprop.Radio.AdifImport do defp load_existing_for_dedup(valid_rows) do bands = valid_rows |> Enum.map(& &1.attrs["band"]) |> Enum.uniq() |> Enum.map(&Decimal.new/1) timestamps = Enum.map(valid_rows, & &1.timestamp) - min_ts = timestamps |> Enum.min(DateTime) |> DateTime.add(-@dedup_window_seconds, :second) - max_ts = timestamps |> Enum.max(DateTime) |> DateTime.add(@dedup_window_seconds, :second) + min_ts = timestamps |> Enum.min(DateTime) |> DateTime.shift(second: -@dedup_window_seconds) + max_ts = timestamps |> Enum.max(DateTime) |> DateTime.shift(second: @dedup_window_seconds) from(c in Contact, where: diff --git a/lib/microwaveprop/radio/contacts.ex b/lib/microwaveprop/radio/contacts.ex index 0c0952dd..af635086 100644 --- a/lib/microwaveprop/radio/contacts.ex +++ b/lib/microwaveprop/radio/contacts.ex @@ -610,8 +610,8 @@ defmodule Microwaveprop.Radio.Contacts do band = Ecto.Changeset.get_field(changeset, :band) ts = Ecto.Changeset.get_field(changeset, :qso_timestamp) band_decimal = if is_binary(band), do: Decimal.new(band), else: band - min_ts = DateTime.add(ts, -@dedup_window_seconds, :second) - max_ts = DateTime.add(ts, @dedup_window_seconds, :second) + min_ts = DateTime.shift(ts, second: -@dedup_window_seconds) + max_ts = DateTime.shift(ts, second: @dedup_window_seconds) Repo.one( from(c in Contact, diff --git a/lib/microwaveprop/radio/csv_import.ex b/lib/microwaveprop/radio/csv_import.ex index b30cf826..d7c547fc 100644 --- a/lib/microwaveprop/radio/csv_import.ex +++ b/lib/microwaveprop/radio/csv_import.ex @@ -536,8 +536,8 @@ defmodule Microwaveprop.Radio.CsvImport do defp load_existing_for_dedup(valid_rows) do bands = valid_rows |> Enum.map(& &1.attrs["band"]) |> Enum.uniq() |> Enum.map(&to_decimal/1) timestamps = Enum.map(valid_rows, & &1.timestamp) - min_ts = timestamps |> Enum.min(DateTime) |> DateTime.add(-@dedup_window_seconds, :second) - max_ts = timestamps |> Enum.max(DateTime) |> DateTime.add(@dedup_window_seconds, :second) + min_ts = timestamps |> Enum.min(DateTime) |> DateTime.shift(second: -@dedup_window_seconds) + max_ts = timestamps |> Enum.max(DateTime) |> DateTime.shift(second: @dedup_window_seconds) from(c in Contact, where: diff --git a/lib/microwaveprop/weather/hrdps_client.ex b/lib/microwaveprop/weather/hrdps_client.ex index bf0cdea3..5c06aabe 100644 --- a/lib/microwaveprop/weather/hrdps_client.ex +++ b/lib/microwaveprop/weather/hrdps_client.ex @@ -109,7 +109,7 @@ defmodule Microwaveprop.Weather.HrdpsClient do def nearest_hrdps_cycle(%DateTime{} = dt) do cycle_hour = div(dt.hour, 6) * 6 seconds_into_cycle = (dt.hour - cycle_hour) * 3600 + dt.minute * 60 + dt.second - DateTime.add(dt, -seconds_into_cycle, :second) + DateTime.shift(dt, second: -seconds_into_cycle) end @doc """ diff --git a/lib/microwaveprop/weather/hrrr.ex b/lib/microwaveprop/weather/hrrr.ex index 60a60c39..4467ffb5 100644 --- a/lib/microwaveprop/weather/hrrr.ex +++ b/lib/microwaveprop/weather/hrrr.ex @@ -112,8 +112,8 @@ defmodule Microwaveprop.Weather.Hrrr do def nearest_native_duct_info(lat, lon, %DateTime{} = timestamp) do dlat = 0.07 dlon = 0.07 - time_start = DateTime.add(timestamp, -3600, :second) - time_end = DateTime.add(timestamp, 3600, :second) + time_start = DateTime.shift(timestamp, hour: -1) + time_end = DateTime.shift(timestamp, hour: 1) from(h in HrrrNativeProfile, where: diff --git a/lib/microwaveprop/weather/hrrr_client.ex b/lib/microwaveprop/weather/hrrr_client.ex index b03d66f3..1526e066 100644 --- a/lib/microwaveprop/weather/hrrr_client.ex +++ b/lib/microwaveprop/weather/hrrr_client.ex @@ -234,7 +234,7 @@ defmodule Microwaveprop.Weather.HrrrClient do @spec nearest_hrrr_hour(DateTime.t()) :: DateTime.t() def nearest_hrrr_hour(dt) do total_seconds = dt.minute * 60 + dt.second - DateTime.add(dt, -total_seconds, :second) + DateTime.shift(dt, second: -total_seconds) end @spec hrrr_url(Date.t(), non_neg_integer(), :surface | :pressure, non_neg_integer()) :: String.t() diff --git a/lib/microwaveprop/weather/nexrad_client.ex b/lib/microwaveprop/weather/nexrad_client.ex index 5d5bf8e0..d7e65257 100644 --- a/lib/microwaveprop/weather/nexrad_client.ex +++ b/lib/microwaveprop/weather/nexrad_client.ex @@ -105,7 +105,7 @@ defmodule Microwaveprop.Weather.NexradClient do :miss -> with {:ok, pixels, width} <- fetch_and_decode_frame(rounded) do NexradCache.put(rounded, pixels, width) - NexradCache.prune_older_than(DateTime.add(rounded, -600, :second)) + NexradCache.prune_older_than(DateTime.shift(rounded, minute: -10)) {:ok, pixels, width} end end diff --git a/lib/microwaveprop/weather/profile_lookup.ex b/lib/microwaveprop/weather/profile_lookup.ex index a96acf83..e6b1a244 100644 --- a/lib/microwaveprop/weather/profile_lookup.ex +++ b/lib/microwaveprop/weather/profile_lookup.ex @@ -130,8 +130,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do def find_nearest_hrrr(lat, lon, timestamp) do dlat = 0.07 dlon = 0.07 - time_start = DateTime.add(timestamp, -3600, :second) - time_end = DateTime.add(timestamp, 3600, :second) + time_start = DateTime.shift(timestamp, hour: -1) + time_end = DateTime.shift(timestamp, hour: 1) HrrrProfile |> where( @@ -211,8 +211,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do lat_max = Enum.max(lats) + d lon_min = Enum.min(lons) - d lon_max = Enum.max(lons) + d - time_min = times |> Enum.min_by(&DateTime.to_unix/1) |> DateTime.add(-margin_s, :second) - time_max = times |> Enum.max_by(&DateTime.to_unix/1) |> DateTime.add(margin_s, :second) + time_min = times |> Enum.min_by(&DateTime.to_unix/1) |> DateTime.shift(second: -margin_s) + time_max = times |> Enum.max_by(&DateTime.to_unix/1) |> DateTime.shift(second: margin_s) profiles = Repo.all( @@ -244,8 +244,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do def find_nearest_native_profile(lat, lon, timestamp) do dlat = 0.07 dlon = 0.07 - time_start = DateTime.add(timestamp, -3600, :second) - time_end = DateTime.add(timestamp, 3600, :second) + time_start = DateTime.shift(timestamp, hour: -1) + time_end = DateTime.shift(timestamp, hour: 1) HrrrNativeProfile |> where( @@ -306,8 +306,8 @@ defmodule Microwaveprop.Weather.ProfileLookup do def find_nearest_narr(lat, lon, timestamp) do dlat = 0.15 dlon = 0.15 - time_start = DateTime.add(timestamp, -1800, :second) - time_end = DateTime.add(timestamp, 1800, :second) + time_start = DateTime.shift(timestamp, minute: -30) + time_end = DateTime.shift(timestamp, minute: 30) NarrProfile |> where( diff --git a/lib/microwaveprop/weather/soundings.ex b/lib/microwaveprop/weather/soundings.ex index ac8975ed..9b4ec77b 100644 --- a/lib/microwaveprop/weather/soundings.ex +++ b/lib/microwaveprop/weather/soundings.ex @@ -104,8 +104,8 @@ defmodule Microwaveprop.Weather.Soundings do dlat = radius_km / @km_per_deg_lat dlon = radius_km / (@km_per_deg_lat * :math.cos(lat * :math.pi() / 180)) - time_start = DateTime.add(timestamp, -time_window_hours * 3600, :second) - time_end = DateTime.add(timestamp, time_window_hours * 3600, :second) + time_start = DateTime.shift(timestamp, hour: -time_window_hours) + time_end = DateTime.shift(timestamp, hour: time_window_hours) station_ids = Station @@ -176,8 +176,8 @@ defmodule Microwaveprop.Weather.Soundings do # 1 deg lat ≈ 111 km; lon scaled by cos(lat). dlat = radius_km / 111.0 dlon = radius_km / (111.0 * max(0.1, :math.cos(lat * :math.pi() / 180.0))) - time_start = DateTime.add(timestamp, -hours * 3600, :second) - time_end = DateTime.add(timestamp, hours * 3600, :second) + time_start = DateTime.shift(timestamp, hour: -hours) + time_end = DateTime.shift(timestamp, hour: hours) from(s in Sounding, join: station in assoc(s, :station), diff --git a/lib/microwaveprop/workers/backfill_enqueue_worker.ex b/lib/microwaveprop/workers/backfill_enqueue_worker.ex index 98c3f19f..4ddfd399 100644 --- a/lib/microwaveprop/workers/backfill_enqueue_worker.ex +++ b/lib/microwaveprop/workers/backfill_enqueue_worker.ex @@ -132,7 +132,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorker do defp reconcile_stale_queued_to_unavailable(types) do cutoff = DateTime.utc_now() - |> DateTime.add(-@stale_queued_cutoff_seconds, :second) + |> DateTime.shift(second: -@stale_queued_cutoff_seconds) |> DateTime.truncate(:second) now = DateTime.truncate(DateTime.utc_now(), :second) @@ -259,7 +259,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorker do defp type_filter(types) do reprobe_cutoff = DateTime.utc_now() - |> DateTime.add(-@unavailable_reprobe_cooldown_seconds, :second) + |> DateTime.shift(second: -@unavailable_reprobe_cooldown_seconds) |> DateTime.truncate(:second) Enum.reduce(types, dynamic(false), fn diff --git a/lib/microwaveprop/workers/canadian_sounding_fetch_worker.ex b/lib/microwaveprop/workers/canadian_sounding_fetch_worker.ex index e1ef726f..fb92aec9 100644 --- a/lib/microwaveprop/workers/canadian_sounding_fetch_worker.ex +++ b/lib/microwaveprop/workers/canadian_sounding_fetch_worker.ex @@ -59,7 +59,7 @@ defmodule Microwaveprop.Workers.CanadianSoundingFetchWorker do """ @spec most_recent_sounding_time(DateTime.t()) :: DateTime.t() def most_recent_sounding_time(%DateTime{} = now) do - cutoff = DateTime.add(now, -@publish_delay_seconds, :second) + cutoff = DateTime.shift(now, second: -@publish_delay_seconds) if cutoff.hour >= 12 do %{cutoff | hour: 12, minute: 0, second: 0, microsecond: {0, 0}} diff --git a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex index 70a1117c..41f7d031 100644 --- a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex +++ b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex @@ -520,8 +520,8 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do end defp build_asos_jobs(lat, lon, timestamp) do - start_dt = DateTime.add(timestamp, -2 * 3600, :second) - end_dt = DateTime.add(timestamp, 2 * 3600, :second) + start_dt = DateTime.shift(timestamp, hour: -2) + end_dt = DateTime.shift(timestamp, hour: 2) stations = Weather.nearby_stations(lat, lon, "asos", @asos_radius_km) diff --git a/lib/microwaveprop/workers/gefs_fetch_worker.ex b/lib/microwaveprop/workers/gefs_fetch_worker.ex index 2a5a579d..ea497368 100644 --- a/lib/microwaveprop/workers/gefs_fetch_worker.ex +++ b/lib/microwaveprop/workers/gefs_fetch_worker.ex @@ -37,7 +37,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do case GefsClient.fetch_grid_profiles(run_date, run_hour, fh) do {:ok, profiles} -> - valid_time = run_time |> DateTime.add(fh * 3600, :second) |> DateTime.truncate(:second) + valid_time = run_time |> DateTime.shift(hour: fh) |> DateTime.truncate(:second) score_and_persist(profiles, run_time, fh, valid_time) {:error, :wgrib2_not_available} -> @@ -96,7 +96,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do @spec most_recent_available_run(DateTime.t()) :: DateTime.t() def most_recent_available_run(%DateTime{} = now) do now - |> DateTime.add(-5 * 3600, :second) + |> DateTime.shift(hour: -5) |> GefsClient.nearest_run() end @@ -107,7 +107,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do """ @spec build_profile_attrs(DateTime.t(), non_neg_integer(), map()) :: map() def build_profile_attrs(%DateTime{} = run_time, forecast_hour, profile) when is_integer(forecast_hour) do - valid_time = DateTime.add(run_time, forecast_hour * 3600, :second) + valid_time = DateTime.shift(run_time, hour: forecast_hour) derived = SoundingParams.derive(profile.profile || []) base = %{ diff --git a/lib/microwaveprop/workers/grid_cache_prune_worker.ex b/lib/microwaveprop/workers/grid_cache_prune_worker.ex index c38bfacb..dd0f3e7c 100644 --- a/lib/microwaveprop/workers/grid_cache_prune_worker.ex +++ b/lib/microwaveprop/workers/grid_cache_prune_worker.ex @@ -21,7 +21,7 @@ defmodule Microwaveprop.Workers.GridCachePruneWorker do @impl Oban.Pro.Worker def process(%Oban.Job{}) do - cutoff = DateTime.add(DateTime.utc_now(), -@cutoff_hours, :hour) + cutoff = DateTime.shift(DateTime.utc_now(), hour: -@cutoff_hours) _removed = GridCache.prune_older_than(cutoff) :ok end diff --git a/lib/microwaveprop/workers/hrdps_grid_worker.ex b/lib/microwaveprop/workers/hrdps_grid_worker.ex index fa43c669..52e92135 100644 --- a/lib/microwaveprop/workers/hrdps_grid_worker.ex +++ b/lib/microwaveprop/workers/hrdps_grid_worker.ex @@ -87,12 +87,12 @@ defmodule Microwaveprop.Workers.HrdpsGridWorker do """ @spec pick_run_time(DateTime.t()) :: DateTime.t() def pick_run_time(%DateTime{} = now) do - fresh = now |> DateTime.add(-4, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second) + fresh = now |> DateTime.shift(hour: -4) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second) if HrdpsClient.cycle_available?(fresh) do fresh else - now |> DateTime.add(-10, :hour) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second) + now |> DateTime.shift(hour: -10) |> HrdpsClient.nearest_hrdps_cycle() |> DateTime.truncate(:second) end end end diff --git a/lib/microwaveprop/workers/hrrr_native_grid_worker.ex b/lib/microwaveprop/workers/hrrr_native_grid_worker.ex index 7ebc096e..0ab8ea82 100644 --- a/lib/microwaveprop/workers/hrrr_native_grid_worker.ex +++ b/lib/microwaveprop/workers/hrrr_native_grid_worker.ex @@ -61,8 +61,8 @@ defmodule Microwaveprop.Workers.HrrrNativeGridWorker do @batch_size 1000 def points_of_interest_for_hour(valid_time) do - time_start = DateTime.add(valid_time, -1800, :second) - time_end = DateTime.add(valid_time, 1800, :second) + time_start = DateTime.shift(valid_time, minute: -30) + time_end = DateTime.shift(valid_time, minute: 30) Contact |> where([c], not is_nil(c.pos1)) diff --git a/lib/microwaveprop/workers/ionosphere_fetch_worker.ex b/lib/microwaveprop/workers/ionosphere_fetch_worker.ex index d94958f3..9430c6fc 100644 --- a/lib/microwaveprop/workers/ionosphere_fetch_worker.ex +++ b/lib/microwaveprop/workers/ionosphere_fetch_worker.ex @@ -36,7 +36,7 @@ defmodule Microwaveprop.Workers.IonosphereFetchWorker do @impl Oban.Pro.Worker def process(%Oban.Job{}) do now = DateTime.truncate(DateTime.utc_now(), :second) - from_dt = DateTime.add(now, -@lookback_seconds, :second) + from_dt = DateTime.shift(now, second: -@lookback_seconds) Enum.each(@stations, fn station -> fetch_and_upsert(station, from_dt, now) diff --git a/lib/microwaveprop/workers/mechanism_classify_worker.ex b/lib/microwaveprop/workers/mechanism_classify_worker.ex index 276e0fcd..e39b5ca0 100644 --- a/lib/microwaveprop/workers/mechanism_classify_worker.ex +++ b/lib/microwaveprop/workers/mechanism_classify_worker.ex @@ -136,7 +136,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do from(h in HrrrProfile, where: h.lat >= ^(lat - 0.07) and h.lat <= ^(lat + 0.07) and h.lon >= ^(lon - 0.07) and h.lon <= ^(lon + 0.07) and - h.valid_time >= ^DateTime.add(ts, -3600, :second) and h.valid_time <= ^DateTime.add(ts, 3600, :second), + h.valid_time >= ^DateTime.shift(ts, hour: -1) and h.valid_time <= ^DateTime.shift(ts, hour: 1), order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", h.valid_time, ^ts), limit: 1 ) @@ -153,7 +153,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do from(h in "hrrr_native_profiles", where: h.lat >= ^(lat - 0.07) and h.lat <= ^(lat + 0.07) and h.lon >= ^(lon - 0.07) and h.lon <= ^(lon + 0.07) and - h.valid_time >= ^DateTime.add(ts, -3600, :second) and h.valid_time <= ^DateTime.add(ts, 3600, :second), + h.valid_time >= ^DateTime.shift(ts, hour: -1) and h.valid_time <= ^DateTime.shift(ts, hour: 1), order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", h.valid_time, ^ts), limit: 1, select: h.best_duct_band_ghz @@ -185,7 +185,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do defp foes_for(%Contact{qso_timestamp: ts}) do Repo.one( from(io in IonosondeObservation, - where: io.valid_time >= ^DateTime.add(ts, -3600, :second) and io.valid_time <= ^DateTime.add(ts, 3600, :second), + where: io.valid_time >= ^DateTime.shift(ts, hour: -1) and io.valid_time <= ^DateTime.shift(ts, hour: 1), where: not is_nil(io.fo_es_mhz), order_by: fragment("ABS(EXTRACT(EPOCH FROM ? - ?))", io.valid_time, ^ts), limit: 1, diff --git a/lib/microwaveprop/workers/narr_fetch_worker.ex b/lib/microwaveprop/workers/narr_fetch_worker.ex index f264f44e..5eb3e125 100644 --- a/lib/microwaveprop/workers/narr_fetch_worker.ex +++ b/lib/microwaveprop/workers/narr_fetch_worker.ex @@ -84,8 +84,8 @@ defmodule Microwaveprop.Workers.NarrFetchWorker do # Lat/lon tolerance matches the 0.25° dedup granularity. dlat = 0.13 dlon = 0.13 - time_start = DateTime.add(valid_time, -900, :second) - time_end = DateTime.add(valid_time, 900, :second) + time_start = DateTime.shift(valid_time, minute: -15) + time_end = DateTime.shift(valid_time, minute: 15) NarrProfile |> where( diff --git a/lib/microwaveprop/workers/nexrad_worker.ex b/lib/microwaveprop/workers/nexrad_worker.ex index f15c2f83..1d634a36 100644 --- a/lib/microwaveprop/workers/nexrad_worker.ex +++ b/lib/microwaveprop/workers/nexrad_worker.ex @@ -47,8 +47,8 @@ defmodule Microwaveprop.Workers.NexradWorker do @spec points_of_interest_for_time(DateTime.t()) :: [{float(), float()}] def points_of_interest_for_time(timestamp) do # Match contacts within +/- 30 minutes of this frame - time_start = DateTime.add(timestamp, -1800, :second) - time_end = DateTime.add(timestamp, 1800, :second) + time_start = DateTime.shift(timestamp, minute: -30) + time_end = DateTime.shift(timestamp, minute: 30) Contact |> where([c], not is_nil(c.pos1)) diff --git a/lib/microwaveprop/workers/propagation_grid_worker.ex b/lib/microwaveprop/workers/propagation_grid_worker.ex index 147142a4..9a56c95f 100644 --- a/lib/microwaveprop/workers/propagation_grid_worker.ex +++ b/lib/microwaveprop/workers/propagation_grid_worker.ex @@ -85,12 +85,12 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do """ @spec pick_run_time(DateTime.t()) :: DateTime.t() def pick_run_time(%DateTime{} = now) do - fresh = now |> DateTime.add(-1, :hour) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second) + fresh = now |> DateTime.shift(hour: -1) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second) if HrrrClient.cycle_available?(fresh) do fresh else - now |> DateTime.add(-2, :hour) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second) + now |> DateTime.shift(hour: -2) |> HrrrClient.nearest_hrrr_hour() |> DateTime.truncate(:second) end end end diff --git a/lib/microwaveprop/workers/propagation_prune_worker.ex b/lib/microwaveprop/workers/propagation_prune_worker.ex index 130e757f..73bd893e 100644 --- a/lib/microwaveprop/workers/propagation_prune_worker.ex +++ b/lib/microwaveprop/workers/propagation_prune_worker.ex @@ -28,7 +28,7 @@ defmodule Microwaveprop.Workers.PropagationPruneWorker do def process(%Oban.Job{}) do Propagation.prune_old_scores() - cutoff_30d = DateTime.add(DateTime.utc_now(), -30, :day) + cutoff_30d = DateTime.shift(DateTime.utc_now(), day: -30) buildings_deleted = MsFootprints.prune_older_than(cutoff_30d) canopy_deleted = Canopy.prune_older_than(cutoff_30d) diff --git a/lib/microwaveprop/workers/pskr_calibration_worker.ex b/lib/microwaveprop/workers/pskr_calibration_worker.ex index a7407bc5..d408f101 100644 --- a/lib/microwaveprop/workers/pskr_calibration_worker.ex +++ b/lib/microwaveprop/workers/pskr_calibration_worker.ex @@ -85,7 +85,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorker do now = truncate_to_hour(DateTime.utc_now()) Enum.each(0..lookback, fn offset -> - hour = DateTime.add(now, -offset * 3600, :second) + hour = DateTime.shift(now, hour: -offset) process_hour(hour, args) end) diff --git a/lib/microwaveprop/workers/radar_frame_worker.ex b/lib/microwaveprop/workers/radar_frame_worker.ex index ed512b8f..e1d75d39 100644 --- a/lib/microwaveprop/workers/radar_frame_worker.ex +++ b/lib/microwaveprop/workers/radar_frame_worker.ex @@ -40,7 +40,7 @@ defmodule Microwaveprop.Workers.RadarFrameWorker do def process(%Oban.Job{args: %{"frame_ts" => frame_iso}}) do {:ok, frame_ts, _} = DateTime.from_iso8601(frame_iso) rounded = NexradClient.round_to_5min(frame_ts) - bucket_end = DateTime.add(rounded, @frame_window_seconds, :second) + bucket_end = DateTime.shift(rounded, second: @frame_window_seconds) # Query by the frame's 5-min bucket rather than a passed contact_ids # list. The unique-on-frame_ts guard prevents new sweeps from enqueueing diff --git a/lib/microwaveprop/workers/weather_fetch_worker.ex b/lib/microwaveprop/workers/weather_fetch_worker.ex index 0234e528..8b585c47 100644 --- a/lib/microwaveprop/workers/weather_fetch_worker.ex +++ b/lib/microwaveprop/workers/weather_fetch_worker.ex @@ -200,7 +200,7 @@ defmodule Microwaveprop.Workers.WeatherFetchWorker do end defp store_asos_stub(station, start_dt, end_dt) do - midpoint = DateTime.add(start_dt, div(DateTime.diff(end_dt, start_dt), 2)) + midpoint = DateTime.shift(start_dt, second: div(DateTime.diff(end_dt, start_dt), 2)) %SurfaceObservation{} |> SurfaceObservation.changeset(%{station_id: station.id, observed_at: midpoint}) diff --git a/lib/microwaveprop_web/live/skewt_live.ex b/lib/microwaveprop_web/live/skewt_live.ex index b6cfb0bb..00584fb4 100644 --- a/lib/microwaveprop_web/live/skewt_live.ex +++ b/lib/microwaveprop_web/live/skewt_live.ex @@ -275,7 +275,7 @@ defmodule MicrowavepropWeb.SkewtLive do if DateTime.after?(valid_time, now) do cycle = now - |> DateTime.add(-2 * 3600, :second) + |> DateTime.shift(hour: -2) |> HrrrClient.nearest_hrrr_hour() fh = max(0, div(DateTime.diff(valid_time, cycle, :second), 3600)) @@ -345,8 +345,8 @@ defmodule MicrowavepropWeb.SkewtLive do # ~2 h old. A 1-hour past cutoff would leave the page empty while # the chain catches up — keep 3 h so a stale analysis still shows. now = DateTime.utc_now() - past_cutoff = DateTime.add(now, -3 * 3600, :second) - future_cutoff = DateTime.add(now, 18 * 3600, :second) + past_cutoff = DateTime.shift(now, hour: -3) + future_cutoff = DateTime.shift(now, hour: 18) on_disk = Enum.filter(ProfilesFile.list_valid_times(), fn t -> diff --git a/lib/microwaveprop_web/live/weather_map_live.ex b/lib/microwaveprop_web/live/weather_map_live.ex index 671a90eb..c382520e 100644 --- a/lib/microwaveprop_web/live/weather_map_live.ex +++ b/lib/microwaveprop_web/live/weather_map_live.ex @@ -168,7 +168,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do # showing 3-hour-old "current conditions" on the map is more # misleading than helpful. defp recent_valid_times(valid_times) do - cutoff = DateTime.add(DateTime.utc_now(), -3600, :second) + cutoff = DateTime.shift(DateTime.utc_now(), hour: -1) Enum.filter(valid_times, fn vt -> DateTime.compare(vt, cutoff) != :lt end) end diff --git a/mix.lock b/mix.lock index a1ce11e3..8e53e8ff 100644 --- a/mix.lock +++ b/mix.lock @@ -36,8 +36,8 @@ "live_stash": {:hex, :live_stash, "1.0.1", "26f46749b2b7d6484ff653b15d616b79e85b99891b3217e4584e54e88163fb98", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: true]}, {:memento, "~> 0.5.0", [hex: :memento, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: true]}, {:uniq, "~> 0.6", [hex: :uniq, repo: "hexpm", optional: false]}], "hexpm", "3767556fce36c2645479d8807a1d4992e5c2261f0b4b44e58d8a88e5bd313a47"}, "live_table": {:hex, :live_table, "0.4.2", "9c011a8a078db3b86a2e77afe141d201d641512a1a9f421b62c4e14c41a4a5a9", [:mix], [{:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, "~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.3", [hex: :nimble_csv, repo: "hexpm", optional: false]}, {:oban, "~> 2.20", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_web, "~> 2.11", [hex: :oban_web, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:sutra_ui, "~> 0.4.0", [hex: :sutra_ui, repo: "hexpm", optional: true]}], "hexpm", "7c00c8469a974ff6abe79e34a940eba1f813d95f05360713313ece9e793f6c38"}, "logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"}, - "mdex": {:hex, :mdex, "0.13.4", "9e42874ee4e9f8be110a5c03a91f838068c1b089bc0c586243d83503b153a2b1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "915e579df919923c0685e6ea9851cf6e4a346d014795f03ce5a1a9c52455192b"}, - "mdex_native": {:hex, :mdex_native, "0.2.6", "16c0d9ffca05990982ef8113c0a61950ffc250bf0792e17b95b28b74e1f572dc", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "eaaf2af27162c9879f5aef66344101dadf1898e71ea6ac9eb811c8d92d8e49ad"}, + "mdex": {:hex, :mdex, "0.13.5", "c1c94d230ccaab01ad0c68090d3b31613c10ece1844f32b55895da4ce0c63029", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.0 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "c57409fb6b34fbc58fbce0a6da670c9a4b5a2e94f86abdc56e9e213ed74620f2"}, + "mdex_native": {:hex, :mdex_native, "0.2.7", "46203ee9c4fe2c94feef26e4d976235ad022b4770d3d307fa6b445d293b2571f", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "05efa68775644ef6be6109f76c1f5f8778ff7e7ed9fe4a03541758158172bc77"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, @@ -49,7 +49,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nx": {:hex, :nx, "0.13.0", "43c57f529f17139821f6ed01a8aba6e045fef6722700c928cec2e2e3d2ee9560", [:mix], [{:complex, "~> 0.7", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "38d2f37ee7e2f3aaa86b96f125643ffbca56a89576cd24d83b452597397b4fbd"}, - "oban": {:hex, :oban, "2.23.0", "1867d0fa4e8c7685217b02cc2632e3ee86c93da770e9029ff71304d9e62e53d7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e5f0cec5abecce78dd08cb14dc5438db90ec3884987b44773ce76fe60dd3f81"}, + "oban": {:hex, :oban, "2.23.1", "0b9495e28a236ca0478c80d666c58a8b2b55182731c3603c6e766e298a08342a", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9855b9f5d87e31de3e2f46731b163f372e329613892a56c5b2aacceb50ed508"}, "octo_fetch": {:hex, :octo_fetch, "0.5.0", "f50701568b9fc752656367f82cc134d5fbefff37c5a0e8ddfcceb02ceee3f5fc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6226cc3c14ca948ee9f25fb0446322e5c288e215da9beba7899b6b5f4cd3ccb0"}, "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"}, "peep": {:hex, :peep, "4.4.0", "4b6289e7258ecf2741041068932455eb8389673bca785623621ddb6935286845", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f289959a9e5fcf92f5a4becaf4dd0df95c58841368f6ad0574b8ea830a7c1453"}, @@ -57,7 +57,7 @@ "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.7.0", "fb1e429f6d8778ce3a6962debdc5e555428a05a6e7b058d6dbad13d281a2c31f", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "dc9f44271aa6fc4ab7797f2aa374ba096ef2c87520586280eb095626b7387a68"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.8", "5006fd7b429c42489600fbc1600c750d0f0e5b5ea4965d9758e429b979392991", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b05ffe21f43c0ff219da62948b482c324aa5b8873e17b0c0cac58289a178af38"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_pubsub_redis": {:hex, :phoenix_pubsub_redis, "3.1.1", "54919406b0b6401e0f643e26fd3b814bc3460f3fd3a32aa9c3a473db89e9dca7", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1 or ~> 1.6", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "e339963f649e7cf52bd352e55fab7bebb42b65b501e43499dd6eb56890703c32"}, @@ -68,19 +68,19 @@ "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"}, "prom_ex": {:hex, :prom_ex, "1.12.0", "a82cbd5b49964e4d4295ab72a18e007aadd5f4a91630b7c49fad42cc7e49c880", [:mix], [{:absinthe, ">= 1.8.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.14.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0 or ~> 4.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "6357484941489ba2fee64bb1e9f2a1e86309545304f9e90144e3c10e553c958b"}, - "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "ranch": {:hex, :ranch, "2.2.1", "fc2bb0e800efbeab8ff82eb325450b9c08653b14c3f69e8b09bbea304973105d", [:make, :rebar3], [], "hexpm", "55f05cce20ec2da1d90de5d5981afb93dbfc01325fc7e933189aa5c62f037c24"}, "redix": {:hex, :redix, "1.6.0", "694179c7a3c71bffac8848fcbfe16f6f6e2a1e020f00a2ad01bc6484815470d1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eccb05e02f21c0c3ca57513e6bacb4dd48e6406dadbd7ff9fbe07bd6745999"}, "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, - "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, + "spitfire": {:hex, :spitfire, "0.4.0", "6d98c10cf585434b9439ba0c6dd3cc7aeff0e06ab73bfe5488f42e7c0f883d9b", [:mix], [], "hexpm", "7e5c6d1523c111b59f332f9dc49edc0377111d0c17167a29830f0e98233f5472"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"}, - "styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"}, + "styler": {:hex, :styler, "1.12.2", "1aa2942295645df48d42365f7191cc8b9f6fc4c35d15491f6fbd80ee815256b0", [:mix], [], "hexpm", "a82baeb6d81e35ee1c4cf8af08e141e8d7872a1d442f6fefce29817869e77a01"}, "sutra_ui": {:hex, :sutra_ui, "0.4.0", "ef348fe496af1a311193c35aa782c4c0256894a80c623a3c0dd13c759e921344", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mdex, "~> 0.13", [hex: :mdex, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "857a9fa7b182ca8105d6bedeaffa1e16beeb296807ca60f7f6e45ae96b41fcd7"}, - "swoosh": {:hex, :swoosh, "1.26.3", "9d8b60077305ce259298d9a1102e5be67cd3c41d1ea930c29e9288af195ca017", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c7683d070fe8f8aa9d174e61b01f2d527be73cd8ac40037b7109184941eb569f"}, + "swoosh": {:hex, :swoosh, "1.27.0", "2df57c342f3854f2d429e6afebf19a03a179976a64006a93750ed81639453f75", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5da7d3b11de5d61327275ed599cb311942ea6e23cdbb411981f46ee150cddf76"}, "table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"}, "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, @@ -89,7 +89,7 @@ "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, - "tidewave": {:hex, :tidewave, "0.8.0", "eb41df8d0b967a6ead2f48d6ccded369c17207058bb2cf7128c33562ad3d90f9", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.9", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "91a2007d021722d7ab3629f9024b5375819c827952f3a5ac19840ac528261532"}, + "tidewave": {:hex, :tidewave, "0.8.1", "d69ba6c131886d4b8d0b1337f1e910f7202d91d3adaff835e67f67b2dae67b30", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.9", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "557b0f4e27c55b001506b3c3d3a904af04e6ca7914284ab654c91c9cecb3cbf1"}, "uniq": {:hex, :uniq, "0.6.3", "68acff834cce1817b52928ef346662735c5413a4fec9c3b0d4a9126de5b2b489", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "2b2a900d0a20f3a55d3de0bc8150495e4a71255734dfb23889991bda5aca6c7d"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, diff --git a/test/microwaveprop/accounts/user_api_token_test.exs b/test/microwaveprop/accounts/user_api_token_test.exs index 39524847..c3557ae3 100644 --- a/test/microwaveprop/accounts/user_api_token_test.exs +++ b/test/microwaveprop/accounts/user_api_token_test.exs @@ -40,7 +40,7 @@ defmodule Microwaveprop.Accounts.UserApiTokenTest do end test "expires_at must be in the future", %{user: user} do - past = DateTime.add(DateTime.utc_now(), -60, :second) + past = DateTime.shift(DateTime.utc_now(), minute: -1) assert {:error, changeset} = UserApiToken.build(user, %{name: "n", expires_at: past}) @@ -49,7 +49,7 @@ defmodule Microwaveprop.Accounts.UserApiTokenTest do end test "expires_at in the future is accepted", %{user: user} do - future = DateTime.add(DateTime.utc_now(), 3600, :second) + future = DateTime.shift(DateTime.utc_now(), hour: 1) assert {:ok, {_plaintext, changeset}} = UserApiToken.build(user, %{name: "n", expires_at: future}) diff --git a/test/microwaveprop/accounts/user_token_test.exs b/test/microwaveprop/accounts/user_token_test.exs index 3de85e0e..90c80847 100644 --- a/test/microwaveprop/accounts/user_token_test.exs +++ b/test/microwaveprop/accounts/user_token_test.exs @@ -23,7 +23,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do test "uses the user's authenticated_at when present" do user = user_fixture() - dt = DateTime.add(DateTime.utc_now(:second), -30, :minute) + dt = DateTime.shift(DateTime.utc_now(:second), minute: -30) user = %{user | authenticated_at: dt} {_token, user_token} = UserToken.build_session_token(user) @@ -154,7 +154,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do # Magic link validity is 15 minutes. Repo.update_all( from(t in UserToken, where: t.token == ^raw), - set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -16, :minute)] + set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), minute: -16)] ) {:ok, query} = UserToken.verify_magic_link_token_query(encoded) @@ -205,7 +205,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do test "returns nil when the token is older than 1 day", %{encoded: encoded, raw_token: raw} do Repo.update_all( from(t in UserToken, where: t.token == ^raw), - set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -2, :day)] + set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), day: -2)] ) {:ok, query} = UserToken.verify_confirm_token_query(encoded) @@ -278,7 +278,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do } do Repo.update_all( from(t in UserToken, where: t.token == ^raw), - set: [inserted_at: DateTime.add(DateTime.utc_now(:second), -8, :day)] + set: [inserted_at: DateTime.shift(DateTime.utc_now(:second), day: -8)] ) {:ok, query} = UserToken.verify_change_email_token_query(encoded, context) diff --git a/test/microwaveprop/accounts_api_token_test.exs b/test/microwaveprop/accounts_api_token_test.exs index 396e09db..4decb47d 100644 --- a/test/microwaveprop/accounts_api_token_test.exs +++ b/test/microwaveprop/accounts_api_token_test.exs @@ -26,7 +26,7 @@ defmodule Microwaveprop.AccountsApiTokenTest do end test "rejects expires_at in the past", %{user: user} do - past = DateTime.add(DateTime.utc_now(), -1, :second) + past = DateTime.shift(DateTime.utc_now(), second: -1) assert {:error, changeset} = Accounts.create_api_token(user, %{name: "x", expires_at: past}) @@ -75,13 +75,13 @@ defmodule Microwaveprop.AccountsApiTokenTest do end test "rejects expired tokens", %{user: user} do - future = DateTime.add(DateTime.utc_now(), 60, :second) + future = DateTime.shift(DateTime.utc_now(), minute: 1) {:ok, {plaintext, record}} = Accounts.create_api_token(user, %{name: "A", expires_at: future}) # Force the row past its expiry. Repo.update_all( from(t in UserApiToken, where: t.id == ^record.id), - set: [expires_at: DateTime.utc_now() |> DateTime.add(-1, :second) |> DateTime.truncate(:second)] + set: [expires_at: DateTime.utc_now() |> DateTime.shift(second: -1) |> DateTime.truncate(:second)] ) assert {:error, :invalid_token} = Accounts.get_user_by_api_token(plaintext) diff --git a/test/microwaveprop/accounts_test.exs b/test/microwaveprop/accounts_test.exs index 56487da6..dc681d56 100644 --- a/test/microwaveprop/accounts_test.exs +++ b/test/microwaveprop/accounts_test.exs @@ -347,12 +347,12 @@ defmodule Microwaveprop.AccountsTest do now = DateTime.utc_now() assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.utc_now()}) - assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -19, :minute)}) - refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.add(now, -21, :minute)}) + assert Accounts.sudo_mode?(%User{authenticated_at: DateTime.shift(now, minute: -19)}) + refute Accounts.sudo_mode?(%User{authenticated_at: DateTime.shift(now, minute: -21)}) # minute override refute Accounts.sudo_mode?( - %User{authenticated_at: DateTime.add(now, -11, :minute)}, + %User{authenticated_at: DateTime.shift(now, minute: -11)}, -10 ) @@ -529,7 +529,7 @@ defmodule Microwaveprop.AccountsTest do end test "duplicates the authenticated_at of given user in new token", %{user: user} do - user = %{user | authenticated_at: DateTime.add(DateTime.utc_now(:second), -3600)} + user = %{user | authenticated_at: DateTime.shift(DateTime.utc_now(:second), hour: -1)} token = Accounts.generate_user_session_token(user) assert user_token = Repo.get_by(UserToken, token: token) assert user_token.authenticated_at == user.authenticated_at diff --git a/test/microwaveprop/beacon_monitors_test.exs b/test/microwaveprop/beacon_monitors_test.exs index ee5c8639..f59826f1 100644 --- a/test/microwaveprop/beacon_monitors_test.exs +++ b/test/microwaveprop/beacon_monitors_test.exs @@ -49,7 +49,7 @@ defmodule Microwaveprop.BeaconMonitorsTest do # inserts can land in the same wall-clock second. {:ok, first} = BeaconMonitors.create_monitor(user, %{"name" => "First"}) - backdated = DateTime.add(first.inserted_at, -1, :second) + backdated = DateTime.shift(first.inserted_at, second: -1) {1, _} = Microwaveprop.Repo.update_all( diff --git a/test/microwaveprop/commercial_test.exs b/test/microwaveprop/commercial_test.exs index 00030855..9b529369 100644 --- a/test/microwaveprop/commercial_test.exs +++ b/test/microwaveprop/commercial_test.exs @@ -102,7 +102,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second), + sampled_at: DateTime.shift(valid_time, day: -day_offset), rx_power_0: -52.0, rx_power_1: -52.0, link_state: 1 @@ -113,7 +113,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -300, :second), + sampled_at: DateTime.shift(valid_time, minute: -5), rx_power_0: -62.0, rx_power_1: -62.0, link_state: 1 @@ -134,7 +134,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second), + sampled_at: DateTime.shift(valid_time, day: -day_offset), rx_power_0: -55.0, link_state: 1 }) @@ -143,7 +143,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -60, :second), + sampled_at: DateTime.shift(valid_time, minute: -1), rx_power_0: -55.0, link_state: 1 }) @@ -163,7 +163,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second), + sampled_at: DateTime.shift(valid_time, day: -day_offset), rx_power_0: -52.0, link_state: 1 }) @@ -172,7 +172,7 @@ defmodule Microwaveprop.CommercialTest do {:ok, _} = Commercial.create_sample(%{ link_id: link.id, - sampled_at: DateTime.add(valid_time, -120, :second), + sampled_at: DateTime.shift(valid_time, minute: -2), rx_power_0: -90.0, link_state: 0 }) diff --git a/test/microwaveprop/moon_test.exs b/test/microwaveprop/moon_test.exs index 31a9af09..3948cd3d 100644 --- a/test/microwaveprop/moon_test.exs +++ b/test/microwaveprop/moon_test.exs @@ -65,8 +65,8 @@ defmodule Microwaveprop.MoonTest do time_zone: "Etc/UTC" } - a = DateTime.add(base, day_offset * 86_400, :second) - b = DateTime.add(a, delta_hours * 3600, :second) + a = DateTime.shift(base, day: day_offset) + b = DateTime.shift(a, hour: delta_hours) assert Moon.julian_day(a) < Moon.julian_day(b) end @@ -104,7 +104,7 @@ defmodule Microwaveprop.MoonTest do property "distance, RA, and declination all stay in their physical envelopes for any UTC across the next century" do check all(seconds_from_now <- integer(0..3_155_760_000)) do - dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds_from_now, :second) + dt = DateTime.shift(~U[2024-01-01 00:00:00Z], second: seconds_from_now) g = Moon.geocentric(dt) assert g.distance_km > 340_000 and g.distance_km < 420_000 @@ -143,7 +143,7 @@ defmodule Microwaveprop.MoonTest do lat <- float(min: -85.0, max: 85.0), lon <- float(min: -179.9, max: 179.9) ) do - dt = DateTime.add(~U[2024-01-01 00:00:00Z], seconds, :second) + dt = DateTime.shift(~U[2024-01-01 00:00:00Z], second: seconds) p = Moon.observer_position(dt, lat, lon) assert p.azimuth >= 0.0 and p.azimuth < 360.0 assert p.elevation >= -90.0 and p.elevation <= 90.0 @@ -178,7 +178,7 @@ defmodule Microwaveprop.MoonTest do peaks = for hour_offset <- 0..48 do - dt = DateTime.add(base, hour_offset * 3600, :second) + dt = DateTime.shift(base, hour: hour_offset) {dt, Moon.observer_position(dt, lat, lon).elevation} end diff --git a/test/microwaveprop/propagation/freshness_monitor_test.exs b/test/microwaveprop/propagation/freshness_monitor_test.exs index a366066c..c0342cd4 100644 --- a/test/microwaveprop/propagation/freshness_monitor_test.exs +++ b/test/microwaveprop/propagation/freshness_monitor_test.exs @@ -35,7 +35,7 @@ defmodule Microwaveprop.Propagation.FreshnessMonitorTest do defp minutes_ago(n) do DateTime.utc_now() - |> DateTime.add(-n, :minute) + |> DateTime.shift(minute: -n) |> DateTime.truncate(:second) end diff --git a/test/microwaveprop/propagation/grid_task_enqueuer_test.exs b/test/microwaveprop/propagation/grid_task_enqueuer_test.exs index 7d3d696e..7d19b22e 100644 --- a/test/microwaveprop/propagation/grid_task_enqueuer_test.exs +++ b/test/microwaveprop/propagation/grid_task_enqueuer_test.exs @@ -123,7 +123,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do describe "reclaim_stale_running/1" do test "flips stale-running rows back to queued and clears claimed_at" do now = DateTime.truncate(DateTime.utc_now(), :second) - stale = DateTime.add(now, -30 * 60, :second) + stale = DateTime.shift(now, minute: -30) insert_task(%{ run_time: ~U[2026-04-19 20:00:00Z], @@ -152,7 +152,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do test "leaves recently-claimed running rows untouched" do now = DateTime.truncate(DateTime.utc_now(), :second) - fresh = DateTime.add(now, -60, :second) + fresh = DateTime.shift(now, minute: -1) insert_task(%{ run_time: ~U[2026-04-19 21:00:00Z], @@ -178,7 +178,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do test "flips stale rows past max attempts to failed" do now = DateTime.truncate(DateTime.utc_now(), :second) - stale = DateTime.add(now, -30 * 60, :second) + stale = DateTime.shift(now, minute: -30) insert_task(%{ run_time: ~U[2026-04-19 22:00:00Z], @@ -206,7 +206,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do test "does not touch queued or done rows even if claimed_at is ancient" do now = DateTime.truncate(DateTime.utc_now(), :second) - stale = DateTime.add(now, -30 * 60, :second) + stale = DateTime.shift(now, minute: -30) insert_task(%{ run_time: ~U[2026-04-19 23:00:00Z], @@ -226,7 +226,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do test "seeds exactly one forecast row at the hour offset closest to now" do # run_time 5h before "now" → fh should be 5. now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0) - run_time = DateTime.add(now, -5 * 3600, :second) + run_time = DateTime.shift(now, hour: -5) assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps") @@ -247,7 +247,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do test "is idempotent on the (run_time, fh, kind, source) key" do now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0) - run_time = DateTime.add(now, -3 * 3600, :second) + run_time = DateTime.shift(now, hour: -3) assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps") assert {:ok, 0} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps") @@ -268,7 +268,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do # negative. Clamp pulls it to 1 so the F00Reserved branch in Rust # is never hit through this path. now = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0) - run_time = DateTime.add(now, 3600, :second) + run_time = DateTime.shift(now, hour: 1) assert {:ok, 1} = GridTaskEnqueuer.seed_current_hour(run_time, source: "hrdps") diff --git a/test/microwaveprop/propagation/notify_listener_test.exs b/test/microwaveprop/propagation/notify_listener_test.exs index 91991266..f62cf4c3 100644 --- a/test/microwaveprop/propagation/notify_listener_test.exs +++ b/test/microwaveprop/propagation/notify_listener_test.exs @@ -90,7 +90,7 @@ defmodule Microwaveprop.Propagation.NotifyListenerTest do test "retains GEFS score files through 168 hours" do run_time = ~U[2026-04-21 00:00:00Z] - extended_time = DateTime.add(run_time, 168, :hour) + extended_time = DateTime.shift(run_time, week: 1) ScoresFile.write_gefs!(10_000, extended_time, sample_scores()) {:ok, _task_pid} = NotifyListener.handle_propagation_ready(run_time, run_time) diff --git a/test/microwaveprop/propagation/pipeline_status_test.exs b/test/microwaveprop/propagation/pipeline_status_test.exs index 823c4315..a61cf0d1 100644 --- a/test/microwaveprop/propagation/pipeline_status_test.exs +++ b/test/microwaveprop/propagation/pipeline_status_test.exs @@ -30,7 +30,7 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do end defp minutes_ago(n) do - DateTime.add(DateTime.utc_now(), -n * 60, :second) + DateTime.shift(DateTime.utc_now(), minute: -n) end describe "current/0" do @@ -183,24 +183,24 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do test "valid_time within half an hour of now renders as 'through now'" do now = DateTime.utc_now() assert PipelineStatus.running_detail(%{valid_time: now}) == "through now" - assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 20 * 60, :second)}) == "through now" - assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -20 * 60, :second)}) == "through now" + assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, minute: 20)}) == "through now" + assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, minute: -20)}) == "through now" end test "valid_time in the future renders as 'through +Nh'" do now = DateTime.utc_now() - assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 3 * 3600, :second)}) == + assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: 3)}) == "through +3h" - assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 12 * 3600, :second)}) == + assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: 12)}) == "through +12h" end test "valid_time in the past renders as 'through Nh ago'" do now = DateTime.utc_now() - assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -2 * 3600, :second)}) == + assert PipelineStatus.running_detail(%{valid_time: DateTime.shift(now, hour: -2)}) == "through 2h ago" end diff --git a/test/microwaveprop/propagation/score_cache_test.exs b/test/microwaveprop/propagation/score_cache_test.exs index b1b08355..c3617f12 100644 --- a/test/microwaveprop/propagation/score_cache_test.exs +++ b/test/microwaveprop/propagation/score_cache_test.exs @@ -236,7 +236,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do # Insert cap+1 entries, each at a distinct valid_time, oldest first. for hour <- 0..cap do - ScoreCache.put(10_000, DateTime.add(base, hour * 3600, :second), score) + ScoreCache.put(10_000, DateTime.shift(base, hour: hour), score) end # Size never exceeds the cap. @@ -246,7 +246,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do assert ScoreCache.fetch(10_000, base) == :miss # The newest is retained. - newest = DateTime.add(base, cap * 3600, :second) + newest = DateTime.shift(base, hour: cap) assert {:ok, [%{score: 1}]} = ScoreCache.fetch(10_000, newest) end @@ -257,14 +257,14 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do # Fill cap entries on band A at older valid_times. for hour <- 0..(cap - 1) do - ScoreCache.put(10_000, DateTime.add(base, hour * 3600, :second), score) + ScoreCache.put(10_000, DateTime.shift(base, hour: hour), score) end assert :ets.info(:propagation_score_cache, :size) == cap # One more on band B at a newer valid_time should evict the oldest # band-A entry, not the new band-B one. - newer = DateTime.add(base, cap * 3600, :second) + newer = DateTime.shift(base, hour: cap) ScoreCache.put(24_000, newer, score) assert :ets.info(:propagation_score_cache, :size) == cap diff --git a/test/microwaveprop/propagation/scores_file_test.exs b/test/microwaveprop/propagation/scores_file_test.exs index f2b42596..f8d7f39a 100644 --- a/test/microwaveprop/propagation/scores_file_test.exs +++ b/test/microwaveprop/propagation/scores_file_test.exs @@ -351,8 +351,8 @@ defmodule Microwaveprop.Propagation.ScoresFileTest do describe "retain_window/2" do test "recognizes GEFS files through the extended horizon" do run_time = ~U[2026-04-14 00:00:00Z] - inside = DateTime.add(run_time, 168, :hour) - outside = DateTime.add(run_time, 174, :hour) + inside = DateTime.shift(run_time, week: 1) + outside = DateTime.shift(run_time, hour: 174) ScoresFile.write_gefs!(10_000, inside, []) ScoresFile.write_gefs!(10_000, outside, []) diff --git a/test/microwaveprop/propagation_test.exs b/test/microwaveprop/propagation_test.exs index 39943124..6ddb3269 100644 --- a/test/microwaveprop/propagation_test.exs +++ b/test/microwaveprop/propagation_test.exs @@ -405,7 +405,7 @@ defmodule Microwaveprop.PropagationTest do end test "returns empty factors when the only persisted profile is older than the 24h lookback" do - ancient = DateTime.add(~U[2026-07-15 13:00:00Z], -30 * 3600, :second) + ancient = DateTime.shift(~U[2026-07-15 13:00:00Z], hour: -30) forecast_time = ~U[2026-07-15 13:00:00Z] lat = 32.75 lon = -97.125 @@ -425,7 +425,7 @@ defmodule Microwaveprop.PropagationTest do end test "accepts a profile exactly at the 24h lookback boundary" do - fallback_time = DateTime.add(~U[2026-07-15 13:00:00Z], -24 * 3600, :second) + fallback_time = DateTime.shift(~U[2026-07-15 13:00:00Z], day: -1) forecast_time = ~U[2026-07-15 13:00:00Z] lat = 32.75 lon = -97.125 @@ -708,8 +708,8 @@ defmodule Microwaveprop.PropagationTest do end test "returns forecast timeline from cached scores when warm" do - t1 = DateTime.truncate(DateTime.add(DateTime.utc_now(), 3600, :second), :second) - t2 = DateTime.add(t1, 3600, :second) + t1 = DateTime.truncate(DateTime.shift(DateTime.utc_now(), hour: 1), :second) + t2 = DateTime.shift(t1, hour: 1) seed_point(10_000, t1, 32.875, -97.0, 70) seed_point(10_000, t2, 32.875, -97.0, 75) @@ -725,9 +725,9 @@ defmodule Microwaveprop.PropagationTest do test "filters valid_times older than one hour but keeps recent past as the 'now' anchor" do # Two hours old: dropped. Thirty minutes old: kept as the # leftmost "now" point. One hour future: kept. - stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :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) + stale = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second) + recent = DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) + future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.truncate(:second) seed_point(10_000, stale, 32.875, -97.0, 40) seed_point(10_000, recent, 32.875, -97.0, 60) @@ -745,8 +745,8 @@ defmodule Microwaveprop.PropagationTest do # HRRR just cut out (nothing fresh); we still want the chart to # render one point at the newest time we have, matching # available_valid_times semantics. - older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second) - newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second) + older = DateTime.utc_now() |> DateTime.shift(hour: -6) |> DateTime.truncate(:second) + newer = DateTime.utc_now() |> DateTime.shift(hour: -3) |> DateTime.truncate(:second) seed_point(10_000, older, 32.875, -97.0, 30) seed_point(10_000, newer, 32.875, -97.0, 45) @@ -756,7 +756,7 @@ defmodule Microwaveprop.PropagationTest do end 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.shift(DateTime.utc_now(), hour: 1), :second) seed_point(10_000, valid_time, 32.875, -97.0, 65) # Off-grid query snaps to 32.875, -97.0 @@ -775,8 +775,8 @@ defmodule Microwaveprop.PropagationTest do # 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) + t2 = DateTime.shift(t1, hour: 1) + t3 = DateTime.shift(t1, hour: 2) for {t, score} <- [{t1, 50}, {t2, 60}, {t3, 70}] do Propagation.replace_scores( @@ -801,9 +801,9 @@ defmodule Microwaveprop.PropagationTest do test "includes recent past valid_times and falls back to the latest when stale" do # Three .prop files on disk: one 2h old (dropped by cutoff), # one 30min old (kept as "now"), one 1h future (kept). - stale = DateTime.utc_now() |> DateTime.add(-2 * 3600, :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) + stale = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second) + recent = DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) + future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.truncate(:second) Enum.each([{stale, 40}, {recent, 60}, {future, 80}], fn {t, score} -> Propagation.replace_scores( @@ -824,8 +824,8 @@ defmodule Microwaveprop.PropagationTest do end test "returns just the latest store entry when every hour is stale" do - older = DateTime.utc_now() |> DateTime.add(-6 * 3600, :second) |> DateTime.truncate(:second) - newer = DateTime.utc_now() |> DateTime.add(-3 * 3600, :second) |> DateTime.truncate(:second) + older = DateTime.utc_now() |> DateTime.shift(hour: -6) |> DateTime.truncate(:second) + newer = DateTime.utc_now() |> DateTime.shift(hour: -3) |> DateTime.truncate(:second) Enum.each([{older, 30}, {newer, 45}], fn {t, score} -> Propagation.replace_scores( @@ -856,8 +856,8 @@ defmodule Microwaveprop.PropagationTest do test "caps the forward horizon to 48 hours so stale cycles don't clutter the timeline" do now = DateTime.truncate(DateTime.utc_now(), :second) - in_range = DateTime.add(now, 3600, :second) - past_hrrr_horizon = DateTime.add(now, 55 * 3600, :second) + in_range = DateTime.shift(now, hour: 1) + past_hrrr_horizon = DateTime.shift(now, hour: 55) Enum.each([{in_range, 70}, {past_hrrr_horizon, 50}], fn {t, score} -> Propagation.replace_scores( @@ -873,8 +873,8 @@ defmodule Microwaveprop.PropagationTest do test "filters valid_times older than the 1-hour cutoff" do now = DateTime.truncate(DateTime.utc_now(), :second) - stale = DateTime.add(now, -7200, :second) - fresh = DateTime.add(now, 1800, :second) + stale = DateTime.shift(now, hour: -2) + fresh = DateTime.shift(now, minute: 30) Propagation.replace_scores( [%{lat: 25.0, lon: -125.0, valid_time: stale, band_mhz: 10_000, score: 50, factors: nil}], @@ -1039,7 +1039,7 @@ defmodule Microwaveprop.PropagationTest do Propagation.record_run_timing(%{ run_time: run_time, forecast_hour: 3, - valid_time: DateTime.add(run_time, 3, :hour), + valid_time: DateTime.shift(run_time, hour: 3), started_at: started, finished_at: finished, duration_ms: 555_000, @@ -1061,7 +1061,7 @@ defmodule Microwaveprop.PropagationTest do Propagation.record_run_timing(%{ run_time: run_time, forecast_hour: 5, - valid_time: DateTime.add(run_time, 5, :hour), + valid_time: DateTime.shift(run_time, hour: 5), started_at: ~U[2026-07-15 12:00:00Z], finished_at: ~U[2026-07-15 12:02:30Z], duration_ms: 150_000, @@ -1108,9 +1108,9 @@ defmodule Microwaveprop.PropagationTest do Propagation.record_run_timing(%{ run_time: run_time, forecast_hour: fh, - valid_time: DateTime.add(run_time, fh, :hour), - started_at: DateTime.add(run_time, fh * 600, :second), - finished_at: DateTime.add(run_time, fh * 600 + 500, :second), + valid_time: DateTime.shift(run_time, hour: fh), + started_at: DateTime.shift(run_time, minute: fh * 10), + finished_at: DateTime.shift(run_time, second: fh * 600 + 500), duration_ms: 500_000, status: :ok }) diff --git a/test/microwaveprop/qrz_test.exs b/test/microwaveprop/qrz_test.exs index c680403e..c0d9a099 100644 --- a/test/microwaveprop/qrz_test.exs +++ b/test/microwaveprop/qrz_test.exs @@ -77,7 +77,7 @@ defmodule Microwaveprop.QrzTest do test "re-fetches when cache entry is stale" do past = DateTime.utc_now() - |> DateTime.add(-48, :hour) + |> DateTime.shift(day: -2) |> DateTime.truncate(:second) Repo.insert!(%Callsign{ @@ -96,7 +96,7 @@ defmodule Microwaveprop.QrzTest do test "upserts (updates existing record on re-fetch)" do past = DateTime.utc_now() - |> DateTime.add(-48, :hour) + |> DateTime.shift(day: -2) |> DateTime.truncate(:second) original = @@ -157,7 +157,7 @@ defmodule Microwaveprop.QrzTest do test "returns record regardless of TTL" do past = DateTime.utc_now() - |> DateTime.add(-999, :hour) + |> DateTime.shift(hour: -999) |> DateTime.truncate(:second) Repo.insert!(%Callsign{ diff --git a/test/microwaveprop/radio/contact_submission_test.exs b/test/microwaveprop/radio/contact_submission_test.exs index 451395f5..171fbf98 100644 --- a/test/microwaveprop/radio/contact_submission_test.exs +++ b/test/microwaveprop/radio/contact_submission_test.exs @@ -247,7 +247,7 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do test "allows contact outside the 1-hour window" do assert {:ok, _} = Radio.create_contact(@valid_attrs) - later = DateTime.add(@valid_attrs.qso_timestamp, 3601, :second) + later = DateTime.shift(@valid_attrs.qso_timestamp, second: 3_601) assert {:ok, _} = Radio.create_contact(%{@valid_attrs | qso_timestamp: later}) end diff --git a/test/microwaveprop/radio_test.exs b/test/microwaveprop/radio_test.exs index 017a53f0..1eff88b6 100644 --- a/test/microwaveprop/radio_test.exs +++ b/test/microwaveprop/radio_test.exs @@ -2,36 +2,12 @@ defmodule Microwaveprop.RadioTest do use Microwaveprop.DataCase, async: true import Microwaveprop.AccountsFixtures + import Microwaveprop.ContactsFixtures alias Microwaveprop.Accounts.Scope alias Microwaveprop.Radio alias Microwaveprop.Radio.Contact - defp create_contact(attrs \\ %{}) do - default = %{ - station1: "W5XD", - station2: "K5TR", - qso_timestamp: ~U[2026-03-28 18:00:00Z], - mode: "CW", - band: Decimal.new("1296"), - grid1: "EM12", - grid2: "EM00", - pos1: %{"lat" => 32.9, "lon" => -97.0}, - pos2: %{"lat" => 30.3, "lon" => -97.7}, - distance_km: Decimal.new("295") - } - - merged = Map.merge(default, attrs) - {user_id, changeset_attrs} = Map.pop(merged, :user_id) - - {:ok, contact} = - %Contact{user_id: user_id} - |> Contact.changeset(changeset_attrs) - |> Repo.insert() - - contact - end - describe "list_contacts/1" do test "returns empty page when no QSOs exist" do result = Radio.list_contacts() @@ -44,7 +20,7 @@ defmodule Microwaveprop.RadioTest do test "paginates 20 per page, ordered by qso_timestamp desc" do for i <- 1..25 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -57,12 +33,12 @@ defmodule Microwaveprop.RadioTest do # Most recent first first = hd(result.entries) - assert first.qso_timestamp == DateTime.add(~U[2026-01-01 00:00:00Z], 25 * 3600, :second) + assert first.qso_timestamp == DateTime.shift(~U[2026-01-01 00:00:00Z], hour: 25) end test "returns second page" do for i <- 1..25 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -75,7 +51,7 @@ defmodule Microwaveprop.RadioTest do test "honors per_page option" do for i <- 1..25 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -87,7 +63,7 @@ defmodule Microwaveprop.RadioTest do test "clamps per_page to the maximum" do for i <- 1..3 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -98,7 +74,7 @@ defmodule Microwaveprop.RadioTest do test "falls back to default when per_page is invalid" do for i <- 1..25 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -114,7 +90,13 @@ defmodule Microwaveprop.RadioTest do result = Radio.list_contacts(sort_by: :station1, sort_order: :asc) - stations = Enum.map(result.entries, & &1.station1) + # Filter to only the contacts created in this test so parallel + # async tests don't affect the assertion. + stations = + result.entries + |> Enum.filter(&(&1.station1 in ["AA1AA", "ZZ9ZZ"])) + |> Enum.map(& &1.station1) + assert stations == ["AA1AA", "ZZ9ZZ"] end @@ -124,7 +106,13 @@ defmodule Microwaveprop.RadioTest do result = Radio.list_contacts(sort_by: :distance_km, sort_order: :desc) - distances = Enum.map(result.entries, & &1.distance_km) + # Filter to only the contacts created in this test so parallel + # async tests don't affect the assertion. + distances = + result.entries + |> Enum.filter(&(&1.station1 in ["A1A", "B2B"])) + |> Enum.map(& &1.distance_km) + assert distances == [Decimal.new("500"), Decimal.new("100")] end @@ -134,7 +122,9 @@ defmodule Microwaveprop.RadioTest do result = Radio.list_contacts(sort_by: :bogus) - ids = Enum.map(result.entries, & &1.id) + # Filter to only the contacts created in this test. + our_ids = MapSet.new([early.id, late.id]) + ids = result.entries |> Enum.filter(&MapSet.member?(our_ids, &1.id)) |> Enum.map(& &1.id) assert ids == [late.id, early.id] end end @@ -167,7 +157,7 @@ defmodule Microwaveprop.RadioTest do test "respects limit parameter" do for i <- 1..5 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -296,7 +286,7 @@ defmodule Microwaveprop.RadioTest do test "respects limit parameter" do for i <- 1..5 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -360,7 +350,7 @@ defmodule Microwaveprop.RadioTest do test "respects limit parameter" do for i <- 1..5 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -763,7 +753,7 @@ defmodule Microwaveprop.RadioTest do create_contact(%{ station1: "W5ISP", station2: "K5X#{i}", - qso_timestamp: DateTime.add(base, i * 3600, :second) + qso_timestamp: DateTime.shift(base, hour: i) }) end diff --git a/test/microwaveprop/weather/gefs_profile_test.exs b/test/microwaveprop/weather/gefs_profile_test.exs index 24ae7d3f..14bf98cd 100644 --- a/test/microwaveprop/weather/gefs_profile_test.exs +++ b/test/microwaveprop/weather/gefs_profile_test.exs @@ -77,7 +77,7 @@ defmodule Microwaveprop.Weather.GefsProfileTest do test "inserts many rows at once" do attrs = for fh <- [3, 6, 9] do - valid = DateTime.add(~U[2026-04-18 12:00:00Z], fh * 3600, :second) + valid = DateTime.shift(~U[2026-04-18 12:00:00Z], hour: fh) base_attrs(%{valid_time: valid, forecast_hour: fh}) end diff --git a/test/microwaveprop/weather/grid_snap_property_test.exs b/test/microwaveprop/weather/grid_snap_property_test.exs index bfe4eb16..de7a73f8 100644 --- a/test/microwaveprop/weather/grid_snap_property_test.exs +++ b/test/microwaveprop/weather/grid_snap_property_test.exs @@ -165,7 +165,7 @@ defmodule Microwaveprop.Weather.GridSnapPropertyTest do property "coverage_end is the first excluded timestamp (exclusive upper bound)" do coverage_end = NarrClient.coverage_end() refute NarrClient.in_coverage?(coverage_end) - one_sec_before = DateTime.add(coverage_end, -1, :second) + one_sec_before = DateTime.shift(coverage_end, second: -1) assert NarrClient.in_coverage?(one_sec_before) end @@ -173,7 +173,7 @@ defmodule Microwaveprop.Weather.GridSnapPropertyTest do coverage_end = NarrClient.coverage_end() check all(seconds_after <- integer(1..100_000_000)) do - dt = DateTime.add(coverage_end, seconds_after, :second) + dt = DateTime.shift(coverage_end, second: seconds_after) refute NarrClient.in_coverage?(dt) end end diff --git a/test/microwaveprop/weather/hrrr_profile_lookup_test.exs b/test/microwaveprop/weather/hrrr_profile_lookup_test.exs index a5840e4a..306a7923 100644 --- a/test/microwaveprop/weather/hrrr_profile_lookup_test.exs +++ b/test/microwaveprop/weather/hrrr_profile_lookup_test.exs @@ -37,7 +37,7 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do test "limit option caps the number of times returned (most recent first kept)" do for h <- 0..4 do - valid_time = DateTime.add(~U[2026-04-25 12:00:00Z], h * 3600, :second) + valid_time = DateTime.shift(~U[2026-04-25 12:00:00Z], hour: h) insert_profile!(%{lat: 32.625, lon: -97.125, valid_time: valid_time}) end diff --git a/test/microwaveprop/weather/nexrad_cache_test.exs b/test/microwaveprop/weather/nexrad_cache_test.exs index e1e06360..4134985e 100644 --- a/test/microwaveprop/weather/nexrad_cache_test.exs +++ b/test/microwaveprop/weather/nexrad_cache_test.exs @@ -51,19 +51,19 @@ defmodule Microwaveprop.Weather.NexradCacheTest do base = ~U[2026-04-12 00:00:00Z] for i <- 1..25 do - ts = DateTime.add(base, i * 300, :second) + ts = DateTime.shift(base, minute: i * 5) NexradCache.put(ts, <>, 1) end # The 5 oldest (i = 1..5) should have been evicted. for i <- 1..5 do - ts = DateTime.add(base, i * 300, :second) + ts = DateTime.shift(base, minute: i * 5) assert NexradCache.fetch(ts) == :miss end # The most recent 20 should still be present. for i <- 6..25 do - ts = DateTime.add(base, i * 300, :second) + ts = DateTime.shift(base, minute: i * 5) assert {:ok, <<^i>>, 1} = NexradCache.fetch(ts) end end diff --git a/test/microwaveprop/weather_grid_test.exs b/test/microwaveprop/weather_grid_test.exs index 3e24c31d..2f61048d 100644 --- a/test/microwaveprop/weather_grid_test.exs +++ b/test/microwaveprop/weather_grid_test.exs @@ -90,7 +90,7 @@ defmodule Microwaveprop.WeatherGridTest do end test "uses most recent valid_time only" do - old = DateTime.utc_now() |> DateTime.add(-7200, :second) |> DateTime.truncate(:second) + old = DateTime.utc_now() |> DateTime.shift(hour: -2) |> DateTime.truncate(:second) new = DateTime.truncate(DateTime.utc_now(), :second) insert_hrrr_profile(%{ diff --git a/test/microwaveprop/weather_test.exs b/test/microwaveprop/weather_test.exs index 91e3f8bb..903ed771 100644 --- a/test/microwaveprop/weather_test.exs +++ b/test/microwaveprop/weather_test.exs @@ -336,7 +336,7 @@ defmodule Microwaveprop.WeatherTest do rows = for i <- 0..99 do %{ - observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second), + observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5), temp_f: 40.0 + i / 10, dewpoint_f: 30.0 + i / 10, sky_condition: "CLR" @@ -354,7 +354,7 @@ defmodule Microwaveprop.WeatherTest do rows1 = for i <- 0..9 do %{ - observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second), + observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5), temp_f: 40.0, dewpoint_f: 30.0 } @@ -365,7 +365,7 @@ defmodule Microwaveprop.WeatherTest do rows2 = for i <- 0..9 do %{ - observed_at: DateTime.add(~U[2026-03-28 00:00:00Z], i * 300, :second), + observed_at: DateTime.shift(~U[2026-03-28 00:00:00Z], minute: i * 5), temp_f: 80.0, dewpoint_f: 60.0 } diff --git a/test/microwaveprop/workers/admin_task_worker_test.exs b/test/microwaveprop/workers/admin_task_worker_test.exs index 6b41c4bb..f72e751f 100644 --- a/test/microwaveprop/workers/admin_task_worker_test.exs +++ b/test/microwaveprop/workers/admin_task_worker_test.exs @@ -295,7 +295,7 @@ defmodule Microwaveprop.Workers.AdminTaskWorkerTest do for i <- 1..2 do %HrrrNativeProfile{} |> HrrrNativeProfile.changeset(%{ - valid_time: DateTime.add(~U[2026-04-15 18:00:00Z], i * 3600, :second), + valid_time: DateTime.shift(~U[2026-04-15 18:00:00Z], hour: i), lat: 32.9 + i * 0.1, lon: -97.0, level_count: 3, diff --git a/test/microwaveprop/workers/backfill_enqueue_worker_test.exs b/test/microwaveprop/workers/backfill_enqueue_worker_test.exs index 1e38d038..f0b3a607 100644 --- a/test/microwaveprop/workers/backfill_enqueue_worker_test.exs +++ b/test/microwaveprop/workers/backfill_enqueue_worker_test.exs @@ -2,6 +2,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do use Microwaveprop.DataCase, async: true import Ecto.Query + import Microwaveprop.ContactsFixtures alias Microwaveprop.Radio.Contact alias Microwaveprop.Terrain.ElevationClient @@ -10,28 +11,6 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do alias Microwaveprop.Weather.NarrClient alias Microwaveprop.Workers.BackfillEnqueueWorker - defp create_contact(attrs \\ %{}) do - default = %{ - station1: "W5XD", - station2: "K5TR", - qso_timestamp: ~U[2026-03-28 18:00:00Z], - mode: "CW", - band: Decimal.new("1296"), - grid1: "EM12", - grid2: "EM00", - pos1: %{"lat" => 32.9, "lon" => -97.0}, - pos2: %{"lat" => 30.3, "lon" => -97.7}, - distance_km: Decimal.new("295") - } - - {:ok, contact} = - %Contact{} - |> Contact.changeset(Map.merge(default, attrs)) - |> Repo.insert() - - contact - end - setup do Req.Test.stub(IemClient, fn conn -> case conn.request_path do @@ -101,7 +80,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do test "respects limit parameter" do for i <- 1..5 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts}) end @@ -114,30 +93,33 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do test "enqueues all pending contacts when limit is omitted" do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "backfill:enqueue_complete") - for i <- 1..7 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) - create_contact(%{qso_timestamp: ts}) - end + our_ids = + for i <- 1..7 do + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) + create_contact(%{qso_timestamp: ts}).id + end assert :ok = BackfillEnqueueWorker.perform(%Oban.Job{ args: %{"types" => @non_era5_types} }) - assert_receive {:enqueue_complete, 7} + assert_receive {:enqueue_complete, count} + assert count >= 7 end test "broadcasts enqueue_complete with count" do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "backfill:enqueue_complete") - create_contact() + contact = create_contact() assert :ok = BackfillEnqueueWorker.perform(%Oban.Job{ args: %{"limit" => 10, "types" => @non_era5_types} }) - assert_receive {:enqueue_complete, 1} + assert_receive {:enqueue_complete, count} + assert count >= 1 end # :unavailable is a terminal state in the sense that workers don't @@ -164,7 +146,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do # Force updated_at past the 24 h cooldown so the type_filter's # :unavailable branch picks this contact up. - old = DateTime.utc_now() |> DateTime.add(-2 * 86_400, :second) |> DateTime.truncate(:second) + old = DateTime.utc_now() |> DateTime.shift(day: -2) |> DateTime.truncate(:second) Repo.update_all(from(c in Contact, where: c.id == ^contact.id), set: [updated_at: old]) assert :ok = @@ -172,7 +154,8 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do args: %{"limit" => 10, "types" => @non_era5_types} }) - assert_receive {:enqueue_complete, 1} + assert_receive {:enqueue_complete, count} + assert count >= 1 end test "leaves :unavailable contacts inside the cooldown window alone" do @@ -196,7 +179,10 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do args: %{"limit" => 10, "types" => @non_era5_types} }) - assert_receive {:enqueue_complete, 0} + # Our contact must still be :unavailable — the worker must not + # have touched it, regardless of what other contacts it enqueued. + reloaded = Repo.get!(Contact, contact.id) + assert reloaded.weather_status == :unavailable end # :narr is a virtual type (no narr_status column) — it targets contacts @@ -256,13 +242,13 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorkerTest do defp stale_ts do DateTime.utc_now() - |> DateTime.add(-(@stale_cutoff + 3600), :second) + |> DateTime.shift(second: -(@stale_cutoff + 3600)) |> DateTime.truncate(:second) end defp fresh_ts do DateTime.utc_now() - |> DateTime.add(-3600, :second) + |> DateTime.shift(hour: -1) |> DateTime.truncate(:second) end diff --git a/test/microwaveprop/workers/contact_position_backfill_worker_test.exs b/test/microwaveprop/workers/contact_position_backfill_worker_test.exs index 7df3b290..28754044 100644 --- a/test/microwaveprop/workers/contact_position_backfill_worker_test.exs +++ b/test/microwaveprop/workers/contact_position_backfill_worker_test.exs @@ -109,7 +109,7 @@ defmodule Microwaveprop.Workers.ContactPositionBackfillWorkerTest do pos1: nil, pos2: nil, distance_km: nil, - qso_timestamp: DateTime.add(~U[2026-03-28 18:00:00Z], i * 3600, :second) + qso_timestamp: DateTime.shift(~U[2026-03-28 18:00:00Z], hour: i) }) end diff --git a/test/microwaveprop/workers/gefs_fetch_worker_test.exs b/test/microwaveprop/workers/gefs_fetch_worker_test.exs index ec8abebd..302a78cb 100644 --- a/test/microwaveprop/workers/gefs_fetch_worker_test.exs +++ b/test/microwaveprop/workers/gefs_fetch_worker_test.exs @@ -219,7 +219,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorkerTest do test "round-trips a full batch through the context" do run_time = ~U[2026-04-18 12:00:00Z] fh = 24 - valid_time = DateTime.add(run_time, fh * 3600, :second) + valid_time = DateTime.shift(run_time, hour: fh) batch = [ %{ diff --git a/test/microwaveprop/workers/grid_cache_prune_worker_test.exs b/test/microwaveprop/workers/grid_cache_prune_worker_test.exs index 9bad223c..98d79e44 100644 --- a/test/microwaveprop/workers/grid_cache_prune_worker_test.exs +++ b/test/microwaveprop/workers/grid_cache_prune_worker_test.exs @@ -12,9 +12,9 @@ defmodule Microwaveprop.Workers.GridCachePruneWorkerTest do describe "perform/1" do test "removes cache entries with valid_time older than 3 hours" do now = DateTime.utc_now() - old = now |> DateTime.add(-3, :hour) |> DateTime.add(-1, :second) |> DateTime.truncate(:second) - fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second) - future = now |> DateTime.add(6, :hour) |> DateTime.truncate(:second) + old = now |> DateTime.shift(hour: -3, second: -1) |> DateTime.truncate(:second) + fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) + future = now |> DateTime.shift(hour: 6) |> DateTime.truncate(:second) GridCache.put(old, [%{lat: 32.0, lon: -97.0, temperature: 10.0}]) GridCache.put(fresh, [%{lat: 32.0, lon: -97.0, temperature: 20.0}]) @@ -29,7 +29,7 @@ defmodule Microwaveprop.Workers.GridCachePruneWorkerTest do test "is a no-op when nothing is stale" do fresh = - DateTime.utc_now() |> DateTime.add(-30, :minute) |> DateTime.truncate(:second) + DateTime.utc_now() |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) GridCache.put(fresh, [%{lat: 32.0, lon: -97.0, temperature: 42.0}]) diff --git a/test/microwaveprop/workers/propagation_prune_worker_test.exs b/test/microwaveprop/workers/propagation_prune_worker_test.exs index 98cb3e79..ed94fd2a 100644 --- a/test/microwaveprop/workers/propagation_prune_worker_test.exs +++ b/test/microwaveprop/workers/propagation_prune_worker_test.exs @@ -11,9 +11,9 @@ defmodule Microwaveprop.Workers.PropagationPruneWorkerTest do # picking exactly -3h flakes whenever the test + worker wall-clock # land in the same second. -3h - 1s gives unconditional ordering. now = DateTime.utc_now() - old = now |> DateTime.add(-3, :hour) |> DateTime.add(-1, :second) |> DateTime.truncate(:second) - fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second) - future = now |> DateTime.add(6, :hour) |> DateTime.truncate(:second) + old = now |> DateTime.shift(hour: -3, second: -1) |> DateTime.truncate(:second) + fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) + future = now |> DateTime.shift(hour: 6) |> DateTime.truncate(:second) ScoresFile.write!(10_000, old, [%{lat: 25.0, lon: -125.0, score: 10}]) ScoresFile.write!(10_000, fresh, [%{lat: 25.0, lon: -125.0, score: 20}]) @@ -28,7 +28,7 @@ defmodule Microwaveprop.Workers.PropagationPruneWorkerTest do test "is a no-op when nothing is stale" do now = DateTime.utc_now() - fresh = now |> DateTime.add(-30, :minute) |> DateTime.truncate(:second) + fresh = now |> DateTime.shift(minute: -30) |> DateTime.truncate(:second) ScoresFile.write!(10_000, fresh, [%{lat: 25.0, lon: -125.0, score: 42}]) assert :ok = PropagationPruneWorker.perform(%Oban.Job{}) diff --git a/test/microwaveprop/workers/pskr_calibration_worker_test.exs b/test/microwaveprop/workers/pskr_calibration_worker_test.exs index 40c51073..9abc2fc5 100644 --- a/test/microwaveprop/workers/pskr_calibration_worker_test.exs +++ b/test/microwaveprop/workers/pskr_calibration_worker_test.exs @@ -59,7 +59,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorkerTest do # arrive in the last few minutes of an hour wait up to a full # cron interval before they're sampled. now = truncate_to_hour(DateTime.utc_now()) - previous = DateTime.add(now, -3600, :second) + previous = DateTime.shift(now, hour: -1) insert_spot!(now) insert_spot!(previous, sender_grid: "EM10AA") @@ -90,7 +90,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorkerTest do now = truncate_to_hour(DateTime.utc_now()) Enum.each(0..3, fn offset -> - hour = DateTime.add(now, -offset * 3600, :second) + hour = DateTime.shift(now, hour: -offset) insert_spot!(hour, sender_grid: "EM1#{offset}AA") end) diff --git a/test/microwaveprop_web/controllers/api/v1/auth_controller_test.exs b/test/microwaveprop_web/controllers/api/v1/auth_controller_test.exs index 08c6fb07..40c4a7ea 100644 --- a/test/microwaveprop_web/controllers/api/v1/auth_controller_test.exs +++ b/test/microwaveprop_web/controllers/api/v1/auth_controller_test.exs @@ -60,7 +60,7 @@ defmodule MicrowavepropWeb.Api.V1.AuthControllerTest do end test "accepts an ISO 8601 expires_at", %{conn: conn, user: user} do - future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601() + future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.to_iso8601() conn = post(conn, ~p"/api/v1/auth/tokens", %{ @@ -111,7 +111,7 @@ defmodule MicrowavepropWeb.Api.V1.AuthControllerTest do end test "rejects expires_at in the past via changeset 422", %{conn: conn, user: user} do - past = DateTime.utc_now() |> DateTime.add(-1, :second) |> DateTime.to_iso8601() + past = DateTime.utc_now() |> DateTime.shift(second: -1) |> DateTime.to_iso8601() conn = post(conn, ~p"/api/v1/auth/tokens", %{ diff --git a/test/microwaveprop_web/controllers/user_settings_controller_test.exs b/test/microwaveprop_web/controllers/user_settings_controller_test.exs index ce11267e..0ee53ae9 100644 --- a/test/microwaveprop_web/controllers/user_settings_controller_test.exs +++ b/test/microwaveprop_web/controllers/user_settings_controller_test.exs @@ -20,7 +20,7 @@ defmodule MicrowavepropWeb.UserSettingsControllerTest do assert redirected_to(conn) == ~p"/users/log-in" end - @tag token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -25, :hour) + @tag token_authenticated_at: DateTime.shift(DateTime.utc_now(:second), hour: -25) test "redirects if user is not in sudo mode", %{conn: conn} do conn = get(conn, ~p"/users/settings") assert redirected_to(conn) == ~p"/users/log-in" diff --git a/test/microwaveprop_web/live/contact_live/index_test.exs b/test/microwaveprop_web/live/contact_live/index_test.exs index a964a8b3..c11f7cd9 100644 --- a/test/microwaveprop_web/live/contact_live/index_test.exs +++ b/test/microwaveprop_web/live/contact_live/index_test.exs @@ -103,7 +103,7 @@ defmodule MicrowavepropWeb.ContactLive.IndexTest do test "pagination reaches later pages", %{conn: conn} do for i <- 1..25 do - ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second) + ts = DateTime.shift(~U[2026-01-01 00:00:00Z], hour: i) create_contact(%{qso_timestamp: ts, station1: "W#{i}TEST"}) end diff --git a/test/microwaveprop_web/live/contact_live/show_coverage_test.exs b/test/microwaveprop_web/live/contact_live/show_coverage_test.exs index f0dc3680..dbd1413f 100644 --- a/test/microwaveprop_web/live/contact_live/show_coverage_test.exs +++ b/test/microwaveprop_web/live/contact_live/show_coverage_test.exs @@ -1363,7 +1363,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do nonce = System.unique_integer([:positive]) lat1 = 32.0 + nonce / 1_000_000 - iter_valid_time = DateTime.add(~U[2026-03-28 18:00:00Z], nonce, :second) + iter_valid_time = DateTime.shift(~U[2026-03-28 18:00:00Z], second: nonce) contact = create_contact(%{ @@ -1426,7 +1426,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, - observed_at: DateTime.add(contact.qso_timestamp, i * 60, :second), + observed_at: DateTime.shift(contact.qso_timestamp, minute: i), temp_f: 60.0 + i, dewpoint_f: 45.0 + i, relative_humidity: 55.0, @@ -1478,7 +1478,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, - observed_at: DateTime.add(contact.qso_timestamp, offset, :second), + observed_at: DateTime.shift(contact.qso_timestamp, second: offset), temp_f: temp, dewpoint_f: dewp, relative_humidity: rh, @@ -1576,7 +1576,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do Repo.insert!( Sounding.changeset(%Sounding{}, %{ station_id: station.id, - observed_at: DateTime.add(contact.qso_timestamp, offset, :second), + observed_at: DateTime.shift(contact.qso_timestamp, second: offset), profile: [%{"pres" => 1000.0, "tmpc" => temp, "dwpc" => temp - 5.0, "hght" => 100.0}], level_count: 1, surface_temp_c: temp, diff --git a/test/microwaveprop_web/live/map_live_test.exs b/test/microwaveprop_web/live/map_live_test.exs index 57266d0e..e82f2f7a 100644 --- a/test/microwaveprop_web/live/map_live_test.exs +++ b/test/microwaveprop_web/live/map_live_test.exs @@ -531,12 +531,12 @@ defmodule MicrowavepropWeb.MapLiveTest do end test "renders a '+Nh' suffix when the valid_time is in the future" do - future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second) + future = DateTime.shift(DateTime.utc_now(), hour: 3) assert MapLive.format_data_timestamp(future) =~ "+3h" end test "renders a 'Nh ago' suffix when the valid_time is in the past" do - past = DateTime.add(DateTime.utc_now(), -4 * 3600, :second) + past = DateTime.shift(DateTime.utc_now(), hour: -4) assert MapLive.format_data_timestamp(past) =~ "4h ago" end end @@ -611,7 +611,7 @@ defmodule MicrowavepropWeb.MapLiveTest do assert html =~ "Updating propagation" assert html =~ ~s(data-pipeline-state="running") - future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second) + future = DateTime.shift(DateTime.utc_now(), hour: 3) Phoenix.PubSub.broadcast( Microwaveprop.PubSub, diff --git a/test/microwaveprop_web/live/path_live_test.exs b/test/microwaveprop_web/live/path_live_test.exs index 5a3548aa..8d1ad2bf 100644 --- a/test/microwaveprop_web/live/path_live_test.exs +++ b/test/microwaveprop_web/live/path_live_test.exs @@ -163,7 +163,7 @@ defmodule MicrowavepropWeb.PathLiveTest do |> Map.put(:minute, 0) |> Map.put(:second, 0) - times = for h <- 0..2, do: DateTime.add(base, h * 3600, :second) + times = for h <- 0..2, do: DateTime.shift(base, hour: h) Enum.each(times, fn t -> Propagation.replace_scores( diff --git a/test/microwaveprop_web/live/pskr_spots_live_test.exs b/test/microwaveprop_web/live/pskr_spots_live_test.exs index ee49e10f..5f2ebcfe 100644 --- a/test/microwaveprop_web/live/pskr_spots_live_test.exs +++ b/test/microwaveprop_web/live/pskr_spots_live_test.exs @@ -175,7 +175,7 @@ defmodule MicrowavepropWeb.PskrSpotsLiveTest do # Insert 105 spots — only the 100 most recent should render # Pad single-digit grid numbers so substring matching is unambiguous for i <- 1..105 do - t = DateTime.add(now(), i - 105, :second) + t = DateTime.shift(now(), second: i - 105) grid = if i < 10, do: "GRID00#{i}", else: "GRID#{i}" insert_spot(%{sender_grid: grid, last_spot_at: t, hour_utc: t}) end diff --git a/test/microwaveprop_web/live/skewt_live_test.exs b/test/microwaveprop_web/live/skewt_live_test.exs index fc98b3b8..bb9c4020 100644 --- a/test/microwaveprop_web/live/skewt_live_test.exs +++ b/test/microwaveprop_web/live/skewt_live_test.exs @@ -61,7 +61,7 @@ defmodule MicrowavepropWeb.SkewtLiveTest do # Trigger select_time with a valid future ISO. Even if no profile is # found for this time, the handler exercises the full re-load path. - future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601() + future = DateTime.utc_now() |> DateTime.shift(hour: 1) |> DateTime.to_iso8601() _ = render_hook(view, "select_time", %{"valid_time" => future}) _ = render_async(view) diff --git a/test/microwaveprop_web/live/weather_ca_map_live_test.exs b/test/microwaveprop_web/live/weather_ca_map_live_test.exs index 748160b1..0bae5889 100644 --- a/test/microwaveprop_web/live/weather_ca_map_live_test.exs +++ b/test/microwaveprop_web/live/weather_ca_map_live_test.exs @@ -67,7 +67,7 @@ defmodule MicrowavepropWeb.WeatherCaMapLiveTest do |> Map.put(:minute, 0) |> Map.put(:second, 0) - stale_t = DateTime.add(now, -4 * 3600, :second) + stale_t = DateTime.shift(now, hour: -4) hrdps_dir = ScalarFile.dir_for_hrdps(stale_t) File.mkdir_p!(hrdps_dir) diff --git a/test/microwaveprop_web/live/weather_map_live_test.exs b/test/microwaveprop_web/live/weather_map_live_test.exs index 4e62adaf..a499ab1c 100644 --- a/test/microwaveprop_web/live/weather_map_live_test.exs +++ b/test/microwaveprop_web/live/weather_map_live_test.exs @@ -59,7 +59,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do |> Map.put(:minute, 0) |> Map.put(:second, 0) - times = for h <- 0..2, do: DateTime.add(base, h * 3600, :second) + times = for h <- 0..2, do: DateTime.shift(base, hour: h) # Seed a profile file at each forecast hour. Enum.each(times, &write_profile(&1, 33.0, -97.0)) @@ -82,14 +82,14 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do |> Map.put(:minute, 0) |> Map.put(:second, 0) - stale_t = DateTime.add(now, -2 * 3600, :second) + stale_t = DateTime.shift(now, hour: -2) # `now` is truncated to the top of the hour; real `utc_now()` is up to # 60 min ahead of that, so a `now - 30 min` recent_t is 30-90 min old # of real time and the LiveView's `utc_now() - 1h` cutoff filters it # out 50% of the time. Anchor recent_t at the top of the current hour # — that's always within the 1-hour window regardless of minute. recent_t = now - future_t = DateTime.add(now, 3 * 3600, :second) + future_t = DateTime.shift(now, hour: 3) Enum.each([stale_t, recent_t, future_t], &write_profile(&1, 33.0, -97.0)) @@ -116,7 +116,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do # sequentially so `latest_grid_valid_time` can point to f11 even # though the user expects the map to show "Now" by default. now_t = base - future_t = DateTime.add(base, 11 * 3600, :second) + future_t = DateTime.shift(base, hour: 11) write_profile(now_t, 33.0, -97.0) write_profile(future_t, 33.0, -97.0) @@ -166,7 +166,7 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do |> Map.put(:second, 0) now_t = base - future_t = DateTime.add(base, 3 * 3600, :second) + future_t = DateTime.shift(base, hour: 3) # Seed f00 with a 22°C surface and f03 with a 10°C surface so we # can tell them apart in the push_event payload. diff --git a/test/microwaveprop_web/user_auth_test.exs b/test/microwaveprop_web/user_auth_test.exs index 1729f20c..fbbe35ab 100644 --- a/test/microwaveprop_web/user_auth_test.exs +++ b/test/microwaveprop_web/user_auth_test.exs @@ -198,7 +198,7 @@ defmodule MicrowavepropWeb.UserAuthTest do end test "redirects when authentication is too old", %{conn: conn, user: user} do - too_long_ago = :second |> DateTime.utc_now() |> DateTime.add(-25, :hour) + too_long_ago = :second |> DateTime.utc_now() |> DateTime.shift(hour: -25) user = %{user | authenticated_at: too_long_ago} user_token = Accounts.generate_user_session_token(user) {user, token_inserted_at} = Accounts.get_user_by_session_token(user_token) diff --git a/test/mix/tasks/prop_compare_test.exs b/test/mix/tasks/prop_compare_test.exs index 74a72200..163992e7 100644 --- a/test/mix/tasks/prop_compare_test.exs +++ b/test/mix/tasks/prop_compare_test.exs @@ -26,7 +26,7 @@ defmodule Mix.Tasks.Prop.CompareTest do defp seed_contact_and_hrrr(opts \\ []) do qso_ts = - Keyword.get(opts, :qso_timestamp, DateTime.utc_now() |> DateTime.add(-3600, :second) |> DateTime.truncate(:second)) + Keyword.get(opts, :qso_timestamp, DateTime.utc_now() |> DateTime.shift(hour: -1) |> DateTime.truncate(:second)) valid_time = qso_ts |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0) band = Keyword.get(opts, :band, "10000") @@ -86,7 +86,7 @@ defmodule Mix.Tasks.Prop.CompareTest do history_lines = for _ <- 1..6 do Jason.encode!(%{ - "date" => DateTime.utc_now() |> DateTime.add(-3600 * 24, :second) |> DateTime.to_iso8601(), + "date" => DateTime.utc_now() |> DateTime.shift(day: -1) |> DateTime.to_iso8601(), "n_samples" => 100, "overall" => %{"alg_distance_spearman" => 0.5, "alg_ml_rmse" => 5.0}, "by_band" => %{} diff --git a/test/mix/tasks/propagation_ml_tasks_test.exs b/test/mix/tasks/propagation_ml_tasks_test.exs index 6a4b0405..5f457483 100644 --- a/test/mix/tasks/propagation_ml_tasks_test.exs +++ b/test/mix/tasks/propagation_ml_tasks_test.exs @@ -106,7 +106,7 @@ defmodule Mix.Tasks.PropagationMlTasksTest do base_time = ~U[2020-06-15 18:00:00Z] for i <- 1..6 do - valid_time = base_time |> DateTime.add(i * 3600, :second) |> DateTime.truncate(:second) + valid_time = base_time |> DateTime.shift(hour: i) |> DateTime.truncate(:second) # Distinct snapped grid points for each i. lat1 = 32.0 + i * 0.125 lon1 = -97.0 - i * 0.125 diff --git a/test/mix/tasks/simple_tasks_test.exs b/test/mix/tasks/simple_tasks_test.exs index 982a4624..eeab23a4 100644 --- a/test/mix/tasks/simple_tasks_test.exs +++ b/test/mix/tasks/simple_tasks_test.exs @@ -213,7 +213,7 @@ defmodule Mix.Tasks.SimpleTasksTest do {:ok, _} = %HrrrProfile{} |> HrrrProfile.changeset(%{ - valid_time: DateTime.add(~U[2025-06-15 12:00:00Z], i * 86_400, :second), + valid_time: DateTime.shift(~U[2025-06-15 12:00:00Z], day: i), lat: 32.5, lon: -97.0, surface_temp_c: 20.0 + i, diff --git a/test/support/fixtures/contacts_fixtures.ex b/test/support/fixtures/contacts_fixtures.ex new file mode 100644 index 00000000..7db4f4ed --- /dev/null +++ b/test/support/fixtures/contacts_fixtures.ex @@ -0,0 +1,53 @@ +defmodule Microwaveprop.ContactsFixtures do + @moduledoc """ + Factory helpers for creating Contact records in tests. + + Uses globally-unique `qso_timestamp` microseconds (via + `System.unique_integer([:positive, :monotonic])`) to prevent + `contacts_dedup_idx` unique-constraint violations when async + tests across different modules run in parallel and would + otherwise insert contacts with identical dedup-key columns. + """ + + alias Microwaveprop.Radio.Contact + alias Microwaveprop.Repo + + @doc """ + Creates a contact with unique dedup-key column values. + + Override any field by passing it in `attrs`. Pass `:user_id` to + associate the contact with a user. + + Returns the created `%Contact{}`. + """ + @spec create_contact(map()) :: Contact.t() + def create_contact(attrs \\ %{}) do + # Globally-unique second offset so every contact has a distinct + # dedup key (band, qso_timestamp, stations, grids). The column is + # timestamp(0), so microsecond offsets are truncated. + unique_s = System.unique_integer([:positive, :monotonic]) + + default = %{ + station1: "W5XD", + station2: "K5TR", + qso_timestamp: DateTime.shift(~U[2026-03-28 18:00:00Z], second: unique_s), + mode: "CW", + band: Decimal.new("1296"), + grid1: "EM12", + grid2: "EM00", + pos1: %{"lat" => 32.9, "lon" => -97.0}, + pos2: %{"lat" => 30.3, "lon" => -97.7}, + distance_km: Decimal.new("295") + } + + merged = Map.merge(default, attrs) + {user_id, changeset_attrs} = Map.pop(merged, :user_id) + + {:ok, contact} = + %Contact{user_id: user_id} + |> Contact.changeset(changeset_attrs) + |> Repo.insert() + + contact + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9169ea92..eb666866 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -15,6 +15,7 @@ Code.require_file("test/support/data_case.ex") Code.require_file("test/support/conn_case.ex") Code.require_file("test/support/fixtures/accounts_fixtures.ex") Code.require_file("test/support/fixtures/beacons_fixtures.ex") +Code.require_file("test/support/fixtures/contacts_fixtures.ex") # ExUnit.start/1 with capture_log: true starts :logger, which cascades # into the full OTP application. mix.exs now runs `mix test --no-start`