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.
This commit is contained in:
Willem van den Ende 2026-05-14 11:15:37 +01:00
parent 061787e897
commit 7a50f2d4e7
8 changed files with 298 additions and 2 deletions

View File

@ -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

View File

@ -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"""
<div class="max-w-6xl mx-auto">
<h1 class="text-2xl font-bold mb-2">Microprints</h1>
<p class="text-sm text-zinc-500 mb-6">
Visual fingerprints of source code files. Click a line to highlight it.
Click a card to expand and view the source.
</p>
<.microprint_legend />
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
<%= for %{path: path, microprint: microprint, source: source} = item <- @microprints do %>
{error = item[:error]}
<div class="card bg-base-100 shadow-sm border border-zinc-200">
<div class="card-body p-4">
<h3 class="text-sm font-mono font-medium truncate" title={path}>
{path}
</h3>
<%= if microprint do %>
<.microprint
microprint={microprint}
width={200}
max_height={100}
clickable={true}
file_path={path}
highlighted_line={@highlighted_line}
/>
<button
phx-click="toggle_expand"
phx-value-path={path}
class="btn btn-xs btn-ghost mt-2 w-full"
>
<%= if @expanded_path == path do %>
Collapse
<% else %>
Expand
<% end %>
</button>
<%= if @expanded_path == path and source do %>
<.source_viewer
content={source}
highlighted_line={@highlighted_line}
language="elixir"
/>
<% end %>
<% else %>
<div class="text-xs text-red-500 mt-1">
Error: {inspect(error)}
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
"""
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("<", "&lt;")
|> String.replace(">", "&gt;")
{: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

View File

@ -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

View File

@ -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

View File

@ -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"},

View File

@ -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(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world")
assert response =~
~s(<meta property="og:url" content="http://localhost:4002/blog/engineering/hello-world")
end
end

View File

@ -0,0 +1,56 @@
defmodule FirehoseWeb.MicroprintsLiveTest do
use ExUnit.Case, async: true
describe "scan_source_files/0" do
test "returns only .ex files from app/ and blogex/ directories" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
# Should include app/lib files
assert "lib/firehose.ex" in files
end
test "does not include files from _build/ directory" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
refute Enum.any?(files, &String.starts_with?(&1, "_build/"))
end
test "does not include files from deps/ directory" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
refute Enum.any?(files, &String.starts_with?(&1, "deps/"))
end
test "does not include files from examples/ directory" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
refute Enum.any?(files, &String.contains?(&1, "/examples/"))
end
test "does not include test files" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
refute Enum.any?(files, &String.contains?(&1, "/test/"))
end
test "paths are relative (no leading slash or absolute path)" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
refute Enum.any?(files, &String.starts_with?(&1, "/"))
end
test "app paths do not contain source dir prefix" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
app_files = Enum.filter(files, &String.starts_with?(&1, "lib/"))
refute Enum.any?(app_files, &String.starts_with?(&1, "app/"))
end
test "blogex paths start with ../blogex/" do
files = FirehoseWeb.MicroprintsLive.scan_source_files()
blogex_files = Enum.filter(files, &String.starts_with?(&1, "../blogex/"))
refute blogex_files == []
end
end
end

View File

@ -2,4 +2,4 @@
elixir = "latest"
erlang = "latest"
node = "latest"
go = "latest"
go = "1.26"