From d22bf73b575c9d795b0ac05e7f1afdec9b2b0199 Mon Sep 17 00:00:00 2001 From: "robbie.sundstrom" Date: Thu, 9 Jul 2026 15:54:43 -0400 Subject: [PATCH 1/4] chore: move ETT functionality out of permanent_config --- .../{screens_config.ex => config_updater.ex} | 95 +- lib/screenplay/permanent_config.ex | 3 +- .../alert_controller.ex | 9 +- .../config_updater_test.exs | 220 +++ test/screenplay/permanent_config_test.exs | 1246 ++++++++--------- 5 files changed, 941 insertions(+), 632 deletions(-) rename lib/screenplay/emergency_takeover_tool/{screens_config.ex => config_updater.ex} (56%) create mode 100644 test/screenplay/emergency_takeover_tool/config_updater_test.exs diff --git a/lib/screenplay/emergency_takeover_tool/screens_config.ex b/lib/screenplay/emergency_takeover_tool/config_updater.ex similarity index 56% rename from lib/screenplay/emergency_takeover_tool/screens_config.ex rename to lib/screenplay/emergency_takeover_tool/config_updater.ex index a5a8d351..01208e54 100644 --- a/lib/screenplay/emergency_takeover_tool/screens_config.ex +++ b/lib/screenplay/emergency_takeover_tool/config_updater.ex @@ -1,22 +1,111 @@ -defmodule Screenplay.EmergencyTakeoverTool.ScreensConfig do +defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdater do @moduledoc """ Module responsible for building the EmergencyTakeover struct used in Screens configurations of active Emergency Takeover Alerts. """ + require Logger alias Screenplay.EmergencyTakeoverTool.CannedMessages alias Screenplay.EmergencyTakeoverTool.EmergencyTakeover, as: EmergencyTakeoverContext - alias ScreensConfig.{EmergencyMessagingLocation, EmergencyTakeover, Screen} + alias Screenplay.ScreensConfig.Fetch, as: PublishedScreensFetch + alias ScreensConfig.{Config, EmergencyMessagingLocation, EmergencyTakeover, Screen} @image_store Application.compile_env!(:screenplay, :image_store_module) + def add_emergency_takeover_configs(alert_id, showtime_screen_ids, message) do + with {published_config, _published_version_id} <- get_current_published_config(), + {:ok, published_config_deserialized} <- Jason.decode(published_config) do + %Config{screens: published_screens, devops: devops} = + published_config_deserialized |> Config.from_json() + + updated_screens = + update_screens_with_emergency_takeover( + published_screens, + showtime_screen_ids, + alert_id, + message + ) + + %Config{screens: updated_screens, devops: devops} + |> publish_new_config() + else + _error -> + {:error, "Could not fetch published screens config"} + end + end + + defp update_screens_with_emergency_takeover(screens, screen_ids, alert_id, message) do + for {id, screen} <- screens, + into: %{} do + if id in screen_ids do + case screen do + %Screen{app_params: %{emergency_messaging_location: eml}} when not is_nil(eml) -> + emergency_takeover = build_emergency_takeover(message, alert_id, screen.app_id, eml) + + {id, + put_in( + screen, + [Access.key!(:app_params), Access.key!(:emergency_takeover)], + emergency_takeover + )} + + _ -> + Logger.error("Tried to takeover #{id} without an emergency_messaging_location") + + {id, screen} + end + else + {id, screen} + end + end + end + + def clear_emergency_takeover_configs(showtime_screen_ids) do + with {published_config, _published_version_id} <- get_current_published_config(), + {:ok, published_config_deserialized} <- Jason.decode(published_config) do + %Config{screens: published_screens, devops: devops} = + published_config_deserialized |> Config.from_json() + + updated_screens = clear_screens_emergency_takeover(published_screens, showtime_screen_ids) + + %Config{screens: updated_screens, devops: devops} + |> publish_new_config() + else + _error -> + {:error, "Could not fetch published screens config"} + end + end + + defp clear_screens_emergency_takeover(screens, screen_ids) do + for {id, screen} <- screens, into: %{} do + if id in screen_ids do + {id, put_in(screen, [Access.key!(:app_params), Access.key!(:emergency_takeover)], nil)} + else + {id, screen} + end + end + end + + defp get_current_published_config do + case PublishedScreensFetch.fetch_config() do + {:ok, config, version_id} -> {config, version_id} + error -> error + end + end + + def publish_new_config(new_config) do + with(:ok <- PublishedScreensFetch.put_config(new_config)) do + PublishedScreensFetch.commit() + end + end + @spec build_emergency_takeover( EmergencyTakeoverContext.message(), String.t(), Screen.app_id(), EmergencyMessagingLocation.t() ) :: EmergencyTakeover.t() - def build_emergency_takeover(message, alert_id, app_id, eml) do + defp build_emergency_takeover(message, alert_id, app_id, eml) do %EmergencyTakeover{ audio_asset_path: audio_path(message, eml), text_for_audio: text_for_audio(message, eml), diff --git a/lib/screenplay/permanent_config.ex b/lib/screenplay/permanent_config.ex index 91ed396f..d119d94e 100644 --- a/lib/screenplay/permanent_config.ex +++ b/lib/screenplay/permanent_config.ex @@ -6,7 +6,7 @@ defmodule Screenplay.PermanentConfig do require Logger - alias Screenplay.EmergencyTakeoverTool.ScreensConfig, as: EmergencyTakeoverConfig + alias Screenplay.EmergencyTakeoverTool.ConfigUpdater, as: EmergencyTakeoverConfig alias Screenplay.PendingScreensConfig.Fetch, as: PendingScreensFetch alias Screenplay.Places alias Screenplay.Places.Fetch @@ -459,6 +459,7 @@ defmodule Screenplay.PermanentConfig do defp screen_to_place_id(%Screen{app_id: app_id}), do: raise("screen_to_place_id/1 not implemented for app_id: #{app_id}") + # TODO: This will be deleted, but I want a clean commit without doing too much work that is about to be deleted def add_emergency_takeover_configs(alert_id, showtime_screen_ids, message) do with {published_config, _published_version_id} <- get_current_published_config(), {:ok, published_config_deserialized} <- Jason.decode(published_config) do diff --git a/lib/screenplay_web/controllers/emergency_takeover_tool/alert_controller.ex b/lib/screenplay_web/controllers/emergency_takeover_tool/alert_controller.ex index 7fae0c45..1713ed30 100644 --- a/lib/screenplay_web/controllers/emergency_takeover_tool/alert_controller.ex +++ b/lib/screenplay_web/controllers/emergency_takeover_tool/alert_controller.ex @@ -2,9 +2,8 @@ defmodule ScreenplayWeb.EmergencyTakeoverTool.AlertController do use ScreenplayWeb, :controller alias Screenplay.EmergencyTakeovers - alias Screenplay.EmergencyTakeoverTool.EmergencyTakeover + alias Screenplay.EmergencyTakeoverTool.{ConfigUpdater, EmergencyTakeover} alias Screenplay.Outfront.SFTP - alias Screenplay.PermanentConfig alias Screenplay.Places alias Screenplay.Places.Place.ShowtimeScreen alias ScreenplayWeb.UserActionLogger @@ -167,7 +166,7 @@ defmodule ScreenplayWeb.EmergencyTakeoverTool.AlertController do %{String.t() => String.t()} ) :: :ok | {:error, String.t()} def add_showtime_takeovers(alert_id, screen_ids, message = %{type: :canned}, _images) do - case PermanentConfig.add_emergency_takeover_configs(alert_id, screen_ids, message) do + case ConfigUpdater.add_emergency_takeover_configs(alert_id, screen_ids, message) do :ok -> :ok {:error, reason} -> {:error, "Failed to add canned message takeovers: #{reason}"} end @@ -175,7 +174,7 @@ defmodule ScreenplayWeb.EmergencyTakeoverTool.AlertController do def add_showtime_takeovers(alert_id, screen_ids, message = %{type: :custom}, images) do with :ok <- upload_takeover_images(alert_id, images), - :ok <- PermanentConfig.add_emergency_takeover_configs(alert_id, screen_ids, message) do + :ok <- ConfigUpdater.add_emergency_takeover_configs(alert_id, screen_ids, message) do :ok else {:error, reason} -> {:error, "Failed to add custom message takeovers: #{reason}"} @@ -272,7 +271,7 @@ defmodule ScreenplayWeb.EmergencyTakeoverTool.AlertController do defp remove_takeovers_from_showtime_screens(station_ids) do station_ids |> showtime_screens_at_stations() - |> PermanentConfig.clear_emergency_takeover_configs() + |> ConfigUpdater.clear_emergency_takeover_configs() end @spec showtime_screens_at_stations(list(String.t())) :: list(String.t()) diff --git a/test/screenplay/emergency_takeover_tool/config_updater_test.exs b/test/screenplay/emergency_takeover_tool/config_updater_test.exs new file mode 100644 index 00000000..4ec57627 --- /dev/null +++ b/test/screenplay/emergency_takeover_tool/config_updater_test.exs @@ -0,0 +1,220 @@ +defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdaterTest do + use ExUnit.Case + + alias Screenplay.EmergencyTakeoverTool.ConfigUpdater + + alias ScreensConfig.{ + Alerts, + Config, + ContentSummary, + Departures, + ElevatorStatus, + EmergencyTakeover, + Footer, + Header, + LineMap, + Screen + } + + alias ScreensConfig.Screen.{GlEink, PreFare} + + @screen_without_takeover %Screen{ + vendor: :mercury, + device_id: nil, + name: nil, + app_id: :pre_fare_v2, + refresh_if_loaded_before: nil, + disabled: false, + hidden_from_screenplay: false, + app_params: %PreFare{ + emergency_messaging_location: :inside, + emergency_takeover: nil, + content_summary: %ContentSummary{parent_station_id: "place-test"}, + elevator_status: %ElevatorStatus{parent_station_id: "place-test"}, + full_line_map: [], + header: %Header.StopId{stop_id: "place-test"}, + reconstructed_alert_widget: %ScreensConfig.Alerts{stop_id: "place-test"} + }, + tags: [] + } + @gl_eink_screen %Screen{ + vendor: :mercury, + device_id: nil, + name: nil, + app_id: :gl_eink_v2, + refresh_if_loaded_before: nil, + disabled: false, + hidden_from_screenplay: false, + app_params: %GlEink{ + departures: %Departures{ + sections: [ + %Departures.Section{ + query: %Departures.Query{ + params: %Departures.Query.Params{ + stop_ids: ["place-test"], + route_ids: ["Green-B"], + direction_id: 1 + } + } + } + ] + }, + footer: %Footer{stop_id: "place-test"}, + header: %Header.Destination{ + route_id: "Green-B", + direction_id: 1 + }, + alerts: %Alerts{stop_id: "456"}, + line_map: %LineMap{ + stop_id: "456", + station_id: "place-test", + direction_id: 1, + route_id: "Green-B" + }, + evergreen_content: [], + platform_location: :back + }, + tags: [] + } + + def get_fixture_path(file_name) do + Path.join(~w[#{File.cwd!()} test fixtures #{file_name}]) + end + + setup_all do + on_exit(fn -> + empty_config = %{screens: %{}} + published_screens_path = get_fixture_path("screens_config.json") + + File.write( + published_screens_path, + Jason.encode!(empty_config) + ) + + File.rm(published_screens_path <> ".temp") + end) + end + + describe "add_emergency_takeover_configs/3" do + setup do + published_screens_path = get_fixture_path("screens_config.json") + + config = + %Config{ + screens: %{ + "PRE-1" => @screen_without_takeover, + "PRE-2" => @screen_without_takeover, + "GL-1" => @gl_eink_screen + } + } + |> Config.to_json() + |> Jason.encode!() + + File.write(published_screens_path, config) + end + + test "adds an emergency takeover config to a screen" do + alert_id = "alert-1" + takeover_screen_id = "PRE-1" + message = %{type: :custom, text: %{indoor: "Indoor Message", outdoor: "Outdoor Message"}} + + assert ConfigUpdater.add_emergency_takeover_configs( + alert_id, + [takeover_screen_id], + message + ) == :ok + + {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + + expected_takeover = %EmergencyTakeover{ + audio_asset_path: nil, + text_for_audio: "Indoor Message", + visual_asset_path: "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" + } + + assert screens[takeover_screen_id] == + put_in( + @screen_without_takeover.app_params.emergency_takeover, + expected_takeover + ) + + assert screens["PRE-2"] == @screen_without_takeover + assert screens["GL-1"] == @gl_eink_screen + end + + test "adds a canned emergency takeover config to a screen" do + alert_id = "alert-1" + takeover_screen_id = "PRE-1" + message = %{type: :canned, id: 1} + + assert ConfigUpdater.add_emergency_takeover_configs( + alert_id, + [takeover_screen_id], + message + ) == :ok + + {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + + expected_takeover = %EmergencyTakeover{ + audio_asset_path: + "test/fixtures/emergency_takeover_images/canned/audio/LeaveStation-Indoor.mp3", + text_for_audio: nil, + visual_asset_path: + "test/fixtures/emergency_takeover_images/canned/images/LeaveStation-indoor-portrait.gif" + } + + assert screens[takeover_screen_id] == + put_in( + @screen_without_takeover.app_params.emergency_takeover, + expected_takeover + ) + + assert screens["PRE-2"] == @screen_without_takeover + end + end + + describe "clear_emergency_takeover_configs/1" do + setup do + published_screens_path = get_fixture_path("screens_config.json") + + screen_with_takeover = + put_in( + @screen_without_takeover.app_params.emergency_takeover, + %EmergencyTakeover{ + audio_asset_path: nil, + text_for_audio: "Indoor Message", + visual_asset_path: + "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" + } + ) + + config = + %Config{ + screens: %{ + "PRE-1" => screen_with_takeover, + "PRE-2" => @screen_without_takeover, + "GL-1" => @gl_eink_screen + } + } + |> Config.to_json() + |> Jason.encode!() + + File.write(published_screens_path, config) + end + + test "clears emergency takeover configs from screens" do + takeover_screen_id = "PRE-1" + + assert ConfigUpdater.clear_emergency_takeover_configs([takeover_screen_id]) == :ok + + {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + + assert screens[takeover_screen_id] == @screen_without_takeover + assert screens["PRE-2"] == @screen_without_takeover + assert screens["GL-1"] == @gl_eink_screen + end + end +end diff --git a/test/screenplay/permanent_config_test.exs b/test/screenplay/permanent_config_test.exs index c0bf77b4..99209204 100644 --- a/test/screenplay/permanent_config_test.exs +++ b/test/screenplay/permanent_config_test.exs @@ -1,623 +1,623 @@ -defmodule Screenplay.PermanentConfigTest do - use ExUnit.Case - - import Mox - - alias Screenplay.PendingScreensConfig.Fetch.Local - alias Screenplay.PermanentConfig - alias Screenplay.Places.{Cache, Place} - alias Screenplay.Places.Place.ShowtimeScreen - alias ScreensConfig.ContentSummary - alias ScreensConfig.ElevatorStatus - alias ScreensConfig.Screen.PreFare - - alias ScreensConfig.{ - Alerts, - Config, - Departures, - EmergencyTakeover, - Footer, - Header, - LineMap, - PendingConfig, - Screen - } - - alias ScreensConfig.Screen.GlEink - - @screen_without_takeover %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :pre_fare_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %PreFare{ - emergency_messaging_location: :inside, - emergency_takeover: nil, - content_summary: %ContentSummary{parent_station_id: "place-test"}, - elevator_status: %ElevatorStatus{parent_station_id: "place-test"}, - full_line_map: [], - header: %Header.StopId{stop_id: "place-test"}, - reconstructed_alert_widget: %ScreensConfig.Alerts{stop_id: "place-test"} - }, - tags: [] - } - @gl_eink_screen %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :gl_eink_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %GlEink{ - departures: %Departures{ - sections: [ - %Departures.Section{ - query: %Departures.Query{ - params: %Departures.Query.Params{ - stop_ids: ["place-test"], - route_ids: ["Green-B"], - direction_id: 1 - } - } - } - ] - }, - footer: %Footer{stop_id: "place-test"}, - header: %Header.Destination{ - route_id: "Green-B", - direction_id: 1 - }, - alerts: %Alerts{stop_id: "456"}, - line_map: %LineMap{ - stop_id: "456", - station_id: "place-test", - direction_id: 1, - route_id: "Green-B" - }, - evergreen_content: [], - platform_location: :back - }, - tags: [] - } - - def fetch_current_config_version do - {:ok, _config, metadata} = Local.fetch_config() - metadata.version_id - end - - def get_fixture_path(file_name) do - Path.join(~w[#{File.cwd!()} test fixtures #{file_name}]) - end - - setup_all do - start_supervised!(Screenplay.Places.Cache) - - on_exit(fn -> - empty_config = %{screens: %{}} - pending_screens_path = get_fixture_path("pending_config.json") - published_screens_path = get_fixture_path("screens_config.json") - - File.write( - pending_screens_path, - Jason.encode!(empty_config) - ) - - File.write( - published_screens_path, - Jason.encode!(empty_config) - ) - - File.rm(pending_screens_path <> ".temp") - File.rm(published_screens_path <> ".temp") - end) - end - - describe "put_pending_screens/3" do - setup do - empty_config = %{screens: %{}} - pending_screens_path = get_fixture_path("pending_config.json") - published_screens_path = get_fixture_path("screens_config.json") - - File.write( - pending_screens_path, - Jason.encode!(empty_config) - ) - - File.write( - published_screens_path, - Jason.encode!(empty_config) - ) - end - - test "adds and updates a new config for GL E-Ink" do - version = fetch_current_config_version() - - expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, - route_id -> - assert stop_id == "place-test" - assert route_id == "Green-B" - - {"123", "456"} - end) - - places_and_screens = %{ - "place-test" => %{ - "updated_pending_screens" => [], - "new_pending_screens" => [ - %{ - "new_id" => "1234", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - } - ] - } - } - - assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok - - expected_file_contents = - %PendingConfig{ - screens: %{ - "1234" => %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :gl_eink_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %GlEink{ - departures: %Departures{ - sections: [ - %Departures.Section{ - query: %Departures.Query{ - params: %Departures.Query.Params{ - stop_ids: ["place-test"], - route_ids: ["Green-B"], - direction_id: 0 - } - } - } - ] - }, - footer: %Footer{stop_id: "place-test"}, - header: %Header.Destination{ - route_id: "Green-B", - direction_id: 0 - }, - alerts: %Alerts{stop_id: "123"}, - line_map: %LineMap{ - stop_id: "123", - station_id: "place-test", - direction_id: 0, - route_id: "Green-B" - }, - evergreen_content: [], - platform_location: "front" - }, - tags: [] - } - } - } - |> PendingConfig.to_json() - |> Jason.encode!(pretty: true) - - {:ok, config, metadata} = Local.fetch_config() - assert expected_file_contents == config - - places_and_screens = %{ - "place-test" => %{ - "updated_pending_screens" => [ - %{ - "new_id" => "12345", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 1}, - "platform_location" => "back" - }, - "screen_id" => "1234" - } - ], - "new_pending_screens" => [] - } - } - - assert PermanentConfig.put_pending_screens( - places_and_screens, - :gl_eink_v2, - metadata.version_id - ) == :ok - - expected_file_contents = - %PendingConfig{ - screens: %{ - "12345" => %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :gl_eink_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %GlEink{ - departures: %Departures{ - sections: [ - %Departures.Section{ - query: %Departures.Query{ - params: %Departures.Query.Params{ - stop_ids: ["place-test"], - route_ids: ["Green-B"], - direction_id: 1 - } - } - } - ] - }, - footer: %Footer{stop_id: "place-test"}, - header: %Header.Destination{ - route_id: "Green-B", - direction_id: 1 - }, - alerts: %Alerts{stop_id: "456"}, - line_map: %LineMap{ - stop_id: "456", - station_id: "place-test", - direction_id: 1, - route_id: "Green-B" - }, - evergreen_content: [], - platform_location: "back" - }, - tags: [] - } - } - } - |> PendingConfig.to_json() - |> Jason.encode!(pretty: true) - - {:ok, config, _metadata} = Local.fetch_config() - assert expected_file_contents == config - end - - test "returns version_mismatch error if version is outdated" do - places_and_screens = %{ - "place-test" => %{ - "updated_pending_screens" => [], - "new_pending_screens" => [ - %{ - "new_id" => "1234", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - } - ] - } - } - - assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, "1234") == - {:error, :version_mismatch} - end - - test "returns error when duplicate screen IDs are found" do - version = fetch_current_config_version() - - expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, - route_id -> - assert stop_id == "place-test" - assert route_id == "Green-B" - - {"123", "456"} - end) - - places_and_screens = %{ - "place-test" => %{ - "updated_pending_screens" => [], - "new_pending_screens" => [ - %{ - "new_id" => "1234", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - } - ] - } - } - - assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok - version = fetch_current_config_version() - - places_and_screens = %{ - "place-test" => %{ - "updated_pending_screens" => [], - "new_pending_screens" => [ - %{ - "new_id" => "1234", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - }, - %{ - "new_id" => "5678", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - }, - %{ - "new_id" => "5678", - "app_params" => %{ - "header" => %{"route_id" => "Green-B", "direction_id" => 0}, - "platform_location" => "front" - } - } - ] - } - } - - assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == - {:error, {:duplicate_screen_ids, ["1234", "5678"]}} - end - end - - describe "publish_pending_screens/1" do - setup do - pending_screens_path = get_fixture_path("pending_config.json") - - config = - %PendingConfig{ - screens: %{ - "12345" => %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :gl_eink_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %GlEink{ - departures: %Departures{ - sections: [ - %Departures.Section{ - query: %Departures.Query{ - params: %Departures.Query.Params{ - stop_ids: ["place-test"], - route_ids: ["Green-B"], - direction_id: 1 - } - } - } - ] - }, - footer: %Footer{stop_id: "place-test"}, - header: %Header.Destination{ - route_id: "Green-B", - direction_id: 1 - }, - alerts: %Alerts{stop_id: "456"}, - line_map: %LineMap{ - stop_id: "456", - station_id: "place-test", - direction_id: 1, - route_id: "Green-B" - }, - evergreen_content: [], - platform_location: "back" - }, - tags: [] - }, - "23456" => %Screen{ - vendor: :mercury, - device_id: nil, - name: nil, - app_id: :gl_eink_v2, - refresh_if_loaded_before: nil, - disabled: false, - hidden_from_screenplay: false, - app_params: %GlEink{ - departures: %Departures{ - sections: [ - %Departures.Section{ - query: %Departures.Query{ - params: %Departures.Query.Params{ - stop_ids: ["place-test"], - route_ids: ["Green-B"], - direction_id: 1 - } - } - } - ] - }, - footer: %Footer{stop_id: "place-test"}, - header: %Header.Destination{ - route_id: "Green-B", - direction_id: 1 - }, - alerts: %Alerts{stop_id: "456"}, - line_map: %LineMap{ - stop_id: "456", - station_id: "place-test", - direction_id: 1, - route_id: "Green-B" - }, - evergreen_content: [], - platform_location: "back" - }, - tags: [] - } - } - } - |> PendingConfig.to_json() - |> Jason.encode!() - - File.write(pending_screens_path, config) - - [ - %Place{ - id: "place-test", - name: "Test Place", - routes: ["Green-B"], - screens: [], - description: nil - } - ] - |> Enum.map(&{&1.id, &1}) - |> Cache.put_all() - - on_exit(fn -> - Cache.delete_all() - end) - end - - test "publishes pending screens" do - assert {:ok, - [ - %Place{ - id: "place-test", - name: "Test Place", - routes: ["Green-B"], - screens: [ - %ShowtimeScreen{ - id: "12345", - type: :gl_eink_v2, - disabled: false, - direction_id: nil, - location: "" - } - ], - description: nil - } - ]} = PermanentConfig.publish_pending_screens("place-test", :gl_eink_v2, ["23456"]) - end - end - - describe "add_emergency_takeover_configs/3" do - setup do - published_screens_path = get_fixture_path("screens_config.json") - - config = - %Config{ - screens: %{ - "PRE-1" => @screen_without_takeover, - "PRE-2" => @screen_without_takeover, - "GL-1" => @gl_eink_screen - } - } - |> Config.to_json() - |> Jason.encode!() - - File.write(published_screens_path, config) - end - - test "adds an emergency takeover config to a screen" do - alert_id = "alert-1" - takeover_screen_id = "PRE-1" - message = %{type: :custom, text: %{indoor: "Indoor Message", outdoor: "Outdoor Message"}} - - assert PermanentConfig.add_emergency_takeover_configs( - alert_id, - [takeover_screen_id], - message - ) == :ok - - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() - %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - - expected_takeover = %EmergencyTakeover{ - audio_asset_path: nil, - text_for_audio: "Indoor Message", - visual_asset_path: "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" - } - - assert screens[takeover_screen_id] == - put_in( - @screen_without_takeover.app_params.emergency_takeover, - expected_takeover - ) - - assert screens["PRE-2"] == @screen_without_takeover - assert screens["GL-1"] == @gl_eink_screen - end - - test "adds a canned emergency takeover config to a screen" do - alert_id = "alert-1" - takeover_screen_id = "PRE-1" - message = %{type: :canned, id: 1} - - assert PermanentConfig.add_emergency_takeover_configs( - alert_id, - [takeover_screen_id], - message - ) == :ok - - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() - %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - - expected_takeover = %EmergencyTakeover{ - audio_asset_path: - "test/fixtures/emergency_takeover_images/canned/audio/LeaveStation-Indoor.mp3", - text_for_audio: nil, - visual_asset_path: - "test/fixtures/emergency_takeover_images/canned/images/LeaveStation-indoor-portrait.gif" - } - - assert screens[takeover_screen_id] == - put_in( - @screen_without_takeover.app_params.emergency_takeover, - expected_takeover - ) - - assert screens["PRE-2"] == @screen_without_takeover - end - end - - describe "clear_emergency_takeover_configs/1" do - setup do - published_screens_path = get_fixture_path("screens_config.json") - - screen_with_takeover = - put_in( - @screen_without_takeover.app_params.emergency_takeover, - %EmergencyTakeover{ - audio_asset_path: nil, - text_for_audio: "Indoor Message", - visual_asset_path: - "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" - } - ) - - config = - %Config{ - screens: %{ - "PRE-1" => screen_with_takeover, - "PRE-2" => @screen_without_takeover, - "GL-1" => @gl_eink_screen - } - } - |> Config.to_json() - |> Jason.encode!() - - File.write(published_screens_path, config) - end - - test "clears emergency takeover configs from screens" do - takeover_screen_id = "PRE-1" - - assert PermanentConfig.clear_emergency_takeover_configs([takeover_screen_id]) == :ok - - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() - %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - - assert screens[takeover_screen_id] == @screen_without_takeover - assert screens["PRE-2"] == @screen_without_takeover - assert screens["GL-1"] == @gl_eink_screen - end - end -end +# defmodule Screenplay.PermanentConfigTest do +# use ExUnit.Case + +# import Mox + +# alias Screenplay.PendingScreensConfig.Fetch.Local +# alias Screenplay.PermanentConfig +# alias Screenplay.Places.{Cache, Place} +# alias Screenplay.Places.Place.ShowtimeScreen +# alias ScreensConfig.ContentSummary +# alias ScreensConfig.ElevatorStatus +# alias ScreensConfig.Screen.PreFare + +# alias ScreensConfig.{ +# Alerts, +# Config, +# Departures, +# EmergencyTakeover, +# Footer, +# Header, +# LineMap, +# PendingConfig, +# Screen +# } + +# alias ScreensConfig.Screen.GlEink + +# @screen_without_takeover %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :pre_fare_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %PreFare{ +# emergency_messaging_location: :inside, +# emergency_takeover: nil, +# content_summary: %ContentSummary{parent_station_id: "place-test"}, +# elevator_status: %ElevatorStatus{parent_station_id: "place-test"}, +# full_line_map: [], +# header: %Header.StopId{stop_id: "place-test"}, +# reconstructed_alert_widget: %ScreensConfig.Alerts{stop_id: "place-test"} +# }, +# tags: [] +# } +# @gl_eink_screen %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :gl_eink_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %GlEink{ +# departures: %Departures{ +# sections: [ +# %Departures.Section{ +# query: %Departures.Query{ +# params: %Departures.Query.Params{ +# stop_ids: ["place-test"], +# route_ids: ["Green-B"], +# direction_id: 1 +# } +# } +# } +# ] +# }, +# footer: %Footer{stop_id: "place-test"}, +# header: %Header.Destination{ +# route_id: "Green-B", +# direction_id: 1 +# }, +# alerts: %Alerts{stop_id: "456"}, +# line_map: %LineMap{ +# stop_id: "456", +# station_id: "place-test", +# direction_id: 1, +# route_id: "Green-B" +# }, +# evergreen_content: [], +# platform_location: :back +# }, +# tags: [] +# } + +# def fetch_current_config_version do +# {:ok, _config, metadata} = Local.fetch_config() +# metadata.version_id +# end + +# def get_fixture_path(file_name) do +# Path.join(~w[#{File.cwd!()} test fixtures #{file_name}]) +# end + +# setup_all do +# start_supervised!(Screenplay.Places.Cache) + +# on_exit(fn -> +# empty_config = %{screens: %{}} +# pending_screens_path = get_fixture_path("pending_config.json") +# published_screens_path = get_fixture_path("screens_config.json") + +# File.write( +# pending_screens_path, +# Jason.encode!(empty_config) +# ) + +# File.write( +# published_screens_path, +# Jason.encode!(empty_config) +# ) + +# File.rm(pending_screens_path <> ".temp") +# File.rm(published_screens_path <> ".temp") +# end) +# end + +# describe "put_pending_screens/3" do +# setup do +# empty_config = %{screens: %{}} +# pending_screens_path = get_fixture_path("pending_config.json") +# published_screens_path = get_fixture_path("screens_config.json") + +# File.write( +# pending_screens_path, +# Jason.encode!(empty_config) +# ) + +# File.write( +# published_screens_path, +# Jason.encode!(empty_config) +# ) +# end + +# test "adds and updates a new config for GL E-Ink" do +# version = fetch_current_config_version() + +# expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, +# route_id -> +# assert stop_id == "place-test" +# assert route_id == "Green-B" + +# {"123", "456"} +# end) + +# places_and_screens = %{ +# "place-test" => %{ +# "updated_pending_screens" => [], +# "new_pending_screens" => [ +# %{ +# "new_id" => "1234", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# } +# ] +# } +# } + +# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok + +# expected_file_contents = +# %PendingConfig{ +# screens: %{ +# "1234" => %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :gl_eink_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %GlEink{ +# departures: %Departures{ +# sections: [ +# %Departures.Section{ +# query: %Departures.Query{ +# params: %Departures.Query.Params{ +# stop_ids: ["place-test"], +# route_ids: ["Green-B"], +# direction_id: 0 +# } +# } +# } +# ] +# }, +# footer: %Footer{stop_id: "place-test"}, +# header: %Header.Destination{ +# route_id: "Green-B", +# direction_id: 0 +# }, +# alerts: %Alerts{stop_id: "123"}, +# line_map: %LineMap{ +# stop_id: "123", +# station_id: "place-test", +# direction_id: 0, +# route_id: "Green-B" +# }, +# evergreen_content: [], +# platform_location: "front" +# }, +# tags: [] +# } +# } +# } +# |> PendingConfig.to_json() +# |> Jason.encode!(pretty: true) + +# {:ok, config, metadata} = Local.fetch_config() +# assert expected_file_contents == config + +# places_and_screens = %{ +# "place-test" => %{ +# "updated_pending_screens" => [ +# %{ +# "new_id" => "12345", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 1}, +# "platform_location" => "back" +# }, +# "screen_id" => "1234" +# } +# ], +# "new_pending_screens" => [] +# } +# } + +# assert PermanentConfig.put_pending_screens( +# places_and_screens, +# :gl_eink_v2, +# metadata.version_id +# ) == :ok + +# expected_file_contents = +# %PendingConfig{ +# screens: %{ +# "12345" => %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :gl_eink_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %GlEink{ +# departures: %Departures{ +# sections: [ +# %Departures.Section{ +# query: %Departures.Query{ +# params: %Departures.Query.Params{ +# stop_ids: ["place-test"], +# route_ids: ["Green-B"], +# direction_id: 1 +# } +# } +# } +# ] +# }, +# footer: %Footer{stop_id: "place-test"}, +# header: %Header.Destination{ +# route_id: "Green-B", +# direction_id: 1 +# }, +# alerts: %Alerts{stop_id: "456"}, +# line_map: %LineMap{ +# stop_id: "456", +# station_id: "place-test", +# direction_id: 1, +# route_id: "Green-B" +# }, +# evergreen_content: [], +# platform_location: "back" +# }, +# tags: [] +# } +# } +# } +# |> PendingConfig.to_json() +# |> Jason.encode!(pretty: true) + +# {:ok, config, _metadata} = Local.fetch_config() +# assert expected_file_contents == config +# end + +# test "returns version_mismatch error if version is outdated" do +# places_and_screens = %{ +# "place-test" => %{ +# "updated_pending_screens" => [], +# "new_pending_screens" => [ +# %{ +# "new_id" => "1234", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# } +# ] +# } +# } + +# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, "1234") == +# {:error, :version_mismatch} +# end + +# test "returns error when duplicate screen IDs are found" do +# version = fetch_current_config_version() + +# expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, +# route_id -> +# assert stop_id == "place-test" +# assert route_id == "Green-B" + +# {"123", "456"} +# end) + +# places_and_screens = %{ +# "place-test" => %{ +# "updated_pending_screens" => [], +# "new_pending_screens" => [ +# %{ +# "new_id" => "1234", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# } +# ] +# } +# } + +# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok +# version = fetch_current_config_version() + +# places_and_screens = %{ +# "place-test" => %{ +# "updated_pending_screens" => [], +# "new_pending_screens" => [ +# %{ +# "new_id" => "1234", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# }, +# %{ +# "new_id" => "5678", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# }, +# %{ +# "new_id" => "5678", +# "app_params" => %{ +# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, +# "platform_location" => "front" +# } +# } +# ] +# } +# } + +# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == +# {:error, {:duplicate_screen_ids, ["1234", "5678"]}} +# end +# end + +# describe "publish_pending_screens/1" do +# setup do +# pending_screens_path = get_fixture_path("pending_config.json") + +# config = +# %PendingConfig{ +# screens: %{ +# "12345" => %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :gl_eink_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %GlEink{ +# departures: %Departures{ +# sections: [ +# %Departures.Section{ +# query: %Departures.Query{ +# params: %Departures.Query.Params{ +# stop_ids: ["place-test"], +# route_ids: ["Green-B"], +# direction_id: 1 +# } +# } +# } +# ] +# }, +# footer: %Footer{stop_id: "place-test"}, +# header: %Header.Destination{ +# route_id: "Green-B", +# direction_id: 1 +# }, +# alerts: %Alerts{stop_id: "456"}, +# line_map: %LineMap{ +# stop_id: "456", +# station_id: "place-test", +# direction_id: 1, +# route_id: "Green-B" +# }, +# evergreen_content: [], +# platform_location: "back" +# }, +# tags: [] +# }, +# "23456" => %Screen{ +# vendor: :mercury, +# device_id: nil, +# name: nil, +# app_id: :gl_eink_v2, +# refresh_if_loaded_before: nil, +# disabled: false, +# hidden_from_screenplay: false, +# app_params: %GlEink{ +# departures: %Departures{ +# sections: [ +# %Departures.Section{ +# query: %Departures.Query{ +# params: %Departures.Query.Params{ +# stop_ids: ["place-test"], +# route_ids: ["Green-B"], +# direction_id: 1 +# } +# } +# } +# ] +# }, +# footer: %Footer{stop_id: "place-test"}, +# header: %Header.Destination{ +# route_id: "Green-B", +# direction_id: 1 +# }, +# alerts: %Alerts{stop_id: "456"}, +# line_map: %LineMap{ +# stop_id: "456", +# station_id: "place-test", +# direction_id: 1, +# route_id: "Green-B" +# }, +# evergreen_content: [], +# platform_location: "back" +# }, +# tags: [] +# } +# } +# } +# |> PendingConfig.to_json() +# |> Jason.encode!() + +# File.write(pending_screens_path, config) + +# [ +# %Place{ +# id: "place-test", +# name: "Test Place", +# routes: ["Green-B"], +# screens: [], +# description: nil +# } +# ] +# |> Enum.map(&{&1.id, &1}) +# |> Cache.put_all() + +# on_exit(fn -> +# Cache.delete_all() +# end) +# end + +# test "publishes pending screens" do +# assert {:ok, +# [ +# %Place{ +# id: "place-test", +# name: "Test Place", +# routes: ["Green-B"], +# screens: [ +# %ShowtimeScreen{ +# id: "12345", +# type: :gl_eink_v2, +# disabled: false, +# direction_id: nil, +# location: "" +# } +# ], +# description: nil +# } +# ]} = PermanentConfig.publish_pending_screens("place-test", :gl_eink_v2, ["23456"]) +# end +# end + +# describe "add_emergency_takeover_configs/3" do +# setup do +# published_screens_path = get_fixture_path("screens_config.json") + +# config = +# %Config{ +# screens: %{ +# "PRE-1" => @screen_without_takeover, +# "PRE-2" => @screen_without_takeover, +# "GL-1" => @gl_eink_screen +# } +# } +# |> Config.to_json() +# |> Jason.encode!() + +# File.write(published_screens_path, config) +# end + +# test "adds an emergency takeover config to a screen" do +# alert_id = "alert-1" +# takeover_screen_id = "PRE-1" +# message = %{type: :custom, text: %{indoor: "Indoor Message", outdoor: "Outdoor Message"}} + +# assert PermanentConfig.add_emergency_takeover_configs( +# alert_id, +# [takeover_screen_id], +# message +# ) == :ok + +# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() +# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + +# expected_takeover = %EmergencyTakeover{ +# audio_asset_path: nil, +# text_for_audio: "Indoor Message", +# visual_asset_path: "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" +# } + +# assert screens[takeover_screen_id] == +# put_in( +# @screen_without_takeover.app_params.emergency_takeover, +# expected_takeover +# ) + +# assert screens["PRE-2"] == @screen_without_takeover +# assert screens["GL-1"] == @gl_eink_screen +# end + +# test "adds a canned emergency takeover config to a screen" do +# alert_id = "alert-1" +# takeover_screen_id = "PRE-1" +# message = %{type: :canned, id: 1} + +# assert PermanentConfig.add_emergency_takeover_configs( +# alert_id, +# [takeover_screen_id], +# message +# ) == :ok + +# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() +# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + +# expected_takeover = %EmergencyTakeover{ +# audio_asset_path: +# "test/fixtures/emergency_takeover_images/canned/audio/LeaveStation-Indoor.mp3", +# text_for_audio: nil, +# visual_asset_path: +# "test/fixtures/emergency_takeover_images/canned/images/LeaveStation-indoor-portrait.gif" +# } + +# assert screens[takeover_screen_id] == +# put_in( +# @screen_without_takeover.app_params.emergency_takeover, +# expected_takeover +# ) + +# assert screens["PRE-2"] == @screen_without_takeover +# end +# end + +# describe "clear_emergency_takeover_configs/1" do +# setup do +# published_screens_path = get_fixture_path("screens_config.json") + +# screen_with_takeover = +# put_in( +# @screen_without_takeover.app_params.emergency_takeover, +# %EmergencyTakeover{ +# audio_asset_path: nil, +# text_for_audio: "Indoor Message", +# visual_asset_path: +# "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" +# } +# ) + +# config = +# %Config{ +# screens: %{ +# "PRE-1" => screen_with_takeover, +# "PRE-2" => @screen_without_takeover, +# "GL-1" => @gl_eink_screen +# } +# } +# |> Config.to_json() +# |> Jason.encode!() + +# File.write(published_screens_path, config) +# end + +# test "clears emergency takeover configs from screens" do +# takeover_screen_id = "PRE-1" + +# assert PermanentConfig.clear_emergency_takeover_configs([takeover_screen_id]) == :ok + +# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() +# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() + +# assert screens[takeover_screen_id] == @screen_without_takeover +# assert screens["PRE-2"] == @screen_without_takeover +# assert screens["GL-1"] == @gl_eink_screen +# end +# end +# end From 94489da113109ab0971c66b1e95cc7ce505ba924 Mon Sep 17 00:00:00 2001 From: "robbie.sundstrom" Date: Fri, 10 Jul 2026 14:05:45 -0400 Subject: [PATCH 2/4] chore: Remove permanent configuration feature --- .../css/dashboard/pending-screen-detail.scss | 93 -- .../css/dashboard/pending-screens-page.scss | 87 -- assets/css/screenplay.scss | 9 - assets/js/components/App.tsx | 15 - assets/js/components/Dashboard/Dashboard.tsx | 1 - .../js/components/Dashboard/OverridesPage.tsx | 21 - .../Dashboard/PendingScreenDetail.tsx | 170 --- .../Dashboard/PendingScreensPage.tsx | 208 --- .../PendingScreensPlaceRowAccordion.tsx | 176 --- .../PermanentConfiguration/AppBar.tsx | 23 - .../BottomActionBar.tsx | 47 - .../PermanentConfiguration/ButtonImage.tsx | 26 - .../ConfigureScreensPage.tsx | 17 - .../PlacesSearchBar.tsx | 68 - .../SelectScreenType.tsx | 73 -- .../WorkflowPlacesList.tsx | 58 - .../Workflows/GlEink/ConfigureScreensPage.tsx | 577 --------- .../Workflows/GlEink/GlEinkWorkflow.tsx | 330 ----- .../Workflows/GlEink/StationSelectPage.tsx | 158 --- assets/js/components/Dashboard/PlaceRow.tsx | 1 - .../components/Dashboard/ScreenSimulation.tsx | 12 +- assets/js/components/Dashboard/Sidebar.tsx | 10 +- assets/js/hooks/useScreenplayContext.tsx | 46 +- assets/js/models/configValidationErrors.ts | 8 - assets/js/models/screen_configuration.ts | 23 - assets/js/util.ts | 42 +- assets/js/utils/api.ts | 125 +- assets/js/utils/auth.ts | 1 - assets/js/utils/emergencyMessages.ts | 2 +- assets/js/utils/errorHandler.tsx | 2 +- assets/package-lock.json | 1121 +++++++++++++++-- assets/package.json | 4 +- config/config.exs | 1 - config/dev.exs | 3 - config/test.exs | 3 - .../pending_screens_config/fetch.ex | 39 - .../pending_screens_config/fetch/local.ex | 79 -- .../pending_screens_config/fetch/s3.ex | 80 -- lib/screenplay/permanent_config.ex | 548 -------- lib/screenplay_web/auth_manager.ex | 1 - .../controllers/config_controller.ex | 172 --- lib/screenplay_web/router.ex | 24 - .../templates/layout/app.html.heex | 3 - test/screenplay/permanent_config_test.exs | 623 --------- .../controllers/auth_controller_test.exs | 2 +- test/support/conn_case.ex | 2 +- 46 files changed, 1072 insertions(+), 4062 deletions(-) delete mode 100644 assets/css/dashboard/pending-screen-detail.scss delete mode 100644 assets/css/dashboard/pending-screens-page.scss delete mode 100644 assets/js/components/Dashboard/OverridesPage.tsx delete mode 100644 assets/js/components/Dashboard/PendingScreenDetail.tsx delete mode 100644 assets/js/components/Dashboard/PendingScreensPage.tsx delete mode 100644 assets/js/components/Dashboard/PendingScreensPlaceRowAccordion.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/AppBar.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/BottomActionBar.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/ButtonImage.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/ConfigureScreensPage.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/PlacesSearchBar.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/SelectScreenType.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/WorkflowPlacesList.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx delete mode 100644 assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/StationSelectPage.tsx delete mode 100644 assets/js/models/configValidationErrors.ts delete mode 100644 assets/js/models/screen_configuration.ts delete mode 100644 lib/screenplay/pending_screens_config/fetch.ex delete mode 100644 lib/screenplay/pending_screens_config/fetch/local.ex delete mode 100644 lib/screenplay/pending_screens_config/fetch/s3.ex delete mode 100644 lib/screenplay/permanent_config.ex delete mode 100644 lib/screenplay_web/controllers/config_controller.ex delete mode 100644 test/screenplay/permanent_config_test.exs diff --git a/assets/css/dashboard/pending-screen-detail.scss b/assets/css/dashboard/pending-screen-detail.scss deleted file mode 100644 index 567e2f97..00000000 --- a/assets/css/dashboard/pending-screen-detail.scss +++ /dev/null @@ -1,93 +0,0 @@ -.pending-screen-detail { - background-color: #0f1417; - border-radius: 8px; - margin: 0px 16px 16px; - padding: 0px 0px 16px 16px; - color: white; - - .row { - width: 100%; - min-height: 56px; - align-items: center; - - .location-description { - height: 72px; - line-height: 72px; - } - - .live-icon { - // It refused to vertically align with the text. - transform: translateY(-2px); - } - - .pending-or-live { - display: inline-block; - margin-left: 8px; - font-weight: 700; - font-size: 16px; - } - - .pending-url-advisory { - font-weight: 400; - font-size: 14px; - line-height: 21px; - margin-top: -8px; - margin-bottom: 17px; - } - - .screen-url { - display: inline-block; - line-height: 69px; - } - - .screen-id { - line-height: 56px; - } - - .screen-url-row-button { - display: inline-block; - // Buttons are spaced only 8px apart, so we cancel out one of the 8px margins - margin-right: -8px; - } - - .hide-on-places-page { - display: inline-block; - margin-right: 8px; - height: 73px; - line-height: 73px; - } - - &.screen-simulation { - height: fit-content; - } - - &:not(:first-child) { - &::before { - content: ""; - width: 100%; - height: 1px; - background-color: #41474d; - } - } - - &:last-child { - &::before { - content: ""; - width: 100%; - height: 2px; - background-color: #41474d; - } - } - - .url { - margin-left: 16px; - margin-right: 4px; - color: white; - - &--inactive { - font-weight: 400; - font-size: 16px; - } - } - } -} diff --git a/assets/css/dashboard/pending-screens-page.scss b/assets/css/dashboard/pending-screens-page.scss deleted file mode 100644 index 9d0263b1..00000000 --- a/assets/css/dashboard/pending-screens-page.scss +++ /dev/null @@ -1,87 +0,0 @@ -@use "../variables"; - -.pending-screens-page { - height: 100%; - - .add-new-button { - height: 48px; - background-color: variables.$button-primary; - color: variables.$button-secondary; - } -} - -.no-screens-text { - margin-left: 16px; -} - -.last-modified { - height: 24px; - - line-height: 24px; - font-weight: 700; - font-size: 16px; - - margin-left: 16px; - margin-bottom: 24px; -} - -.pending-screens-place-row-accordion { - border-radius: 8px; - background-image: linear-gradient( - 180deg, - rgba(255, 255, 255, 0.2) 0, - rgba(255, 255, 255, 0) 1px - ); - - background-color: variables.$cool-gray-30; - box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.25); - margin-bottom: 2px; - border-bottom: 0px; - position: relative; - - &.open { - padding: 0px 0px 16px; - } - - &__header { - font-size: 16px; - font-weight: 700; - line-height: 24px; - letter-spacing: 0em; - text-align: left; - - .row { - height: 82px; - width: 100%; - } - - .place-info-col { - gap: 16px; - } - - .buttons > .btn { - height: 38px; - } - - .buttons { - gap: 8px; - - .edit-button { - &__icon { - display: inline-block; - transform: translateY(-2px); - margin-right: 8px; - } - - &__text { - display: inline-block; - } - } - - .publish-button { - background-color: variables.$button-primary; - color: variables.$button-secondary; - } - } - } -} diff --git a/assets/css/screenplay.scss b/assets/css/screenplay.scss index da3e5492..d68a5fe3 100644 --- a/assets/css/screenplay.scss +++ b/assets/css/screenplay.scss @@ -13,14 +13,7 @@ @use "dashboard/places-page"; @use "dashboard/alerts-page"; @use "dashboard/overrides-page"; -@use "dashboard/pending-screens-page"; -@use "dashboard/pending-screen-detail"; @use "dashboard/alert-details"; -@use "dashboard/configure-screens-page"; -@use "dashboard/button-image"; -@use "dashboard/appbar"; -@use "dashboard/bottom-action-bar"; -@use "dashboard/configure-screens-workflow-page"; @use "dashboard/pa-messages-page"; @use "dashboard/new-pa-message/new-pa-message"; @use "place-row"; @@ -36,9 +29,7 @@ @use "alert-banner"; @use "animation"; @use "screen-detail-action-bar"; -@use "workflow"; @use "sort-label"; -@use "search-bar"; @use "dashboard/picker"; @use "error-modal"; @use "dashboard/toast"; diff --git a/assets/js/components/App.tsx b/assets/js/components/App.tsx index 25cc8498..0d734244 100644 --- a/assets/js/components/App.tsx +++ b/assets/js/components/App.tsx @@ -1,7 +1,6 @@ import React, { ReactElement } from "react"; import { Routes, Route, BrowserRouter } from "react-router-dom"; import { ScreenplayProvider } from "../hooks/useScreenplayContext"; -import GlEinkWorkflow from "Components/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow"; import ErrorModal from "./Dashboard/ErrorModal"; const EmergencyTakeoverTool = React.lazy( @@ -11,15 +10,6 @@ const Dashboard = React.lazy(() => import("Components/Dashboard")); const PlacesPage = React.lazy(() => import("Components/PlacesPage")); const AlertsPage = React.lazy(() => import("Components/AlertsPage")); const AlertDetails = React.lazy(() => import("Components/AlertDetails")); -const PendingScreensPage = React.lazy( - () => import("Components/PendingScreensPage"), -); -const ConfigureScreensPage = React.lazy( - () => import("Components/PermanentConfiguration/ConfigureScreensPage"), -); -const SelectScreenTypeComponent = React.lazy( - () => import("Components/PermanentConfiguration/SelectScreenType"), -); const PaMessagesPage = React.lazy(() => import("Components/PaMessagesPage")); const NewPaMessage = React.lazy(() => import("Components/NewPaMessage")); const EditPaMessage = React.lazy(() => import("Components/EditPaMessage")); @@ -50,11 +40,6 @@ class AppRoutes extends React.Component { }> }> }> - }> - }> - } /> - } /> - } /> } /> } /> diff --git a/assets/js/components/Dashboard/Dashboard.tsx b/assets/js/components/Dashboard/Dashboard.tsx index edf993af..b05a2853 100644 --- a/assets/js/components/Dashboard/Dashboard.tsx +++ b/assets/js/components/Dashboard/Dashboard.tsx @@ -154,7 +154,6 @@ const Dashboard: ComponentType = () => { const pathname = useLocation().pathname; const showAlertBanner = - !pathname.includes("configure-screens") && !pathname.includes("pa-messages") && !pathname.includes("prediction-suppression") && bannerAlert?.alert; diff --git a/assets/js/components/Dashboard/OverridesPage.tsx b/assets/js/components/Dashboard/OverridesPage.tsx deleted file mode 100644 index aff12c50..00000000 --- a/assets/js/components/Dashboard/OverridesPage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { ComponentType } from "react"; -import classNames from "classnames"; - -interface Props { - isVisible: boolean; -} - -const OverridesPage: ComponentType = (props: Props) => { - return ( -
-
Overrides
-
{null}
-
- ); -}; - -export default OverridesPage; diff --git a/assets/js/components/Dashboard/PendingScreenDetail.tsx b/assets/js/components/Dashboard/PendingScreenDetail.tsx deleted file mode 100644 index f78010fa..00000000 --- a/assets/js/components/Dashboard/PendingScreenDetail.tsx +++ /dev/null @@ -1,170 +0,0 @@ -// Note: component name is a bit unclear--this can show detail for both pending and live screens, -// but its presentation is particular to the pending screens page. - -import React, { ComponentType, SyntheticEvent } from "react"; -import { ScreenConfiguration } from "Models/screen_configuration"; -import ScreenSimulation from "Components/ScreenSimulation"; -import { Col, Container, Form, Row } from "react-bootstrap"; -import { capitalize } from "../../util"; -import OpenInTabButton from "Components/OpenInTabButton"; -import CopyLinkButton from "Components/CopyLinkButton"; -import { useScreenplayState } from "Hooks/useScreenplayContext"; -import { LightningChargeFill, ClockFill } from "react-bootstrap-icons"; - -interface Props { - screenID: string; - isLive: boolean; - config: ScreenConfiguration; - isHiddenOnPlacesPage: boolean; - onClickHideOnPlacesPage: () => void; -} - -const getGLScreenLocationDescription = ( - config: ScreenConfiguration & { app_id: "gl_eink_v2" }, -) => { - let direction = ""; - switch (config.app_params.header.direction_id) { - case 0: - direction = "Westbound"; - break; - case 1: - direction = "Eastbound"; - } - - const platformLocation = capitalize( - config.app_params.platform_location ?? "", - ); - - return [direction, platformLocation].join(" "); -}; - -const getScreenLocationDescription = (config: ScreenConfiguration) => { - switch (config.app_id) { - case "gl_eink_v2": - return getGLScreenLocationDescription(config); - default: - console.warn( - `getScreenLocationDescription not implemented for ${config.app_id}`, - ); - return ""; - } -}; - -const PendingScreenDetail: ComponentType = ({ - screenID, - isLive, - config, - isHiddenOnPlacesPage, - onClickHideOnPlacesPage, -}: Props): JSX.Element => { - const { setShowLinkCopied } = useScreenplayState(); - const screensUrl = document - .querySelector("meta[name=screens-url]") - ?.getAttribute("content"); - - const queueToastExpiration = () => { - setTimeout(() => setShowLinkCopied(false), 5000); - }; - - const locationDescription = getScreenLocationDescription(config); - - const fullScreenUrl = isLive - ? `${screensUrl}/v2/screen/${screenID}` - : `${screensUrl}/v2/screen/pending/${screenID}`; - - // If screen is already live, this matches `fullScreenUrl`. - // If screen is pending, this is its future (but not yet working) live url. - const liveScreenUrl = `${screensUrl}/v2/screen/${screenID}`; - - return ( -
e.stopPropagation()} - > - - - {locationDescription && ( - - {locationDescription} - - )} - - {isLive ? ( - <> -
- -
-
Live · Read-only
- - ) : ( - <> -
- -
-
Pending
- - )} - -
- - - Screen ID:{" "} - {screenID} - - - - -
Live screen URL:
- {isLive ? ( - - {liveScreenUrl} - - ) : ( - {liveScreenUrl} - )} -
- -
-
- -
- {!isLive && ( -
- Screen will be available at this URL once published -
- )} - -
- {!isLive && ( - - - -
- Hide on Places page -
- -
- )} - - - -
-
- ); -}; - -export default PendingScreenDetail; diff --git a/assets/js/components/Dashboard/PendingScreensPage.tsx b/assets/js/components/Dashboard/PendingScreensPage.tsx deleted file mode 100644 index 66a8b295..00000000 --- a/assets/js/components/Dashboard/PendingScreensPage.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, { - ComponentType, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; -import { - PendingAndLiveScreens, - fetchExistingScreensAtPlacesWithPendingScreens, - publishScreensForPlace, -} from "Utils/api"; -import { Accordion, Button, Col, Container, Row } from "react-bootstrap"; -import PendingScreensPlaceRowAccordion from "Components/PendingScreensPlaceRowAccordion"; -import { ScreenConfiguration } from "Models/screen_configuration"; -import { useScreenplayState } from "Hooks/useScreenplayContext"; -import { format } from "date-fns/format"; -import { Place } from "Models/place"; -import { useNavigate } from "react-router-dom"; - -const PendingScreensPage: ComponentType = () => { - const { places, setPlaces, showActionOutcome, hideActionOutcome } = - useScreenplayState(); - const [existingScreens, setExistingScreens] = useState( - {}, - ); - const [etag, setEtag] = useState(null); - const [lastModified, setLastModified] = useState(null); - const [isPublishing, setIsPublishing] = useState(false); - - const placesByID: Record = useMemo( - () => places.reduce((acc, place) => ({ ...acc, [place.id]: place }), {}), - [places], - ); - - const navigate = useNavigate(); - - const fetchData = useCallback(async () => { - const data = await fetchExistingScreensAtPlacesWithPendingScreens(); - if (data) { - const { places_and_screens, etag, last_modified_ms } = data; - setExistingScreens(places_and_screens); - setEtag(etag); - if (last_modified_ms !== null) { - setLastModified(new Date(last_modified_ms)); - } - } - }, [setExistingScreens, setEtag, setLastModified]); - - const publish = useCallback( - async ( - placeID: string, - appID: string, - hiddenFromScreenplayIDs: string[], - ) => { - if (isPublishing) { - // Prevent multiple publish requests from being fired if user accidentally double clicks the button. - return; - } - - setIsPublishing(true); - try { - // We know etag is not null at this point because it's not possible for a "Publish" button - // to be rendered without the ETag also being set--both state values are set together in - // `fetchData`. - const { status, message, newConfig } = await publishScreensForPlace( - placeID, - appID, - hiddenFromScreenplayIDs, - etag!, - ); - - const defaultErrorMessage = "Server error. Please contact an engineer."; - switch (status) { - case 200: - if (newConfig) { - setPlaces(newConfig); - } - - showActionOutcome(true, "Screens published to places"); - // Since the publish succeeded, let's update the page data immediately - // so the new state is reflected. - fetchData(); - break; - case 412: - showActionOutcome( - false, - "Page is out of date. Please reload and try again.", - ); - break; - case 500: - showActionOutcome(false, message || defaultErrorMessage); - break; - default: - showActionOutcome(false, defaultErrorMessage); - console.error(`Bad publish response status: ${status}`); - } - } catch (e) { - showActionOutcome(false, "Unknown error. Please contact an engineer."); - console.error(e); - } - - setIsPublishing(false); - setTimeout(() => hideActionOutcome(), 5000); - }, - [ - etag, - setPlaces, - showActionOutcome, - hideActionOutcome, - fetchData, - isPublishing, - ], - ); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - const screens = Object.entries(existingScreens); - let layout; - - if (screens.length) { - layout = ( - <> - {lastModified && ( -
- Updated {format(lastModified, "MMMM d, y")} -
- )} - - {Object.entries(existingScreens).map( - ([ - placeAndAppGroupID, - { live_screens, pending_screens, place_id, app_id }, - ]) => { - const place = placesByID[place_id]; - return place ? ( - - ) : null; - }, - )} - - - ); - } else { - layout = ( -
There are no pending screens.
- ); - } - - return ( -
-
- - - Pending - - - - - -
-
- {layout} -
-
- ); -}; - -const mergeLiveAndPendingByID = ( - liveScreens: PendingAndLiveScreens[string]["live_screens"], - pendingScreens: PendingAndLiveScreens[string]["pending_screens"], -) => - [ - ...Object.entries(liveScreens ?? {}).map(addIsLive(true)), - ...Object.entries(pendingScreens).map(addIsLive(false)), - ].sort(({ screenID: id1 }, { screenID: id2 }) => { - if (id1 < id2) return -1; - if (id1 === id2) return 0; - return 1; - }); - -const addIsLive = - (isLive: boolean) => - ([screenID, config]: [string, ScreenConfiguration]) => ({ - isLive, - screenID, - config, - }); - -export default PendingScreensPage; diff --git a/assets/js/components/Dashboard/PendingScreensPlaceRowAccordion.tsx b/assets/js/components/Dashboard/PendingScreensPlaceRowAccordion.tsx deleted file mode 100644 index e3280c87..00000000 --- a/assets/js/components/Dashboard/PendingScreensPlaceRowAccordion.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, { - ComponentType, - SyntheticEvent, - useContext, - useState, -} from "react"; -import { SCREEN_TYPES } from "Constants/constants"; -import { ScreenConfiguration } from "Models/screen_configuration"; -import PendingScreenDetail from "Components/PendingScreenDetail"; -import { - Accordion, - AccordionContext, - Button, - Col, - Container, - Row, - useAccordionButton, -} from "react-bootstrap"; -import { Place } from "Models/place"; -import { AccordionToggle } from "Components/PlaceRow"; -import { capitalizeTerminalStops } from "../../util"; -import { PencilSquare } from "react-bootstrap-icons"; -import classNames from "classnames"; -import { useNavigate } from "react-router-dom"; - -interface Props { - place: Place; - appID: string; - placeID: string; - screens: LiveOrPendingScreen[]; - buttonsDisabled: boolean; - publishCallback: ( - placeID: string, - appID: string, - hiddenFromScreenplayIDs: string[], - ) => void; -} - -interface LiveOrPendingScreen { - isLive: boolean; - screenID: string; - config: ScreenConfiguration; -} - -const formatAppID = (appID: string) => { - switch (appID) { - // A few labels are presented differently on this page, - // to avoid a weird double-":" situation. - case "gl_eink_v2": - return "Green Line E-Ink"; - - case "bus_eink_v2": - return "Bus E-Ink"; - - // All other IDs are formatted as usual. - default: - return SCREEN_TYPES.find(({ ids }) => ids.includes(appID))?.label ?? ""; - } -}; - -// When included in the /configure-screens/* route, app IDs are changed as follows: -// 1. Remove `_v2` suffix -// 2. Convert from snake_case to kebab-case -const appIDAsRoutePart = (appID: string) => - appID.replace("_v2", "").replace("_", "-"); - -const PendingScreensPlaceRowAccordion: ComponentType = ({ - place, - appID, - placeID, - screens, - buttonsDisabled, - publishCallback, -}: Props) => { - const [hiddenFromScreenplayIDs, setHiddenFromScreenplayIDs] = useState< - string[] - >([]); - const { activeEventKey } = useContext(AccordionContext); - const isOpen = activeEventKey?.includes(place.id); - const onRowClick = useAccordionButton(place.id); - const navigate = useNavigate(); - - const handleClickHideCheckbox = (screenID: string) => { - if (hiddenFromScreenplayIDs.includes(screenID)) { - setHiddenFromScreenplayIDs((prevState) => - prevState.filter((id) => id !== screenID), - ); - } else { - setHiddenFromScreenplayIDs((prevState) => [...prevState, screenID]); - } - }; - - const handleClickEdit = (e: SyntheticEvent) => { - // Prevent the button click from also causing the accordion to expand/collapse. - e.stopPropagation(); - - navigate(`/configure-screens/${appIDAsRoutePart(appID)}`, { - state: { place_id: placeID }, - }); - }; - - const handleClickPublish = (e: SyntheticEvent) => { - // Prevent the button click from also causing the accordion to expand/collapse. - e.stopPropagation(); - - publishCallback(placeID, appID, hiddenFromScreenplayIDs); - }; - - return ( -
-
- - - - - -
- - <> - {isOpen && - screens.map((screen: LiveOrPendingScreen) => ( - - handleClickHideCheckbox(screen.screenID) - } - key={screen.screenID} - /> - ))} - - -
- ); -}; - -export default PendingScreensPlaceRowAccordion; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/AppBar.tsx b/assets/js/components/Dashboard/PermanentConfiguration/AppBar.tsx deleted file mode 100644 index 35836496..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/AppBar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { ComponentType } from "react"; -import { CloseButton, Container, Navbar } from "react-bootstrap"; -import { useNavigate } from "react-router-dom"; - -interface AppBarProps { - title: string; -} - -const Appbar: ComponentType = (props: AppBarProps) => { - const { title } = props; - const navigate = useNavigate(); - - return ( - - - {title} - navigate(-1)} /> - - - ); -}; - -export default Appbar; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/BottomActionBar.tsx b/assets/js/components/Dashboard/PermanentConfiguration/BottomActionBar.tsx deleted file mode 100644 index e9d4f878..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/BottomActionBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { ComponentType } from "react"; -import { Navbar, Container, Button } from "react-bootstrap"; -import { ArrowLeft, ArrowRight } from "react-bootstrap-icons"; - -interface BottomActionBarProps { - backButtonLabel?: string | null; - forwardButtonLabel?: string; - cancelButtonLabel?: string; - onBack?: () => void; - onForward?: () => void; - onCancel?: () => void; -} - -const BottomActionBar: ComponentType = ({ - backButtonLabel, - forwardButtonLabel, - cancelButtonLabel, - onBack, - onForward, - onCancel, -}: BottomActionBarProps) => { - return ( - - - {cancelButtonLabel && ( - - )} - {backButtonLabel && ( - - )} - {forwardButtonLabel && ( - - )} - - - ); -}; - -export default BottomActionBar; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/ButtonImage.tsx b/assets/js/components/Dashboard/PermanentConfiguration/ButtonImage.tsx deleted file mode 100644 index faf6b938..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/ButtonImage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { ComponentType } from "react"; -import { Button } from "react-bootstrap"; - -interface ButtonImageProps { - fileName: string; - label: string; - onClick: () => unknown; -} - -const ButtonImage: ComponentType = ( - props: ButtonImageProps, -) => { - const { fileName, label, onClick } = props; - return ( -
- -
- ); -}; - -export default ButtonImage; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/ConfigureScreensPage.tsx b/assets/js/components/Dashboard/PermanentConfiguration/ConfigureScreensPage.tsx deleted file mode 100644 index f852d5cc..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/ConfigureScreensPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { ComponentType } from "react"; -import AppBar from "Components/PermanentConfiguration/AppBar"; -import { Outlet } from "react-router-dom"; -import { useHideSidebar } from "Hooks/useHideSidebar"; - -const ConfigureScreensPage: ComponentType = () => { - useHideSidebar(); - - return ( -
- - -
- ); -}; - -export default ConfigureScreensPage; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/PlacesSearchBar.tsx b/assets/js/components/Dashboard/PermanentConfiguration/PlacesSearchBar.tsx deleted file mode 100644 index 12c467ed..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/PlacesSearchBar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable jsx-a11y/no-autofocus */ -import React, { ComponentType, useState } from "react"; -import { ReactSearchAutocomplete } from "react-search-autocomplete"; - -interface SearchItem { - id: string; - name: string; -} - -interface PlacesSearchBarProps { - places: SearchItem[]; - handleSearchResultClick: (item: SearchItem) => void; -} - -const PlacesSearchBar: ComponentType = ({ - places, - handleSearchResultClick, -}: PlacesSearchBarProps) => { - // When selecting an item, the text in the input is not cleared properly. - // This is likely due to a bug in the package. - // To resolve this, we can force a rerender of the component so it resets to default. - const [reset, setReset] = useState(-1); - - const formatResult = (item: SearchItem) => { - return ( - - {item.name} · Station ID: {item.id} - - ); - }; - - const handleOnSelect = (place: SearchItem) => { - handleSearchResultClick(place); - setReset(1 - reset); - }; - - return ( - - ); -}; - -export { SearchItem }; -export default PlacesSearchBar; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/SelectScreenType.tsx b/assets/js/components/Dashboard/PermanentConfiguration/SelectScreenType.tsx deleted file mode 100644 index decb8f01..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/SelectScreenType.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { ComponentType } from "react"; -import { useNavigate } from "react-router-dom"; -import { Col, Container, Row } from "react-bootstrap"; -import ButtonImage from "Components/PermanentConfiguration/ButtonImage"; - -const SelectScreenTypeComponent: ComponentType = () => { - const navigate = useNavigate(); - const selectScreenType = (screenType: string) => { - switch (screenType) { - case "gl-eink": - navigate("/configure-screens/gl-eink", { replace: true }); - break; - } - }; - - return ( - - - -
Select screen type
- - - -
- - - selectScreenType("bus-eink")} - /> - - - selectScreenType("bus-shelter")} - /> - - - selectScreenType("gl-eink")} - /> - - - selectScreenType("dup")} - /> - - - selectScreenType("pre-fare")} - /> - - - selectScreenType("sectional")} - /> - - -
- ); -}; - -export default SelectScreenTypeComponent; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/WorkflowPlacesList.tsx b/assets/js/components/Dashboard/PermanentConfiguration/WorkflowPlacesList.tsx deleted file mode 100644 index d947d9f5..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/WorkflowPlacesList.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { ComponentType } from "react"; -import SortLabel from "Components/SortLabel"; -import { SORT_LABELS } from "Constants/constants"; -import { DirectionID } from "Models/direction_id"; -import { sortByStationOrder } from "../../../util"; -import { Place } from "Models/place"; -import PlaceRow from "Components/PlaceRow"; - -interface WorkflowPlacesListProps { - sortDirection: DirectionID; - setSortDirection: React.Dispatch>; - selectedPlaces: Set; - places: Place[]; - onRowClick: (place: Place, checked?: boolean) => void; -} - -const WorkflowPlacesList: ComponentType = ({ - sortDirection, - setSortDirection, - selectedPlaces, - places, - onRowClick, -}: WorkflowPlacesListProps) => { - return ( -
- - setSortDirection((prevSD) => (1 - prevSD) as DirectionID) - } - className="mx-3 mb-4" - /> -
-
- - {selectedPlaces.size} - {" "} - stations selected -
-
- {sortByStationOrder(places, "Green", sortDirection === 1).map((place) => ( - onRowClick(place, checked)} - /> - ))} -
- ); -}; - -export default WorkflowPlacesList; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage.tsx b/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage.tsx deleted file mode 100644 index 1fec5a72..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage.tsx +++ /dev/null @@ -1,577 +0,0 @@ -import React, { - ComponentType, - ForwardedRef, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import { Place } from "Models/place"; -import { - GLScreenConfiguration, - ScreenConfiguration, -} from "Models/screen_configuration"; -import { ValidationErrorsForScreen } from "Models/configValidationErrors"; -import { - Button, - ButtonGroup, - Col, - Container, - Dropdown, - Form, - Row, - Table, -} from "react-bootstrap"; -import classNames from "classnames"; -import { - ArrowLeft, - ArrowRight, - Dot, - LightningChargeFill, - Plus, - ThreeDotsVertical, - TrashFill, -} from "react-bootstrap-icons"; -import { - ExistingScreens, - ExistingScreensAtPlace, - fetchExistingScreens, -} from "Utils/api"; -import { useConfigValidationState } from "Hooks/useScreenplayContext"; - -interface PlaceIdsAndExistingScreens { - [place_id: string]: ExistingScreensAtPlace; -} - -interface PlaceIdsAndNewScreens { - [place_id: string]: { - updated_pending_screens: ScreenConfiguration[]; - new_pending_screens?: ScreenConfiguration[]; - existing_pending_screens: ScreenConfiguration[]; - }; -} - -interface ConfigureScreensWorkflowPageProps { - selectedPlaces: Place[]; - setPlacesAndScreensToUpdate: React.Dispatch< - React.SetStateAction - >; - setConfigVersion: React.Dispatch>; - isEditing: boolean; -} - -const ConfigureScreensWorkflowPage: ComponentType< - ConfigureScreensWorkflowPageProps -> = ({ - selectedPlaces, - setPlacesAndScreensToUpdate, - setConfigVersion, - isEditing, -}: ConfigureScreensWorkflowPageProps) => { - const [existingScreens, setExistingScreens] = useState({}); - - const { - newScreenValidationErrors, - pendingScreenValidationErrors, - setValidationErrors, - } = useConfigValidationState(); - const initializeExistingScreenValidationErrors = useCallback( - (placesAndScreens: PlaceIdsAndExistingScreens) => { - for (const place_id in placesAndScreens) { - const screens = placesAndScreens[place_id]; - - Object.keys(screens.pending_screens).map((_screen, index) => { - pendingScreenValidationErrors[place_id][index] = { - missingFields: [], - isDuplicateScreenId: false, - }; - }); - } - - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - }, - [ - setValidationErrors, - newScreenValidationErrors, - pendingScreenValidationErrors, - ], - ); - - // This hook will run in two different scenarios: - // 1. Runs at initial render if navigated to from the StationSelectPage. - // 2. Runs after initial render if navigated to from the Edit Pending button on the Pending page. - // The items in selectedPlaces are guaranteed to stay the same while this page is being used. - useEffect(() => { - if (selectedPlaces.length) { - fetchExistingScreens( - "gl_eink_v2", - selectedPlaces.map((place) => place.id), - ).then((data) => { - if (data) { - const { places_and_screens, version_id } = data; - initializeExistingScreenValidationErrors(places_and_screens); - setConfigVersion(version_id); - setExistingScreens(places_and_screens); - } - }); - } - }, [ - selectedPlaces.length, - initializeExistingScreenValidationErrors, - selectedPlaces, - setConfigVersion, - ]); - - const getTitle = () => - isEditing ? "Edit Pending" : "Configure Green Line Stations"; - - return ( - -
{getTitle()}
- {selectedPlaces.map((place) => { - return ( - - ); - })} -
- ); -}; - -interface ConfigurePlaceCardProps { - place: Place; - existingScreens: ExistingScreensAtPlace; - setPlacesAndScreensToUpdate: React.Dispatch< - React.SetStateAction - >; -} - -const ConfigurePlaceCard: ComponentType = ({ - place, - existingScreens, - setPlacesAndScreensToUpdate, -}: ConfigurePlaceCardProps) => { - const [existingPendingScreens, setExistingPendingScreens] = useState<{ - [screen_id: string]: ScreenConfiguration; - }>({}); - - const [updatedPendingScreens, setUpdatedPendingScreens] = useState< - ScreenConfiguration[] - >([]); - - const [newScreens, setNewScreens] = useState([]); - const existingLiveScreens: { - [screen_id: string]: ScreenConfiguration; - } = existingScreens?.live_screens ?? {}; - const { - newScreenValidationErrors, - pendingScreenValidationErrors, - setValidationErrors, - } = useConfigValidationState(); - - useEffect(() => { - if (!existingScreens) return; - - setExistingPendingScreens(existingScreens.pending_screens); - }, [existingScreens]); - - const existingScreensToArray = (existingPendingScreens: { - [screen_id: string]: ScreenConfiguration; - }) => { - return Object.entries(existingPendingScreens).map(([screen_id, screen]) => { - screen.screen_id = screen_id; - return screen; - }); - }; - - useEffect(() => { - setPlacesAndScreensToUpdate((placesAndScreens) => { - return { - ...placesAndScreens, - [place.id]: { - updated_pending_screens: updatedPendingScreens, - new_pending_screens: newScreens, - existing_pending_screens: existingScreensToArray( - existingPendingScreens, - ), - }, - }; - }); - }, [ - updatedPendingScreens, - existingPendingScreens, - newScreens, - place.id, - setPlacesAndScreensToUpdate, - ]); - - const hasRows = - Object.keys(existingLiveScreens).length > 0 || - Object.values(existingPendingScreens).filter((config) => !config.is_deleted) - .length > 0 || - newScreens.length > 0; - - const deleteExistingPendingRow = ( - screenID: string, - screen: ScreenConfiguration, - ) => { - setExistingPendingScreens((prevState) => { - return { - ...prevState, - [screenID]: { ...prevState[screenID], is_deleted: true }, - }; - }); - - setUpdatedPendingScreens((prevState) => { - const index = prevState.findIndex( - (screen) => screen.screen_id === screenID, - ); - - if (index === -1) { - const newDeletedScreen: GLScreenConfiguration = { - screen_id: screenID, - is_deleted: true, - app_id: "gl_eink_v2", - app_params: screen.app_params, - }; - - return [...prevState, newDeletedScreen]; - } else { - return [ - ...prevState.slice(0, index), - { ...prevState[index], is_deleted: true }, - ...prevState.slice(index + 1), - ]; - } - }); - }; - - const changeExistingPendingRow = ( - screenID: string, - screen: ScreenConfiguration, - index: number, - ) => { - if (screen.new_id === screenID) { - setUpdatedPendingScreens((prevState) => { - return [ - ...prevState.slice(0, index), - { ...prevState[index], new_id: undefined }, - ...prevState.slice(index + 1), - ]; - }); - } else { - setUpdatedPendingScreens((prevState) => { - return [ - ...prevState.slice(0, index), - screen, - ...prevState.slice(index + 1), - ]; - }); - } - setExistingPendingScreens((prevState) => { - return { - ...prevState, - [screenID]: screen, - }; - }); - }; - - const deleteNewRow = (index: number) => { - newScreenValidationErrors[place.id].splice(index, 1); - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - setNewScreens((prevState) => { - return [...prevState.slice(0, index), ...prevState.slice(index + 1)]; - }); - }; - - const changeNewRow = (screen: ScreenConfiguration, index: number) => { - setNewScreens((prevState) => { - return [ - ...prevState.slice(0, index), - screen, - ...prevState.slice(index + 1), - ]; - }); - }; - - const addNewRow = () => { - setNewScreens((prevState) => [ - ...prevState, - { - new_id: "EIG-", - app_params: { header: { route_id: place.routes[0] } }, - app_id: "gl_eink_v2", - }, - ]); - - newScreenValidationErrors[place.id].push({ - missingFields: [], - isDuplicateScreenId: false, - }); - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - }; - - return ( - - - {place.name.toUpperCase()} - Station ID: {place.id} - - {hasRows && ( - - - - - - - - - - - - {Object.entries(existingLiveScreens).map(([screenID, screen]) => { - return ( - undefined} - onChange={() => undefined} - validationErrors={{ - missingFields: [], - isDuplicateScreenId: false, - }} - /> - ); - })} - {Object.entries(existingPendingScreens).map( - ([screenID, screen], index) => { - return ( - - deleteExistingPendingRow(screenID, screen) - } - onChange={(screen: ScreenConfiguration) => - changeExistingPendingRow(screenID, screen, index) - } - className={screen.is_deleted ? "hidden" : ""} - validationErrors={ - pendingScreenValidationErrors[place.id][index] - } - /> - ); - }, - )} - {newScreens.map((screen, index) => { - return ( - deleteNewRow(index)} - onChange={(screen: ScreenConfiguration) => - changeNewRow(screen, index) - } - className={screen.is_deleted ? "hidden" : ""} - validationErrors={ - newScreenValidationErrors[place.id][index] - } - /> - ); - })} - -
Screen IDDirectionPlatform Location
-
- )} - - - -
- ); -}; - -interface CustomToggleProps { - children?: React.ReactNode; - onClick: React.MouseEventHandler; -} - -const CustomToggle = React.forwardRef( - ( - { children, onClick }: CustomToggleProps, - ref: ForwardedRef, - ) => ( - - ), -); - -interface ConfigureScreenRowProps { - screenID: string; - config: ScreenConfiguration; - isLive?: boolean; - onChange: (screen: ScreenConfiguration) => void; - handleDelete: () => void; - className?: string; - validationErrors: ValidationErrorsForScreen; -} -const ConfigureScreenRow: ComponentType = ({ - screenID, - config, - isLive, - onChange, - handleDelete, - className = "", - validationErrors, -}: ConfigureScreenRowProps) => { - const direction = config.app_params?.header.direction_id; - const platformLocation = config.app_params.platform_location; - const dropdownRef = useRef(null); - - const screenIdError = () => { - if (validationErrors.missingFields.includes("screen_id")) { - return
Screen ID is required
; - } else if (validationErrors.isDuplicateScreenId) { - return
Duplicate Screen ID
; - } else { - return null; - } - }; - - return ( - - - { - const newConfig = { ...config, new_id: e.target.value }; - onChange(newConfig); - }} - placeholder="EIG-" - /> - {screenIdError()} - - - - - - - {validationErrors.missingFields.includes("direction_id") && ( -
Direction is required
- )} - - - - - - - {validationErrors.missingFields.includes("platform_location") && ( -
Platform Location is required
- )} - - - {isLive ? ( -
- Live · Read-only -
- ) : ( -
- Just added - - - - - Delete - - - -
- )} - - - ); -}; - -export { PlaceIdsAndNewScreens }; - -export default ConfigureScreensWorkflowPage; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx b/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx deleted file mode 100644 index c1879823..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/GlEinkWorkflow.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import React, { - ComponentType, - useLayoutEffect, - useMemo, - useState, -} from "react"; -import ConfigureScreensWorkflowPage, { - PlaceIdsAndNewScreens, -} from "Components/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage"; -import BottomActionBar from "Components/PermanentConfiguration/BottomActionBar"; -import { useLocation, useNavigate } from "react-router-dom"; -import StationSelectPage from "Components/PermanentConfiguration/Workflows/GlEink/StationSelectPage"; -import { Alert } from "react-bootstrap"; -import { ExclamationCircleFill } from "react-bootstrap-icons"; -import { - useConfigValidationState, - useScreenplayState, -} from "Hooks/useScreenplayContext"; -import { putPendingScreens } from "Utils/api"; -import { displayErrorModal } from "Utils/errorHandler"; - -interface EditNavigationState { - place_id: string; -} - -const GlEinkWorkflow: ComponentType = () => { - const { places } = useScreenplayState(); - const location = useLocation(); - const navigate = useNavigate(); - - const [selectedPlaces, setSelectedPlaces] = useState>(new Set()); - const [configVersion, setConfigVersion] = useState(""); - const [isEditing, setIsEditing] = useState(false); - const [configStep, setConfigStep] = useState(0); - - const [placesAndScreensToUpdate, setPlacesAndScreensToUpdate] = - useState({}); - - const getPlacesList = () => { - return places.filter((place) => - place.routes.some((route) => route.startsWith("Green")), - ); - }; - - const [showValidationAlert, setShowValidationAlert] = useState(true); - const { - newScreenValidationErrors, - pendingScreenValidationErrors, - setValidationErrors, - } = useConfigValidationState(); - const [validationErrorMessage, setValidationErrorMessage] = - useState(""); - - useLayoutEffect(() => { - if (location.state) { - const { place_id } = location.state as EditNavigationState; - - setConfigStep(1); - setSelectedPlaces(new Set([place_id])); - setIsEditing(true); - newScreenValidationErrors[place_id] = []; - pendingScreenValidationErrors[place_id] = []; - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - } - }, [ - location, - setValidationErrors, - newScreenValidationErrors, - pendingScreenValidationErrors, - ]); - - const generateErrorMessage = (errorSet: Set) => { - if (errorSet.size === 0) { - return ""; - } - - const capitalizedErrors = Array.from(errorSet).map((error) => { - const capitalizedWords = error - .split("_") - .map( - (word: string) => - word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), - ); - return capitalizedWords.join(" "); - }); - - if (capitalizedErrors.length === 1) { - return "Correct the following error: " + capitalizedErrors[0] + "."; - } else { - return ( - "Correct the following errors: " + capitalizedErrors.join(", ") + "." - ); - } - }; - - const validateDuplicateScreenIds = ( - placesAndScreens: PlaceIdsAndNewScreens, - duplicateScreenIds: string[] = [], - ) => { - for (const [place_id, screens] of Object.entries(placesAndScreens)) { - screens["new_pending_screens"]?.map((screen, index) => { - if (duplicateScreenIds.includes(screen.new_id ?? "")) { - newScreenValidationErrors[place_id][index].isDuplicateScreenId = true; - } else { - newScreenValidationErrors[place_id][index].isDuplicateScreenId = - false; - } - }); - - screens["existing_pending_screens"].map((screen, index) => { - if ( - duplicateScreenIds.includes(screen.new_id ?? screen.screen_id ?? "") - ) { - pendingScreenValidationErrors[place_id][index].isDuplicateScreenId = - true; - } else { - pendingScreenValidationErrors[place_id][index].isDuplicateScreenId = - false; - } - }); - } - }; - - const validateRequiredFields = (placesAndScreens: PlaceIdsAndNewScreens) => { - const fieldsWithErrors = new Set(); - for (const [place_id, screens] of Object.entries(placesAndScreens)) { - const fieldsByScreen = screens["new_pending_screens"]?.map((screen) => { - // Get what fields are present in the config for this screen - return [ - "screen_id", - ...Object.keys(screen["app_params"]), - ...Object.keys(screen["app_params"].header), - ]; - }); - - // Check if any screens are missing required fields (screen_id, direction_id, platform_location) - const requiredFields = ["screen_id", "direction_id", "platform_location"]; - if (fieldsByScreen) { - fieldsByScreen.forEach((fields, index) => { - const missingFieldsForScreen = requiredFields.filter((field) => { - if (!fields.includes(field)) { - fieldsWithErrors.add(field); - return true; - } else { - return false; - } - }); - - newScreenValidationErrors[place_id][index].missingFields = - missingFieldsForScreen; - }); - } - } - return fieldsWithErrors; - }; - - const handleGlEinkSubmitResponse = async ( - response: Response, - fieldsWithErrors: Set, - ) => { - if (response.ok) { - navigate("/pending"); - } else if (response.status === 400) { - const data = await response.json(); - if (data.duplicate_screen_ids) { - handleDuplicateIdsResponse(data.duplicate_screen_ids, fieldsWithErrors); - } else { - handleVersionMismatch(response, 2000); - } - } else { - setValidationErrorMessage( - "Something went wrong. Please select 'Review Screens' again.", - ); - setShowValidationAlert(true); - } - }; - - const handleDuplicateIdsResponse = ( - duplicate_screen_ids: string[], - fieldsWithErrors: Set, - ) => { - validateDuplicateScreenIds(placesAndScreensToUpdate, duplicate_screen_ids); - fieldsWithErrors.add("screen_id"); - setValidationErrorMessage(generateErrorMessage(fieldsWithErrors)); - setShowValidationAlert(true); - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - }; - - const filteredPlaces = useMemo( - () => places.filter((place) => selectedPlaces.has(place.id)), - [places, selectedPlaces], - ); - - /** - * Handles version mismatch errors by displaying the error modal and automatic page refresh. - */ - const handleVersionMismatch = (error: Response | Error, delay: number) => { - displayErrorModal(error, { - customTitle: "Someone else is configuring these screens", - customMessage: - "In order not to overwrite each other's work, please refresh your browser and fill-out the form again.", - onError: () => { - // Auto-refresh after showing the error - setTimeout(() => window.location.reload(), delay); - }, - }); - }; - - let backButtonLabel; - let forwardButtonLabel; - let cancelButtonLabel; - let onBack; - let onForward; - let onCancel; - let layout; - switch (configStep) { - case 0: - cancelButtonLabel = "Cancel"; - forwardButtonLabel = "Next"; - onCancel = () => { - navigate(-1); - }; - onForward = () => { - setConfigStep(configStep + 1); - }; - layout = ( - - ); - break; - case 1: - backButtonLabel = "Back"; - forwardButtonLabel = "Review Screens"; - cancelButtonLabel = "Cancel"; - onCancel = () => { - navigate(-1); - }; - onBack = () => { - Object.keys(placesAndScreensToUpdate).forEach((placeId) => { - newScreenValidationErrors[placeId] = []; - pendingScreenValidationErrors[placeId] = []; - }); - - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - setShowValidationAlert(false); - setConfigStep(configStep - 1); - }; - onForward = () => { - const fieldsWithErrors = validateRequiredFields( - placesAndScreensToUpdate, - ); - - if (fieldsWithErrors.size === 0) { - putPendingScreens( - placesAndScreensToUpdate, - "gl_eink_v2", - configVersion, - ).then((response) => { - handleGlEinkSubmitResponse(response, fieldsWithErrors); - }); - } else { - validateDuplicateScreenIds(placesAndScreensToUpdate); - setValidationErrorMessage(generateErrorMessage(fieldsWithErrors)); - setShowValidationAlert(true); - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - } - }; - layout = ( - <> - - {validationErrorMessage !== "" && ( -
- setShowValidationAlert(false)} - dismissible - className="config-validation-alert" - > - -
- {validationErrorMessage} -
-
-
- )} - - ); - break; - } - - return ( - <> - {layout} -
- -
- - ); -}; - -export default GlEinkWorkflow; diff --git a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/StationSelectPage.tsx b/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/StationSelectPage.tsx deleted file mode 100644 index 11a460b8..00000000 --- a/assets/js/components/Dashboard/PermanentConfiguration/Workflows/GlEink/StationSelectPage.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, { ComponentType, useState } from "react"; -import { Container } from "react-bootstrap"; -import PlacesSearchBar, { - SearchItem, -} from "Components/PermanentConfiguration/PlacesSearchBar"; -import WorkflowPlacesList from "Components/PermanentConfiguration/WorkflowPlacesList"; -import { DirectionID } from "Models/direction_id"; -import { Place } from "Models/place"; -import BottomActionBar from "Components/PermanentConfiguration/BottomActionBar"; -import { useNavigate } from "react-router-dom"; -import { useConfigValidationState } from "Hooks/useScreenplayContext"; -import { PlaceIdsAndNewScreens } from "./ConfigureScreensPage"; -interface StationSelectPageProps { - places: Place[]; - selectedPlaces: Set; - setSelectedPlaces: React.Dispatch>>; - setPlacesAndScreensToUpdate: React.Dispatch< - React.SetStateAction - >; -} - -const StationSelectPage: ComponentType = ({ - places, - selectedPlaces, - setSelectedPlaces, - setPlacesAndScreensToUpdate, -}: StationSelectPageProps) => { - const [sortDirection, setSortDirection] = useState(0); - const handleSearchResultClick = (item: SearchItem) => { - const placeToAdd = places.find((place) => place.id === item.id); - if (placeToAdd) { - setSelectedPlaces((prev) => new Set([placeToAdd.id, ...prev])); - newScreenValidationErrors[placeToAdd.id] = []; - pendingScreenValidationErrors[placeToAdd.id] = []; - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - } - }; - - const navigate = useNavigate(); - const [configStep, setConfigStep] = useState(0); - - const { - newScreenValidationErrors, - pendingScreenValidationErrors, - setValidationErrors, - } = useConfigValidationState(); - - let backButtonLabel; - let forwardButtonLabel; - let cancelButtonLabel; - let onBack; - let onForward; - let onCancel; - let layout; - switch (configStep) { - case 0: - cancelButtonLabel = "Cancel"; - forwardButtonLabel = "Next"; - onCancel = () => { - navigate(-1); - }; - onForward = () => { - setConfigStep(configStep + 1); - }; - layout = ( - -
-
Select Green Line Stations
-
- Green Line E-Ink screens can only be added at stations on Green - Line branches -
-
-
-
- Enter Station ID or name to select stations -
- -
- { - // Make a new Set so React knows state was changed. - const newSet = new Set(selectedPlaces); - - if (checked) { - newSet.add(place.id); - newScreenValidationErrors[place.id] = []; - pendingScreenValidationErrors[place.id] = []; - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - } else { - newSet.delete(place.id); - delete newScreenValidationErrors[place.id]; - delete pendingScreenValidationErrors[place.id]; - setValidationErrors( - newScreenValidationErrors, - pendingScreenValidationErrors, - ); - } - setPlacesAndScreensToUpdate((placesAndScreens) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [place.id]: _discarded, ...newPlacesAndScreens } = - placesAndScreens; - return newPlacesAndScreens; - }); - setSelectedPlaces(newSet); - }} - /> -
- ); - break; - case 1: - backButtonLabel = "Back"; - forwardButtonLabel = "Review Screens"; - cancelButtonLabel = "Cancel"; - onCancel = () => { - navigate(-1); - }; - onBack = () => { - setConfigStep(configStep - 1); - }; - onForward = () => { - navigate("/pending"); - }; - layout =
Configure Screens Page
; - break; - } - - return ( -
- {layout} -
- -
-
- ); -}; - -export default StationSelectPage; diff --git a/assets/js/components/Dashboard/PlaceRow.tsx b/assets/js/components/Dashboard/PlaceRow.tsx index 8ecbf304..19c0cd29 100644 --- a/assets/js/components/Dashboard/PlaceRow.tsx +++ b/assets/js/components/Dashboard/PlaceRow.tsx @@ -243,5 +243,4 @@ const PlaceRow = ({ ); }; -export { AccordionToggle }; export default PlaceRow; diff --git a/assets/js/components/Dashboard/ScreenSimulation.tsx b/assets/js/components/Dashboard/ScreenSimulation.tsx index 2c003fca..188d7e5c 100644 --- a/assets/js/components/Dashboard/ScreenSimulation.tsx +++ b/assets/js/components/Dashboard/ScreenSimulation.tsx @@ -3,16 +3,14 @@ import { Screen } from "Models/screen"; interface Props { screen: Screen; - isPending?: boolean; } const ScreenSimulation = ({ screen, - isPending = false, }: Props): JSX.Element => { const src = useMemo( - () => generateSource(screen, isPending), - [screen, isPending], + () => generateSource(screen), + [screen], ); return ( @@ -28,15 +26,13 @@ const ScreenSimulation = ({ ); }; -const generateSource = ({ id }: Screen, isPending: boolean) => { +const generateSource = ({ id }: Screen) => { const screensUrl = document .querySelector("meta[name=screens-url]") ?.getAttribute("content"); const queryParams = "requestor=screenplay"; - return `${screensUrl}/v2/screen${ - isPending ? "/pending/" : "/" - }${id}/simulation?${queryParams}`; + return `${screensUrl}/v2/screen/${id}/simulation?${queryParams}`; }; export default ScreenSimulation; diff --git a/assets/js/components/Dashboard/Sidebar.tsx b/assets/js/components/Dashboard/Sidebar.tsx index bcce0765..91273c7f 100644 --- a/assets/js/components/Dashboard/Sidebar.tsx +++ b/assets/js/components/Dashboard/Sidebar.tsx @@ -9,8 +9,6 @@ import { VolumeUpFill, Lightning, LightningFill, - Signpost, - SignpostFill, ArrowDownShort, Icon, } from "react-bootstrap-icons"; @@ -19,8 +17,7 @@ import TLogoBlack from "Images/t-logo-black.svg"; import cx from "classnames"; import * as sidebarStyles from "Styles/sidebar.module.scss"; import * as predictionSuppressionStyles from "Styles/prediction-suppression.module.scss"; - -import { isEmergencyAdmin, isScreensAdmin } from "Utils/auth"; +import { isEmergencyAdmin } from "Utils/auth"; import { usePredictionSuppressionState } from "Hooks/useScreenplayContext"; const SidebarLink = ({ @@ -122,11 +119,6 @@ const Sidebar = () => { Emergency Takeover )} - {isScreensAdmin() && ( - - Configure - - )} ); }; diff --git a/assets/js/hooks/useScreenplayContext.tsx b/assets/js/hooks/useScreenplayContext.tsx index f04020b6..f0ffecc9 100644 --- a/assets/js/hooks/useScreenplayContext.tsx +++ b/assets/js/hooks/useScreenplayContext.tsx @@ -6,7 +6,6 @@ import { DirectionID } from "../models/direction_id"; import { ScreensByAlert } from "../models/screensByAlert"; import { LineStop } from "../models/line_stop"; import { SuppressedPrediction } from "../models/suppressed_prediction"; -import { ConfigValidationErrors } from "../models/configValidationErrors"; import { PLACES_PAGE_MODES_AND_LINES, ALERTS_PAGE_MODES_AND_LINES, @@ -138,40 +137,6 @@ const AlertsListStateContainer = ({ children }: Props) => { ); }; -interface ConfigValidationState { - newScreenValidationErrors: ConfigValidationErrors; - pendingScreenValidationErrors: ConfigValidationErrors; - setValidationErrors: ( - arg0: ConfigValidationErrors, - arg1: ConfigValidationErrors, - ) => void; -} - -const [useConfigValidationState, ConfigValidationStateProvider] = - createGenericContext(); - -const ConfigValidationStateContainer = ({ children }: Props) => { - const [newScreenValidationErrors, setNewScreenValidationErrors] = - useState({}); - const [pendingScreenValidationErrors, setPendingScreenValidationErrors] = - useState({}); - - return ( - { - setNewScreenValidationErrors(newErrors); - setPendingScreenValidationErrors(pendingErrors); - }, - }} - > - {children} - - ); -}; - interface PlacesListState { sortDirection: DirectionID; modeLineFilterValue: FilterValue; @@ -280,11 +245,9 @@ const ScreenplayProvider = ({ children }: Props) => { - - - {children} - - + + {children} + @@ -292,13 +255,12 @@ const ScreenplayProvider = ({ children }: Props) => { }; // Types & Interfaces -export { FilterValue, DirectionID }; +export { DirectionID }; // Values export { useScreenplayState, usePlacesListState, - useConfigValidationState, useAlertsListState, usePredictionSuppressionState, ScreenplayProvider, diff --git a/assets/js/models/configValidationErrors.ts b/assets/js/models/configValidationErrors.ts deleted file mode 100644 index e8658004..00000000 --- a/assets/js/models/configValidationErrors.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ValidationErrorsForScreen { - isDuplicateScreenId: boolean; - missingFields: string[]; -} - -export interface ConfigValidationErrors { - [place_id: string]: ValidationErrorsForScreen[]; -} diff --git a/assets/js/models/screen_configuration.ts b/assets/js/models/screen_configuration.ts deleted file mode 100644 index bd812cbf..00000000 --- a/assets/js/models/screen_configuration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DirectionID } from "./direction_id"; - -interface Destination { - route_id: string; - direction_id?: DirectionID; -} - -interface GlEinkAppParams { - header: Destination; - platform_location?: "front" | "back" | null; -} - -export interface GLScreenConfiguration { - app_id: "gl_eink_v2"; - screen_id?: string; - new_id?: string; - app_params: GlEinkAppParams; - is_live?: boolean; - is_deleted?: boolean; -} - -export type ScreenConfiguration = GLScreenConfiguration; -// | More configuration shapes to be added here as we support them diff --git a/assets/js/util.ts b/assets/js/util.ts index ffd187ef..6a75dcdd 100644 --- a/assets/js/util.ts +++ b/assets/js/util.ts @@ -78,38 +78,6 @@ export const translateRouteID = (id: string) => { } }; -export const getModeFromAffectedList = (affectedList: string[]) => { - if ( - [ - "red", - "orange", - "blue", - "green", - "green-b", - "green-c", - "green-d", - "green-e", - ].some((line) => affectedList.includes(line)) - ) { - return "subway"; - } else { - return affectedList[0]; - } -}; - -export const convertArrayToListString = (array: string[]) => { - if (array.length === 1) { - return array[0]; - } else if (array.length === 2) { - return `${array[0]} and ${array[1]}`; - } else { - return ( - array.slice(0, array.length - 1).join(", ") + - `and ${array[array.length - 1]}` - ); - } -}; - export const matchStation = ( stationId: string, stationScreenOrientationList: StationsByLine, @@ -260,14 +228,6 @@ export const capitalizeTerminalStops = ( return isTerminalStop ? stationName.toUpperCase() : stationName; }; -export const capitalize = (str: string) => { - if (str.length <= 1) { - return str.toUpperCase(); - } else { - return str.charAt(0).toUpperCase() + str.slice(1); - } -}; - export const getAlertEarliestStartLatestEnd = ( activePeriods: ActivePeriod[], ) => { @@ -289,7 +249,7 @@ export const getAlertEarliestStartLatestEnd = ( return [start, end]; }; -export const allRouteIdsAtPlaces = (places: Place[]) => { +const allRouteIdsAtPlaces = (places: Place[]) => { return fp.uniq( places.flatMap((place) => place.screens.flatMap(getRouteIdsForSign)), ); diff --git a/assets/js/utils/api.ts b/assets/js/utils/api.ts index ec208ba4..46dcc5e1 100644 --- a/assets/js/utils/api.ts +++ b/assets/js/utils/api.ts @@ -1,9 +1,7 @@ import fp from "lodash/fp"; import { Alert } from "../models/alert"; import { Place } from "../models/place"; -import { ScreenConfiguration } from "../models/screen_configuration"; import { ScreensByAlert } from "../models/screensByAlert"; -import { PlaceIdsAndNewScreens } from "../components/Dashboard/PermanentConfiguration/Workflows/GlEink/ConfigureScreensPage"; import getCsrfToken from "../csrf"; import type { PaMessageChange } from "Models/pa_message"; import { SuppressedPrediction } from "Models/suppressed_prediction"; @@ -50,7 +48,7 @@ interface AlertsResponse { screens_by_alert: ScreensByAlert; } -export const _fetchAlerts = async (): Promise => { +const _fetchAlerts = async (): Promise => { const response = await fetch("/api/alerts"); if (response.status === 200) { return response.json(); @@ -59,7 +57,7 @@ export const _fetchAlerts = async (): Promise => { } }; -export const _fetchActiveAndFutureAlerts = +const _fetchActiveAndFutureAlerts = async (): Promise => { const response = await fetch("/api/alerts/non_access_alerts"); if (!response.ok) { @@ -79,118 +77,9 @@ export const fetchAlerts = withErrorHandling(_fetchAlerts, { customMessage: `Failed to load alerts. ${REFRESH_PAGE_ERROR_MESSAGE}`, }); -/////////// -// Screens -/////////// -export interface ExistingScreens { - [place_id: string]: ExistingScreensAtPlace; -} - -export interface ExistingScreensAtPlace { - live_screens?: { [screen_id: string]: ScreenConfiguration }; - pending_screens: { [screen_id: string]: ScreenConfiguration }; -} - -const _fetchExistingScreens = async ( - appId: string, - placeIds: string[], -): Promise<{ places_and_screens: ExistingScreens; version_id: string }> => { - const response = await fetch( - `/config/existing-screens/${appId}?place_ids=${placeIds.join(",")}`, - ); - - if (!response.ok) { - throw response; - } - - return response.json(); -}; - -export const fetchExistingScreens = withErrorHandling(_fetchExistingScreens, { - customMessage: `Failed to load existing screens. ${REFRESH_PAGE_ERROR_MESSAGE}`, -}); -export interface PendingAndLiveScreensResponse { - places_and_screens: PendingAndLiveScreens; - etag: string; - version_id: string; - last_modified_ms: number | null; -} - -// Very similar to the `ExistingScreens` interface, except: -// 1. key is a string that combines place and app ID, and -// 2. place and app IDs are added to each `ExistingScreensAtPlace` object, so that we don't have to parse them from the combined string. -export interface PendingAndLiveScreens { - [placeAndAppID: string]: ExistingScreensAtPlace & { - place_id: string; - app_id: string; - }; -} - -const _fetchExistingScreensAtPlacesWithPendingScreens = - async (): Promise => { - const response = await fetch( - "/config/existing-screens-at-places-with-pending-screens", - ); - if (!response.ok) { - throw response; - } - const etag = response.headers.get("etag") as string; - const data = (await response.json()) as Omit< - PendingAndLiveScreensResponse, - "etag" - >; - return { ...data, etag }; - }; - -export const fetchExistingScreensAtPlacesWithPendingScreens = withErrorHandling( - _fetchExistingScreensAtPlacesWithPendingScreens, - { - customMessage: `Failed to load pending screens data. ${REFRESH_PAGE_ERROR_MESSAGE}`, - }, -); - -export const putPendingScreens = async ( - placesAndScreens: PlaceIdsAndNewScreens, - screenType: "gl_eink_v2" | null, - version_id: string, -): Promise => { - return fetch("/config/put", { - ...getPostBodyAndHeaders({ - places_and_screens: placesAndScreens, - screen_type: screenType, - version_id: version_id, - }), - credentials: "include", - }); -}; - -interface PublishScreensForPlaceResponse { - message: string; - new_config?: Place[]; -} - -export const publishScreensForPlace = async ( - placeId: string, - appId: string, - hiddenFromScreenplayIds: string[], - etag: string, -): Promise<{ status: number; message: string; newConfig: Place[] }> => { - const bodyData = { - hidden_from_screenplay_ids: hiddenFromScreenplayIds, - }; - const response = await fetch(`/config/publish/${placeId}/${appId}`, { - ...getPostBodyAndHeaders(bodyData, { "if-match": etag }), - credentials: "include", - }); - - const json: PublishScreensForPlaceResponse = await response.json(); - - return { - status: response.status, - message: json.message, - newConfig: json.new_config ?? [], - }; -}; +/////////////// +// PA Messages +/////////////// export const createNewPaMessage = async ( message: PaMessageChange, @@ -237,6 +126,10 @@ export const updateExistingPaMessage = async ( }; }; +///////////////////////// +// Suppressed Predictions +///////////////////////// + const fetchOk = async ( url: string, options: Omit & { body: any }, diff --git a/assets/js/utils/auth.ts b/assets/js/utils/auth.ts index 026d7e24..b5106774 100644 --- a/assets/js/utils/auth.ts +++ b/assets/js/utils/auth.ts @@ -3,5 +3,4 @@ const hasRoleMeta = (name: string): boolean => export const isEmergencyAdmin = (): boolean => hasRoleMeta("emergency-admin"); export const isPaMessageAdmin = (): boolean => hasRoleMeta("pa-message-admin"); -export const isScreensAdmin = (): boolean => hasRoleMeta("screens-admin"); export const isSuppressionAdmin = () => hasRoleMeta("suppression-admin"); diff --git a/assets/js/utils/emergencyMessages.ts b/assets/js/utils/emergencyMessages.ts index 307f20e1..675fc072 100644 --- a/assets/js/utils/emergencyMessages.ts +++ b/assets/js/utils/emergencyMessages.ts @@ -21,7 +21,7 @@ export interface CannedMessage { }; } -export interface CustomMessage { +interface CustomMessage { type: "custom"; text: { indoor: string; diff --git a/assets/js/utils/errorHandler.tsx b/assets/js/utils/errorHandler.tsx index 4b8e7797..2c81be58 100644 --- a/assets/js/utils/errorHandler.tsx +++ b/assets/js/utils/errorHandler.tsx @@ -85,7 +85,7 @@ export const withErrorHandling = ( /** * Surfaces the error to the user through the error modal. */ -export const displayErrorModal = ( +const displayErrorModal = ( error: Error | Response, options: ErrorHandlingOptions = {}, ) => { diff --git a/assets/package-lock.json b/assets/package-lock.json index e5e01298..276121b7 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -47,6 +47,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "jest": "^29.7.0", "jest-fixed-jsdom": "^0.0.11", + "knip": "^6.17.1", "prettier": "3.2.5", "ts-jest": "^29.1.5", "typescript": "^5.9.3", @@ -533,6 +534,40 @@ "license": "(Apache-2.0 AND BSD-3-Clause)", "peer": true }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", @@ -1905,103 +1940,768 @@ "version": "4.3.0", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz", + "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz", + "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz", + "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz", + "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz", + "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz", + "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz", + "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz", + "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz", + "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz", + "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz", + "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz", + "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz", + "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz", + "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz", + "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz", + "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz", + "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz", + "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz", + "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz", + "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "has-flag": "^4.0.0" + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "license": "MIT" + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", + "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@parcel/watcher": { "version": "2.5.1", @@ -2788,6 +3488,17 @@ "node": ">= 10" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -5402,6 +6113,16 @@ "bser": "2.1.1" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5489,6 +6210,22 @@ "node": ">= 6" } }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -5651,6 +6388,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -8384,6 +9134,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -8518,6 +9278,89 @@ "node": ">=6" } }, + "node_modules/knip": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.25.0.tgz", + "integrity": "sha512-Q3n41VjOOB/aqsbxb8kallAcFKrUz3b2S5fD5pTODljVpP01t+rvAgy2x3j0Cq8yEpRRHNdar1vHuqFfGuIakQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.3.0", + "get-tsconfig": "4.14.0", + "jiti": "^2.7.0", + "oxc-parser": "^0.137.0", + "oxc-resolver": "11.21.3", + "picomatch": "^4.0.4", + "smol-toml": "^1.6.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.1", + "yaml": "^2.9.0", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/knip/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/knip/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "dev": true, @@ -8939,6 +9782,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxc-parser": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz", + "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.137.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.137.0", + "@oxc-parser/binding-android-arm64": "0.137.0", + "@oxc-parser/binding-darwin-arm64": "0.137.0", + "@oxc-parser/binding-darwin-x64": "0.137.0", + "@oxc-parser/binding-freebsd-x64": "0.137.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.137.0", + "@oxc-parser/binding-linux-arm64-musl": "0.137.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.137.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-gnu": "0.137.0", + "@oxc-parser/binding-linux-x64-musl": "0.137.0", + "@oxc-parser/binding-openharmony-arm64": "0.137.0", + "@oxc-parser/binding-wasm32-wasi": "0.137.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.137.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.137.0", + "@oxc-parser/binding-win32-x64-msvc": "0.137.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", + "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.3", + "@oxc-resolver/binding-android-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-x64": "11.21.3", + "@oxc-resolver/binding-freebsd-x64": "11.21.3", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-musl": "11.21.3", + "@oxc-resolver/binding-openharmony-arm64": "11.21.3", + "@oxc-resolver/binding-wasm32-wasi": "11.21.3", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + } + }, "node_modules/p-limit": { "version": "3.1.0", "dev": true, @@ -9498,6 +10410,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "dev": true, @@ -10196,6 +11118,19 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -10579,14 +11514,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -10614,9 +11549,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -10922,6 +11857,16 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/unbash": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.2.tgz", + "integrity": "sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -11067,6 +12012,16 @@ "node": ">=14" } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/walker": { "version": "1.0.8", "dev": true, @@ -11346,6 +12301,22 @@ "version": "3.1.1", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "dev": true, diff --git a/assets/package.json b/assets/package.json index df2e6daa..76b3710f 100644 --- a/assets/package.json +++ b/assets/package.json @@ -10,7 +10,8 @@ "format:check": "prettier --check \"{.,**}/*.{js,mjs,json,ts,tsx,css,scss}\"", "typecheck": "tsc --noEmit", "test": "jest", - "check": "npm run typecheck && npm run lint:check && npm run format:check" + "unused": "knip --tags=-knipignore", + "check": "npm run typecheck && npm run lint:check && npm run format:check && npm run unused" }, "dependencies": { "@fullstory/browser": "^2.0.8", @@ -54,6 +55,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "jest": "^29.7.0", "jest-fixed-jsdom": "^0.0.11", + "knip": "^6.17.1", "prettier": "3.2.5", "ts-jest": "^29.1.5", "typescript": "^5.9.3", diff --git a/config/config.exs b/config/config.exs index 3794e21d..13a8e3ad 100644 --- a/config/config.exs +++ b/config/config.exs @@ -29,7 +29,6 @@ config :screenplay, ScreenplayWeb.Endpoint, config :screenplay, config_fetcher: Screenplay.Places.S3Fetch, screens_config_fetcher: Screenplay.ScreensConfig.Fetch.S3, - pending_screens_config_fetcher: Screenplay.PendingScreensConfig.Fetch.S3, http_client: HTTPoison, config_s3_bucket: "mbta-ctd-config", record_sentry: false, diff --git a/config/dev.exs b/config/dev.exs index 01d063c6..54e63a67 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -70,9 +70,7 @@ config :screenplay, sftp_client_module: Screenplay.Outfront.FakeSFTPClient, config_fetcher: Screenplay.Places.LocalFetch, screens_config_fetcher: Screenplay.ScreensConfig.Fetch.Local, - pending_screens_config_fetcher: Screenplay.PendingScreensConfig.Fetch.Local, local_screens_config_file_spec: "../screens/priv/local.json", - local_pending_screens_config_file_spec: "../screens/priv/local_pending.json", local_place_descriptions_file_spec: {:priv, "place_descriptions.json"}, local_paess_labels_file_spec: {:priv, "paess_labels.json"}, api_v3_key: System.get_env("API_V3_KEY"), @@ -88,7 +86,6 @@ config :ueberauth, Ueberauth, client_id: "dev-client", roles: [ "screenplay-emergency-admin", - "screens-admin", "pa-message-admin", "suppression-admin" ] diff --git a/config/test.exs b/config/test.exs index 8197ed33..72ea6162 100644 --- a/config/test.exs +++ b/config/test.exs @@ -15,9 +15,7 @@ config :screenplay, local_place_descriptions_file_spec: {:test, "place_descriptions.json"}, local_paess_labels_file_spec: {:test, "paess_labels.json"}, screens_config_fetcher: Screenplay.ScreensConfig.Fetch.Local, - pending_screens_config_fetcher: Screenplay.PendingScreensConfig.Fetch.Local, local_screens_config_file_spec: {:test, "screens_config.json"}, - local_pending_screens_config_file_spec: {:test, "pending_config.json"}, api_v3_url: "https://fake-mbta-api", api_key: "test_api_key", sftp_client_module: Screenplay.Outfront.FakeSFTPClient, @@ -36,7 +34,6 @@ config :ueberauth, Ueberauth, auto_redirect: true, roles: [ "screenplay-emergency-admin", - "screens-admin", "pa-message-admin", "suppression-admin" ] diff --git a/lib/screenplay/pending_screens_config/fetch.ex b/lib/screenplay/pending_screens_config/fetch.ex deleted file mode 100644 index fcd47d92..00000000 --- a/lib/screenplay/pending_screens_config/fetch.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Screenplay.PendingScreensConfig.Fetch do - @moduledoc """ - Defines a behaviour for, and delegates to, a module that provides access to - the pending screens config file. - """ - alias ScreensConfig.PendingConfig - - defmodule Metadata do - @moduledoc false - - @type t :: %__MODULE__{ - etag: String.t(), - version_id: String.t(), - last_modified: DateTime.t() - } - - @enforce_keys [:etag, :version_id, :last_modified] - defstruct @enforce_keys - end - - @type fetch_result :: - {:ok, json :: String.t(), metadata :: Metadata.t()} - | :error - - @callback fetch_config() :: fetch_result - @callback put_config(PendingConfig.t()) :: :ok | :error - @callback commit() :: :ok - @callback revert(String.t()) :: :ok - - # The module adopting this behaviour that we use for the current environment. - @config_fetcher Application.compile_env(:screenplay, :pending_screens_config_fetcher) - - # These delegates let other modules call functions from the appropriate Fetch module - # without having to know which it is. - defdelegate fetch_config(), to: @config_fetcher - defdelegate put_config(config), to: @config_fetcher - defdelegate commit(), to: @config_fetcher - defdelegate revert(version), to: @config_fetcher -end diff --git a/lib/screenplay/pending_screens_config/fetch/local.ex b/lib/screenplay/pending_screens_config/fetch/local.ex deleted file mode 100644 index bdc94583..00000000 --- a/lib/screenplay/pending_screens_config/fetch/local.ex +++ /dev/null @@ -1,79 +0,0 @@ -defmodule Screenplay.PendingScreensConfig.Fetch.Local do - @moduledoc """ - Functions to work with a local copy of the pending screens config. - """ - - alias Screenplay.PendingScreensConfig.Fetch - alias ScreensConfig.PendingConfig - - @behaviour Fetch - - @impl true - def fetch_config do - path = local_config_path() - - with {:ok, last_modified} <- get_last_modified(path), - {:ok, contents} <- do_fetch(path) do - last_modified_as_string = DateTime.to_iso8601(last_modified) - - metadata = %Fetch.Metadata{ - etag: last_modified_as_string, - version_id: last_modified_as_string, - last_modified: last_modified - } - - {:ok, contents, metadata} - end - end - - @impl true - # sobelow_skip ["Traversal.FileModule"] - def put_config(config) do - json = config |> PendingConfig.to_json() |> Jason.encode!(pretty: true) - - File.copy!(local_config_path(), local_config_path() <> ".temp") - - case File.write(local_config_path(), json) do - :ok -> :ok - {:error, _} -> :error - end - end - - @impl true - # sobelow_skip ["Traversal.FileModule"] - def commit do - File.rm!(local_config_path() <> ".temp") - end - - @impl true - # sobelow_skip ["Traversal.FileModule"] - def revert(_) do - File.copy!(local_config_path() <> ".temp", local_config_path()) - File.rm!(local_config_path() <> ".temp") - end - - defp local_config_path do - case Application.get_env(:screenplay, :local_pending_screens_config_file_spec) do - {:test, file_name} -> Path.join(~w[#{File.cwd!()} test fixtures #{file_name}]) - path -> path - end - end - - # sobelow_skip ["Traversal.FileModule"] - defp do_fetch(path) do - case File.read(path) do - {:ok, contents} -> {:ok, contents} - _ -> :error - end - end - - defp get_last_modified(path) do - case File.stat(path, time: :posix) do - {:ok, %File.Stat{mtime: mtime}} -> - {:ok, DateTime.from_unix!(mtime)} - - {:error, _} -> - :error - end - end -end diff --git a/lib/screenplay/pending_screens_config/fetch/s3.ex b/lib/screenplay/pending_screens_config/fetch/s3.ex deleted file mode 100644 index 688e64e2..00000000 --- a/lib/screenplay/pending_screens_config/fetch/s3.ex +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Screenplay.PendingScreensConfig.Fetch.S3 do - @moduledoc """ - Functions to work with an S3-hosted copy of the pending screens config. - """ - - require Logger - - alias Screenplay.PendingScreensConfig.Fetch - alias ScreensConfig.PendingConfig - - @behaviour Fetch - - @impl true - def fetch_config do - bucket = Application.get_env(:screenplay, :config_s3_bucket) - path = config_path_for_environment() - - get_operation = ExAws.S3.get_object(bucket, path) - - case ExAws.request(get_operation) do - {:ok, %{body: body, headers: headers, status_code: 200}} -> - metadata = get_metadata(headers) - {:ok, body, metadata} - - {:error, err} -> - _ = Logger.info("s3_pending_screens_config_fetch_error #{inspect(err)}") - :error - end - end - - @impl true - def commit, do: :ok - - @impl true - def revert(version) do - bucket = Application.get_env(:screenplay, :config_s3_bucket) - path = config_path_for_environment() - - get_operation = ExAws.S3.get_object(bucket, path, version_id: version) - %{body: body, status_code: 200} = ExAws.request!(get_operation) - - put_operation = ExAws.S3.put_object(bucket, path, body) - ExAws.request!(put_operation) - end - - @impl true - def put_config(config) do - json = config |> PendingConfig.to_json() |> Jason.encode!(pretty: true) - bucket = Application.get_env(:screenplay, :config_s3_bucket) - path = config_path_for_environment() - put_operation = ExAws.S3.put_object(bucket, path, json) - - case ExAws.request(put_operation) do - {:ok, %{status_code: 200}} -> :ok - _ -> :error - end - end - - defp get_metadata(headers) do - headers = Map.new(headers) - - etag = Map.get(headers, "ETag") - version_id = Map.get(headers, "x-amz-version-id") - - last_modified = - headers - |> Map.get("Last-Modified") - |> Timex.parse("{RFC1123}") - |> case do - {:ok, dt} -> dt - {:error, _} -> nil - end - - %Fetch.Metadata{etag: etag, version_id: version_id, last_modified: last_modified} - end - - defp config_path_for_environment do - "screens/pending-screens-#{Application.get_env(:screenplay, :environment_name)}.json" - end -end diff --git a/lib/screenplay/permanent_config.ex b/lib/screenplay/permanent_config.ex deleted file mode 100644 index d119d94e..00000000 --- a/lib/screenplay/permanent_config.ex +++ /dev/null @@ -1,548 +0,0 @@ -defmodule Screenplay.PermanentConfig do - @moduledoc false - - # Suppress dialyzer warning until more app_ids are implemented. - @dialyzer [{:nowarn_function, get_route_id: 3}, {:nowarn_function, json_to_struct: 4}] - - require Logger - - alias Screenplay.EmergencyTakeoverTool.ConfigUpdater, as: EmergencyTakeoverConfig - alias Screenplay.PendingScreensConfig.Fetch, as: PendingScreensFetch - alias Screenplay.Places - alias Screenplay.Places.Fetch - alias Screenplay.Places.Place.ShowtimeScreen - alias Screenplay.RoutePatterns.RoutePattern - alias Screenplay.ScreensConfig, as: ScreensConfigStore - alias Screenplay.ScreensConfig.Fetch, as: PublishedScreensFetch - alias ScreensConfig.{Alerts, Config, Departures, Footer, LineMap, PendingConfig, Screen} - alias ScreensConfig.Departures.{Query, Section} - alias ScreensConfig.Header.Destination - alias ScreensConfig.Screen.GlEink - - @type screen_type :: :gl_eink_v2 - - @spec get_existing_screens_at_places_with_pending_screens() :: %{ - places_and_screens: %{ - (place_id_app_id_pair :: String.t()) => %{ - place_id: String.t(), - app_id: atom(), - live_screens: %{Config.screen_id() => Screen.t()}, - pending_screens: %{Config.screen_id() => Screen.t()} - } - }, - etag: String.t(), - version_id: String.t(), - last_modified_ms: integer - } - def get_existing_screens_at_places_with_pending_screens do - {pending_screens_config, metadata} = - case PendingScreensFetch.fetch_config() do - {:ok, config, metadata} -> - %PendingConfig{screens: pending_screens} = - config - |> Jason.decode!() - |> PendingConfig.from_json() - - {pending_screens, metadata} - - _ -> - raise( - "Could not fetch pending screens config in existing_screens_at_places_with_pending_screens/2" - ) - end - - existing = - pending_screens_config - |> Enum.group_by(fn {_, screen} -> {screen_to_place_id(screen), screen.app_id} end) - |> Map.new(fn {{place_id, app_id}, pending_screens_at_place} -> - live_screens_of_same_type_at_place = - ScreensConfigStore.screens() - |> Enum.filter(fn {_id, screen} -> - screen.app_id == app_id and screen_to_place_id(screen) == place_id - end) - |> Enum.map(fn {id, screen} -> {id, Screen.to_json(screen)} end) - |> Map.new() - - pending_screens_at_place = - for {id, screen} <- pending_screens_at_place, - into: %{}, - do: {id, Screen.to_json(screen)} - - live_and_pending = %{ - live_screens: live_screens_of_same_type_at_place, - pending_screens: pending_screens_at_place, - place_id: place_id, - app_id: app_id - } - - json_key = "#{place_id}/#{app_id}" - - {json_key, live_and_pending} - end) - - %{ - places_and_screens: existing, - etag: metadata.etag, - version_id: metadata.version_id, - last_modified_ms: DateTime.to_unix(metadata.last_modified, :millisecond) - } - end - - @spec put_pending_screens(map(), screen_type(), Fetch.version_id()) :: - {:error, - :version_mismatch - | :config_not_fetched - | :config_not_written - | {:duplicate_screen_ids, list()}} - | :ok - def put_pending_screens(places_and_screens, screen_type, version_id) do - with {:ok, config_string} <- get_current_pending_config(version_id), - {:ok, deserialized} <- Jason.decode(config_string), - {published_config, _published_version_id} <- get_current_published_config(), - {:ok, published_config_deserialized} <- Jason.decode(published_config), - {:ok, existing_screens} <- - check_for_duplicate_screen_ids( - deserialized, - places_and_screens, - published_config_deserialized - ) do - new_pending_screens_config = - Enum.reduce( - places_and_screens, - existing_screens, - get_config_reducer(screen_type) - ) - - case PendingScreensFetch.put_config(%PendingConfig{screens: new_pending_screens_config}) do - :ok -> :ok - :error -> {:error, :config_not_written} - end - else - error -> - error - end - end - - def publish_pending_screens(place_id, app_id, hidden_from_screenplay_ids) do - {pending_config, pending_version_id} = get_current_pending_config() - - %PendingConfig{screens: pending_screens} = - pending_config |> Jason.decode!() |> PendingConfig.from_json() - - {published_config, published_version_id} = get_current_published_config() - places_config = Places.get() - - {screens_to_publish, new_pending_screens} = - pending_screens - |> Enum.filter(fn {_screen_id, screen} -> screen.app_id == app_id end) - |> Enum.map(fn - {screen_id, %Screen{} = screen} -> - if screen_id in hidden_from_screenplay_ids do - {screen_id, %{screen | hidden_from_screenplay: true}} - else - {screen_id, screen} - end - end) - |> Enum.split_with(&place_has_screen(&1, place_id)) - - new_pending_screens_config = - %PendingConfig{screens: Enum.into(new_pending_screens, %{})} - - new_published_screens_config = get_new_published_screens(published_config, screens_to_publish) - - screenplay_screens_to_add = - Enum.reject(screens_to_publish, fn {_, screen} -> screen.hidden_from_screenplay end) - - new_places_config = - get_new_places_config(places_config, screenplay_screens_to_add) - - with :ok <- PendingScreensFetch.put_config(new_pending_screens_config), - :ok <- PublishedScreensFetch.put_config(new_published_screens_config) do - PendingScreensFetch.commit() - PublishedScreensFetch.commit() - Places.update(new_places_config) - else - _ -> - PendingScreensFetch.revert(pending_version_id) - PublishedScreensFetch.revert(published_version_id) - :error - end - end - - defp check_for_duplicate_screen_ids( - deserialized, - places_and_screens, - published_config_deserialized - ) do - %PendingConfig{screens: existing_screens} = PendingConfig.from_json(deserialized) - %Config{screens: published_screens} = Config.from_json(published_config_deserialized) - - {new_screen_ids, changed_screen_ids, deleted_screen_ids} = - Enum.flat_map(places_and_screens, fn {_place_id, - %{ - "new_pending_screens" => new_screens, - "updated_pending_screens" => updated_pending_screens - }} -> - new_screens ++ updated_pending_screens - end) - |> Enum.reduce({[], [], []}, fn screen, - {new_screen_ids, changed_screen_ids, deleted_screen_ids} = - acc -> - cond do - screen["is_deleted"] -> - {new_screen_ids, changed_screen_ids, [screen["screen_id"] | deleted_screen_ids]} - - # new screen - screen["new_id"] != nil and screen["screen_id"] == nil -> - {[screen["new_id"] | new_screen_ids], changed_screen_ids, deleted_screen_ids} - - # screen is existing but had its ID changed. - screen["new_id"] != nil and screen["screen_id"] != nil -> - {[screen["new_id"] | new_screen_ids], [screen["screen_id"] | changed_screen_ids], - deleted_screen_ids} - - # existing screen is being updated but did not change IDs - true -> - acc - end - end) - - all_screen_ids = - (Map.keys(existing_screens) -- deleted_screen_ids -- changed_screen_ids) ++ - new_screen_ids ++ Map.keys(published_screens) - - duplicate_screen_ids = - all_screen_ids - |> Enum.group_by(& &1) - |> Enum.filter(fn {_string, occurrences} -> length(occurrences) > 1 end) - |> Enum.map(&elem(&1, 0)) - - if Enum.empty?(duplicate_screen_ids), - do: {:ok, existing_screens}, - else: {:error, {:duplicate_screen_ids, duplicate_screen_ids}} - end - - # Config reducers do 3 things: - # 1. Make necessary data requests for retrieving stop related data for screen configurations - # 2. Update any existing pending configurations that were changed. - # 3. Add new pending screen configurations - # Return on each reducer should be all pending configurations, including configurations that were not modified: - # %{ screen_id => Screen.t() } - defp get_config_reducer(:gl_eink_v2), do: &gl_eink_config_reducer/2 - - defp get_config_reducer(app_id), - do: raise("get_config_reducer/1 not implemented for app_id: #{app_id}") - - defp gl_eink_config_reducer(place_and_screens, acc) do - {place_id, - %{ - "updated_pending_screens" => updated_pending_screens, - "new_pending_screens" => new_pending_screens - }} = - place_and_screens - - updated_pending_screens = - Enum.reject(updated_pending_screens, &is_nil(&1)) - - route_id = get_route_id(:gl_eink_v2, updated_pending_screens, new_pending_screens) - - # Need to fetch platform IDs because these screens are direction specific - platform_ids = route_pattern_mod().fetch_platform_ids_for_route_at_stop(place_id, route_id) - - # Update/remove existing pending configs. - new_config = - update_existing_pending_screens(place_id, platform_ids, updated_pending_screens, acc) - - # Add new pending screens - add_new_pending_screens(place_id, platform_ids, new_pending_screens, new_config) - end - - defp get_current_pending_config do - case PendingScreensFetch.fetch_config() do - {:ok, config, metadata} -> {config, metadata.version_id} - error -> error - end - end - - defp get_current_pending_config(version_id) do - # Get config directly from source so we have an up-to-date version_id - case PendingScreensFetch.fetch_config() do - {:ok, config, %{version_id: ^version_id}} -> - {:ok, config} - - :error -> - {:error, :config_not_fetched} - - _ -> - {:error, :version_mismatch} - end - end - - defp json_to_struct(screen, :gl_eink_v2, parent_station_id, platform_ids) do - %{ - "app_params" => %{ - "header" => %{"route_id" => route_id, "direction_id" => direction_id}, - "platform_location" => platform_location - } - } = screen - - platform_id = elem(platform_ids, direction_id) - - struct(Screen, - vendor: :mercury, - app_id: :gl_eink_v2, - app_params: - struct(GlEink, - departures: - struct(Departures, - sections: [ - struct(Section, - query: - struct(Query, - params: %Query.Params{ - stop_ids: [parent_station_id], - route_ids: [route_id], - direction_id: direction_id - } - ) - ) - ] - ), - alerts: %Alerts{stop_id: platform_id}, - line_map: %LineMap{ - station_id: parent_station_id, - stop_id: platform_id, - direction_id: direction_id, - route_id: route_id - }, - header: %Destination{route_id: route_id, direction_id: direction_id}, - platform_location: platform_location, - footer: %Footer{stop_id: parent_station_id} - ) - ) - end - - defp json_to_struct(_, app_id, _, _), - do: raise("json_to_struct/4 not implemented for app_id: #{app_id}") - - defp update_existing_pending_screens(place_id, platform_ids, updated_pending_screens, acc) do - Enum.reduce(updated_pending_screens, acc, fn - %{"is_deleted" => true, "screen_id" => screen_id}, acc -> - # Only delete screen if "is_deleted" is true AND platform is at current place. - # If a screen ID is deleted at one place and moved to another, - # we need to make sure we are removing the correct screen. - - Map.reject(acc, fn - {^screen_id, %Screen{app_params: %GlEink{alerts: %Alerts{stop_id: platform_id}}}} -> - platform_id in Tuple.to_list(platform_ids) - - _ -> - false - end) - - # screen_id was updated - # Delete the old configuration and treat new ID as a new configuration - %{"new_id" => new_id, "screen_id" => screen_id} = config, acc -> - acc - |> Map.delete(screen_id) - |> Map.put( - new_id, - json_to_struct(config, :gl_eink_v2, place_id, platform_ids) - ) - - %{"screen_id" => screen_id} = config, acc -> - Map.put(acc, screen_id, Screen.from_json(config)) - end) - end - - defp add_new_pending_screens(place_id, platform_ids, new_pending_screens, acc) do - Enum.reduce(new_pending_screens, acc, fn - config, acc -> - Map.put( - acc, - config["new_id"], - json_to_struct(config, :gl_eink_v2, place_id, platform_ids) - ) - end) - end - - # Each screen type will look in a different part of the configuration to find it's physical location - defp get_route_id(:gl_eink_v2, updated_pending_screens, new_pending_screens) do - updated_pending_screens - |> Enum.map(fn config -> config end) - |> Enum.concat(new_pending_screens) - |> List.first() - |> get_in(["app_params", "header", "route_id"]) - end - - defp get_route_id(app_id, _, _), - do: raise("get_route_id/3 not implemented for app_id: #{app_id}") - - # Necessary for new mock framework. - # Tests will use a mocked module when calling functions meant for the RoutePattern module. - defp route_pattern_mod do - Application.get_env(:screenplay, :route_pattern_mod, RoutePattern) - end - - defp get_current_published_config do - case PublishedScreensFetch.fetch_config() do - {:ok, config, version_id} -> {config, version_id} - error -> error - end - end - - defp get_new_published_screens(published_config, screens_to_publish) do - %Config{screens: published_screens, devops: devops} = - published_config |> Jason.decode!() |> Config.from_json() - - screens = - screens_to_publish - |> Enum.into(%{}) - |> Map.merge(published_screens) - - %Config{screens: screens, devops: devops} - end - - defp get_new_places_config(places_config, screens_to_add) do - screens_to_add - |> Enum.group_by( - fn - {_, %Screen{app_id: :gl_eink_v2} = config} -> - config.app_params.footer.stop_id - - _ -> - raise("Not implemented") - end, - fn {screen_id, %Screen{app_id: app_id, disabled: disabled}} -> - %ShowtimeScreen{id: screen_id, type: app_id, disabled: disabled} - end - ) - |> Enum.reduce(places_config, fn {stop_id, screens}, acc -> - update_in( - acc, - [Access.filter(&(&1.id == stop_id)), Access.key!(:screens)], - &(&1 ++ screens) - ) - end) - end - - defp place_has_screen( - {_screen_id, %Screen{app_id: :gl_eink_v2} = screen}, - place_id - ) do - %Screen{app_params: %GlEink{footer: %Footer{stop_id: stop_id}}} = screen - stop_id == place_id - end - - defp place_has_screen(_screen, _place_id), do: false - - defp screen_to_place_id(screen = %Screen{app_id: :gl_eink_v2}) do - screen.app_params.footer.stop_id - end - - defp screen_to_place_id(screen = %Screen{app_id: :pre_fare_v2}) do - screen.app_params.header.stop_id - end - - defp screen_to_place_id(screen = %Screen{app_id: :bus_eink_v2}) do - screen.app_params.header.stop_id - end - - defp screen_to_place_id(screen = %Screen{app_id: :bus_shelter_v2}) do - screen.app_params.footer.stop_id - end - - defp screen_to_place_id(screen = %Screen{app_id: :dup_v2}) do - screen.app_params.alerts.stop_id - end - - defp screen_to_place_id(%Screen{app_id: app_id}), - do: raise("screen_to_place_id/1 not implemented for app_id: #{app_id}") - - # TODO: This will be deleted, but I want a clean commit without doing too much work that is about to be deleted - def add_emergency_takeover_configs(alert_id, showtime_screen_ids, message) do - with {published_config, _published_version_id} <- get_current_published_config(), - {:ok, published_config_deserialized} <- Jason.decode(published_config) do - %Config{screens: published_screens, devops: devops} = - published_config_deserialized |> Config.from_json() - - updated_screens = - update_screens_with_emergency_takeover( - published_screens, - showtime_screen_ids, - alert_id, - message - ) - - %Config{screens: updated_screens, devops: devops} - |> publish_new_config() - else - _error -> - {:error, "Could not fetch published screens config"} - end - end - - defp update_screens_with_emergency_takeover(screens, screen_ids, alert_id, message) do - for {id, screen} <- screens, - into: %{} do - if id in screen_ids do - case screen do - %Screen{app_params: %{emergency_messaging_location: eml}} when not is_nil(eml) -> - emergency_takeover = - EmergencyTakeoverConfig.build_emergency_takeover( - message, - alert_id, - screen.app_id, - eml - ) - - {id, - put_in( - screen, - [Access.key!(:app_params), Access.key!(:emergency_takeover)], - emergency_takeover - )} - - _ -> - Logger.error("Tried to takeover #{id} without an emergency_messaging_location") - - {id, screen} - end - else - {id, screen} - end - end - end - - def clear_emergency_takeover_configs(showtime_screen_ids) do - with {published_config, _published_version_id} <- get_current_published_config(), - {:ok, published_config_deserialized} <- Jason.decode(published_config) do - %Config{screens: published_screens, devops: devops} = - published_config_deserialized |> Config.from_json() - - updated_screens = clear_screens_emergency_takeover(published_screens, showtime_screen_ids) - - %Config{screens: updated_screens, devops: devops} - |> publish_new_config() - else - _error -> - {:error, "Could not fetch published screens config"} - end - end - - defp clear_screens_emergency_takeover(screens, screen_ids) do - for {id, screen} <- screens, into: %{} do - if id in screen_ids do - {id, put_in(screen, [Access.key!(:app_params), Access.key!(:emergency_takeover)], nil)} - else - {id, screen} - end - end - end - - def publish_new_config(new_config) do - with(:ok <- PublishedScreensFetch.put_config(new_config)) do - PublishedScreensFetch.commit() - end - end -end diff --git a/lib/screenplay_web/auth_manager.ex b/lib/screenplay_web/auth_manager.ex index 516d98dd..445fd0d2 100644 --- a/lib/screenplay_web/auth_manager.ex +++ b/lib/screenplay_web/auth_manager.ex @@ -12,7 +12,6 @@ defmodule ScreenplayWeb.AuthManager do @roles %{ "screenplay-emergency-admin" => :emergency_admin, - "screens-admin" => :screens_admin, "pa-message-admin" => :pa_message_admin, "suppression-admin" => :suppression_admin } diff --git a/lib/screenplay_web/controllers/config_controller.ex b/lib/screenplay_web/controllers/config_controller.ex deleted file mode 100644 index 43a93a4f..00000000 --- a/lib/screenplay_web/controllers/config_controller.ex +++ /dev/null @@ -1,172 +0,0 @@ -defmodule ScreenplayWeb.ConfigController do - alias ScreensConfig.PendingConfig - use ScreenplayWeb, :controller - - require Logger - - alias Screenplay.PendingScreensConfig.Fetch, as: PendingScreensConfig - alias Screenplay.PermanentConfig - alias Screenplay.ScreensConfig, as: ScreensConfigStore - alias ScreensConfig.Screen - alias ScreensConfig.Screen.GlEink - - plug :check_pending_screens_version when action == :publish - - def index(conn, _params) do - render(conn, "index.html") - end - - def put(conn, %{ - "places_and_screens" => places_and_screens, - "screen_type" => screen_type, - "version_id" => version_id - }) do - case PermanentConfig.put_pending_screens( - places_and_screens, - String.to_existing_atom(screen_type), - version_id - ) do - :ok -> - send_resp(conn, 200, "OK") - - {:error, :version_mismatch} -> - json(%{conn | status: 400}, %{error: :version_mismatch}) - - {:error, :config_not_fetched} -> - send_resp(conn, 500, "S3 Operation Failed: Get") - - {:error, :config_not_written} -> - send_resp(conn, 500, "S3 Operation Failed: Put") - - {:error, {:duplicate_screen_ids, duplicate_screen_ids}} -> - json(%{conn | status: 400}, %{ - duplicate_screen_ids: duplicate_screen_ids, - error: :duplicate_screen_ids - }) - end - end - - def existing_screens(conn, %{"place_ids" => place_ids, "app_id" => app_id}) do - app_id_atom = String.to_existing_atom(app_id) - - {pending_screens_config, version_id} = - case PendingScreensConfig.fetch_config() do - {:ok, config, metadata} -> - %PendingConfig{screens: pending_screens} = - config - |> Jason.decode!() - |> PendingConfig.from_json() - - {pending_screens, metadata.version_id} - - _ -> - raise("Could not fetch pending screens config in existing_screens/2") - end - - places_and_screens = - place_ids - |> String.split(",") - |> Enum.map(fn place_id -> - filter_fn = fn - {_, %Screen{app_id: ^app_id_atom} = config} -> - place_id_has_screen?(place_id, app_id_atom, config) - - _ -> - false - end - - live_screens = - ScreensConfigStore.screens() - |> Enum.filter(filter_fn) - |> Enum.map(fn {k, v} -> {k, Screen.to_json(v)} end) - |> Enum.into(%{}) - - pending_screens = - pending_screens_config - |> Enum.filter(filter_fn) - |> Enum.map(fn {k, v} -> {k, Screen.to_json(v)} end) - |> Enum.into(%{}) - - {place_id, %{live_screens: live_screens, pending_screens: pending_screens}} - end) - |> Enum.into(%{}) - - json(conn, %{ - places_and_screens: places_and_screens, - version_id: version_id - }) - end - - @doc """ - Responds with a map whose top-level keys are `:places_and_screens`, `:version_id`, and `:last_modified_ms`. - The response will also contain an `etag` header. - - The `:places_and_screens` map's keys are unique pairs of place ID+app ID (as strings, for JSON compatibility), - and values are maps containing the live and pending screens at that place / with that app ID, - plus the place ID and app ID as separate values for ease of use on the frontend. - - Each entry in the `:places_and_screens` map corresponds to one accordion in the Pending page UI. - - See spec for `PermanentConfig.get_existing_screens_at_places_with_pending_screens/0` for more detail. - """ - def existing_screens_at_places_with_pending_screens(conn, _params) do - data = PermanentConfig.get_existing_screens_at_places_with_pending_screens() - {etag, data} = Map.pop!(data, :etag) - - conn - |> put_resp_header("etag", etag) - |> json(data) - end - - def publish(conn, %{ - "place_id" => place_id, - "app_id" => app_id, - "hidden_from_screenplay_ids" => hidden_from_screenplay_ids - }) do - app_id_atom = String.to_existing_atom(app_id) - - case PermanentConfig.publish_pending_screens( - place_id, - app_id_atom, - hidden_from_screenplay_ids - ) do - {:ok, new_config} -> - json(conn, %{message: "OK", new_config: new_config}) - - _ -> - conn - |> put_status(500) - |> json(%{message: "Could not publish screens. Please contact an engineer."}) - end - end - - # To be used as a plug on actions that modify pending screens config. - defp check_pending_screens_version(conn, _opts) do - case get_req_header(conn, "if-match") do - [] -> - conn - |> send_resp(428, "Missing if-match header") - |> halt() - - matches -> - {:ok, _, metadata} = PendingScreensConfig.fetch_config() - - if metadata.etag in matches do - conn - else - conn - |> send_resp(412, "Pending screens config version mismatch") - |> halt() - end - end - end - - defp place_id_has_screen?(place_id, :gl_eink_v2, %Screen{ - app_params: %GlEink{footer: %{stop_id: stop_id}} - }), - do: place_id === stop_id - - defp place_id_has_screen?(place_id, app_id, _), - do: - raise("place_id_has_screen/2 not implemented for app_id: #{app_id}, place_id: #{place_id}") -end diff --git a/lib/screenplay_web/router.ex b/lib/screenplay_web/router.ex index fbace4f6..8102981a 100644 --- a/lib/screenplay_web/router.ex +++ b/lib/screenplay_web/router.ex @@ -35,7 +35,6 @@ defmodule ScreenplayWeb.Router do pipeline :ensure_emergency_admin, do: plug(EnsureRole, :emergency_admin) pipeline :ensure_pa_message_admin, do: plug(EnsureRole, :pa_message_admin) - pipeline :ensure_screens_admin, do: plug(EnsureRole, :screens_admin) pipeline :ensure_suppression_admin, do: plug(EnsureRole, :suppression_admin) # Load balancer health check, exempt from authentication, SSL redirects, etc. @@ -128,29 +127,6 @@ defmodule ScreenplayWeb.Router do delete("/", SuppressedPredictionsApiController, :delete) end - # Permanent Configuration - - scope "/", ScreenplayWeb do - pipe_through([:browser, :authenticate, :ensure_screens_admin]) - - get("/pending", ConfigController, :index) - get("/configure-screens/*app_id", ConfigController, :index) - end - - scope "/config", ScreenplayWeb do - pipe_through([:api, :authenticate, :ensure_screens_admin]) - - post("/put", ConfigController, :put) - post("/publish/:place_id/:app_id", ConfigController, :publish) - get("/existing-screens/:app_id", ConfigController, :existing_screens) - - get( - "/existing-screens-at-places-with-pending-screens", - ConfigController, - :existing_screens_at_places_with_pending_screens - ) - end - # Emergency Takeover Tool scope "/emergency-takeover", ScreenplayWeb.EmergencyTakeoverTool do diff --git a/lib/screenplay_web/templates/layout/app.html.heex b/lib/screenplay_web/templates/layout/app.html.heex index dd26cfd9..435da680 100644 --- a/lib/screenplay_web/templates/layout/app.html.heex +++ b/lib/screenplay_web/templates/layout/app.html.heex @@ -23,9 +23,6 @@ <%= if :emergency_admin in @roles do %> <% end %> - <%= if :screens_admin in @roles do %> - - <% end %> <%= if :pa_message_admin in @roles do %> <% end %> diff --git a/test/screenplay/permanent_config_test.exs b/test/screenplay/permanent_config_test.exs deleted file mode 100644 index 99209204..00000000 --- a/test/screenplay/permanent_config_test.exs +++ /dev/null @@ -1,623 +0,0 @@ -# defmodule Screenplay.PermanentConfigTest do -# use ExUnit.Case - -# import Mox - -# alias Screenplay.PendingScreensConfig.Fetch.Local -# alias Screenplay.PermanentConfig -# alias Screenplay.Places.{Cache, Place} -# alias Screenplay.Places.Place.ShowtimeScreen -# alias ScreensConfig.ContentSummary -# alias ScreensConfig.ElevatorStatus -# alias ScreensConfig.Screen.PreFare - -# alias ScreensConfig.{ -# Alerts, -# Config, -# Departures, -# EmergencyTakeover, -# Footer, -# Header, -# LineMap, -# PendingConfig, -# Screen -# } - -# alias ScreensConfig.Screen.GlEink - -# @screen_without_takeover %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :pre_fare_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %PreFare{ -# emergency_messaging_location: :inside, -# emergency_takeover: nil, -# content_summary: %ContentSummary{parent_station_id: "place-test"}, -# elevator_status: %ElevatorStatus{parent_station_id: "place-test"}, -# full_line_map: [], -# header: %Header.StopId{stop_id: "place-test"}, -# reconstructed_alert_widget: %ScreensConfig.Alerts{stop_id: "place-test"} -# }, -# tags: [] -# } -# @gl_eink_screen %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :gl_eink_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %GlEink{ -# departures: %Departures{ -# sections: [ -# %Departures.Section{ -# query: %Departures.Query{ -# params: %Departures.Query.Params{ -# stop_ids: ["place-test"], -# route_ids: ["Green-B"], -# direction_id: 1 -# } -# } -# } -# ] -# }, -# footer: %Footer{stop_id: "place-test"}, -# header: %Header.Destination{ -# route_id: "Green-B", -# direction_id: 1 -# }, -# alerts: %Alerts{stop_id: "456"}, -# line_map: %LineMap{ -# stop_id: "456", -# station_id: "place-test", -# direction_id: 1, -# route_id: "Green-B" -# }, -# evergreen_content: [], -# platform_location: :back -# }, -# tags: [] -# } - -# def fetch_current_config_version do -# {:ok, _config, metadata} = Local.fetch_config() -# metadata.version_id -# end - -# def get_fixture_path(file_name) do -# Path.join(~w[#{File.cwd!()} test fixtures #{file_name}]) -# end - -# setup_all do -# start_supervised!(Screenplay.Places.Cache) - -# on_exit(fn -> -# empty_config = %{screens: %{}} -# pending_screens_path = get_fixture_path("pending_config.json") -# published_screens_path = get_fixture_path("screens_config.json") - -# File.write( -# pending_screens_path, -# Jason.encode!(empty_config) -# ) - -# File.write( -# published_screens_path, -# Jason.encode!(empty_config) -# ) - -# File.rm(pending_screens_path <> ".temp") -# File.rm(published_screens_path <> ".temp") -# end) -# end - -# describe "put_pending_screens/3" do -# setup do -# empty_config = %{screens: %{}} -# pending_screens_path = get_fixture_path("pending_config.json") -# published_screens_path = get_fixture_path("screens_config.json") - -# File.write( -# pending_screens_path, -# Jason.encode!(empty_config) -# ) - -# File.write( -# published_screens_path, -# Jason.encode!(empty_config) -# ) -# end - -# test "adds and updates a new config for GL E-Ink" do -# version = fetch_current_config_version() - -# expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, -# route_id -> -# assert stop_id == "place-test" -# assert route_id == "Green-B" - -# {"123", "456"} -# end) - -# places_and_screens = %{ -# "place-test" => %{ -# "updated_pending_screens" => [], -# "new_pending_screens" => [ -# %{ -# "new_id" => "1234", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# } -# ] -# } -# } - -# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok - -# expected_file_contents = -# %PendingConfig{ -# screens: %{ -# "1234" => %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :gl_eink_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %GlEink{ -# departures: %Departures{ -# sections: [ -# %Departures.Section{ -# query: %Departures.Query{ -# params: %Departures.Query.Params{ -# stop_ids: ["place-test"], -# route_ids: ["Green-B"], -# direction_id: 0 -# } -# } -# } -# ] -# }, -# footer: %Footer{stop_id: "place-test"}, -# header: %Header.Destination{ -# route_id: "Green-B", -# direction_id: 0 -# }, -# alerts: %Alerts{stop_id: "123"}, -# line_map: %LineMap{ -# stop_id: "123", -# station_id: "place-test", -# direction_id: 0, -# route_id: "Green-B" -# }, -# evergreen_content: [], -# platform_location: "front" -# }, -# tags: [] -# } -# } -# } -# |> PendingConfig.to_json() -# |> Jason.encode!(pretty: true) - -# {:ok, config, metadata} = Local.fetch_config() -# assert expected_file_contents == config - -# places_and_screens = %{ -# "place-test" => %{ -# "updated_pending_screens" => [ -# %{ -# "new_id" => "12345", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 1}, -# "platform_location" => "back" -# }, -# "screen_id" => "1234" -# } -# ], -# "new_pending_screens" => [] -# } -# } - -# assert PermanentConfig.put_pending_screens( -# places_and_screens, -# :gl_eink_v2, -# metadata.version_id -# ) == :ok - -# expected_file_contents = -# %PendingConfig{ -# screens: %{ -# "12345" => %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :gl_eink_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %GlEink{ -# departures: %Departures{ -# sections: [ -# %Departures.Section{ -# query: %Departures.Query{ -# params: %Departures.Query.Params{ -# stop_ids: ["place-test"], -# route_ids: ["Green-B"], -# direction_id: 1 -# } -# } -# } -# ] -# }, -# footer: %Footer{stop_id: "place-test"}, -# header: %Header.Destination{ -# route_id: "Green-B", -# direction_id: 1 -# }, -# alerts: %Alerts{stop_id: "456"}, -# line_map: %LineMap{ -# stop_id: "456", -# station_id: "place-test", -# direction_id: 1, -# route_id: "Green-B" -# }, -# evergreen_content: [], -# platform_location: "back" -# }, -# tags: [] -# } -# } -# } -# |> PendingConfig.to_json() -# |> Jason.encode!(pretty: true) - -# {:ok, config, _metadata} = Local.fetch_config() -# assert expected_file_contents == config -# end - -# test "returns version_mismatch error if version is outdated" do -# places_and_screens = %{ -# "place-test" => %{ -# "updated_pending_screens" => [], -# "new_pending_screens" => [ -# %{ -# "new_id" => "1234", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# } -# ] -# } -# } - -# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, "1234") == -# {:error, :version_mismatch} -# end - -# test "returns error when duplicate screen IDs are found" do -# version = fetch_current_config_version() - -# expect(Screenplay.RoutePatterns.Mock, :fetch_platform_ids_for_route_at_stop, 2, fn stop_id, -# route_id -> -# assert stop_id == "place-test" -# assert route_id == "Green-B" - -# {"123", "456"} -# end) - -# places_and_screens = %{ -# "place-test" => %{ -# "updated_pending_screens" => [], -# "new_pending_screens" => [ -# %{ -# "new_id" => "1234", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# } -# ] -# } -# } - -# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == :ok -# version = fetch_current_config_version() - -# places_and_screens = %{ -# "place-test" => %{ -# "updated_pending_screens" => [], -# "new_pending_screens" => [ -# %{ -# "new_id" => "1234", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# }, -# %{ -# "new_id" => "5678", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# }, -# %{ -# "new_id" => "5678", -# "app_params" => %{ -# "header" => %{"route_id" => "Green-B", "direction_id" => 0}, -# "platform_location" => "front" -# } -# } -# ] -# } -# } - -# assert PermanentConfig.put_pending_screens(places_and_screens, :gl_eink_v2, version) == -# {:error, {:duplicate_screen_ids, ["1234", "5678"]}} -# end -# end - -# describe "publish_pending_screens/1" do -# setup do -# pending_screens_path = get_fixture_path("pending_config.json") - -# config = -# %PendingConfig{ -# screens: %{ -# "12345" => %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :gl_eink_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %GlEink{ -# departures: %Departures{ -# sections: [ -# %Departures.Section{ -# query: %Departures.Query{ -# params: %Departures.Query.Params{ -# stop_ids: ["place-test"], -# route_ids: ["Green-B"], -# direction_id: 1 -# } -# } -# } -# ] -# }, -# footer: %Footer{stop_id: "place-test"}, -# header: %Header.Destination{ -# route_id: "Green-B", -# direction_id: 1 -# }, -# alerts: %Alerts{stop_id: "456"}, -# line_map: %LineMap{ -# stop_id: "456", -# station_id: "place-test", -# direction_id: 1, -# route_id: "Green-B" -# }, -# evergreen_content: [], -# platform_location: "back" -# }, -# tags: [] -# }, -# "23456" => %Screen{ -# vendor: :mercury, -# device_id: nil, -# name: nil, -# app_id: :gl_eink_v2, -# refresh_if_loaded_before: nil, -# disabled: false, -# hidden_from_screenplay: false, -# app_params: %GlEink{ -# departures: %Departures{ -# sections: [ -# %Departures.Section{ -# query: %Departures.Query{ -# params: %Departures.Query.Params{ -# stop_ids: ["place-test"], -# route_ids: ["Green-B"], -# direction_id: 1 -# } -# } -# } -# ] -# }, -# footer: %Footer{stop_id: "place-test"}, -# header: %Header.Destination{ -# route_id: "Green-B", -# direction_id: 1 -# }, -# alerts: %Alerts{stop_id: "456"}, -# line_map: %LineMap{ -# stop_id: "456", -# station_id: "place-test", -# direction_id: 1, -# route_id: "Green-B" -# }, -# evergreen_content: [], -# platform_location: "back" -# }, -# tags: [] -# } -# } -# } -# |> PendingConfig.to_json() -# |> Jason.encode!() - -# File.write(pending_screens_path, config) - -# [ -# %Place{ -# id: "place-test", -# name: "Test Place", -# routes: ["Green-B"], -# screens: [], -# description: nil -# } -# ] -# |> Enum.map(&{&1.id, &1}) -# |> Cache.put_all() - -# on_exit(fn -> -# Cache.delete_all() -# end) -# end - -# test "publishes pending screens" do -# assert {:ok, -# [ -# %Place{ -# id: "place-test", -# name: "Test Place", -# routes: ["Green-B"], -# screens: [ -# %ShowtimeScreen{ -# id: "12345", -# type: :gl_eink_v2, -# disabled: false, -# direction_id: nil, -# location: "" -# } -# ], -# description: nil -# } -# ]} = PermanentConfig.publish_pending_screens("place-test", :gl_eink_v2, ["23456"]) -# end -# end - -# describe "add_emergency_takeover_configs/3" do -# setup do -# published_screens_path = get_fixture_path("screens_config.json") - -# config = -# %Config{ -# screens: %{ -# "PRE-1" => @screen_without_takeover, -# "PRE-2" => @screen_without_takeover, -# "GL-1" => @gl_eink_screen -# } -# } -# |> Config.to_json() -# |> Jason.encode!() - -# File.write(published_screens_path, config) -# end - -# test "adds an emergency takeover config to a screen" do -# alert_id = "alert-1" -# takeover_screen_id = "PRE-1" -# message = %{type: :custom, text: %{indoor: "Indoor Message", outdoor: "Outdoor Message"}} - -# assert PermanentConfig.add_emergency_takeover_configs( -# alert_id, -# [takeover_screen_id], -# message -# ) == :ok - -# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() -# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - -# expected_takeover = %EmergencyTakeover{ -# audio_asset_path: nil, -# text_for_audio: "Indoor Message", -# visual_asset_path: "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" -# } - -# assert screens[takeover_screen_id] == -# put_in( -# @screen_without_takeover.app_params.emergency_takeover, -# expected_takeover -# ) - -# assert screens["PRE-2"] == @screen_without_takeover -# assert screens["GL-1"] == @gl_eink_screen -# end - -# test "adds a canned emergency takeover config to a screen" do -# alert_id = "alert-1" -# takeover_screen_id = "PRE-1" -# message = %{type: :canned, id: 1} - -# assert PermanentConfig.add_emergency_takeover_configs( -# alert_id, -# [takeover_screen_id], -# message -# ) == :ok - -# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() -# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - -# expected_takeover = %EmergencyTakeover{ -# audio_asset_path: -# "test/fixtures/emergency_takeover_images/canned/audio/LeaveStation-Indoor.mp3", -# text_for_audio: nil, -# visual_asset_path: -# "test/fixtures/emergency_takeover_images/canned/images/LeaveStation-indoor-portrait.gif" -# } - -# assert screens[takeover_screen_id] == -# put_in( -# @screen_without_takeover.app_params.emergency_takeover, -# expected_takeover -# ) - -# assert screens["PRE-2"] == @screen_without_takeover -# end -# end - -# describe "clear_emergency_takeover_configs/1" do -# setup do -# published_screens_path = get_fixture_path("screens_config.json") - -# screen_with_takeover = -# put_in( -# @screen_without_takeover.app_params.emergency_takeover, -# %EmergencyTakeover{ -# audio_asset_path: nil, -# text_for_audio: "Indoor Message", -# visual_asset_path: -# "test/fixtures/emergency_takeover_images/alert-1/indoor_portrait.png" -# } -# ) - -# config = -# %Config{ -# screens: %{ -# "PRE-1" => screen_with_takeover, -# "PRE-2" => @screen_without_takeover, -# "GL-1" => @gl_eink_screen -# } -# } -# |> Config.to_json() -# |> Jason.encode!() - -# File.write(published_screens_path, config) -# end - -# test "clears emergency takeover configs from screens" do -# takeover_screen_id = "PRE-1" - -# assert PermanentConfig.clear_emergency_takeover_configs([takeover_screen_id]) == :ok - -# {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() -# %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() - -# assert screens[takeover_screen_id] == @screen_without_takeover -# assert screens["PRE-2"] == @screen_without_takeover -# assert screens["GL-1"] == @gl_eink_screen -# end -# end -# end diff --git a/test/screenplay_web/controllers/auth_controller_test.exs b/test/screenplay_web/controllers/auth_controller_test.exs index 56992873..3c1a3321 100644 --- a/test/screenplay_web/controllers/auth_controller_test.exs +++ b/test/screenplay_web/controllers/auth_controller_test.exs @@ -9,7 +9,7 @@ defmodule ScreenplayWeb.Controllers.AuthControllerTest do conn |> init_test_session(%{}) |> Plug.Conn.put_session(:previous_path_from_auth, "/test") - |> get("/auth/keycloak/callback?email=user@test.com&roles[]=screens-admin") + |> get("/auth/keycloak/callback?email=user@test.com&roles[]=pa-message-admin") assert redirected_to(conn) == "/test" end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 1670a936..74d1441e 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -63,7 +63,7 @@ defmodule ScreenplayWeb.ConnCase do Phoenix.ConnTest.build_conn() |> Plug.Test.init_test_session(%{}) |> Guardian.Plug.sign_in(ScreenplayWeb.AuthManager, user, %{ - "roles" => ["screens-admin"] + "roles" => ["pa-message-admin"] }) |> Plug.Conn.put_session(:username, user) From fe914b6c8ec9c4952354760cde00bf4e19bea3e1 Mon Sep 17 00:00:00 2001 From: "robbie.sundstrom" Date: Fri, 10 Jul 2026 14:24:19 -0400 Subject: [PATCH 3/4] deps: Add knip and remove unused deps --- assets/.knip.jsonc | 6 + assets/js/app.tsx | 2 - assets/js/components/Dashboard/AlertCard.tsx | 2 +- assets/js/components/Dashboard/AlertsPage.tsx | 2 +- .../Dashboard/PaMessageForm/PaMessageForm.tsx | 2 +- .../components/Dashboard/ScreenSimulation.tsx | 9 +- .../Tables/Rows/AssociateAlertRow.tsx | 2 +- .../components/Tables/Rows/PaMessageRow.tsx | 2 +- assets/js/util.ts | 2 +- assets/js/utils/api.ts | 15 +- assets/package-lock.json | 218 +++++------------- assets/package.json | 5 +- 12 files changed, 77 insertions(+), 190 deletions(-) create mode 100644 assets/.knip.jsonc diff --git a/assets/.knip.jsonc b/assets/.knip.jsonc new file mode 100644 index 00000000..b9e5cb95 --- /dev/null +++ b/assets/.knip.jsonc @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema-jsonc.json", + // Used only by the Phoenix `watchers` configuration, not detectable by Knip + "ignoreDependencies": ["@fullstory/browser", "@sentry/fullstory", "@sentry/react", "bootstrap"], + "ignoreFiles": ["js/app.tsx"] +} diff --git a/assets/js/app.tsx b/assets/js/app.tsx index adde6d32..6f0b4cbc 100644 --- a/assets/js/app.tsx +++ b/assets/js/app.tsx @@ -1,7 +1,5 @@ import "../css/app.scss"; -import "regenerator-runtime/runtime"; - import React from "react"; import { createRoot } from "react-dom/client"; diff --git a/assets/js/components/Dashboard/AlertCard.tsx b/assets/js/components/Dashboard/AlertCard.tsx index f205ef08..0ef4e9f5 100644 --- a/assets/js/components/Dashboard/AlertCard.tsx +++ b/assets/js/components/Dashboard/AlertCard.tsx @@ -1,4 +1,4 @@ -import moment from "moment"; +import moment from "moment-timezone"; import React from "react"; import { Container, Fade } from "react-bootstrap"; import { ChevronRight } from "react-bootstrap-icons"; diff --git a/assets/js/components/Dashboard/AlertsPage.tsx b/assets/js/components/Dashboard/AlertsPage.tsx index f832e609..c5b808ee 100644 --- a/assets/js/components/Dashboard/AlertsPage.tsx +++ b/assets/js/components/Dashboard/AlertsPage.tsx @@ -21,7 +21,7 @@ import { useScreenplayState, } from "Hooks/useScreenplayContext"; import { usePrevious } from "Hooks/usePrevious"; -import moment from "moment"; +import moment from "moment-timezone"; const AlertsPage: ComponentType = () => { const { places, alerts, screensByAlertMap } = useScreenplayState(); diff --git a/assets/js/components/Dashboard/PaMessageForm/PaMessageForm.tsx b/assets/js/components/Dashboard/PaMessageForm/PaMessageForm.tsx index 2a8940a1..d977b773 100644 --- a/assets/js/components/Dashboard/PaMessageForm/PaMessageForm.tsx +++ b/assets/js/components/Dashboard/PaMessageForm/PaMessageForm.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import moment from "moment"; +import moment from "moment-timezone"; import MainForm from "./MainForm"; import { AudioPreview, Page } from "./types"; import SelectStationsAndZones from "./SelectStationsAndZones"; diff --git a/assets/js/components/Dashboard/ScreenSimulation.tsx b/assets/js/components/Dashboard/ScreenSimulation.tsx index 188d7e5c..9744937f 100644 --- a/assets/js/components/Dashboard/ScreenSimulation.tsx +++ b/assets/js/components/Dashboard/ScreenSimulation.tsx @@ -5,13 +5,8 @@ interface Props { screen: Screen; } -const ScreenSimulation = ({ - screen, -}: Props): JSX.Element => { - const src = useMemo( - () => generateSource(screen), - [screen], - ); +const ScreenSimulation = ({ screen }: Props): JSX.Element => { + const src = useMemo(() => generateSource(screen), [screen]); return (
=> { } }; -const _fetchActiveAndFutureAlerts = - async (): Promise => { - const response = await fetch("/api/alerts/non_access_alerts"); - if (!response.ok) { - throw response; - } - return response.json(); - }; +const _fetchActiveAndFutureAlerts = async (): Promise => { + const response = await fetch("/api/alerts/non_access_alerts"); + if (!response.ok) { + throw response; + } + return response.json(); +}; export const fetchActiveAndFutureAlerts = withErrorHandling( _fetchActiveAndFutureAlerts, diff --git a/assets/package-lock.json b/assets/package-lock.json index 276121b7..ebf26557 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -23,10 +23,9 @@ "react-bootstrap": "^2.10.10", "react-bootstrap-icons": "^1.11.6", "react-dom": "^18.3.1", + "react-router": "^6.22.3", "react-router-dom": "^6.3.0", - "react-search-autocomplete": "^8.5.2", "react-tooltip": "^4.2.21", - "regenerator-runtime": "^0.14.1", "swr": "^2.2.5" }, "devDependencies": { @@ -35,10 +34,8 @@ "@testing-library/react": "^16.0.1", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.23", - "@types/phoenix": "^1.6.7", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@types/react-test-renderer": "^18.3.0", "esbuild": "^0.27.7", "esbuild-sass-plugin": "^3.3.1", "eslint": "^9.39.2", @@ -70,6 +67,7 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -83,6 +81,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -95,6 +94,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -103,6 +103,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -132,6 +133,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, "dependencies": { "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", @@ -142,21 +144,11 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -172,6 +164,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -183,6 +176,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -195,6 +189,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", @@ -213,6 +208,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -221,6 +217,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -233,6 +230,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -244,6 +242,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -252,6 +251,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -260,6 +260,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -268,6 +269,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, "dependencies": { "@babel/template": "^7.25.0", "@babel/types": "^7.25.0" @@ -280,6 +282,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -294,6 +297,7 @@ "version": "7.25.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, "dependencies": { "@babel/types": "^7.25.2" }, @@ -361,6 +365,7 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.24.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.0" @@ -482,6 +487,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/parser": "^7.25.0", @@ -495,6 +501,7 @@ "version": "7.25.3", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.25.0", @@ -512,6 +519,7 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -568,33 +576,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", - "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -2002,6 +1983,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -2014,6 +1996,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2021,6 +2004,7 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2028,10 +2012,12 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3653,13 +3639,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/phoenix": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/pluralize": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", @@ -3689,16 +3668,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-test-renderer": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz", - "integrity": "sha512-HW4MuEYxfDbOHQsVlY/XtOvNHftCVEPhJF2pQXXwcUiUF+Oyb0usgp48HSgpK5rt8m9KZb22yqOeZm+rrVG8gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "license": "MIT", @@ -4101,6 +4070,7 @@ }, "node_modules/ansi-styles": { "version": "3.2.1", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -4437,22 +4407,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "dev": true, @@ -4499,6 +4453,7 @@ "version": "2.9.11", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -4550,6 +4505,7 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4677,19 +4633,11 @@ "node": ">=6" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001762", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4708,6 +4656,7 @@ }, "node_modules/chalk": { "version": "2.4.2", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -4800,6 +4749,7 @@ }, "node_modules/color-convert": { "version": "1.9.3", + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -4807,6 +4757,7 @@ }, "node_modules/color-name": { "version": "1.1.3", + "dev": true, "license": "MIT" }, "node_modules/colorjs.io": { @@ -4837,6 +4788,7 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", + "dev": true, "license": "MIT" }, "node_modules/create-jest": { @@ -4938,26 +4890,6 @@ "node": ">= 8" } }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "license": "MIT", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "node_modules/css.escape": { "version": "1.5.1", "dev": true, @@ -5088,6 +5020,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5259,6 +5192,7 @@ "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, "license": "ISC" }, "node_modules/emittery": { @@ -5530,6 +5464,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5537,6 +5472,7 @@ }, "node_modules/escape-string-regexp": { "version": "1.0.5", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -6280,13 +6216,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuse.js": { - "version": "6.6.2", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6299,6 +6228,7 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6453,6 +6383,7 @@ }, "node_modules/globals": { "version": "11.12.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6508,6 +6439,7 @@ }, "node_modules/has-flag": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -9208,6 +9140,7 @@ }, "node_modules/jsesc": { "version": "2.5.2", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -9239,6 +9172,7 @@ }, "node_modules/json5": { "version": "2.2.3", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -9441,6 +9375,7 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -9573,6 +9508,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -9597,6 +9533,7 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -9956,10 +9893,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10006,10 +9945,6 @@ "node": ">= 0.4" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -10243,18 +10178,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-search-autocomplete": { - "version": "8.5.2", - "license": "MIT", - "dependencies": { - "fuse.js": "^6.5.3", - "styled-components": "^5.3.3" - }, - "peerDependencies": { - "react": "^17.0.2 || ^16.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^16.0.2 || ^18.0.0" - } - }, "node_modules/react-tooltip": { "version": "4.5.1", "license": "MIT", @@ -10949,6 +10872,7 @@ }, "node_modules/semver": { "version": "6.3.1", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10999,12 +10923,6 @@ "node": ">= 0.4" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -11385,38 +11303,9 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, "node_modules/supports-color": { "version": "5.5.0", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -11568,6 +11457,7 @@ }, "node_modules/to-fast-properties": { "version": "2.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11918,6 +11808,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12299,6 +12190,7 @@ }, "node_modules/yallist": { "version": "3.1.1", + "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/assets/package.json b/assets/package.json index 76b3710f..0d4987ad 100644 --- a/assets/package.json +++ b/assets/package.json @@ -31,10 +31,9 @@ "react-bootstrap": "^2.10.10", "react-bootstrap-icons": "^1.11.6", "react-dom": "^18.3.1", + "react-router": "^6.22.3", "react-router-dom": "^6.3.0", - "react-search-autocomplete": "^8.5.2", "react-tooltip": "^4.2.21", - "regenerator-runtime": "^0.14.1", "swr": "^2.2.5" }, "devDependencies": { @@ -43,10 +42,8 @@ "@testing-library/react": "^16.0.1", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.23", - "@types/phoenix": "^1.6.7", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@types/react-test-renderer": "^18.3.0", "esbuild": "^0.27.7", "esbuild-sass-plugin": "^3.3.1", "eslint": "^9.39.2", From c7a3679a5ebd20834dcc57486746270e3127cda2 Mon Sep 17 00:00:00 2001 From: "robbie.sundstrom" Date: Fri, 10 Jul 2026 14:34:16 -0400 Subject: [PATCH 4/4] fix credo --strict --- .../emergency_takeover_tool/config_updater_test.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/screenplay/emergency_takeover_tool/config_updater_test.exs b/test/screenplay/emergency_takeover_tool/config_updater_test.exs index 4ec57627..e676305e 100644 --- a/test/screenplay/emergency_takeover_tool/config_updater_test.exs +++ b/test/screenplay/emergency_takeover_tool/config_updater_test.exs @@ -2,6 +2,7 @@ defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdaterTest do use ExUnit.Case alias Screenplay.EmergencyTakeoverTool.ConfigUpdater + alias Screenplay.ScreensConfig.Fetch.Local alias ScreensConfig.{ Alerts, @@ -124,7 +125,7 @@ defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdaterTest do message ) == :ok - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + {:ok, file_contents, _metadata} = Local.fetch_config() %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() expected_takeover = %EmergencyTakeover{ @@ -154,7 +155,7 @@ defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdaterTest do message ) == :ok - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + {:ok, file_contents, _metadata} = Local.fetch_config() %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() expected_takeover = %EmergencyTakeover{ @@ -209,7 +210,7 @@ defmodule Screenplay.EmergencyTakeoverTool.ConfigUpdaterTest do assert ConfigUpdater.clear_emergency_takeover_configs([takeover_screen_id]) == :ok - {:ok, file_contents, _metadata} = Screenplay.ScreensConfig.Fetch.Local.fetch_config() + {:ok, file_contents, _metadata} = Local.fetch_config() %Config{screens: screens} = file_contents |> Jason.decode!() |> Config.from_json() assert screens[takeover_screen_id] == @screen_without_takeover