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(