From 7a50f2d4e7b5397dfbc4b54440cadb9115ea834a Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Thu, 14 May 2026 11:15:37 +0100 Subject: [PATCH] WIP microprints source now showing also added go to install showboat and rodney for the next steps. microprints work in qwan tracker, but not in firehose. some of the rendering is in the project, maybe the library should provide sample webpages. --- app/lib/firehose/application.ex | 1 + app/lib/firehose_web/live/microprints_live.ex | 228 ++++++++++++++++++ app/lib/firehose_web/router.ex | 6 + app/mix.exs | 3 + app/mix.lock | 1 + .../controllers/blog_controller_test.exs | 3 +- .../live/microprints_live_test.exs | 56 +++++ mise.toml | 2 +- 8 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 app/lib/firehose_web/live/microprints_live.ex create mode 100644 app/test/firehose_web/live/microprints_live_test.exs 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(