From ea96c93e2d1220afe0682a3892320ed217294d45 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 1 Apr 2026 12:28:24 -0500 Subject: [PATCH] Rename QSO to Contact in UI, add higher bands, improve submit page - All menus and UI text now say Contacts instead of QSOs - Add 68, 122, 134, 241 GHz bands to submit form and validation - Add info box on submit page explaining why contacts matter - Larger submit button with icon - Make HRRR partition migration idempotent for partial re-runs --- assets/js/app.js | 3 +- assets/js/contact_map_hook.js | 41 +++++++++++++++++++ lib/microwaveprop/radio/contact.ex | 2 +- lib/microwaveprop_web/components/layouts.ex | 2 +- .../live/contact_live/index.ex | 12 +++--- .../live/contact_live/show.ex | 18 +++++++- lib/microwaveprop_web/live/map_live.ex | 6 +-- lib/microwaveprop_web/live/submit_live.ex | 22 ++++++++-- ...20260401154846_partition_hrrr_profiles.exs | 34 +++++++++++---- test/microwaveprop_web/live/map_live_test.exs | 2 +- 10 files changed, 116 insertions(+), 26 deletions(-) create mode 100644 assets/js/contact_map_hook.js diff --git a/assets/js/app.js b/assets/js/app.js index 7afaf312..db25699d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -30,6 +30,7 @@ import {hooks as colocatedHooks} from "phoenix-colocated/microwaveprop" import topbar from "../vendor/topbar" import {PropagationMap} from "./propagation_map_hook" +import {ContactMap} from "./contact_map_hook" const UtcClock = { mounted() { @@ -51,7 +52,7 @@ const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, - hooks: {...colocatedHooks, PropagationMap, UtcClock}, + hooks: {...colocatedHooks, PropagationMap, UtcClock, ContactMap}, }) // Show progress bar on live navigation and form submits diff --git a/assets/js/contact_map_hook.js b/assets/js/contact_map_hook.js new file mode 100644 index 00000000..812bc283 --- /dev/null +++ b/assets/js/contact_map_hook.js @@ -0,0 +1,41 @@ +export const ContactMap = { + mounted() { + const pos1 = JSON.parse(this.el.dataset.pos1) + const pos2 = JSON.parse(this.el.dataset.pos2) + + const lat1 = pos1.lat + const lon1 = pos1.lon || pos1.lng + const lat2 = pos2.lat + const lon2 = pos2.lon || pos2.lng + + const map = L.map(this.el, { + zoomControl: true, + attributionControl: false, + scrollWheelZoom: false + }) + + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19 + }).addTo(map) + + const marker1 = L.circleMarker([lat1, lon1], { + radius: 6, color: "#2563eb", fillColor: "#3b82f6", fillOpacity: 0.9, weight: 2 + }).addTo(map) + + const marker2 = L.circleMarker([lat2, lon2], { + radius: 6, color: "#dc2626", fillColor: "#ef4444", fillOpacity: 0.9, weight: 2 + }).addTo(map) + + const station1 = this.el.dataset.station1 + const station2 = this.el.dataset.station2 + if (station1) marker1.bindTooltip(station1, {permanent: true, direction: "top", offset: [0, -8]}) + if (station2) marker2.bindTooltip(station2, {permanent: true, direction: "top", offset: [0, -8]}) + + L.polyline([[lat1, lon1], [lat2, lon2]], { + color: "#6366f1", weight: 2, dashArray: "6 4", opacity: 0.8 + }).addTo(map) + + const bounds = L.latLngBounds([[lat1, lon1], [lat2, lon2]]) + map.fitBounds(bounds, {padding: [40, 40], maxZoom: 10}) + } +} diff --git a/lib/microwaveprop/radio/contact.ex b/lib/microwaveprop/radio/contact.ex index 9a21712f..ddf88543 100644 --- a/lib/microwaveprop/radio/contact.ex +++ b/lib/microwaveprop/radio/contact.ex @@ -41,7 +41,7 @@ defmodule Microwaveprop.Radio.Contact do @submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email)a @allowed_modes ~w(CW SSB FM FT8 FT4) - @allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 75000), &Decimal.new/1) + @allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1) def submission_changeset(contact, attrs) do contact diff --git a/lib/microwaveprop_web/components/layouts.ex b/lib/microwaveprop_web/components/layouts.ex index 30c97fdb..ba9b63cd 100644 --- a/lib/microwaveprop_web/components/layouts.ex +++ b/lib/microwaveprop_web/components/layouts.ex @@ -41,7 +41,7 @@ defmodule MicrowavepropWeb.Layouts do Microwaveprop diff --git a/lib/microwaveprop_web/live/contact_live/index.ex b/lib/microwaveprop_web/live/contact_live/index.ex index c3868c93..3c3850a6 100644 --- a/lib/microwaveprop_web/live/contact_live/index.ex +++ b/lib/microwaveprop_web/live/contact_live/index.ex @@ -30,12 +30,12 @@ defmodule MicrowavepropWeb.ContactLive.Index do {:noreply, assign(socket, - page_title: "QSOs", + page_title: "Contacts", page: result.page, total_pages: result.total_pages, total_entries: result.total_entries, - qsos: result.entries, - grouped_qsos: result.grouped_entries, + contacts: result.entries, + grouped_contacts: result.grouped_entries, sort_by: sort_by, sort_order: sort_order, search: search @@ -76,7 +76,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do <:subtitle>{@total_entries} contacts <:actions> <.link navigate={~p"/submit"} class="btn btn-primary"> - <.icon name="hero-plus" class="w-5 h-5" /> Submit QSO + <.icon name="hero-plus" class="w-5 h-5" /> Submit Contact @@ -111,7 +111,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do - <%= for {primary, reciprocals} <- @grouped_qsos do %> + <%= for {primary, reciprocals} <- @grouped_contacts do %> @@ -126,7 +126,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do {length(reciprocals) + 1} diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex index 965e72e7..19e6ad20 100644 --- a/lib/microwaveprop_web/live/contact_live/show.ex +++ b/lib/microwaveprop_web/live/contact_live/show.ex @@ -145,14 +145,28 @@ defmodule MicrowavepropWeb.ContactLive.Show do <:subtitle>{Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")} <:actions> <.link navigate={~p"/contacts"} class="btn btn-sm btn-ghost"> - <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to QSOs + <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Contacts + <%= if @contact.pos1 && @contact.pos2 do %> +

Locations approximate, in the center of the grid squares

+
+ <% end %> +
-

QSO Details

+

Contact Details

<.list> <:item title="Station 1">{@contact.station1} <:item title="Station 2">{@contact.station2} diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 8e32d205..66ffb083 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -337,10 +337,10 @@ defmodule MicrowavepropWeb.MapLive do <.icon name="hero-calculator" class="size-3.5" /> Scoring Algorithm <.link navigate="/submit" class="btn btn-xs btn-ghost justify-start gap-1.5"> - <.icon name="hero-arrow-up-tray" class="size-3.5" /> Submit a QSO + <.icon name="hero-arrow-up-tray" class="size-3.5" /> Submit a Contact - <.link navigate="/qsos" class="btn btn-xs btn-ghost justify-start gap-1.5"> - <.icon name="hero-signal" class="size-3.5" /> QSO Training Data + <.link navigate="/contacts" class="btn btn-xs btn-ghost justify-start gap-1.5"> + <.icon name="hero-signal" class="size-3.5" /> Contact Training Data
diff --git a/lib/microwaveprop_web/live/submit_live.ex b/lib/microwaveprop_web/live/submit_live.ex index f5821178..1c6253ce 100644 --- a/lib/microwaveprop_web/live/submit_live.ex +++ b/lib/microwaveprop_web/live/submit_live.ex @@ -14,7 +14,11 @@ defmodule MicrowavepropWeb.SubmitLive do {"10 GHz", "10000"}, {"24 GHz", "24000"}, {"47 GHz", "47000"}, - {"76 GHz", "75000"} + {"68 GHz", "68000"}, + {"76 GHz", "75000"}, + {"122 GHz", "122000"}, + {"134 GHz", "134000"}, + {"241 GHz", "241000"} ] @mode_options ~w(CW SSB FM FT8 FT4) @@ -80,9 +84,19 @@ defmodule MicrowavepropWeb.SubmitLive do <.header> Submit Contact - <:subtitle>Submit a microwave contact for propagation analysis + <:subtitle>Help us build a better propagation model +
+

Every contact matters

+

+ Our propagation model improves with real-world data. Each verified microwave contact + you submit is matched against atmospheric conditions at the time of your contact, + helping us calibrate predictions for all bands from 10 GHz to 241 GHz. The more contacts + we have, the better our forecasts get for everyone. +

+
+ <.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
<.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" /> @@ -133,7 +147,9 @@ defmodule MicrowavepropWeb.SubmitLive do />
- <.button phx-disable-with="Submitting..." class="btn-primary">Submit Contact + <.button phx-disable-with="Submitting..." class="btn btn-primary btn-lg w-full sm:w-auto"> + <.icon name="hero-arrow-up-tray" class="w-5 h-5" /> Submit Contact +
diff --git a/priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs b/priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs index 9b7bac31..f5910683 100644 --- a/priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs +++ b/priv/repo/migrations/20260401154846_partition_hrrr_profiles.exs @@ -5,6 +5,28 @@ defmodule Microwaveprop.Repo.Migrations.PartitionHrrrProfiles do @disable_migration_lock true def up do + # Clean up any partial previous run: drop the partitioned table and orphaned + # partition tables, then rename _old back so we can start fresh. + execute """ + DO $$ + DECLARE + tbl text; + BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'hrrr_profiles_old' AND schemaname = 'public') THEN + DROP TABLE IF EXISTS hrrr_profiles CASCADE; + -- Drop orphaned partition tables left behind after CASCADE + FOR tbl IN + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' AND tablename LIKE 'hrrr_profiles_%' + AND tablename != 'hrrr_profiles_old' + LOOP + EXECUTE 'DROP TABLE IF EXISTS ' || tbl || ' CASCADE'; + END LOOP; + ALTER TABLE hrrr_profiles_old RENAME TO hrrr_profiles; + END IF; + END $$ + """ + # 1. Rename old table execute "ALTER TABLE hrrr_profiles RENAME TO hrrr_profiles_old" @@ -31,7 +53,7 @@ defmodule Microwaveprop.Repo.Migrations.PartitionHrrrProfiles do ) PARTITION BY RANGE (valid_time) """ - # 3. Create partitions (quarterly for historical, monthly for recent/future) + # 3. Create partitions partitions = [ {"2016-01-01", "2017-01-01"}, {"2017-01-01", "2018-01-01"}, @@ -74,18 +96,15 @@ defmodule Microwaveprop.Repo.Migrations.PartitionHrrrProfiles do """ end - # 4. Drop old indexes (they reference the renamed table) and create new ones + # 4. Indexes on partitioned table execute "DROP INDEX IF EXISTS hrrr_profiles_lat_lon_valid_time_index" execute "DROP INDEX IF EXISTS hrrr_profiles_valid_time_index" execute "CREATE UNIQUE INDEX hrrr_profiles_lat_lon_valid_time_index ON hrrr_profiles (lat, lon, valid_time)" - execute "CREATE INDEX hrrr_profiles_valid_time_index ON hrrr_profiles (valid_time)" - # 5. Migrate data from old table in batches to avoid long locks - execute """ - INSERT INTO hrrr_profiles SELECT * FROM hrrr_profiles_old - """ + # 5. Migrate data + execute "INSERT INTO hrrr_profiles SELECT * FROM hrrr_profiles_old" # 6. Drop old table execute "DROP TABLE hrrr_profiles_old" @@ -120,7 +139,6 @@ defmodule Microwaveprop.Repo.Migrations.PartitionHrrrProfiles do execute "ALTER TABLE hrrr_profiles_flat RENAME TO hrrr_profiles" execute "CREATE UNIQUE INDEX hrrr_profiles_lat_lon_valid_time_index ON hrrr_profiles (lat, lon, valid_time)" - execute "CREATE INDEX hrrr_profiles_valid_time_index ON hrrr_profiles (valid_time)" end end diff --git a/test/microwaveprop_web/live/map_live_test.exs b/test/microwaveprop_web/live/map_live_test.exs index d2556364..4063c6bc 100644 --- a/test/microwaveprop_web/live/map_live_test.exs +++ b/test/microwaveprop_web/live/map_live_test.exs @@ -43,7 +43,7 @@ defmodule MicrowavepropWeb.MapLiveTest do test "renders submit QSO link", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/map") - assert html =~ "Submit a QSO" + assert html =~ "Submit a Contact" assert html =~ ~s(/submit) end end