diff --git a/app/lib/firehose/application.ex b/app/lib/firehose/application.ex index b025303..4a4975a 100644 --- a/app/lib/firehose/application.ex +++ b/app/lib/firehose/application.ex @@ -12,6 +12,7 @@ defmodule Firehose.Application do Firehose.Repo, {DNSCluster, query: Application.get_env(:firehose, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Firehose.PubSub}, + {Microprints.MicroprintCache, pubsub: Firehose.PubSub}, # Start a worker by calling: Firehose.Worker.start_link(arg) # {Firehose.Worker, arg}, # Start to serve requests, typically the last entry diff --git a/app/lib/firehose_web/live/microprints_live.ex b/app/lib/firehose_web/live/microprints_live.ex new file mode 100644 index 0000000..f44f84a --- /dev/null +++ b/app/lib/firehose_web/live/microprints_live.ex @@ -0,0 +1,228 @@ +defmodule FirehoseWeb.MicroprintsLive do + use FirehoseWeb, :live_view + + alias Microprints.MicroprintCache + alias Microprints.MicroprintComponent + + @source_dirs ["app", "blogex"] + + @impl true + def mount(_params, _session, socket) do + files = scan_source_files() + + microprints = + files + |> Enum.map(&process_file/1) + + {:ok, + socket + |> assign(:page_title, "Microprints") + |> assign(:microprints, microprints) + |> assign(:expanded_path, nil) + |> assign(:highlighted_path, nil) + |> assign(:highlighted_line, nil)} + rescue + e -> + {:ok, + socket + |> assign(:page_title, "Microprints") + |> assign(:microprints, []) + |> assign(:expanded_path, nil) + |> assign(:highlighted_path, nil) + |> assign(:highlighted_line, nil) + |> put_flash(:error, "Error loading microprints: #{inspect(e)}")} + end + + @impl true + def render(assigns) do + ~H""" +
+

Microprints

+

+ Visual fingerprints of source code files. Click a line to highlight it. + Click a card to expand and view the source. +

+ + <.microprint_legend /> + +
+ <%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %> + {error = item[:error]} +
+
+

+ {path} +

+ + <%= if microprint do %> + <.microprint + microprint={microprint} + width={200} + max_height={100} + clickable={true} + file_path={path} + highlighted_line={@highlighted_line} + /> + + + + <%= if @expanded_path == path and source do %> + <.source_viewer + content={source} + highlighted_line={@highlighted_line} + language="elixir" + /> + <% end %> + <% else %> +
+ Error: {inspect(error)} +
+ <% end %> +
+
+ <% end %> +
+
+ """ + end + + @impl true + def handle_event("highlight_line", %{"line" => line, "path" => path}, socket) do + highlighted = + case socket.assigns.highlighted_path do + ^path -> nil + _ -> String.to_integer(line) + end + + {:noreply, + socket + |> assign(:highlighted_path, path) + |> assign(:highlighted_line, highlighted)} + end + + @impl true + def handle_event("toggle_expand", %{"path" => path}, socket) do + expanded = + case socket.assigns.expanded_path do + ^path -> nil + _ -> path + end + + {:noreply, assign(socket, :expanded_path, expanded)} + end + + # Private helpers + + @doc false + def scan_source_files do + @source_dirs + |> Enum.flat_map(&collect_elixir_files/1) + |> Enum.uniq() + |> Enum.sort() + end + + defp resolve_absolute_paths(files) do + app_root = Mix.Project.project_file() |> Path.dirname() + + Enum.map(files, fn path -> + case Path.split(path) do + [".." | _rest] -> + Path.join(app_root, path) + + _ -> + Path.expand(path) + end + end) + end + + defp excluded_path?(path) do + path =~ "/_build/" or + path =~ "/deps/" or + path =~ "/examples/" or + path =~ "/test/" or + path =~ "/lib_dev/" + end + + defp collect_elixir_files(dir) do + app_root = Mix.Project.project_file() |> Path.dirname() + monorepo_root = app_root |> Path.dirname() + base = Path.join(monorepo_root, dir) + + case File.dir?(base) do + true -> + base + |> Path.join("**/*.ex") + |> Path.wildcard() + |> Enum.filter(&File.regular?(&1)) + |> Enum.reject(&excluded_path?/1) + |> Enum.map(&format_path(&1, monorepo_root, dir)) + + false -> + [] + end + end + + defp format_path(path, monorepo_root, dir) do + relative = Path.relative_to(path, monorepo_root) + + case dir do + "app" -> + String.replace_prefix(relative, "app/", "") + + _ -> + "../" <> relative + end + end + + defp process_file(rel_path) do + abs_path = resolve_absolute_paths([rel_path]) |> List.first() + + case MicroprintCache.get_microprint(abs_path) do + {:ok, microprint} -> + # Add line numbers to each line for highlighting + lines_with_numbers = + microprint.lines + |> Enum.with_index(1) + |> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end) + + # Read source code for expand/contract + source = read_source(abs_path) + + %{ + path: rel_path, + microprint: Map.put(microprint, :lines, lines_with_numbers), + source: source + } + + {:error, reason} -> + %{path: rel_path, microprint: nil, error: reason} + end + end + + defp read_source(abs_path) do + case File.read(abs_path) do + {:ok, content} -> + content + |> String.replace("<", "<") + |> String.replace(">", ">") + + {:error, _} -> + nil + end + end + + # Delegate to MicroprintComponent + defdelegate microprint(assigns), to: MicroprintComponent + defdelegate microprint_legend(assigns), to: MicroprintComponent + defdelegate source_viewer(assigns), to: MicroprintComponent +end diff --git a/app/lib/firehose_web/router.ex b/app/lib/firehose_web/router.ex index 16e9979..9b45bd3 100644 --- a/app/lib/firehose_web/router.ex +++ b/app/lib/firehose_web/router.ex @@ -33,6 +33,12 @@ defmodule FirehoseWeb.Router do get "/:blog_id/:slug", BlogController, :show end + scope "/", FirehoseWeb do + pipe_through :browser + + live "/microprints", MicroprintsLive + end + # JSON API + feeds (no Phoenix layout) scope "/api/blog" do forward "/engineering", Blogex.Router, blog: Firehose.EngineeringBlog diff --git a/app/mix.exs b/app/mix.exs index b49a9e9..eb2fe5e 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -70,6 +70,9 @@ defmodule Firehose.MixProject do {:dns_cluster, "~> 0.2.0"}, {:bandit, "~> 1.5"}, {:blogex, path: "../blogex"}, + {:microprints, + git: "ssh://git@gitea.apps.sustainabledelivery.com:3022/QWAN/microprints-phoenix.git", + branch: "main"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false} ] end diff --git a/app/mix.lock b/app/mix.lock index 46f07af..d0efcce 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -27,6 +27,7 @@ "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "microprints": {:git, "ssh://git@gitea.apps.sustainabledelivery.com:3022/QWAN/microprints-phoenix.git", "29ef59ff6eb41853b6f91872d8fffdfba4d85a62", [branch: "main"]}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, diff --git a/app/test/firehose_web/controllers/blog_controller_test.exs b/app/test/firehose_web/controllers/blog_controller_test.exs index f11bcd9..9b24bfb 100644 --- a/app/test/firehose_web/controllers/blog_controller_test.exs +++ b/app/test/firehose_web/controllers/blog_controller_test.exs @@ -103,7 +103,8 @@ defmodule FirehoseWeb.BlogControllerTest do test "meta tags have correct og_url", %{conn: conn} do response = conn |> get(~p"/blog/engineering/hello-world") |> html_response(200) - assert response =~ ~s(