From 636746293ff4c9dff49d7ab49699c9a114422098 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 27 Oct 2025 16:57:05 -0400 Subject: [PATCH 1/9] livebook: Add livebook to test out service --> daily-schedule grouping --- lib/services/service.ex | 14 +++++----- livebooks/service_daily_schedules.livemd | 34 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 livebooks/service_daily_schedules.livemd diff --git a/lib/services/service.ex b/lib/services/service.ex index cc93983143..a63ceed19e 100644 --- a/lib/services/service.ex +++ b/lib/services/service.ex @@ -164,13 +164,13 @@ defmodule Services.Service do end @spec all_valid_dates_for_service(t()) :: [Date.t()] - defp all_valid_dates_for_service(%__MODULE__{ - start_date: from, - end_date: until, - added_dates: added_dates, - removed_dates: removed_dates, - valid_days: valid_days - }) do + def all_valid_dates_for_service(%__MODULE__{ + start_date: from, + end_date: until, + added_dates: added_dates, + removed_dates: removed_dates, + valid_days: valid_days + }) do # fallback to today if either start or end date are nil from = from || Timex.today() until = until || Timex.today() diff --git a/livebooks/service_daily_schedules.livemd b/livebooks/service_daily_schedules.livemd new file mode 100644 index 0000000000..755c519d6f --- /dev/null +++ b/livebooks/service_daily_schedules.livemd @@ -0,0 +1,34 @@ +# Service --> Daily Schedules + +## Services to Daily Schedules + +```elixir +services = Services.Repo.by_route_id("39") + +services +|> Enum.flat_map(fn svc -> + svc + |> Services.Service.all_valid_dates_for_service() + |> Enum.map(&{&1, svc}) +end) +|> Enum.group_by(fn {date, _svc} -> date end) +|> Enum.map(fn {date, list} -> + {date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)} +end) +|> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2) +|> Enum.group_by(fn {_date, svc_list} -> svc_list end) +|> Enum.each(fn {svc_list, date_tuples} -> + dates = + date_tuples + |> Enum.map(fn {date, _svc_list} -> date end) + |> Enum.map(&"#{&1}") + |> Enum.join(", ") + + IO.puts(svc_list |> Enum.map(& &1.id) |> Enum.join(", ")) + IO.puts(" #{svc_list |> Enum.map(& &1.name) |> Enum.join(", ")}") + IO.puts(" #{svc_list |> Enum.map(& &1.type) |> Enum.join(", ")}") + IO.puts(" #{svc_list |> Enum.map(& &1.typicality) |> Enum.join(", ")}") + IO.puts(" #{dates}") + IO.puts("") +end) +``` From bf01cb0a4877c8c2f8b2144f0d8ca34823691d77 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 3 Nov 2025 12:59:19 -0500 Subject: [PATCH 2/9] feat: LiveView for exploring daily schedules for different bus routes --- lib/dotcom_web/live/daily_schedules.ex | 231 +++++++++++++++++++++++++ lib/dotcom_web/router.ex | 9 + 2 files changed, 240 insertions(+) create mode 100644 lib/dotcom_web/live/daily_schedules.ex diff --git a/lib/dotcom_web/live/daily_schedules.ex b/lib/dotcom_web/live/daily_schedules.ex new file mode 100644 index 0000000000..6b4844ab75 --- /dev/null +++ b/lib/dotcom_web/live/daily_schedules.ex @@ -0,0 +1,231 @@ +defmodule DotcomWeb.Live.DailySchedules do + @moduledoc """ + A simple basic-auth gated page to explore what kinds of daily schedules are actually available for the routes we serve. + """ + + use DotcomWeb, :live_view + + alias Phoenix.{LiveView, LiveView.JS} + + @impl LiveView + def mount(params, _session, socket) do + { + :ok, + socket + } + end + + @impl true + def handle_params(params, _uri, socket) do + {:noreply, socket |> assign(:route_id, params |> Map.get("route_id", "1"))} + end + + @impl true + def handle_event("change-route", %{"route_id" => route_id}, socket) do + new_path = socket |> live_path(__MODULE__, %{"route_id" => route_id}) + {:noreply, socket |> push_patch(to: new_path)} + end + + @impl true + def render(%{route_id: route_id} = assigns) do + services = Services.Repo.by_route_id(route_id) + + daily_schedules = + services + |> Enum.flat_map(fn svc -> + svc + |> Services.Service.all_valid_dates_for_service() + |> Enum.map(&{&1, svc}) + end) + |> Enum.group_by(fn {date, _svc} -> date end) + |> Enum.map(fn {date, list} -> + {date, list |> Enum.map(fn {_date, svc} -> svc end) |> Enum.sort_by(& &1.id)} + end) + |> Enum.sort_by(fn {date, _svc_list} -> date end, &Date.before?/2) + |> Enum.group_by(fn {_date, svc_list} -> svc_list end) + |> Enum.map(fn {svc_list, date_tuples} -> + %{ + services: svc_list, + dates: date_tuples |> Enum.map(fn {date, _svc_list} -> date end) + } + end) + |> Enum.map(&add_holiday_status/1) + |> Enum.map(&add_title/1) + |> Enum.sort_by(& &1.title) + + {holiday_schedules, non_holiday_schedules} = daily_schedules |> Enum.split_with(& &1.holiday?) + + assigns = + assigns + |> assign(:route_id, route_id) + |> assign(:daily_schedules, daily_schedules) + |> assign(:holiday_schedules, holiday_schedules) + |> assign(:non_holiday_schedules, non_holiday_schedules) + + ~H""" +

Daily Schedules

+ + <.route_changer selected_route_id={@route_id} /> + +

Regular Schedules

+ <.daily_schedule :for={schedule <- @non_holiday_schedules} schedule={schedule} /> + +

Holiday Schedules

+ <.daily_schedule :for={schedule <- @holiday_schedules} schedule={schedule} /> + """ + end + + defp add_holiday_status(schedule) do + schedule + |> Map.put(:holiday?, schedule.services |> Enum.any?(&(&1.typicality == :holiday_service))) + end + + defp add_title(schedule) do + schedule + |> Map.put(:title, title(schedule)) + end + + defp title(schedule) do + days_of_week = schedule.dates |> Enum.map(&Date.day_of_week/1) |> MapSet.new() + date_set = schedule.dates |> MapSet.new() + + [ + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-27]])) end, "Thanksgiving"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-28]])) end, + "Day After Thanksgiving"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-11-11]])) end, "Veterans Day"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25]])) end, "Christmas Day"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-31]])) end, "New Year's Eve"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-01]])) end, "New Year's Day"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2025-12-25], ~D[2026-01-01]])) end, + "Christmas and New Year's"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19]])) end, + "Martin Luther King Jr. Day"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-02-16]])) end, "Presidents Day"}, + {fn -> MapSet.subset?(date_set, MapSet.new([~D[2026-01-19], ~D[2026-02-16]])) end, + "Martin Luther King Jr. and Presidents' Day"}, + {fn -> MapSet.subset?(days_of_week, MapSet.new([5])) end, "Friday"}, + {fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4])) end, "Monday - Thursday"}, + {fn -> MapSet.subset?(days_of_week, MapSet.new([1, 2, 3, 4, 5])) end, "Weekday"}, + {fn -> MapSet.subset?(days_of_week, MapSet.new([6])) end, "Saturday"}, + {fn -> MapSet.subset?(days_of_week, MapSet.new([7])) end, "Sunday"}, + {fn -> true end, "???"} + ] + |> Stream.filter(fn {condition_fun, _title} -> condition_fun.() end) + |> Stream.map(fn {_condition_fun, title} -> title end) + |> Enum.take(1) + |> List.first() + end + + defp daily_schedule(assigns) do + %{services: services} = assigns.schedule + title = services |> Enum.map(& &1.id) |> Enum.sort() |> Enum.join(", ") + assigns = assigns |> assign(:title, title) + + ~H""" +
+ +
{@schedule.title}
+
+
{svc.id}
+
+ <.icon + name="chevron-down" + class="ml-auto h-3 w-3 group-open/schedule:rotate-180 transition-all" + /> +
+ +
+
Services
+
+
+
{svc.id}
+
+
Name
+
{svc.name}
+ +
Start Date
+
{svc.start_date}
+ +
End Date
+
{svc.end_date}
+
+ +
Added Dates
+
+
{date}
+
+ +
Removed Dates
+
+
{date}
+
+
+
+
+ +
+
Active Dates
+
+
+ {date} ({day_of_week(date)}) +
+
+
+ +
+
Unique days of the week
+
+
Enum.map(&Date.day_of_week/1) |> Enum.uniq() |> Enum.sort() + } + class="p-1 border-xs rounded text-xs" + > + {pretty_day_of_week(day_of_week)} +
+
+
+ +
+ +
Full Schedule Data Blob
+ <.icon + name="chevron-down" + class="ml-auto h-3 w-3 group-open/code:rotate-180 transition-all" + /> +
+
{inspect @schedule, pretty: true}
+
+
+ """ + end + + defp day_of_week(date), do: date |> Date.day_of_week() |> pretty_day_of_week() + + defp pretty_day_of_week(1), do: "Mon" + defp pretty_day_of_week(2), do: "Tue" + defp pretty_day_of_week(3), do: "Wed" + defp pretty_day_of_week(4), do: "Thu" + defp pretty_day_of_week(5), do: "Fri" + defp pretty_day_of_week(6), do: "Sat" + defp pretty_day_of_week(7), do: "Sun" + defp pretty_day_of_week(_), do: "?" + + defp route_changer(assigns) do + assigns = + assigns + |> assign(:bus_routes, Routes.Repo.by_type(3)) + + ~H""" +
+ +
+ """ + end +end diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index ba9396365a..e98028fe2f 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -302,6 +302,15 @@ defmodule DotcomWeb.Router do end end + scope "/", DotcomWeb do + import Phoenix.LiveView.Router + pipe_through([:browser, :browser_live, :basic_auth_readonly]) + + live_session :internal, layout: {DotcomWeb.LayoutView, :live} do + live("/daily-schedules", Live.DailySchedules) + end + end + scope "/preview", DotcomWeb do import Phoenix.LiveView.Router pipe_through([:browser, :browser_live, :basic_auth_readonly]) From 764282d0fe931303e7df3c9afbc7bd2ed1ed03b7 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Wed, 3 Dec 2025 14:41:30 -0500 Subject: [PATCH 3/9] tweak: Move `/daily-schedules` under `/preview` --- lib/dotcom_web/router.ex | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index e98028fe2f..f1ee67395b 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -302,15 +302,6 @@ defmodule DotcomWeb.Router do end end - scope "/", DotcomWeb do - import Phoenix.LiveView.Router - pipe_through([:browser, :browser_live, :basic_auth_readonly]) - - live_session :internal, layout: {DotcomWeb.LayoutView, :live} do - live("/daily-schedules", Live.DailySchedules) - end - end - scope "/preview", DotcomWeb do import Phoenix.LiveView.Router pipe_through([:browser, :browser_live, :basic_auth_readonly]) @@ -318,6 +309,7 @@ defmodule DotcomWeb.Router do live_session :default, layout: {DotcomWeb.LayoutView, :preview} do live "/system-status", Live.SystemStatus live "/schedules/:route_id/:direction_id", ScheduleFinderLive + live "/daily-schedules", Live.DailySchedules end end From 93ba86345ee716f96feb6f8e2dcfc2f4b4cfad15 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Wed, 3 Dec 2025 14:43:00 -0500 Subject: [PATCH 4/9] tweak: Rename `Live.DailySchedules` --> `DailySchedulesLive` --- .../live/{daily_schedules.ex => daily_schedules_live.ex} | 2 +- lib/dotcom_web/router.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/dotcom_web/live/{daily_schedules.ex => daily_schedules_live.ex} (99%) diff --git a/lib/dotcom_web/live/daily_schedules.ex b/lib/dotcom_web/live/daily_schedules_live.ex similarity index 99% rename from lib/dotcom_web/live/daily_schedules.ex rename to lib/dotcom_web/live/daily_schedules_live.ex index 6b4844ab75..0385f9835d 100644 --- a/lib/dotcom_web/live/daily_schedules.ex +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -1,4 +1,4 @@ -defmodule DotcomWeb.Live.DailySchedules do +defmodule DotcomWeb.DailySchedulesLive do @moduledoc """ A simple basic-auth gated page to explore what kinds of daily schedules are actually available for the routes we serve. """ diff --git a/lib/dotcom_web/router.ex b/lib/dotcom_web/router.ex index f1ee67395b..164fa4e59b 100644 --- a/lib/dotcom_web/router.ex +++ b/lib/dotcom_web/router.ex @@ -309,7 +309,7 @@ defmodule DotcomWeb.Router do live_session :default, layout: {DotcomWeb.LayoutView, :preview} do live "/system-status", Live.SystemStatus live "/schedules/:route_id/:direction_id", ScheduleFinderLive - live "/daily-schedules", Live.DailySchedules + live "/daily-schedules", DailySchedulesLive end end From 8f479280cb62d2364aec53f7268e3889d3e4b12d Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Wed, 3 Dec 2025 14:50:01 -0500 Subject: [PATCH 5/9] feat: Add `valid_days` --- lib/dotcom_web/live/daily_schedules_live.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex index 0385f9835d..621b531c99 100644 --- a/lib/dotcom_web/live/daily_schedules_live.ex +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -149,6 +149,13 @@ defmodule DotcomWeb.DailySchedulesLive do
End Date
{svc.end_date}
+ +
Valid Days
+
+
+ {pretty_day_of_week(day)} +
+
Added Dates
From b56c6aaf14506ff656e24a414d418fa47cf4ae6d Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 15 Dec 2025 16:58:57 -0500 Subject: [PATCH 6/9] chore: Add `/preview` page entry --- lib/dotcom_web/live/preview_live.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/dotcom_web/live/preview_live.ex b/lib/dotcom_web/live/preview_live.ex index 8c4ebcdef5..e31156c20a 100644 --- a/lib/dotcom_web/live/preview_live.ex +++ b/lib/dotcom_web/live/preview_live.ex @@ -5,11 +5,19 @@ defmodule DotcomWeb.PreviewLive do use DotcomWeb, :live_view + alias DotcomWeb.DailySchedulesLive alias DotcomWeb.Router.Helpers alias DotcomWeb.ScheduleFinderLive alias Phoenix.LiveView @pages [ + %{ + arguments: [], + icon_name: "calendar-days", + icon_type: "solid", + module: DailySchedulesLive, + title: "Daily Schedules Experiment" + }, %{ arguments: ["Red", "0"], icon_name: "icon-realtime-tracking", From 3fb90f38c10d7f050289ffa4f475d93bfbcfcc4e Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Mon, 15 Dec 2025 16:59:32 -0500 Subject: [PATCH 7/9] cleanup(warnings): Remove or _underscore unused declarations --- lib/dotcom_web/live/daily_schedules_live.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex index 621b531c99..3e28f0f3b9 100644 --- a/lib/dotcom_web/live/daily_schedules_live.ex +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -5,10 +5,10 @@ defmodule DotcomWeb.DailySchedulesLive do use DotcomWeb, :live_view - alias Phoenix.{LiveView, LiveView.JS} + alias Phoenix.LiveView @impl LiveView - def mount(params, _session, socket) do + def mount(_params, _session, socket) do { :ok, socket From 7482382404d6cc9d985a11c686ce8b8a2d476ca8 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Fri, 2 Jan 2026 17:19:39 -0500 Subject: [PATCH 8/9] feat: Show typicality --- lib/dotcom_web/live/daily_schedules_live.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex index 3e28f0f3b9..7cdf88543d 100644 --- a/lib/dotcom_web/live/daily_schedules_live.ex +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -156,6 +156,9 @@ defmodule DotcomWeb.DailySchedulesLive do {pretty_day_of_week(day)} + +
Typicality
+
{svc.typicality}
Added Dates
From 5e6fc8d12fe667a2157794ee3d74aa972ad96634 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Fri, 2 Jan 2026 17:19:48 -0500 Subject: [PATCH 9/9] feat: Show all routes, not just bus --- lib/dotcom_web/live/daily_schedules_live.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dotcom_web/live/daily_schedules_live.ex b/lib/dotcom_web/live/daily_schedules_live.ex index 7cdf88543d..29b68e0c9b 100644 --- a/lib/dotcom_web/live/daily_schedules_live.ex +++ b/lib/dotcom_web/live/daily_schedules_live.ex @@ -226,12 +226,12 @@ defmodule DotcomWeb.DailySchedulesLive do defp route_changer(assigns) do assigns = assigns - |> assign(:bus_routes, Routes.Repo.by_type(3)) + |> assign(:routes, Routes.Repo.all()) ~H"""