prop/lib/microwaveprop_web/router.ex
Graham McIntire 578b4ede86 Serve /contacts/map payload via HTTP endpoint, parallel hydrate
- Add ContactMapController serving /api/contacts/map with pre-gzipped
  JSON from Microwaveprop.Cache. Browsers handle Content-Encoding: gzip
  natively; payload drops from 5.7 MB raw to 1.5 MB on the wire (73%).
  Cache TTL 10 min, invalidated on contact insert alongside the other
  contact-map cache keys.
- Move contact map payload out of the LiveView's initial HTML entirely.
  Shell renders with the filter chrome (count + band list); the hook
  fetches contacts from the HTTP endpoint in parallel with map init.
  This also avoids Phoenix.Socket's 64 KB frame limit (push_event would
  have required chunked transfer or a global frame size bump).
- Parallelize ContactLive.Show hydrate: five independent DB loads
  (weather, solar, hrrr_path, terrain, iemre) now run concurrently via
  Task.async + Task.await_many. Total latency drops from sum(each) to
  max(each), cutting contact show hydrate time roughly 5x.
2026-04-12 13:10:51 -05:00

149 lines
4.8 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
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