From 1c459d79343edd606c0b6863bb9036ca21e83ea7 Mon Sep 17 00:00:00 2001 From: Paul Swartz Date: Sun, 19 Apr 2026 11:51:07 -0400 Subject: [PATCH 1/5] feat(infra): production service --- .../live/gtfs/station_report_2_components.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/gtfs_planner_web/live/gtfs/station_report_2_components.ex b/lib/gtfs_planner_web/live/gtfs/station_report_2_components.ex index b22f17e9..b5bfe98c 100644 --- a/lib/gtfs_planner_web/live/gtfs/station_report_2_components.ex +++ b/lib/gtfs_planner_web/live/gtfs/station_report_2_components.ex @@ -42,6 +42,11 @@ defmodule GtfsPlannerWeb.Gtfs.StationReport2Components do id: "report2-pathway-field-completeness", label: "Pathway Field Completeness", desc: "Fill rates for optional pathway fields like traversal time, stair count, and slope." + }, + %{ + id: "report2-accessibility", + label: "Accessibility", + desc: "Wheelchair boarding and accessible pathway coverage." } ] From 48ae4c4b2d380f879a947cbae5377772b926554c Mon Sep 17 00:00:00 2001 From: Ryan Mahoney Date: Mon, 4 May 2026 23:46:38 -0400 Subject: [PATCH 2/5] chore(deps): add ex_aws and sweet_xml for Swoosh ExAws SES adapter (#582) --- mix.exs | 2 ++ mix.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/mix.exs b/mix.exs index e74018e5..41789e35 100644 --- a/mix.exs +++ b/mix.exs @@ -65,6 +65,8 @@ defmodule GtfsPlanner.MixProject do {:swoosh, "~> 1.16"}, {:gen_smtp, "~> 1.0"}, {:phoenix_swoosh, "~> 1.2"}, + {:ex_aws, "~> 2.1"}, + {:sweet_xml, "~> 0.6"}, {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 7e94dc55..23d86833 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, + "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, "ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"}, "ex_cldr_plugs": {:hex, :ex_cldr_plugs, "1.3.4", "8c996a5c838f137a93610c3110d36aee81309a27d77bd2d0ab436df6bc01a085", [:mix], [{:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "30829e097eac403013101dc087e6cabf5e01a1c5e3a6b23ea4562e85521ff52a"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, @@ -50,6 +51,7 @@ "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, + "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, From c620a8a776e0c24839b3c2b74f6366a0365849c0 Mon Sep 17 00:00:00 2001 From: Ryan Mahoney Date: Mon, 4 May 2026 23:47:51 -0400 Subject: [PATCH 3/5] feat(mailer): use Swoosh ExAws SES adapter with task-role credentials in prod (#582) --- config/runtime.exs | 46 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index e22f0832..278c7db5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -232,45 +232,27 @@ if config_env() == :prod do # # Check `Plug.SSL` for all available options in `force_ssl`. - # ## Configuring the mailer with AWS SES - # - # In production, emails are sent via AWS Simple Email Service (SES). - # The API client is configured at compile-time in config/prod.exs with: - # config :swoosh, :api_client, Swoosh.ApiClient.Req - # - # Required environment variables: - # - AWS_REGION or AWS_SES_REGION: AWS region (e.g., us-east-1) - # - AWS_ACCESS_KEY_ID: IAM access key with SES permissions - # - AWS_SECRET_ACCESS_KEY: IAM secret key - # - # Optional: - # - AWS_SES_CONFIGURATION_SET: SES configuration set for tracking/analytics - # - # See https://hexdocs.pm/swoosh/Swoosh.Adapters.AmazonSES.html for details. - ses_region = System.get_env("AWS_SES_REGION") || System.get_env("AWS_REGION") - ses_access_key = System.get_env("AWS_ACCESS_KEY_ID") - ses_secret_key = System.get_env("AWS_SECRET_ACCESS_KEY") - - if ses_region && ses_access_key && ses_secret_key do - ses_config = [ - adapter: Swoosh.Adapters.AmazonSES, - region: ses_region, - access_key: ses_access_key, - secret: ses_secret_key + + if ses_region do + config :ex_aws, + access_key_id: [:instance_role], + secret_access_key: [:instance_role], + region: ses_region + + mailer_config = [ + adapter: Swoosh.Adapters.ExAwsAmazonSES, + region: ses_region ] - # Add configuration set if provided - ses_config = + mailer_config = case System.get_env("AWS_SES_CONFIGURATION_SET") do - nil -> ses_config - config_set -> Keyword.put(ses_config, :configuration_set_name, config_set) + nil -> mailer_config + set -> Keyword.put(mailer_config, :configuration_set_name, set) end - config :gtfs_planner, GtfsPlanner.Mailer, ses_config + config :gtfs_planner, GtfsPlanner.Mailer, mailer_config else - # Fallback to Logger adapter if AWS SES is not configured - # This logs emails instead of sending them - suitable for staging config :gtfs_planner, GtfsPlanner.Mailer, adapter: Swoosh.Adapters.Logger end end From f857268e4b7f1d237254a6a9d04a00d8b5b8faca Mon Sep 17 00:00:00 2001 From: Ryan Mahoney Date: Mon, 4 May 2026 23:48:40 -0400 Subject: [PATCH 4/5] feat(infra): pass AWS_REGION to ECS task for SES adapter selection (#582) --- infra/modules/service/task.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/modules/service/task.tf b/infra/modules/service/task.tf index 53a09a2a..37309fcb 100644 --- a/infra/modules/service/task.tf +++ b/infra/modules/service/task.tf @@ -12,6 +12,7 @@ locals { OTP_JAR_PATH = "/opt/otp/otp.jar" OTP_OSM_PATH = "/opt/otp/data/philadelphia.osm.pbf" GEOAPIFY_API_KEY = var.geoapify_api_key + AWS_REGION = data.aws_region.current.region } } From d77d1f0b39a3a47d154cf0c30a216fe3cbb36647 Mon Sep 17 00:00:00 2001 From: Ryan Mahoney Date: Tue, 5 May 2026 00:05:28 -0400 Subject: [PATCH 5/5] fix(mailer): configure ExAws to use Req as HTTP client (#582) Without this, ExAws falls back to its default ExAws.Request.Hackney, which is not in our deps. Instance-role credential resolution makes HTTP calls to the ECS metadata endpoint via the configured http_client, so SES sends would fail at runtime. --- config/runtime.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/runtime.exs b/config/runtime.exs index 278c7db5..50a52dfa 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -236,6 +236,7 @@ if config_env() == :prod do if ses_region do config :ex_aws, + http_client: ExAws.Request.Req, access_key_id: [:instance_role], secret_access_key: [:instance_role], region: ses_region