tarakan/lib/tarakan_web/live/infestation_live/index.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

119 lines
3.9 KiB
Elixir

defmodule TarakanWeb.InfestationLive.Index do
@moduledoc "Cross-repo infestation list: patterns spanning multiple listed repositories."
use TarakanWeb, :live_view
alias Tarakan.Infestations
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Infestations")
|> assign(
:meta_description,
"Cross-repo infestations: the same finding title in multiple listed repositories."
)
|> assign(:canonical_path, ~p"/infestations")
|> assign(:days, 30)
|> assign(:min_repos, 2)
|> load_infestations()}
end
@impl true
def handle_event("filter", params, socket) do
days = parse_int(params["days"], socket.assigns.days, 1, 365)
min_repos = parse_int(params["min_repos"], socket.assigns.min_repos, 2, 20)
{:noreply,
socket
|> assign(:days, days)
|> assign(:min_repos, min_repos)
|> load_infestations()}
end
defp load_infestations(socket) do
infestations =
Infestations.list_infestations(
days: socket.assigns.days,
min_repos: socket.assigns.min_repos,
limit: 50
)
assign(socket, :infestations, infestations)
end
defp parse_int(value, default, min, max) do
case Integer.parse(to_string(value || "")) do
{n, _} -> n |> max(min) |> min(max)
:error -> default
end
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
<Layouts.page width={:wide}>
<div class="flex flex-col gap-4 border-b-2 border-strong pb-6 sm:flex-row sm:items-end sm:justify-between">
<div class="max-w-2xl">
<h1 class="font-display text-3xl font-medium uppercase tracking-[0.02em] text-ink sm:text-5xl">
Infestations
</h1>
<p class="mt-3 text-sm leading-6 text-ink-muted">
Same finding title in {@min_repos}+ listed repos within the window.
Ranked by affected repo count.
</p>
</div>
<form
id="infestations-filter"
phx-change="filter"
class="flex flex-wrap items-end gap-3 font-mono text-xs"
>
<label class="flex flex-col gap-1 text-ink-faint">
Min repos
<select
name="min_repos"
class="border border-strong bg-panel px-2 py-1.5 text-ink"
>
<option :for={n <- [2, 3, 5, 10]} value={n} selected={@min_repos == n}>{n}</option>
</select>
</label>
<label class="flex flex-col gap-1 text-ink-faint">
Window
<select name="days" class="border border-strong bg-panel px-2 py-1.5 text-ink">
<option :for={n <- [7, 30, 90, 365]} value={n} selected={@days == n}>
{n}d
</option>
</select>
</label>
</form>
</div>
<div
:if={@infestations == []}
id="infestations-empty"
class="mt-10 bg-panel px-6 py-16 text-center"
>
<p class="text-sm font-medium text-ink">No infestations in this window</p>
<p class="mt-2 text-xs text-ink-muted">
Infestations appear when the same finding title hits {@min_repos}+ listed repos.
</p>
</div>
<div :if={@infestations != []} class="mt-8">
<.constellation id="infestations-constellation" infestations={@infestations} />
<ul id="infestations-list" class="sr-only">
<li :for={infestation <- @infestations} id={"infestation-#{infestation.pattern_key}"}>
<.link navigate={~p"/infestations/#{infestation.pattern_key}"}>
{infestation.title} · {infestation.repo_count} repos
</.link>
</li>
</ul>
</div>
</Layouts.page>
</Layouts.app>
"""
end
end