defmodule MoundHuntersWeb.Router do @moduledoc """ Main HTTP router for Hopewell Road application. """ use Plug.Router require Logger # Request logging (must be first to capture timing) plug(MoundHuntersWeb.Plugs.RequestLogger) # Parse query params and request body plug(Plug.Parsers, parsers: [:urlencoded, :json], json_decoder: Jason ) # Bounds checking for Ohio coordinates plug(MoundHuntersWeb.Plugs.BoundsCheck) # Match routes plug(:match) # Dispatch to matched route plug(:dispatch) # Tile endpoints get "/tiles/request" do MoundHuntersWeb.TileController.request(conn) end match "/tiles/:format/:tile_id", via: [:get, :head] do conn = put_private(conn, :path_params, %{"tile_id" => tile_id, "format" => format}) MoundHuntersWeb.TileController.serve(conn) end # Geometry sharing API post "/api/share" do MoundHuntersWeb.ApiController.create_share(conn) end get "/api/share/:id" do conn = put_private(conn, :path_params, %{"id" => id}) MoundHuntersWeb.ApiController.get_share(conn) end # Tile metadata API get "/api/meta/tile/:id" do conn = put_private(conn, :path_params, %{"id" => id}) MoundHuntersWeb.ApiController.get_tile(conn) end get "/api/meta/tile" do MoundHuntersWeb.ApiController.get_tile_by_coords(conn) end # Health check get "/health" do send_resp(conn, 200, Jason.encode!(%{status: "ok"})) end get "/" do static_dir = Application.app_dir(:mound_hunters, "priv/static") index_path = Path.join(static_dir, "index.html") if File.exists?(index_path) do conn |> put_resp_header("content-type", "text/html; charset=utf-8") |> put_resp_header("cache-control", "no-cache") |> send_file(200, index_path) else send_resp(conn, 404, "Not found") end end # Static files (with cache busting support via manifest) get "/*path" do serve_static(conn, path) end # 404 handler match _ do send_resp(conn, 404, Jason.encode!(%{error: "Not found"})) end # Serve static files from priv/static defp serve_static(conn, path) do static_dir = Application.app_dir(:mound_hunters, "priv/static") file_path = resolve_static_path(path, static_dir) cond do file_path && File.regular?(file_path) -> # Determine content type content_type = MIME.from_path(file_path) # Set cache headers (immutable for hashed files) cache_control = if String.contains?(file_path, "-") do "public, max-age=31536000, immutable" else "public, max-age=3600" end conn |> put_resp_header("content-type", content_type) |> put_resp_header("cache-control", cache_control) |> send_file(200, file_path) path == [] or path == [""] -> # Serve index.html for root index_path = Path.join(static_dir, "index.html") if File.exists?(index_path) do conn |> put_resp_header("content-type", "text/html") |> put_resp_header("cache-control", "no-cache") |> send_file(200, index_path) else send_resp(conn, 404, "Not found") end true -> send_resp(conn, 404, "Not found") end end # Resolve static file path, checking manifest for hashed versions defp resolve_static_path(path, static_dir) do requested = Path.join(path) direct_path = Path.join(static_dir, requested) cond do File.regular?(direct_path) -> direct_path # Check manifest for hashed version manifest_path = Path.join(static_dir, "manifest.json") -> case load_manifest(manifest_path) do {:ok, manifest} -> hashed = Map.get(manifest, requested) if hashed do hashed_path = Path.join(static_dir, hashed) if File.regular?(hashed_path), do: hashed_path, else: nil else nil end _ -> nil end true -> nil end end # Load and cache manifest defp load_manifest(path) do if File.exists?(path) do case File.read(path) do {:ok, content} -> Jason.decode(content) _ -> {:error, :read_failed} end else {:error, :not_found} end end end