diff --git a/lib/towerops/application.ex b/lib/towerops/application.ex index ec1d6bc3..ac47ddd6 100644 --- a/lib/towerops/application.ex +++ b/lib/towerops/application.ex @@ -5,6 +5,9 @@ defmodule Towerops.Application do use Application + # Capture the build/deploy timestamp at compile time + @build_timestamp DateTime.utc_now() + @impl true def start(_type, _args) do # Run migrations on startup (Ecto handles locking for concurrent runs) @@ -44,4 +47,13 @@ defmodule Towerops.Application do ToweropsWeb.Endpoint.config_change(changed, removed) :ok end + + @doc """ + Returns the build/deploy timestamp. + + This timestamp is captured at compile time and represents when the + application was last built/deployed. + """ + @spec build_timestamp() :: DateTime.t() + def build_timestamp, do: @build_timestamp end diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index dbcff59c..83da1381 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -411,15 +411,28 @@ defmodule ToweropsWeb.Layouts do end @doc """ - Renders the application footer with copyright notice. + Renders the application footer with copyright notice and deploy timestamp. """ def footer(assigns) do + alias ToweropsWeb.TimeHelpers + + build_time = Towerops.Application.build_timestamp() + + assigns = + assigns + |> assign(:build_time, build_time) + |> assign(:time_ago, TimeHelpers.format_time_ago(build_time)) + |> assign(:formatted_time, TimeHelpers.format_iso8601(build_time)) + ~H""" """ diff --git a/lib/towerops_web/helpers/time_helpers.ex b/lib/towerops_web/helpers/time_helpers.ex new file mode 100644 index 00000000..cc4c7990 --- /dev/null +++ b/lib/towerops_web/helpers/time_helpers.ex @@ -0,0 +1,106 @@ +defmodule ToweropsWeb.TimeHelpers do + @moduledoc """ + Helper functions for formatting time and date values. + """ + + @doc """ + Formats a DateTime into a human-readable "time ago" string. + + Supports short time periods (seconds, minutes, hours, days) and + long time periods (months, years) for things like build timestamps. + + ## Examples + + iex> datetime = DateTime.add(DateTime.utc_now(), -65, :second) + iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime) + "1m ago" + + iex> datetime = DateTime.add(DateTime.utc_now(), -7200, :second) + iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime) + "2h ago" + + """ + @spec format_time_ago(DateTime.t() | nil) :: String.t() + def format_time_ago(nil), do: "Never" + + def format_time_ago(datetime) do + now = DateTime.utc_now() + diff = DateTime.diff(now, datetime, :second) + + cond do + diff < 60 -> + "#{diff}s ago" + + diff < 3600 -> + minutes = div(diff, 60) + "#{minutes}m ago" + + diff < 86_400 -> + hours = div(diff, 3600) + "#{hours}h ago" + + diff < 2_592_000 -> + days = div(diff, 86_400) + "#{days}d ago" + + diff < 31_536_000 -> + months = div(diff, 2_592_000) + "#{months}mo ago" + + true -> + years = div(diff, 31_536_000) + "#{years}y ago" + end + end + + @doc """ + Formats a DateTime into a full date and time string. + + ## Examples + + iex> datetime = ~U[2026-01-15 14:34:00Z] + iex> ToweropsWeb.TimeHelpers.format_datetime(datetime) + "Jan 15, 2026 at 02:34 PM UTC" + + """ + @spec format_datetime(DateTime.t() | nil) :: String.t() + def format_datetime(nil), do: "Never" + + def format_datetime(datetime) do + Calendar.strftime(datetime, "%b %d, %Y at %I:%M %p %Z") + end + + @doc """ + Formats a DateTime into a short date string. + + ## Examples + + iex> datetime = ~U[2026-01-15 14:34:00Z] + iex> ToweropsWeb.TimeHelpers.format_date(datetime) + "Jan 15, 2026" + + """ + @spec format_date(DateTime.t() | nil) :: String.t() + def format_date(nil), do: "Never" + + def format_date(datetime) do + Calendar.strftime(datetime, "%b %d, %Y") + end + + @doc """ + Formats a DateTime into ISO 8601 format with UTC timezone. + + ## Examples + + iex> datetime = ~U[2026-01-15 14:34:00Z] + iex> ToweropsWeb.TimeHelpers.format_iso8601(datetime) + "2026-01-15 14:34:00 UTC" + + """ + @spec format_iso8601(DateTime.t() | nil) :: String.t() + def format_iso8601(nil), do: "Never" + + def format_iso8601(datetime) do + Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S UTC") + end +end diff --git a/lib/towerops_web/live/agent_live/helpers.ex b/lib/towerops_web/live/agent_live/helpers.ex index 60b6bbfc..f9353917 100644 --- a/lib/towerops_web/live/agent_live/helpers.ex +++ b/lib/towerops_web/live/agent_live/helpers.ex @@ -57,19 +57,10 @@ defmodule ToweropsWeb.AgentLive.Helpers do Formats a DateTime into a human-readable "time ago" string. Returns strings like "5s ago", "3m ago", "2h ago", or "5d ago". + + Delegates to ToweropsWeb.TimeHelpers.format_time_ago/1. """ - def format_last_seen(nil), do: "Never" - - def format_last_seen(datetime) do - seconds_ago = DateTime.diff(DateTime.utc_now(), datetime, :second) - - cond do - seconds_ago < 60 -> "#{seconds_ago}s ago" - seconds_ago < 3600 -> "#{div(seconds_ago, 60)}m ago" - seconds_ago < 86_400 -> "#{div(seconds_ago, 3600)}h ago" - true -> "#{div(seconds_ago, 86_400)}d ago" - end - end + defdelegate format_last_seen(datetime), to: ToweropsWeb.TimeHelpers, as: :format_time_ago @doc """ Formats uptime in seconds to a human-readable string. @@ -94,23 +85,19 @@ defmodule ToweropsWeb.AgentLive.Helpers do Formats a DateTime into a full date and time string. Returns strings like "Jan 15, 2026 at 2:34 PM UTC". - """ - def format_datetime(nil), do: "Never" - def format_datetime(datetime) do - Calendar.strftime(datetime, "%b %d, %Y at %I:%M %p %Z") - end + Delegates to ToweropsWeb.TimeHelpers.format_datetime/1. + """ + defdelegate format_datetime(datetime), to: ToweropsWeb.TimeHelpers @doc """ Formats a DateTime into a short date string. Returns strings like "Jan 15, 2026". - """ - def format_date(nil), do: "Never" - def format_date(datetime) do - Calendar.strftime(datetime, "%b %d, %Y") - end + Delegates to ToweropsWeb.TimeHelpers.format_date/1. + """ + defdelegate format_date(datetime), to: ToweropsWeb.TimeHelpers @doc """ Formats a DateTime into a relative time with full datetime in parentheses.