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
This commit is contained in:
parent
2c16fd979d
commit
ea96c93e2d
10 changed files with 116 additions and 26 deletions
|
|
@ -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
|
||||
|
|
|
|||
41
assets/js/contact_map_hook.js
Normal file
41
assets/js/contact_map_hook.js
Normal file
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ defmodule MicrowavepropWeb.Layouts do
|
|||
<a href="/" class="font-semibold">Microwaveprop</a>
|
||||
<nav class="flex gap-2">
|
||||
<.link navigate="/map" class="btn btn-ghost btn-sm">Map</.link>
|
||||
<.link navigate="/qsos" class="btn btn-ghost btn-sm">QSOs</.link>
|
||||
<.link navigate="/contacts" class="btn btn-ghost btn-sm">Contacts</.link>
|
||||
<.link navigate="/submit" class="btn btn-ghost btn-sm">Submit</.link>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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</:subtitle>
|
||||
<: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
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
|
@ -111,7 +111,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {primary, reciprocals} <- @grouped_qsos do %>
|
||||
<%= for {primary, reciprocals} <- @grouped_contacts do %>
|
||||
<tr class="h-1">
|
||||
<td colspan="10"></td>
|
||||
</tr>
|
||||
|
|
@ -126,7 +126,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
|||
<span
|
||||
:if={reciprocals != []}
|
||||
class="badge badge-primary badge-xs"
|
||||
title="Has reciprocal QSO"
|
||||
title="Has reciprocal contact"
|
||||
>
|
||||
{length(reciprocals) + 1}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -145,14 +145,28 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
<:subtitle>{Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}</:subtitle>
|
||||
<: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
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<%= if @contact.pos1 && @contact.pos2 do %>
|
||||
<p class="text-xs text-base-content/50 italic mb-1">Locations approximate, in the center of the grid squares</p>
|
||||
<div
|
||||
id="contact-map"
|
||||
phx-hook="ContactMap"
|
||||
phx-update="ignore"
|
||||
data-pos1={Jason.encode!(@contact.pos1)}
|
||||
data-pos2={Jason.encode!(@contact.pos2)}
|
||||
data-station1={@contact.station1}
|
||||
data-station2={@contact.station2}
|
||||
class="w-full h-64 rounded-lg border border-base-300 mb-4"
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
<h2 class="text-base font-semibold mb-2">QSO Details</h2>
|
||||
<h2 class="text-base font-semibold mb-2">Contact Details</h2>
|
||||
<.list>
|
||||
<:item title="Station 1">{@contact.station1}</:item>
|
||||
<:item title="Station 2">{@contact.station2}</:item>
|
||||
|
|
|
|||
|
|
@ -337,10 +337,10 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
<.icon name="hero-calculator" class="size-3.5" /> Scoring Algorithm
|
||||
</.link>
|
||||
<.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>
|
||||
<.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
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Submit Contact
|
||||
<:subtitle>Submit a microwave contact for propagation analysis</:subtitle>
|
||||
<:subtitle>Help us build a better propagation model</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="bg-info/10 border border-info/30 rounded-box p-4 mb-6 text-sm leading-relaxed">
|
||||
<p class="font-semibold text-info mb-1">Every contact matters</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" />
|
||||
|
|
@ -133,7 +147,9 @@ defmodule MicrowavepropWeb.SubmitLive do
|
|||
/>
|
||||
|
||||
<div class="mt-6">
|
||||
<.button phx-disable-with="Submitting..." class="btn-primary">Submit Contact</.button>
|
||||
<.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
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue