diff --git a/lib/towerops_web/changelog_parser.ex b/lib/towerops_web/changelog_parser.ex new file mode 100644 index 00000000..14361a51 --- /dev/null +++ b/lib/towerops_web/changelog_parser.ex @@ -0,0 +1,48 @@ +defmodule ToweropsWeb.ChangelogParser do + @moduledoc "Parses priv/static/changelog.txt into structured entries." + + @doc "Returns a list of %{date: Date.t(), title: String.t(), items: [String.t()]}" + def parse do + path = Application.app_dir(:towerops, "priv/static/changelog.txt") + + case File.read(path) do + {:ok, content} -> parse_content(content) + {:error, _} -> [] + end + end + + defp parse_content(content) do + content + |> String.split("\n") + |> Enum.reduce([], fn line, acc -> + cond do + # Date header with title: "2026-02-13 — Title" + Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s*[—–-]\s*.+/, line) -> + [date_str, title] = String.split(line, ~r/\s*[—–-]\s*/, parts: 2) + entry = %{date: Date.from_iso8601!(String.trim(date_str)), title: String.trim(title), items: []} + [entry | acc] + + # Date header without title: "2026-02-13" + Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s*$/, line) -> + date = Date.from_iso8601!(String.trim(line)) + entry = %{date: date, title: nil, items: []} + [entry | acc] + + # Bullet point + String.starts_with?(String.trim(line), "* ") -> + case acc do + [current | rest] -> + item = String.trim(line) |> String.trim_leading("* ") + [%{current | items: current.items ++ [item]} | rest] + + [] -> + acc + end + + true -> + acc + end + end) + |> Enum.reverse() + end +end diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex index d4575f30..e93868b1 100644 --- a/lib/towerops_web/components/layouts.ex +++ b/lib/towerops_web/components/layouts.ex @@ -366,6 +366,14 @@ defmodule ToweropsWeb.Layouts do > My Data + <.link + role="menuitem" + navigate={~p"/changelog"} + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white" + phx-click={JS.hide(to: "#org-menu")} + > + 🆕 What's New + <.link role="menuitem" href={~p"/users/log-out"} diff --git a/lib/towerops_web/live/changelog_live.ex b/lib/towerops_web/live/changelog_live.ex new file mode 100644 index 00000000..417eebf4 --- /dev/null +++ b/lib/towerops_web/live/changelog_live.ex @@ -0,0 +1,34 @@ +defmodule ToweropsWeb.ChangelogLive do + use ToweropsWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + entries = ToweropsWeb.ChangelogParser.parse() + {:ok, assign(socket, entries: entries, page_title: "What's New", active_page: "changelog")} + end + + @impl true + def render(assigns) do + ~H""" +
+

What's New

+

Latest updates and improvements to TowerOps.

+ +
+
+
+
+ + {Calendar.strftime(entry.date, "%b %d, %Y")} + +
+

{entry.title}

+
    +
  • {item}
  • +
+
+
+
+ """ + end +end diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex index bef19474..f6602453 100644 --- a/lib/towerops_web/router.ex +++ b/lib/towerops_web/router.ex @@ -371,6 +371,9 @@ defmodule ToweropsWeb.Router do # Activity feed live "/activity", ActivityFeedLive, :index + # Changelog + live "/changelog", ChangelogLive, :index + # Network map route live "/network-map", NetworkMapLive, :index end