From de5cf78d63afdf4585135c9010e9b1d66153c2ef Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 7 Aug 2026 09:21:10 -0400 Subject: [PATCH 1/4] perf: Move API calls out of render into subway_status --- lib/dotcom/system_status/subway.ex | 61 ++++++++++++++++--- .../system_status/status_row_heading.ex | 21 ++++--- .../components/system_status/subway_status.ex | 39 ++++++++++-- .../system_status/subway_status_test.exs | 9 ++- 4 files changed, 102 insertions(+), 28 deletions(-) diff --git a/lib/dotcom/system_status/subway.ex b/lib/dotcom/system_status/subway.ex index 23be2a9d59..ed9dd3c2ea 100644 --- a/lib/dotcom/system_status/subway.ex +++ b/lib/dotcom/system_status/subway.ex @@ -7,15 +7,25 @@ defmodule Dotcom.SystemStatus.Subway do import Dotcom.Alerts, only: [route_alert?: 2, systemwide_mode_alert?: 2] alias Alerts.Alert + @affected_stops Application.compile_env!(:dotcom, :affected_stops_module) + @endpoint_stops Application.compile_env!(:dotcom, :endpoint_stops_module) + @type status_time() :: :current | {:future, DateTime.t()} @type status_t() :: :normal | Dotcom.Alerts.service_effect_t() + @type subheading_data_t() :: + {:affected_stops, [map()]} + | {:endpoint_stops, [tuple()]} + | {:delay} + | nil + @type status_entry() :: %{ alerts: [Alert.t()], status: status_t(), multiple: boolean(), - time: status_time() + time: status_time(), + subheading_data: subheading_data_t() } @type status_entry_group() :: %{ @@ -185,7 +195,10 @@ defmodule Dotcom.SystemStatus.Subway do |> alerts_for_routes(alerts) |> Enum.group_by(&affected_green_line_branch_ids/1) |> Enum.map(fn {branch_ids, alerts} -> - %{branch_ids: branch_ids, status_entries: alerts_to_statuses(alerts, time)} + %{ + branch_ids: branch_ids, + status_entries: alerts |> alerts_to_statuses(time, branch_ids) + } end) |> maybe_add_normal_entry() |> Enum.map(&maybe_collapse_branch_ids/1) @@ -302,8 +315,7 @@ defmodule Dotcom.SystemStatus.Subway do [] mattapan_alerts -> - mattapan_statuses = mattapan_alerts |> alerts_to_statuses(time) - + mattapan_statuses = mattapan_alerts |> alerts_to_statuses(time, ["Mattapan"]) [status_entry_group(mattapan_statuses, ["Mattapan"])] end end @@ -314,7 +326,7 @@ defmodule Dotcom.SystemStatus.Subway do defp statuses_for_route(route_id, alerts, time) do route_id |> alerts_for_route(alerts) - |> alerts_to_statuses(time) + |> alerts_to_statuses(time, [route_id]) end # Returns a status_entry_group, to be used in the @@ -357,12 +369,40 @@ defmodule Dotcom.SystemStatus.Subway do # - Identical alerts are grouped together and pluralized. # - Times are given as a kitchen-formatted string, nil, or "Now". # - Statuses are sorted alphabetically. - @spec alerts_to_statuses([Alert.t()], DateTime.t()) :: [status_entry()] - defp alerts_to_statuses(alerts, time) do + # - Subheading information is added + @spec alerts_to_statuses([Alert.t()], DateTime.t(), [Routes.Route.id_t()]) :: [status_entry()] + defp alerts_to_statuses(alerts, time, route_ids) do alerts |> alerts_to_statuses_naive(time) |> consolidate_duplicates() |> sort_statuses() + |> Enum.map(&add_subheading_data(&1, route_ids)) + end + + @spec add_subheading_data(status_entry(), [Routes.Route.id_t()]) :: status_entry() + defp add_subheading_data(%{status: :station_closure, alerts: alerts} = entry, route_ids) + when route_ids != [] do + %{ + entry + | subheading_data: {:affected_stops, @affected_stops.affected_stops(alerts, route_ids)} + } + end + + defp add_subheading_data(%{status: :delay} = entry, _route_ids) do + %{entry | subheading_data: {:delay}} + end + + defp add_subheading_data(%{status: status, alerts: alerts} = entry, route_ids) + when status in [:service_change, :shuttle, :single_tracking, :suspension] and + route_ids != [] do + %{ + entry + | subheading_data: {:endpoint_stops, @endpoint_stops.endpoint_stops(alerts, route_ids)} + } + end + + defp add_subheading_data(entry, _route_ids) do + %{entry | subheading_data: nil} end # Naively maps a list of alerts to a list of statuses, where a @@ -389,7 +429,7 @@ defmodule Dotcom.SystemStatus.Subway do @spec normal_status() :: status_entry() defp normal_status() do - %{multiple: false, status: :normal, time: :current, alerts: []} + %{multiple: false, status: :normal, time: :current, alerts: [], subheading_data: nil} end # Translates an alert to a status: @@ -400,7 +440,7 @@ defmodule Dotcom.SystemStatus.Subway do @spec alert_to_status(Alert.t(), DateTime.t()) :: status_entry() defp alert_to_status(alert, time) do time = future_start_time(alert.active_period, time) - %{alerts: [alert], multiple: false, status: alert.effect, time: time} + %{alerts: [alert], multiple: false, status: alert.effect, time: time, subheading_data: nil} end # - If the active period is in the future, returns its start_time. @@ -441,7 +481,8 @@ defmodule Dotcom.SystemStatus.Subway do time: time, status: effect, multiple: length(grouped_statuses) > 1, - alerts: Enum.flat_map(grouped_statuses, & &1.alerts) |> Enum.uniq() + alerts: Enum.flat_map(grouped_statuses, & &1.alerts) |> Enum.uniq(), + subheading_data: nil } end) end diff --git a/lib/dotcom_web/components/system_status/status_row_heading.ex b/lib/dotcom_web/components/system_status/status_row_heading.ex index 68c13ec7a3..8a5ba8017c 100644 --- a/lib/dotcom_web/components/system_status/status_row_heading.ex +++ b/lib/dotcom_web/components/system_status/status_row_heading.ex @@ -12,9 +12,6 @@ defmodule DotcomWeb.Components.SystemStatus.StatusRowHeading do alias Alerts.Alert - @affected_stops Application.compile_env!(:dotcom, :affected_stops_module) - @endpoint_stops Application.compile_env!(:dotcom, :endpoint_stops_module) - attr :alerts, :list, default: [] attr :future, :boolean, default: false attr :hide_route_pill, :boolean, default: false @@ -23,6 +20,7 @@ defmodule DotcomWeb.Components.SystemStatus.StatusRowHeading do attr :prefix, :string, default: nil attr :route_ids, :list, required: true attr :status, :atom, required: true + attr :subheading_data, :any, default: nil def status_row_heading(assigns) do %{ @@ -112,16 +110,21 @@ defmodule DotcomWeb.Components.SystemStatus.StatusRowHeading do """ end - defp decorations(%{status: :station_closure, alerts: alerts, route_ids: route_ids}) do - affected_stops = @affected_stops.affected_stops(alerts, route_ids) - + defp decorations(%{ + status: :station_closure, + subheading_data: {:affected_stops, affected_stops} + }) do %{ plural: affected_stops |> Enum.count() > 1, subheading_text: affected_stops |> humanize_affected_stops() } end - defp decorations(%{status: :delay, alerts: alerts}) do + defp decorations(%{ + status: :delay, + alerts: alerts, + subheading_data: {:delay} + }) do all_single_tracking? = alerts |> Enum.all?(&(&1.cause == :single_tracking)) subheading_text = if all_single_tracking?, do: ~t"Due to Single Tracking" @@ -131,10 +134,8 @@ defmodule DotcomWeb.Components.SystemStatus.StatusRowHeading do } end - defp decorations(%{status: status, alerts: alerts, route_ids: route_ids}) + defp decorations(%{status: status, subheading_data: {:endpoint_stops, endpoints}}) when status in [:service_change, :shuttle, :single_tracking, :suspension] do - endpoints = @endpoint_stops.endpoint_stops(alerts, route_ids) - %{ subheading_text: endpoints |> humanize_endpoint_list(), subheading_aria_label: endpoints |> humanize_endpoint_list_a11y() diff --git a/lib/dotcom_web/components/system_status/subway_status.ex b/lib/dotcom_web/components/system_status/subway_status.ex index 2d24864a6b..e049cd757a 100644 --- a/lib/dotcom_web/components/system_status/subway_status.ex +++ b/lib/dotcom_web/components/system_status/subway_status.ex @@ -93,6 +93,9 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do end defp heading(assigns) do + subheading_data = get_in(assigns.row, [:status_entry, :subheading_data]) + assigns = assigns |> assign(:subheading_data, subheading_data) + ~H""" <.status_row_heading alerts={@row |> Map.get(:alerts)} @@ -101,6 +104,7 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do prefix={@row.status_entry.prefix} plural={@row.status_entry.plural} future={@row.status_entry.future} + subheading_data={@subheading_data} route_ids={[@row.route_info.route_id | @row.route_info.branch_ids]} /> """ @@ -192,14 +196,35 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do end defp combine_status_entries( - %{status: status1, prefix: prefix1, future: future1}, - %{status: status2, prefix: prefix2, future: future2} + %{status: status1, prefix: prefix1, future: future1} = status_entry1, + %{status: status2, prefix: prefix2, future: future2} = status_entry2 ) - when status1 == status2 and prefix1 == prefix2 and future1 == future2, - do: %{status: status1, prefix: prefix1, plural: true, future: future1} + when status1 == status2 and prefix1 == prefix2 and future1 == future2, do: + %{ + status: status1, + prefix: prefix1, + plural: true, + future: future1, + subheading_data: + combine_subheading_data( + Map.get(status_entry1, :subheading_data), + Map.get(status_entry2, :subheading_data) + ) + } defp combine_status_entries(_status_entry1, _status_entry2), do: see_alerts_status() + defp combine_subheading_data({:affected_stops, stops1}, {:affected_stops, stops2}), do: + {:affected_stops, stops1 ++ stops2} + + defp combine_subheading_data({:endpoint_stops, endpoints1}, {:endpoint_stops, endpoints2}), do: + {:endpoint_stops, endpoints1 ++ endpoints2} + + defp combine_subheading_data({:delay}, {:delay}), do: {:delay} + defp combine_subheading_data(data1, nil), do: data1 + defp combine_subheading_data(nil, data2), do: data2 + defp combine_subheading_data(data1, _data2), do: data1 + defp add_url(row) do route_id = route_id_from_route_info(row.route_info) sub_page = if normal?(row.status_entry), do: "line", else: "alerts" @@ -267,7 +292,8 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do status: alert.effect, plural: false, prefix: prefix, - future: future?(status_entry) + future: future?(status_entry), + subheading_data: status_entry.subheading_data }, style: %{ hide_route_pill: true @@ -287,7 +313,8 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do status: status, plural: multiple, prefix: prefix, - future: future?(status_entry) + future: future?(status_entry), + subheading_data: status_entry.subheading_data }, style: %{ hide_route_pill: true diff --git a/test/dotcom_web/components/system_status/subway_status_test.exs b/test/dotcom_web/components/system_status/subway_status_test.exs index 8449efcef2..af0a94ae3e 100644 --- a/test/dotcom_web/components/system_status/subway_status_test.exs +++ b/test/dotcom_web/components/system_status/subway_status_test.exs @@ -774,10 +774,15 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatusTest do stops = Factories.Stops.Stop.build_list(2, :stop) - expect(Dotcom.Alerts.AffectedStops.Mock, :affected_stops, fn - [_], _ -> + [affected_branch1 | affected_branch2] = affected_branches + + expect(Dotcom.Alerts.AffectedStops.Mock, :affected_stops, 2, fn + [_], [^affected_branch1] -> stops |> Enum.take(1) |> Enum.map(&%{stop: &1, direction: :all}) + [_], ^affected_branch2 -> + stops |> Enum.at(1) |> then(&[%{stop: &1, direction: :all}]) + [_, _], _ -> stops |> Enum.map(&%{stop: &1, direction: :all}) end) From d8ecf100f670a894427d921e243822c97be2607f Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 7 Aug 2026 09:35:36 -0400 Subject: [PATCH 2/4] Formatting, update docs --- lib/dotcom/system_status/subway.ex | 28 ++++++++-------- .../components/system_status/subway_status.ex | 32 +++++++++---------- test/dotcom/system_status/subway_test.exs | 2 ++ test/dotcom/system_status_test.exs | 2 ++ 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/dotcom/system_status/subway.ex b/lib/dotcom/system_status/subway.ex index ed9dd3c2ea..a8118749af 100644 --- a/lib/dotcom/system_status/subway.ex +++ b/lib/dotcom/system_status/subway.ex @@ -57,17 +57,17 @@ defmodule Dotcom.SystemStatus.Subway do ...> ] iex> Dotcom.SystemStatus.Subway.subway_status(alerts, Timex.now()) %{ - "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], + "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], "Orange" => [ %{ branch_ids: [], status_entries: [ - %{time: :current, status: :shuttle, multiple: false, alerts: alerts} + %{time: :current, status: :shuttle, multiple: false, alerts: alerts, subheading_data: {:endpoint_stops, []}} ] } ], - "Red" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], - "Green" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}] + "Red" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], + "Green" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}] } Alerts for individual Green line branches are grouped together and @@ -85,20 +85,20 @@ defmodule Dotcom.SystemStatus.Subway do ...> ] iex> Dotcom.SystemStatus.Subway.subway_status(alerts, Timex.now()) %{ - "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], - "Orange" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], - "Red" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], + "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], + "Orange" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], + "Red" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], "Green" => [ %{ branch_ids: ["Green-E"], status_entries: [ - %{time: :current, status: :delay, multiple: false, alerts: alerts} + %{time: :current, status: :delay, multiple: false, alerts: alerts, subheading_data: {:delay}} ] }, %{ branch_ids: ["Green-B", "Green-C", "Green-D"], status_entries: [ - %{time: :current, status: :normal, multiple: false, alerts: []} + %{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil} ] } ] @@ -118,23 +118,23 @@ defmodule Dotcom.SystemStatus.Subway do ...> ] iex> Dotcom.SystemStatus.Subway.subway_status(alerts, Timex.now()) %{ - "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], - "Orange" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}], + "Blue" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], + "Orange" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}], "Red" => [ %{ branch_ids: [], status_entries: [ - %{time: :current, status: :normal, multiple: false, alerts: []} + %{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil } ] }, %{ branch_ids: ["Mattapan"], status_entries: [ - %{time: :current, status: :suspension, multiple: false, alerts: alerts} + %{time: :current, status: :suspension, multiple: false, alerts: alerts, subheading_data: {:endpoint_stops, []}} ] } ], - "Green" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: []}]}] + "Green" => [%{branch_ids: [], status_entries: [%{time: :current, status: :normal, multiple: false, alerts: [], subheading_data: nil}]}] } """ @spec subway_status([Alert.t()], DateTime.t()) :: %{Routes.Route.id_t() => status_entry_group()} diff --git a/lib/dotcom_web/components/system_status/subway_status.ex b/lib/dotcom_web/components/system_status/subway_status.ex index e049cd757a..6d123941f7 100644 --- a/lib/dotcom_web/components/system_status/subway_status.ex +++ b/lib/dotcom_web/components/system_status/subway_status.ex @@ -199,26 +199,26 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do %{status: status1, prefix: prefix1, future: future1} = status_entry1, %{status: status2, prefix: prefix2, future: future2} = status_entry2 ) - when status1 == status2 and prefix1 == prefix2 and future1 == future2, do: - %{ - status: status1, - prefix: prefix1, - plural: true, - future: future1, - subheading_data: - combine_subheading_data( - Map.get(status_entry1, :subheading_data), - Map.get(status_entry2, :subheading_data) - ) - } + when status1 == status2 and prefix1 == prefix2 and future1 == future2, + do: %{ + status: status1, + prefix: prefix1, + plural: true, + future: future1, + subheading_data: + combine_subheading_data( + Map.get(status_entry1, :subheading_data), + Map.get(status_entry2, :subheading_data) + ) + } defp combine_status_entries(_status_entry1, _status_entry2), do: see_alerts_status() - defp combine_subheading_data({:affected_stops, stops1}, {:affected_stops, stops2}), do: - {:affected_stops, stops1 ++ stops2} + defp combine_subheading_data({:affected_stops, stops1}, {:affected_stops, stops2}), + do: {:affected_stops, stops1 ++ stops2} - defp combine_subheading_data({:endpoint_stops, endpoints1}, {:endpoint_stops, endpoints2}), do: - {:endpoint_stops, endpoints1 ++ endpoints2} + defp combine_subheading_data({:endpoint_stops, endpoints1}, {:endpoint_stops, endpoints2}), + do: {:endpoint_stops, endpoints1 ++ endpoints2} defp combine_subheading_data({:delay}, {:delay}), do: {:delay} defp combine_subheading_data(data1, nil), do: data1 diff --git a/test/dotcom/system_status/subway_test.exs b/test/dotcom/system_status/subway_test.exs index fc6e9cddd0..918ace6be8 100644 --- a/test/dotcom/system_status/subway_test.exs +++ b/test/dotcom/system_status/subway_test.exs @@ -9,6 +9,8 @@ defmodule Dotcom.SystemStatus.SubwayTest do @lines_without_branches List.delete(Subway.lines(), "Green") setup _ do + Mox.stub(Dotcom.Alerts.AffectedStops.Mock, :affected_stops, fn _, _ -> [] end) + Mox.stub(Dotcom.Alerts.EndpointStops.Mock, :endpoint_stops, fn _, _ -> [] end) Mox.stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime) :ok end diff --git a/test/dotcom/system_status_test.exs b/test/dotcom/system_status_test.exs index ab0a953c16..87228e9397 100644 --- a/test/dotcom/system_status_test.exs +++ b/test/dotcom/system_status_test.exs @@ -10,6 +10,8 @@ defmodule Dotcom.SystemStatusTest do setup :verify_on_exit! setup _ do + stub(Dotcom.Alerts.AffectedStops.Mock, :affected_stops, fn _, _ -> [] end) + stub(Dotcom.Alerts.EndpointStops.Mock, :endpoint_stops, fn _, _ -> [] end) stub_with(Dotcom.Utils.DateTime.Mock, Dotcom.Utils.DateTime) :ok end From 13640f896c3456ecba9be8b89b98ae914924aa54 Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 7 Aug 2026 09:52:07 -0400 Subject: [PATCH 3/4] Remove todo --- lib/dotcom_web/components/planned_disruptions.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dotcom_web/components/planned_disruptions.ex b/lib/dotcom_web/components/planned_disruptions.ex index bbb3ed95bf..739854a321 100644 --- a/lib/dotcom_web/components/planned_disruptions.ex +++ b/lib/dotcom_web/components/planned_disruptions.ex @@ -92,7 +92,6 @@ defmodule DotcomWeb.Components.PlannedDisruptions do |> format_date_range_for_alert() assigns = assign(assigns, time_range_str: time_range_str) - ~H""" <.status_row_heading future From 22c04670363830b75660499d5222581a0b6283b9 Mon Sep 17 00:00:00 2001 From: Lucy Tan Date: Fri, 7 Aug 2026 09:59:29 -0400 Subject: [PATCH 4/4] Formatting --- lib/dotcom_web/components/planned_disruptions.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/dotcom_web/components/planned_disruptions.ex b/lib/dotcom_web/components/planned_disruptions.ex index 739854a321..bbb3ed95bf 100644 --- a/lib/dotcom_web/components/planned_disruptions.ex +++ b/lib/dotcom_web/components/planned_disruptions.ex @@ -92,6 +92,7 @@ defmodule DotcomWeb.Components.PlannedDisruptions do |> format_date_range_for_alert() assigns = assign(assigns, time_range_str: time_range_str) + ~H""" <.status_row_heading future