fix enrichment
This commit is contained in:
parent
d95098daa9
commit
d275e9e7c4
19 changed files with 239 additions and 193 deletions
|
|
@ -85,17 +85,17 @@ config :microwaveprop, Oban,
|
||||||
# Enable dev routes for dashboard and mailbox
|
# Enable dev routes for dashboard and mailbox
|
||||||
config :microwaveprop, dev_routes: true
|
config :microwaveprop, dev_routes: true
|
||||||
|
|
||||||
|
# HRRR caching handled by nginx proxy on skippy — no local filesystem cache
|
||||||
|
|
||||||
|
# Proxy HRRR downloads through local caching nginx (set to nil to use S3 directly)
|
||||||
|
config :microwaveprop, hrrr_base_url: "http://skippy.w5isp.com:8080"
|
||||||
|
|
||||||
# Load ML model at startup (Nx/Axon/EXLA only available in dev/test)
|
# Load ML model at startup (Nx/Axon/EXLA only available in dev/test)
|
||||||
config :microwaveprop, load_ml_model: true
|
config :microwaveprop, load_ml_model: true
|
||||||
|
|
||||||
# Use local SRTM1 tiles for elevation lookups instead of the Open-Meteo API
|
# Use local SRTM1 tiles for elevation lookups instead of the Open-Meteo API
|
||||||
config :microwaveprop, srtm_tiles_dir: Path.expand("~/srtm/tiles")
|
config :microwaveprop, srtm_tiles_dir: Path.expand("~/srtm/tiles")
|
||||||
|
|
||||||
# HRRR caching handled by nginx proxy on skippy — no local filesystem cache
|
|
||||||
|
|
||||||
# Proxy HRRR downloads through local caching nginx (set to nil to use S3 directly)
|
|
||||||
config :microwaveprop, hrrr_base_url: "http://skippy.w5isp.com:8080"
|
|
||||||
|
|
||||||
# Freshness monitor watches for stale propagation scores
|
# Freshness monitor watches for stale propagation scores
|
||||||
config :microwaveprop, start_freshness_monitor: true
|
config :microwaveprop, start_freshness_monitor: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,43 +46,6 @@ if config_env() == :prod do
|
||||||
|
|
||||||
host = System.get_env("PHX_HOST") || "example.com"
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
|
|
||||||
config :microwaveprop, Microwaveprop.Repo,
|
|
||||||
# ssl: true,
|
|
||||||
url: database_url,
|
|
||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "20"),
|
|
||||||
# For machines with several cores, consider starting multiple pools of `pool_size`
|
|
||||||
# pool_count: 4,
|
|
||||||
socket_options: maybe_ipv6
|
|
||||||
|
|
||||||
config :microwaveprop, MicrowavepropWeb.Endpoint,
|
|
||||||
# SSL terminated by Cloudflare tunnel; generated URLs still use https
|
|
||||||
url: [host: host, port: 443, scheme: "https"],
|
|
||||||
http: [
|
|
||||||
# Enable IPv6 and bind on all interfaces.
|
|
||||||
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
|
||||||
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
|
|
||||||
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
|
||||||
ip: {0, 0, 0, 0, 0, 0, 0, 0}
|
|
||||||
],
|
|
||||||
secret_key_base: secret_key_base
|
|
||||||
|
|
||||||
# Production Oban: live scoring, polling, and on-demand QSO enrichment (no cron backfill)
|
|
||||||
config :microwaveprop, Oban,
|
|
||||||
queues: [propagation: 1, commercial: 2, solar: 1, weather: 5, hrrr: 5, terrain: 2, iemre: 5],
|
|
||||||
plugins: [
|
|
||||||
{Oban.Plugins.Pruner, max_age: 3600 * 24},
|
|
||||||
{Oban.Plugins.Lifeline, rescue_after: 30 * 60 * 1000},
|
|
||||||
{Oban.Plugins.Cron,
|
|
||||||
crontab: [
|
|
||||||
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
|
|
||||||
{"*/5 * * * *", Microwaveprop.Commercial.PollWorker},
|
|
||||||
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}
|
|
||||||
]}
|
|
||||||
]
|
|
||||||
|
|
||||||
config :microwaveprop, srtm_tiles_dir: "/srtm"
|
|
||||||
config :microwaveprop, hrrr_base_url: System.get_env("HRRR_BASE_URL", "http://skippy.w5isp.com:8080")
|
|
||||||
|
|
||||||
# ## SSL Support
|
# ## SSL Support
|
||||||
#
|
#
|
||||||
# To get SSL working, you will need to add the `https` key
|
# To get SSL working, you will need to add the `https` key
|
||||||
|
|
@ -132,5 +95,41 @@ if config_env() == :prod do
|
||||||
auth: :always,
|
auth: :always,
|
||||||
ssl: false
|
ssl: false
|
||||||
|
|
||||||
|
config :microwaveprop, Microwaveprop.Repo,
|
||||||
|
# ssl: true,
|
||||||
|
url: database_url,
|
||||||
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "20"),
|
||||||
|
# For machines with several cores, consider starting multiple pools of `pool_size`
|
||||||
|
# pool_count: 4,
|
||||||
|
socket_options: maybe_ipv6
|
||||||
|
|
||||||
|
config :microwaveprop, MicrowavepropWeb.Endpoint,
|
||||||
|
# SSL terminated by Cloudflare tunnel; generated URLs still use https
|
||||||
|
url: [host: host, port: 443, scheme: "https"],
|
||||||
|
http: [
|
||||||
|
# Enable IPv6 and bind on all interfaces.
|
||||||
|
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
|
||||||
|
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
|
||||||
|
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
|
||||||
|
ip: {0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
],
|
||||||
|
secret_key_base: secret_key_base
|
||||||
|
|
||||||
|
# Production Oban: live scoring, polling, and on-demand QSO enrichment (no cron backfill)
|
||||||
|
config :microwaveprop, Oban,
|
||||||
|
queues: [propagation: 1, commercial: 2, solar: 1, weather: 5, hrrr: 5, terrain: 2, iemre: 5],
|
||||||
|
plugins: [
|
||||||
|
{Oban.Plugins.Pruner, max_age: 3600 * 24},
|
||||||
|
{Oban.Plugins.Lifeline, rescue_after: 30 * 60 * 1000},
|
||||||
|
{Oban.Plugins.Cron,
|
||||||
|
crontab: [
|
||||||
|
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
|
||||||
|
{"*/5 * * * *", Microwaveprop.Commercial.PollWorker},
|
||||||
|
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
|
||||||
config :microwaveprop, :email_from, {"NTMS Propagation", "graham@w5isp.com"}
|
config :microwaveprop, :email_from, {"NTMS Propagation", "graham@w5isp.com"}
|
||||||
|
config :microwaveprop, hrrr_base_url: System.get_env("HRRR_BASE_URL", "http://skippy.w5isp.com:8080")
|
||||||
|
config :microwaveprop, srtm_tiles_dir: "/srtm"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ defmodule Microwaveprop.Markdown do
|
||||||
defp parse_blocks(["```" <> lang | rest], acc) do
|
defp parse_blocks(["```" <> lang | rest], acc) do
|
||||||
{code_lines, remaining} = take_until_fence(rest, [])
|
{code_lines, remaining} = take_until_fence(rest, [])
|
||||||
lang = String.trim(lang)
|
lang = String.trim(lang)
|
||||||
lang_attr = if lang != "", do: " class=\"language-#{lang}\"", else: ""
|
lang_attr = if lang == "", do: "", else: " class=\"language-#{lang}\""
|
||||||
parse_blocks(remaining, [{:code, lang_attr, code_lines} | acc])
|
parse_blocks(remaining, [{:code, lang_attr, code_lines} | acc])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -123,11 +123,11 @@ defmodule Microwaveprop.Markdown do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_block({:paragraph, lines}) do
|
defp render_block({:paragraph, lines}) do
|
||||||
"<p>#{lines |> Enum.map_join(" ", &inline/1)}</p>"
|
"<p>#{Enum.map_join(lines, " ", &inline/1)}</p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_block({:code, lang_attr, lines}) do
|
defp render_block({:code, lang_attr, lines}) do
|
||||||
escaped = lines |> Enum.map_join("\n", &escape_html/1)
|
escaped = Enum.map_join(lines, "\n", &escape_html/1)
|
||||||
"<pre><code#{lang_attr}>#{escaped}</code></pre>"
|
"<pre><code#{lang_attr}>#{escaped}</code></pre>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ defmodule Microwaveprop.Markdown do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_block({:table, lines}) do
|
defp render_block({:table, lines}) do
|
||||||
rows = lines |> Enum.map(&parse_table_row/1)
|
rows = Enum.map(lines, &parse_table_row/1)
|
||||||
|
|
||||||
case rows do
|
case rows do
|
||||||
[header, _separator | body] ->
|
[header, _separator | body] ->
|
||||||
|
|
|
||||||
|
|
@ -112,17 +112,14 @@ defmodule Microwaveprop.Radio do
|
||||||
Repo.all(query)
|
Repo.all(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unprocessed_contacts(limit \\ 500),
|
def unprocessed_contacts(limit \\ 500), do: contacts_needing_enrichment(:weather_status, limit)
|
||||||
do: contacts_needing_enrichment(:weather_status, limit)
|
|
||||||
|
|
||||||
def unprocessed_hrrr_contacts(limit \\ 500),
|
def unprocessed_hrrr_contacts(limit \\ 500), do: contacts_needing_enrichment(:hrrr_status, limit)
|
||||||
do: contacts_needing_enrichment(:hrrr_status, limit)
|
|
||||||
|
|
||||||
def unprocessed_terrain_contacts(limit \\ 500),
|
def unprocessed_terrain_contacts(limit \\ 500),
|
||||||
do: contacts_needing_enrichment(:terrain_status, limit, &where(&1, [q], not is_nil(q.pos2)))
|
do: contacts_needing_enrichment(:terrain_status, limit, &where(&1, [q], not is_nil(q.pos2)))
|
||||||
|
|
||||||
def unprocessed_iemre_contacts(limit \\ 500),
|
def unprocessed_iemre_contacts(limit \\ 500), do: contacts_needing_enrichment(:iemre_status, limit)
|
||||||
do: contacts_needing_enrichment(:iemre_status, limit)
|
|
||||||
|
|
||||||
def set_enrichment_status!(ids, field, status) do
|
def set_enrichment_status!(ids, field, status) do
|
||||||
Contact
|
Contact
|
||||||
|
|
@ -231,7 +228,8 @@ defmodule Microwaveprop.Radio do
|
||||||
|
|
||||||
distance =
|
distance =
|
||||||
if pos1 && pos2 do
|
if pos1 && pos2 do
|
||||||
haversine_km(pos1["lat"], pos1["lon"], pos2["lat"], pos2["lon"])
|
pos1["lat"]
|
||||||
|
|> haversine_km(pos1["lon"], pos2["lat"], pos2["lon"])
|
||||||
|> round()
|
|> round()
|
||||||
|> Decimal.new()
|
|> Decimal.new()
|
||||||
else
|
else
|
||||||
|
|
@ -244,12 +242,12 @@ defmodule Microwaveprop.Radio do
|
||||||
|> then(fn c -> if pos2 && is_nil(contact.pos2), do: Map.put(c, :pos2, pos2), else: c end)
|
|> then(fn c -> if pos2 && is_nil(contact.pos2), do: Map.put(c, :pos2, pos2), else: c end)
|
||||||
|> then(fn c -> if distance && is_nil(contact.distance_km), do: Map.put(c, :distance_km, distance), else: c end)
|
|> then(fn c -> if distance && is_nil(contact.distance_km), do: Map.put(c, :distance_km, distance), else: c end)
|
||||||
|
|
||||||
if changes != %{} do
|
if changes == %{} do
|
||||||
|
contact
|
||||||
|
else
|
||||||
contact
|
contact
|
||||||
|> Ecto.Changeset.change(changes)
|
|> Ecto.Changeset.change(changes)
|
||||||
|> Repo.update!()
|
|> Repo.update!()
|
||||||
else
|
|
||||||
contact
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
contact
|
contact
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
field :mode, :string
|
field :mode, :string
|
||||||
field :band, :decimal
|
field :band, :decimal
|
||||||
field :distance_km, :decimal
|
field :distance_km, :decimal
|
||||||
|
|
||||||
field :hrrr_status, Ecto.Enum,
|
field :hrrr_status, Ecto.Enum,
|
||||||
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
||||||
default: :pending
|
default: :pending
|
||||||
|
|
@ -35,6 +36,7 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
field :iemre_status, Ecto.Enum,
|
field :iemre_status, Ecto.Enum,
|
||||||
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
values: [:pending, :queued, :processing, :complete, :failed, :unavailable],
|
||||||
default: :pending
|
default: :pending
|
||||||
|
|
||||||
field :user_submitted, :boolean, default: false
|
field :user_submitted, :boolean, default: false
|
||||||
field :submitter_email, :string
|
field :submitter_email, :string
|
||||||
field :flagged_invalid, :boolean, default: false
|
field :flagged_invalid, :boolean, default: false
|
||||||
|
|
@ -93,9 +95,7 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
|
|
||||||
# Callsigns: letters, digits, and / only (e.g. W5XD, KG5CCI/P, VE3/W5XD)
|
# Callsigns: letters, digits, and / only (e.g. W5XD, KG5CCI/P, VE3/W5XD)
|
||||||
defp validate_callsign(changeset, field) do
|
defp validate_callsign(changeset, field) do
|
||||||
validate_format(changeset, field, ~r/^[A-Z0-9\/]+$/,
|
validate_format(changeset, field, ~r/^[A-Z0-9\/]+$/, message: "must contain only letters, digits, and /")
|
||||||
message: "must contain only letters, digits, and /"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_grid_format(changeset, field) do
|
defp validate_grid_format(changeset, field) do
|
||||||
|
|
|
||||||
|
|
@ -91,40 +91,38 @@ defmodule Microwaveprop.Radio.CsvImport do
|
||||||
# Each returns {year, month, day, hour, minute, second} or nil.
|
# Each returns {year, month, day, hour, minute, second} or nil.
|
||||||
defp timestamp_parsers do
|
defp timestamp_parsers do
|
||||||
[
|
[
|
||||||
# ISO-ish: 2024-06-15T14:30:00Z, 2024-06-15 14:30, 2024/06/15 14:30:00, etc.
|
# ISO-ish: 2024-06-15T14:30:00Z, 2024-06-15 14:30, 2024/06/15 14:30:00, etc.
|
||||||
# Accepts T or space separator, optional seconds, optional timezone suffix
|
# Accepts T or space separator, optional seconds, optional timezone suffix
|
||||||
{~r"^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T\s]+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$"i,
|
{~r"^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[T\s]+(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$"i,
|
||||||
fn
|
fn
|
||||||
[_, y, m, d, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
[_, y, m, d, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
||||||
[_, y, m, d, h, mi | _] -> {y, m, d, h, mi, "00"}
|
[_, y, m, d, h, mi | _] -> {y, m, d, h, mi, "00"}
|
||||||
end},
|
end},
|
||||||
# Date only: 2024-06-15, 2024/06/15
|
# Date only: 2024-06-15, 2024/06/15
|
||||||
{~r"^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$",
|
{~r"^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$", fn [_, y, m, d] -> {y, m, d, "00", "00", "00"} end},
|
||||||
fn [_, y, m, d] -> {y, m, d, "00", "00", "00"} end},
|
# US with AM/PM: 6/15/2024 2:30 PM, 6-15-2024 2:30PM, 06.15.2024 02:30 am
|
||||||
# US with AM/PM: 6/15/2024 2:30 PM, 6-15-2024 2:30PM, 06.15.2024 02:30 am
|
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)$"i,
|
||||||
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)$"i,
|
fn
|
||||||
fn
|
[_, m, d, y, h, mi, s, ampm] when byte_size(s) > 0 ->
|
||||||
[_, m, d, y, h, mi, s, ampm] when byte_size(s) > 0 ->
|
{y, m, d, to_string(parse_12h(String.to_integer(h), String.upcase(ampm))), mi, s}
|
||||||
{y, m, d, to_string(parse_12h(String.to_integer(h), String.upcase(ampm))), mi, s}
|
|
||||||
|
|
||||||
[_, m, d, y, h, mi, _, ampm] ->
|
[_, m, d, y, h, mi, _, ampm] ->
|
||||||
{y, m, d, to_string(parse_12h(String.to_integer(h), String.upcase(ampm))), mi, "00"}
|
{y, m, d, to_string(parse_12h(String.to_integer(h), String.upcase(ampm))), mi, "00"}
|
||||||
end},
|
end},
|
||||||
# US 24h: 6/15/2024 14:30, 06-15-2024 14:30:00, 6.15.2024 14:30
|
# US 24h: 6/15/2024 14:30, 06-15-2024 14:30:00, 6.15.2024 14:30
|
||||||
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$",
|
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$",
|
||||||
fn
|
fn
|
||||||
[_, m, d, y, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
[_, m, d, y, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
||||||
[_, m, d, y, h, mi | _] -> {y, m, d, h, mi, "00"}
|
[_, m, d, y, h, mi | _] -> {y, m, d, h, mi, "00"}
|
||||||
end},
|
end},
|
||||||
# US date only: 6/15/2024, 06-15-2024
|
# US date only: 6/15/2024, 06-15-2024
|
||||||
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})$",
|
{~r"^(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})$", fn [_, m, d, y] -> {y, m, d, "00", "00", "00"} end},
|
||||||
fn [_, m, d, y] -> {y, m, d, "00", "00", "00"} end},
|
# Compact: 20240615T143000, 20240615 1430, 20240615T1430
|
||||||
# Compact: 20240615T143000, 20240615 1430, 20240615T1430
|
{~r"^(\d{4})(\d{2})(\d{2})[T\s]?(\d{2})(\d{2})(\d{2})?$",
|
||||||
{~r"^(\d{4})(\d{2})(\d{2})[T\s]?(\d{2})(\d{2})(\d{2})?$",
|
fn
|
||||||
fn
|
[_, y, m, d, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
||||||
[_, y, m, d, h, mi, s] when byte_size(s) > 0 -> {y, m, d, h, mi, s}
|
[_, y, m, d, h, mi | _] -> {y, m, d, h, mi, "00"}
|
||||||
[_, y, m, d, h, mi | _] -> {y, m, d, h, mi, "00"}
|
end}
|
||||||
end}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -204,7 +202,7 @@ defmodule Microwaveprop.Radio.CsvImport do
|
||||||
defp parse_quoted_field(<<"\"", rest::binary>>, acc, current) do
|
defp parse_quoted_field(<<"\"", rest::binary>>, acc, current) do
|
||||||
# End of quoted field — skip to next comma or end
|
# End of quoted field — skip to next comma or end
|
||||||
case rest do
|
case rest do
|
||||||
<<"," , rest2::binary>> -> do_parse_fields(rest2, [current | acc], "")
|
<<",", rest2::binary>> -> do_parse_fields(rest2, [current | acc], "")
|
||||||
"" -> [current | acc]
|
"" -> [current | acc]
|
||||||
_ -> do_parse_fields(rest, [current | acc], "")
|
_ -> do_parse_fields(rest, [current | acc], "")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,19 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
# Fine-grained levels below 900mb (~1km) for duct detection, plus standard upper levels.
|
# Fine-grained levels below 900mb (~1km) for duct detection, plus standard upper levels.
|
||||||
# Every 25mb from 1000-900 for ~80m vertical spacing near surface.
|
# Every 25mb from 1000-900 for ~80m vertical spacing near surface.
|
||||||
@pressure_levels [
|
@pressure_levels [
|
||||||
1000, 975, 950, 925, 900,
|
1000,
|
||||||
875, 850, 825, 800, 775, 750, 725, 700
|
975,
|
||||||
|
950,
|
||||||
|
925,
|
||||||
|
900,
|
||||||
|
875,
|
||||||
|
850,
|
||||||
|
825,
|
||||||
|
800,
|
||||||
|
775,
|
||||||
|
750,
|
||||||
|
725,
|
||||||
|
700
|
||||||
]
|
]
|
||||||
|
|
||||||
@surface_messages [
|
@surface_messages [
|
||||||
|
|
@ -371,9 +382,12 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
|
|
||||||
defp read_cache(key) do
|
defp read_cache(key) do
|
||||||
case hrrr_cache_dir() do
|
case hrrr_cache_dir() do
|
||||||
nil -> :miss
|
nil ->
|
||||||
|
:miss
|
||||||
|
|
||||||
dir ->
|
dir ->
|
||||||
path = Path.join(dir, key)
|
path = Path.join(dir, key)
|
||||||
|
|
||||||
case File.read(path) do
|
case File.read(path) do
|
||||||
{:ok, binary} -> {:ok, binary}
|
{:ok, binary} -> {:ok, binary}
|
||||||
{:error, _} -> :miss
|
{:error, _} -> :miss
|
||||||
|
|
@ -383,7 +397,9 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
|
|
||||||
defp write_cache(key, binary) do
|
defp write_cache(key, binary) do
|
||||||
case hrrr_cache_dir() do
|
case hrrr_cache_dir() do
|
||||||
nil -> :ok
|
nil ->
|
||||||
|
:ok
|
||||||
|
|
||||||
dir ->
|
dir ->
|
||||||
File.mkdir_p!(dir)
|
File.mkdir_p!(dir)
|
||||||
path = Path.join(dir, key)
|
path = Path.join(dir, key)
|
||||||
|
|
|
||||||
|
|
@ -27,49 +27,49 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
||||||
dist_km = Decimal.to_float(contact.distance_km || Decimal.new(0))
|
dist_km = Decimal.to_float(contact.distance_km || Decimal.new(0))
|
||||||
freq_ghz = Decimal.to_float(contact.band) / 1000
|
freq_ghz = Decimal.to_float(contact.band) / 1000
|
||||||
|
|
||||||
# Look up HRRR refractivity gradient for dynamic k-factor
|
# Look up HRRR refractivity gradient for dynamic k-factor
|
||||||
k = lookup_k_factor(contact)
|
k = lookup_k_factor(contact)
|
||||||
|
|
||||||
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2, 64, download: true) do
|
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2, 64, download: true) do
|
||||||
{:ok, profile} ->
|
{:ok, profile} ->
|
||||||
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, k_factor: k)
|
analysis = TerrainAnalysis.analyse(profile, dist_km, freq_ghz, k_factor: k)
|
||||||
|
|
||||||
path_points =
|
path_points =
|
||||||
Enum.map(profile, fn p ->
|
Enum.map(profile, fn p ->
|
||||||
%{
|
%{
|
||||||
"lat" => p.lat,
|
"lat" => p.lat,
|
||||||
"lon" => p.lon,
|
"lon" => p.lon,
|
||||||
"d" => p.d,
|
"d" => p.d,
|
||||||
"elev" => p.elev,
|
"elev" => p.elev,
|
||||||
"dist_km" => p.dist_km
|
"dist_km" => p.dist_km
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Terrain.upsert_terrain_profile(%{
|
Terrain.upsert_terrain_profile(%{
|
||||||
qso_id: qso_id,
|
qso_id: qso_id,
|
||||||
sample_count: length(profile),
|
sample_count: length(profile),
|
||||||
path_points: path_points,
|
path_points: path_points,
|
||||||
max_elevation_m: analysis.max_elevation_m,
|
max_elevation_m: analysis.max_elevation_m,
|
||||||
min_clearance_m: analysis.min_clearance_m,
|
min_clearance_m: analysis.min_clearance_m,
|
||||||
diffraction_db: analysis.diffraction_db,
|
diffraction_db: analysis.diffraction_db,
|
||||||
fresnel_hit_count: analysis.fresnel_hit_count,
|
fresnel_hit_count: analysis.fresnel_hit_count,
|
||||||
obstructed_count: analysis.obstructed_count,
|
obstructed_count: analysis.obstructed_count,
|
||||||
verdict: analysis.verdict
|
verdict: analysis.verdict
|
||||||
})
|
})
|
||||||
|
|
||||||
Radio.set_enrichment_status!([qso_id], :terrain_status, :complete)
|
Radio.set_enrichment_status!([qso_id], :terrain_status, :complete)
|
||||||
|
|
||||||
Phoenix.PubSub.broadcast(
|
Phoenix.PubSub.broadcast(
|
||||||
Microwaveprop.PubSub,
|
Microwaveprop.PubSub,
|
||||||
"contact_enrichment:#{qso_id}",
|
"contact_enrichment:#{qso_id}",
|
||||||
{:terrain_ready, qso_id}
|
{:terrain_ready, qso_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
Radio.set_enrichment_status!([qso_id], :terrain_status, :unavailable)
|
Radio.set_enrichment_status!([qso_id], :terrain_status, :unavailable)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
{n, _} -> max(n, 1)
|
{n, _} -> max(n, 1)
|
||||||
:error -> 1
|
:error -> 1
|
||||||
end
|
end
|
||||||
|
|
||||||
sort_by = validate_sort_field(Map.get(params, "sort_by", @default_sort_by))
|
sort_by = validate_sort_field(Map.get(params, "sort_by", @default_sort_by))
|
||||||
sort_order = validate_sort_order(Map.get(params, "sort_order", @default_sort_order))
|
sort_order = validate_sort_order(Map.get(params, "sort_order", @default_sort_order))
|
||||||
search = Map.get(params, "search", "")
|
search = Map.get(params, "search", "")
|
||||||
|
|
|
||||||
|
|
@ -346,7 +346,6 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
defp load_solar(contact) do
|
defp load_solar(contact) do
|
||||||
contact.qso_timestamp
|
contact.qso_timestamp
|
||||||
|> DateTime.to_date()
|
|> DateTime.to_date()
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,10 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Top-left control panel --%>
|
<%!-- Top-left control panel --%>
|
||||||
<div id="map-controls" class="absolute top-2 left-12 z-[1000] flex flex-col gap-2 max-w-[calc(100vw-4rem)] md:left-14 md:top-3 md:max-w-none">
|
<div
|
||||||
|
id="map-controls"
|
||||||
|
class="absolute top-2 left-12 z-[1000] flex flex-col gap-2 max-w-[calc(100vw-4rem)] md:left-14 md:top-3 md:max-w-none"
|
||||||
|
>
|
||||||
<div class="bg-base-100/90 shadow rounded-box border border-base-300 p-2 md:p-3 flex flex-col gap-2">
|
<div class="bg-base-100/90 shadow rounded-box border border-base-300 p-2 md:p-3 flex flex-col gap-2">
|
||||||
<div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
|
<div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,10 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
<button
|
<button
|
||||||
class={[
|
class={[
|
||||||
"px-4 py-2 text-sm font-medium rounded-md transition-colors",
|
"px-4 py-2 text-sm font-medium rounded-md transition-colors",
|
||||||
if(@active_tab == :single, do: "bg-primary text-primary-content", else: "hover:bg-base-300")
|
if(@active_tab == :single,
|
||||||
|
do: "bg-primary text-primary-content",
|
||||||
|
else: "hover:bg-base-300"
|
||||||
|
)
|
||||||
]}
|
]}
|
||||||
phx-click="switch_tab"
|
phx-click="switch_tab"
|
||||||
phx-value-tab="single"
|
phx-value-tab="single"
|
||||||
|
|
@ -275,10 +278,11 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
<code class="text-xs">station1, station2, grid1, grid2, band, mode, qso_timestamp</code>
|
<code class="text-xs">station1, station2, grid1, grid2, band, mode, qso_timestamp</code>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1 text-base-content/60">
|
<ul class="list-disc list-inside mt-2 space-y-1 text-base-content/60">
|
||||||
<li>
|
<li>
|
||||||
Timestamps in most formats accepted (e.g. <code>2024-06-15T14:30:00Z</code>,
|
Timestamps in most formats accepted (e.g. <code>2024-06-15T14:30:00Z</code>, <code>6/15/2024 2:30 PM</code>, <code>2024-06-15 14:30</code>). All times assumed UTC.
|
||||||
<code>6/15/2024 2:30 PM</code>, <code>2024-06-15 14:30</code>). All times assumed UTC.
|
</li>
|
||||||
|
<li>
|
||||||
|
Grid squares should be as detailed as possible (6 characters preferred, e.g. EM12kp)
|
||||||
</li>
|
</li>
|
||||||
<li>Grid squares should be as detailed as possible (6 characters preferred, e.g. EM12kp)</li>
|
|
||||||
<li>Band in MHz (e.g. 10000, 24000)</li>
|
<li>Band in MHz (e.g. 10000, 24000)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ defmodule MicrowavepropWeb.Plugs.RemoteIp do
|
||||||
|
|
||||||
def call(conn, _opts) do
|
def call(conn, _opts) do
|
||||||
ip =
|
ip =
|
||||||
@ip_headers
|
Enum.find_value(@ip_headers, fn header ->
|
||||||
|> Enum.find_value(fn header ->
|
|
||||||
case Plug.Conn.get_req_header(conn, header) do
|
case Plug.Conn.get_req_header(conn, header) do
|
||||||
[value | _] ->
|
[value | _] ->
|
||||||
value
|
value
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,13 @@ defmodule Mix.Tasks.HrrrBackfill do
|
||||||
|
|
||||||
# Get all distinct (rounded_hour, path_points) combinations for contacts
|
# Get all distinct (rounded_hour, path_points) combinations for contacts
|
||||||
contacts =
|
contacts =
|
||||||
from(c in Contact,
|
Repo.all(
|
||||||
where: not is_nil(c.pos1) and c.qso_timestamp >= ^~U[2014-01-01 00:00:00Z],
|
from(c in Contact,
|
||||||
select: %{id: c.id, pos1: c.pos1, pos2: c.pos2, qso_timestamp: c.qso_timestamp},
|
where: not is_nil(c.pos1) and c.qso_timestamp >= ^~U[2014-01-01 00:00:00Z],
|
||||||
order_by: [desc: c.qso_timestamp]
|
select: %{id: c.id, pos1: c.pos1, pos2: c.pos2, qso_timestamp: c.qso_timestamp},
|
||||||
|
order_by: [desc: c.qso_timestamp]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|
||||||
|
|
||||||
# Group by rounded HRRR hour to batch requests
|
# Group by rounded HRRR hour to batch requests
|
||||||
by_hour =
|
by_hour =
|
||||||
|
|
@ -48,7 +49,8 @@ defmodule Mix.Tasks.HrrrBackfill do
|
||||||
|> Enum.flat_map(fn c ->
|
|> Enum.flat_map(fn c ->
|
||||||
hour = HrrrClient.nearest_hrrr_hour(c.qso_timestamp)
|
hour = HrrrClient.nearest_hrrr_hour(c.qso_timestamp)
|
||||||
|
|
||||||
Radio.contact_path_points(c)
|
c
|
||||||
|
|> Radio.contact_path_points()
|
||||||
|> Enum.map(fn {lat, lon} ->
|
|> Enum.map(fn {lat, lon} ->
|
||||||
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
|
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
|
||||||
{hour, {rlat, rlon}}
|
{hour, {rlat, rlon}}
|
||||||
|
|
@ -129,19 +131,21 @@ defmodule Mix.Tasks.HrrrBackfill do
|
||||||
params = SoundingParams.derive(data.profile)
|
params = SoundingParams.derive(data.profile)
|
||||||
|
|
||||||
attrs =
|
attrs =
|
||||||
%{
|
maybe_add_derived(
|
||||||
valid_time: valid_time,
|
%{
|
||||||
lat: lat,
|
valid_time: valid_time,
|
||||||
lon: lon,
|
lat: lat,
|
||||||
run_time: data.run_time,
|
lon: lon,
|
||||||
profile: data.profile,
|
run_time: data.run_time,
|
||||||
hpbl_m: data.hpbl_m,
|
profile: data.profile,
|
||||||
pwat_mm: data.pwat_mm,
|
hpbl_m: data.hpbl_m,
|
||||||
surface_temp_c: data.surface_temp_c,
|
pwat_mm: data.pwat_mm,
|
||||||
surface_dewpoint_c: data.surface_dewpoint_c,
|
surface_temp_c: data.surface_temp_c,
|
||||||
surface_pressure_mb: data.surface_pressure_mb
|
surface_dewpoint_c: data.surface_dewpoint_c,
|
||||||
}
|
surface_pressure_mb: data.surface_pressure_mb
|
||||||
|> maybe_add_derived(params)
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
Weather.upsert_hrrr_profile(attrs)
|
Weather.upsert_hrrr_profile(attrs)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
2
mix.exs
2
mix.exs
|
|
@ -70,7 +70,7 @@ defmodule Microwaveprop.MixProject do
|
||||||
{:axon, "~> 0.7", only: [:dev, :test]},
|
{:axon, "~> 0.7", only: [:dev, :test]},
|
||||||
{:exla, "~> 0.9", only: [:dev, :test]},
|
{:exla, "~> 0.9", only: [:dev, :test]},
|
||||||
{:polaris, "~> 0.1", only: [:dev, :test]},
|
{:polaris, "~> 0.1", only: [:dev, :test]},
|
||||||
{:tidewave, "~> 0.5", only: :dev},
|
{:tidewave, "~> 0.5", only: :dev}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,24 @@ defmodule Microwaveprop.Repo.Migrations.ReplaceEnrichmentBooleansWithEnums do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Partial indexes for finding contacts needing enrichment
|
# Partial indexes for finding contacts needing enrichment
|
||||||
create index(:qsos, [:hrrr_status], where: "hrrr_status != 'complete'", name: :qsos_hrrr_status_pending_index)
|
create index(:qsos, [:hrrr_status],
|
||||||
create index(:qsos, [:weather_status], where: "weather_status != 'complete'", name: :qsos_weather_status_pending_index)
|
where: "hrrr_status != 'complete'",
|
||||||
create index(:qsos, [:terrain_status], where: "terrain_status != 'complete'", name: :qsos_terrain_status_pending_index)
|
name: :qsos_hrrr_status_pending_index
|
||||||
create index(:qsos, [:iemre_status], where: "iemre_status != 'complete'", name: :qsos_iemre_status_pending_index)
|
)
|
||||||
|
|
||||||
|
create index(:qsos, [:weather_status],
|
||||||
|
where: "weather_status != 'complete'",
|
||||||
|
name: :qsos_weather_status_pending_index
|
||||||
|
)
|
||||||
|
|
||||||
|
create index(:qsos, [:terrain_status],
|
||||||
|
where: "terrain_status != 'complete'",
|
||||||
|
name: :qsos_terrain_status_pending_index
|
||||||
|
)
|
||||||
|
|
||||||
|
create index(:qsos, [:iemre_status],
|
||||||
|
where: "iemre_status != 'complete'",
|
||||||
|
name: :qsos_iemre_status_pending_index
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -315,12 +315,14 @@ defmodule Microwaveprop.Radio.CsvImportTest do
|
||||||
use ExUnitProperties
|
use ExUnitProperties
|
||||||
|
|
||||||
property "ISO 8601 format always parses successfully" do
|
property "ISO 8601 format always parses successfully" do
|
||||||
check all year <- integer(2000..2030),
|
check all(
|
||||||
month <- integer(1..12),
|
year <- integer(2000..2030),
|
||||||
day <- integer(1..28),
|
month <- integer(1..12),
|
||||||
hour <- integer(0..23),
|
day <- integer(1..28),
|
||||||
minute <- integer(0..59),
|
hour <- integer(0..23),
|
||||||
second <- integer(0..59) do
|
minute <- integer(0..59),
|
||||||
|
second <- integer(0..59)
|
||||||
|
) do
|
||||||
y = String.pad_leading(to_string(year), 4, "0")
|
y = String.pad_leading(to_string(year), 4, "0")
|
||||||
m = String.pad_leading(to_string(month), 2, "0")
|
m = String.pad_leading(to_string(month), 2, "0")
|
||||||
d = String.pad_leading(to_string(day), 2, "0")
|
d = String.pad_leading(to_string(day), 2, "0")
|
||||||
|
|
@ -334,12 +336,14 @@ defmodule Microwaveprop.Radio.CsvImportTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
property "US date format with 24h time always parses" do
|
property "US date format with 24h time always parses" do
|
||||||
check all year <- integer(2000..2030),
|
check all(
|
||||||
month <- integer(1..12),
|
year <- integer(2000..2030),
|
||||||
day <- integer(1..28),
|
month <- integer(1..12),
|
||||||
hour <- integer(0..23),
|
day <- integer(1..28),
|
||||||
minute <- integer(0..59),
|
hour <- integer(0..23),
|
||||||
sep <- member_of(["/", "-", "."]) do
|
minute <- integer(0..59),
|
||||||
|
sep <- member_of(["/", "-", "."])
|
||||||
|
) do
|
||||||
input = "#{month}#{sep}#{day}#{sep}#{year} #{hour}:#{String.pad_leading(to_string(minute), 2, "0")}"
|
input = "#{month}#{sep}#{day}#{sep}#{year} #{hour}:#{String.pad_leading(to_string(minute), 2, "0")}"
|
||||||
assert {:ok, result} = CsvImport.normalize_timestamp(input)
|
assert {:ok, result} = CsvImport.normalize_timestamp(input)
|
||||||
assert String.ends_with?(result, "Z")
|
assert String.ends_with?(result, "Z")
|
||||||
|
|
@ -351,12 +355,14 @@ defmodule Microwaveprop.Radio.CsvImportTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
property "US date with AM/PM parses correctly" do
|
property "US date with AM/PM parses correctly" do
|
||||||
check all year <- integer(2000..2030),
|
check all(
|
||||||
month <- integer(1..12),
|
year <- integer(2000..2030),
|
||||||
day <- integer(1..28),
|
month <- integer(1..12),
|
||||||
hour_12 <- integer(1..12),
|
day <- integer(1..28),
|
||||||
minute <- integer(0..59),
|
hour_12 <- integer(1..12),
|
||||||
ampm <- member_of(["AM", "PM", "am", "pm"]) do
|
minute <- integer(0..59),
|
||||||
|
ampm <- member_of(["AM", "PM", "am", "pm"])
|
||||||
|
) do
|
||||||
input = "#{month}/#{day}/#{year} #{hour_12}:#{String.pad_leading(to_string(minute), 2, "0")} #{ampm}"
|
input = "#{month}/#{day}/#{year} #{hour_12}:#{String.pad_leading(to_string(minute), 2, "0")} #{ampm}"
|
||||||
assert {:ok, result} = CsvImport.normalize_timestamp(input)
|
assert {:ok, result} = CsvImport.normalize_timestamp(input)
|
||||||
{:ok, dt, _} = DateTime.from_iso8601(result)
|
{:ok, dt, _} = DateTime.from_iso8601(result)
|
||||||
|
|
@ -383,12 +389,14 @@ defmodule Microwaveprop.Radio.CsvImportTest do
|
||||||
fn y, m, d, h, mi -> "#{m}/#{d}/#{y} #{h}:#{mi}" end
|
fn y, m, d, h, mi -> "#{m}/#{d}/#{y} #{h}:#{mi}" end
|
||||||
])
|
])
|
||||||
|
|
||||||
check all year <- integer(2000..2030),
|
check all(
|
||||||
month <- integer(1..12),
|
year <- integer(2000..2030),
|
||||||
day <- integer(1..28),
|
month <- integer(1..12),
|
||||||
hour <- integer(0..23),
|
day <- integer(1..28),
|
||||||
minute <- integer(0..59),
|
hour <- integer(0..23),
|
||||||
fmt <- formats do
|
minute <- integer(0..59),
|
||||||
|
fmt <- formats
|
||||||
|
) do
|
||||||
y = String.pad_leading(to_string(year), 4, "0")
|
y = String.pad_leading(to_string(year), 4, "0")
|
||||||
m = String.pad_leading(to_string(month), 2, "0")
|
m = String.pad_leading(to_string(month), 2, "0")
|
||||||
d = String.pad_leading(to_string(day), 2, "0")
|
d = String.pad_leading(to_string(day), 2, "0")
|
||||||
|
|
@ -409,7 +417,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
property "garbage input never returns ok" do
|
property "garbage input never returns ok" do
|
||||||
check all garbage <- string(:alphanumeric, min_length: 1, max_length: 5) do
|
check all(garbage <- string(:alphanumeric, min_length: 1, max_length: 5)) do
|
||||||
case CsvImport.normalize_timestamp(garbage) do
|
case CsvImport.normalize_timestamp(garbage) do
|
||||||
{:ok, _} -> flunk("Parsed garbage: #{inspect(garbage)}")
|
{:ok, _} -> flunk("Parsed garbage: #{inspect(garbage)}")
|
||||||
{:error, _} -> :ok
|
{:error, _} -> :ok
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
|
||||||
|
|
||||||
# QSO without pos1 should not be marked as queued
|
# QSO without pos1 should not be marked as queued
|
||||||
all_qsos = Repo.all(Contact)
|
all_qsos = Repo.all(Contact)
|
||||||
refute Enum.any?(all_qsos, & &1.weather_status == :queued)
|
refute Enum.any?(all_qsos, &(&1.weather_status == :queued))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "enqueues HRRR jobs and marks QSOs as hrrr_status" do
|
test "enqueues HRRR jobs and marks QSOs as hrrr_status" do
|
||||||
|
|
@ -425,7 +425,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
|
||||||
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
|
||||||
|
|
||||||
all_qsos = Repo.all(Contact)
|
all_qsos = Repo.all(Contact)
|
||||||
refute Enum.any?(all_qsos, & &1.terrain_status == :queued)
|
refute Enum.any?(all_qsos, &(&1.terrain_status == :queued))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ defmodule Mix.Tasks.ResetEnrichmentTest do
|
||||||
# Set all queued flags to true (not in changeset cast)
|
# Set all queued flags to true (not in changeset cast)
|
||||||
Contact
|
Contact
|
||||||
|> where([q], q.id == ^contact.id)
|
|> where([q], q.id == ^contact.id)
|
||||||
|> Repo.update_all(set: [weather_status: "queued", hrrr_status: "queued", iemre_status: "queued", terrain_status: "queued"])
|
|> Repo.update_all(
|
||||||
|
set: [weather_status: "queued", hrrr_status: "queued", iemre_status: "queued", terrain_status: "queued"]
|
||||||
|
)
|
||||||
|
|
||||||
Repo.get!(Contact, contact.id)
|
Repo.get!(Contact, contact.id)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue