prop/lib/microwaveprop_web/router.ex
Graham McIntire fc245367e3
User profiles at /u/:callsign, flexible band input, assorted UX cleanup
Profile page:
  * New MicrowavepropWeb.UserProfileLive at /u/:callsign is a public
    page showing a user's contacts and beacons. Resolves case-
    insensitively so /u/w5isp and /u/W5ISP are the same thing; unknown
    callsigns redirect to /. Uses daisyUI card / stats / table
    components with an avatar-placeholder initial and hero icons.
  * Accounts.get_user_by_callsign/1 (case-insensitive) plus
    Radio.list_contacts_for_user/1 and Beacons.list_beacons_for_user/1
    back the page. Beacons list includes both approved and pending so
    owners see their drafts.
  * The top nav bar and the three LiveView sidebars (MapLive,
    WeatherMapLive, ContactMapLive) now render the logged-in callsign
    as a navigate link to /u/:callsign instead of a static label.
  * Nine new tests cover the lookup, the LiveView render, and the
    ownership-scoped queries.

Flexible band input:
  * New Microwaveprop.Radio.BandResolver module converts any of:
    ADIF wavelength labels ("33cm", "1.25cm", "6mm", case/whitespace
    insensitive), numeric frequency strings ("903.100", "10368.000"),
    and canonical MHz integers into the one of the site's known bands.
    Returns the nearest allowed band for numeric inputs >= 900 MHz,
    nil otherwise.
  * 902 MHz is added to Contact.@allowed_bands, ContactEdit.@allowed_bands,
    AdifImport.@allowed_bands, and the BandResolver list so "33cm"
    round-trips end-to-end.
  * AdifImport and CsvImport now delegate band resolution to
    BandResolver, and Radio.create_contact/2 normalizes the :band attr
    on the way in so the manual form and any API callers benefit too.
    CsvImport's "invalid band" tests previously used 99999 MHz which
    the new resolver snaps to the nearest allowed band; swapped to
    "notaband" which is truly unresolvable.

Contacts and beacons list UX:
  * Remove the "Submitted" column from /contacts — it duplicated info
    already visible on the detail page and was pushing the real
    columns off narrow viewports. submitted_cell/1 and its three
    column-specific tests go with it.
  * Hide the Lat / Lon columns from /beacons — six decimal places of
    coordinates weren't useful next to the grid square and took a
    disproportionate amount of row width.
2026-04-12 16:13:08 -05:00

152 lines
4.9 KiB
Elixir

defmodule MicrowavepropWeb.Router do
use MicrowavepropWeb, :router
import MicrowavepropWeb.UserAuth
import Oban.Web.Router
import Phoenix.LiveDashboard.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :store_remote_ip
plug :store_cf_geo
plug :fetch_live_flash
plug :put_root_layout, html: {MicrowavepropWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_scope_for_user
end
defp store_remote_ip(conn, _opts) do
ip_str = conn.remote_ip |> :inet.ntoa() |> to_string()
Plug.Conn.put_session(conn, :remote_ip, ip_str)
end
# Extract Cloudflare visitor geolocation headers (set by a Cloudflare
# Managed Transform) and stash them in the session so LiveViews can use
# them to center maps on the visitor's approximate location.
defp store_cf_geo(conn, _opts) do
lat = conn |> Plug.Conn.get_req_header("cf-iplatitude") |> List.first()
lon = conn |> Plug.Conn.get_req_header("cf-iplongitude") |> List.first()
with {lat_f, ""} when is_float(lat_f) <- lat && Float.parse(lat),
{lon_f, ""} when is_float(lon_f) <- lon && Float.parse(lon) do
conn
|> Plug.Conn.put_session(:cf_lat, lat_f)
|> Plug.Conn.put_session(:cf_lon, lon_f)
else
_ -> conn
end
end
pipeline :api do
plug :accepts, ["json"]
end
# Health check — no pipeline, minimal overhead
get "/health", MicrowavepropWeb.HealthController, :check, log: false
# Admin-only routes must come first so that literal segments like
# `/beacons/:id/edit` are registered before the public `/beacons/:id`.
scope "/", MicrowavepropWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :admin, on_mount: [{MicrowavepropWeb.UserAuth, :require_admin}] do
live "/beacons/:id/edit", BeaconLive.Form, :edit
live "/admin/backfill", BackfillLive
live "/admin/contact-edits", Admin.ContactEditLive
live "/users", UserManagementLive.Index, :index
live "/users/:id/edit", UserManagementLive.Edit, :edit
end
end
scope "/", MicrowavepropWeb do
pipe_through :browser
get "/", PageController, :home
get "/api/contacts/map", ContactMapController, :show
live_session :public, on_mount: [{MicrowavepropWeb.UserAuth, :default}] do
live "/submit", SubmitLive
live "/map", MapLive
live "/weather", WeatherMapLive
live "/contacts", ContactLive.Index
live "/contacts/map", ContactMapLive
live "/contacts/:id", ContactLive.Show
live "/path", PathLive
live "/rover", RoverLive
live "/algo", AlgoLive
live "/about", AboutLive
live "/privacy", PrivacyLive
# Beacons: public read + submit (pending admin approval)
live "/beacons", BeaconLive.Index, :index
live "/beacons/new", BeaconLive.Form, :new
live "/beacons/:id", BeaconLive.Show, :show
# Public per-user profile (contributions: contacts + beacons submitted)
live "/u/:callsign", UserProfileLive
end
# Redirect old /qsos routes
get "/qsos", PageController, :redirect_contacts
get "/qsos/:id", PageController, :redirect_contact
end
# Other scopes may use custom stacks.
# scope "/api", MicrowavepropWeb do
# pipe_through :api
# end
scope "/" do
pipe_through [:browser, :require_authenticated_user]
live_dashboard "/admin/dashboard",
metrics: MicrowavepropWeb.Telemetry,
ecto_repos: [Microwaveprop.Repo],
ecto_psql_extras_options: [long_running_queries: [threshold: "200 milliseconds"]],
on_mount: [{MicrowavepropWeb.UserAuth, :require_admin}]
oban_dashboard("/admin/oban", on_mount: [{MicrowavepropWeb.UserAuth, :require_admin}])
end
# Enable Swoosh mailbox preview in development
if Application.compile_env(:microwaveprop, :dev_routes) do
scope "/dev" do
pipe_through :browser
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
## Authentication routes
scope "/", MicrowavepropWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
end
scope "/", MicrowavepropWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
post "/users/beacon-monitors", BeaconMonitorController, :create
delete "/users/beacon-monitors/:id", BeaconMonitorController, :delete
end
scope "/", MicrowavepropWeb do
pipe_through [:browser]
get "/users/log-in", UserSessionController, :new
get "/users/confirm/:token", UserSessionController, :confirm
post "/users/log-in", UserSessionController, :create
delete "/users/log-out", UserSessionController, :delete
end
end