Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
310
lib/tarakan_web/live/repository_live/index.ex
Normal file
310
lib/tarakan_web/live/repository_live/index.ex
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
defmodule TarakanWeb.RepositoryLive.Index do
|
||||
use TarakanWeb, :live_view
|
||||
|
||||
alias Tarakan.Activity
|
||||
alias Tarakan.AnalyticsCache
|
||||
alias Tarakan.Community
|
||||
alias Tarakan.FindingMemory
|
||||
alias Tarakan.Fixes
|
||||
alias Tarakan.Infestations
|
||||
alias Tarakan.ModelAnalytics
|
||||
alias Tarakan.Repositories
|
||||
alias Tarakan.Scans
|
||||
alias Tarakan.SecurityPosture
|
||||
alias Tarakan.Work
|
||||
alias TarakanWeb.Presence
|
||||
|
||||
@presence_topic "registry:observers"
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if connected?(socket) do
|
||||
Repositories.subscribe()
|
||||
Activity.subscribe()
|
||||
Community.subscribe()
|
||||
Phoenix.PubSub.subscribe(Tarakan.PubSub, @presence_topic)
|
||||
|
||||
{:ok, _ref} = Presence.track(self(), @presence_topic, socket.id, %{})
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Public security record")
|
||||
|> assign(
|
||||
:meta_description,
|
||||
"Local agents. Public Reports. Pick up Jobs or publish findings at a pinned commit."
|
||||
)
|
||||
|> assign(:canonical_path, ~p"/")
|
||||
|> assign(:stats, Repositories.registry_stats())
|
||||
|> assign(:contributor_count, Scans.public_contributor_count())
|
||||
|> assign(:open_tasks, Work.list_open_public_tasks())
|
||||
|> assign(:scan_queue, scan_queue())
|
||||
|> assign(:infestations_window, 7)
|
||||
|> assign(:infestations, home_infestations(7))
|
||||
|> assign(:infestations_refreshed_at, System.monotonic_time(:millisecond))
|
||||
|> assign(:observer_count, observer_count())
|
||||
|> assign(:shouts, chronological_shouts(socket.assigns.current_scope))
|
||||
|> assign(:shout_form, to_form(Community.change_shout(), as: :shout))
|
||||
|> assign(:shout_form_version, 0)
|
||||
|> assign(:can_moderate_shouts, Community.can_moderate?(socket.assigns.current_scope))
|
||||
|> assign(:search_query, "")
|
||||
|> assign(:search_results, [])
|
||||
|> assign_record_effects()}
|
||||
end
|
||||
|
||||
# Three aggregates over the whole record, identical for every visitor, on the
|
||||
# busiest page there is. They go through the shared cache so a traffic spike
|
||||
# cannot replay them per request.
|
||||
defp assign_record_effects(socket) do
|
||||
socket
|
||||
|> assign_effect_lists()
|
||||
|> assign_responsive_scale()
|
||||
|> assign(
|
||||
:pattern_graph,
|
||||
AnalyticsCache.fetch({:home_pattern_graph, 12}, &encoded_pattern_graph/0,
|
||||
on_unavailable: nil
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# The slowest ranked median, used to scale the responsiveness bars.
|
||||
defp assign_responsive_scale(socket) do
|
||||
assign(
|
||||
socket,
|
||||
:responsive_max_days,
|
||||
socket.assigns.responsive
|
||||
|> Enum.map(& &1.median_days_to_fix)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.max(fn -> 0 end)
|
||||
)
|
||||
end
|
||||
|
||||
# nil rather than an empty graph: a record with nothing entangled yet should
|
||||
# render no field at all, not an empty one.
|
||||
defp encoded_pattern_graph do
|
||||
graph = Infestations.pattern_graph(patterns: 12)
|
||||
|
||||
if length(graph.nodes) >= 2, do: Jason.encode!(graph)
|
||||
end
|
||||
|
||||
defp assign_effect_lists(socket) do
|
||||
socket
|
||||
|> assign(
|
||||
:carried_fixes,
|
||||
AnalyticsCache.fetch(
|
||||
{:home_carried_fixes, 4},
|
||||
fn ->
|
||||
Fixes.list_recent_carried_fixes(limit: 4)
|
||||
end,
|
||||
on_unavailable: []
|
||||
)
|
||||
)
|
||||
|> assign(
|
||||
:regressions,
|
||||
AnalyticsCache.fetch(
|
||||
{:home_regressions, 4},
|
||||
fn ->
|
||||
FindingMemory.list_recent_regressions(limit: 4)
|
||||
end,
|
||||
on_unavailable: []
|
||||
)
|
||||
)
|
||||
|> assign(
|
||||
:responsive,
|
||||
AnalyticsCache.fetch(
|
||||
{:home_responsive, 5},
|
||||
fn ->
|
||||
SecurityPosture.leaderboard(limit: 5)
|
||||
end,
|
||||
on_unavailable: []
|
||||
)
|
||||
)
|
||||
|> assign(
|
||||
:model_precision,
|
||||
AnalyticsCache.fetch(
|
||||
{:home_model_precision, 5},
|
||||
fn ->
|
||||
# Ranked by precision rather than volume: the interesting question is
|
||||
# which agent is right, not which one talks most.
|
||||
ModelAnalytics.cached_model_scoreboard(limit: 25)
|
||||
|> Enum.filter(& &1.precision)
|
||||
|> Enum.sort_by(& &1.precision, :desc)
|
||||
|> Enum.take(5)
|
||||
end,
|
||||
on_unavailable: []
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Bar width relative to the slowest ranked repository. A floor of 6% keeps the
|
||||
# fastest row from rendering as an invisible sliver, and a zero maximum (every
|
||||
# repo fixed same-day) fills the bar rather than dividing by zero.
|
||||
defp home_bar_pct(_value, max) when max in [nil, 0, 0.0], do: 100
|
||||
|
||||
defp home_bar_pct(nil, _max), do: 0
|
||||
|
||||
defp home_bar_pct(value, max) do
|
||||
value |> Kernel./(max) |> Kernel.*(100) |> Float.round(1) |> max(6.0) |> min(100.0)
|
||||
end
|
||||
|
||||
@queue_limit 8
|
||||
|
||||
# list_shouts/2 serves "the most recent N", newest first. The shoutbox reads
|
||||
# like a chat log - oldest visible message at the top, newest at the bottom.
|
||||
defp chronological_shouts(scope) do
|
||||
scope |> Community.list_shouts() |> Enum.reverse()
|
||||
end
|
||||
|
||||
defp scan_queue do
|
||||
Repositories.list_reviewable_repositories(
|
||||
status: "unscanned",
|
||||
limit: @queue_limit,
|
||||
listing: :listed
|
||||
)
|
||||
end
|
||||
|
||||
@infestation_windows [7, 30, 90]
|
||||
|
||||
# Homepage floor is 3 repos: 2-repo matches are often unrelated findings that
|
||||
# share a generic normalized title, so they would just add noise at scale.
|
||||
# `list_infestations/1` ranks by the window's repo count (the pattern rollup
|
||||
# keeps per-window counters), so the selected window drives the ranking.
|
||||
@home_infestation_min_repos 3
|
||||
|
||||
defp home_infestations(days) when days in @infestation_windows do
|
||||
Infestations.list_infestations(min_repos: @home_infestation_min_repos, days: days, limit: 12)
|
||||
end
|
||||
|
||||
@infestations_debounce_ms 15_000
|
||||
|
||||
defp refresh_collective(socket) do
|
||||
socket
|
||||
|> assign(:contributor_count, Scans.public_contributor_count())
|
||||
|> assign(:open_tasks, Work.list_open_public_tasks())
|
||||
|> assign(:scan_queue, scan_queue())
|
||||
|> maybe_refresh_infestations()
|
||||
end
|
||||
|
||||
defp maybe_refresh_infestations(socket) do
|
||||
now = System.monotonic_time(:millisecond)
|
||||
last = Map.get(socket.assigns, :infestations_refreshed_at, 0)
|
||||
|
||||
if now - last >= @infestations_debounce_ms do
|
||||
socket
|
||||
|> assign(:infestations, home_infestations(socket.assigns.infestations_window))
|
||||
|> assign(:infestations_refreshed_at, now)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:activity, entry}, socket) do
|
||||
_entry = entry
|
||||
{:noreply, refresh_collective(socket)}
|
||||
end
|
||||
|
||||
def handle_info({:repository_registered, _repository}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stats, Repositories.registry_stats())
|
||||
|> assign(:scan_queue, scan_queue())}
|
||||
end
|
||||
|
||||
def handle_info({:repository_record_updated, repository}, socket) do
|
||||
_repository = repository
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:stats, Repositories.registry_stats())
|
||||
|> refresh_collective()}
|
||||
end
|
||||
|
||||
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
|
||||
{:noreply, assign(socket, :observer_count, observer_count())}
|
||||
end
|
||||
|
||||
def handle_info({event, _shout}, socket) when event in [:shout_posted, :shout_removed] do
|
||||
{:noreply, assign(socket, :shouts, chronological_shouts(socket.assigns.current_scope))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("infestations_window", %{"days" => days}, socket)
|
||||
when days in ["7", "30", "90"] do
|
||||
days = String.to_integer(days)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:infestations_window, days)
|
||||
|> assign(:infestations, home_infestations(days))
|
||||
|> assign(:infestations_refreshed_at, System.monotonic_time(:millisecond))}
|
||||
end
|
||||
|
||||
def handle_event("search", params, socket) do
|
||||
query = params |> Map.get("q", "") |> String.trim()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:search_query, query)
|
||||
|> assign(:search_results, Repositories.search_repositories(query))}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"post_shout",
|
||||
%{"shout" => attrs},
|
||||
%{assigns: %{current_scope: %{account: account}}} = socket
|
||||
)
|
||||
when not is_nil(account) do
|
||||
case Community.create_shout(socket.assigns.current_scope, attrs) do
|
||||
{:ok, _shout} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:shout_form, to_form(Community.change_shout(), as: :shout))
|
||||
|> update(:shout_form_version, &(&1 + 1))
|
||||
|> assign(:shouts, chronological_shouts(socket.assigns.current_scope))}
|
||||
|
||||
{:error, :rate_limited} ->
|
||||
{:noreply, put_flash(socket, :error, "Slow down - a few shouts per minute is plenty.")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:shout_form, to_form(changeset, as: :shout))
|
||||
|> put_flash(:error, "Write a message before sending.")}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:noreply, put_flash(socket, :error, "That shout could not be posted.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("post_shout", _params, socket) do
|
||||
{:noreply, redirect(socket, to: ~p"/accounts/log-in")}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"remove_shout",
|
||||
%{"id" => id},
|
||||
%{assigns: %{current_scope: %{account: account}}} = socket
|
||||
)
|
||||
when not is_nil(account) do
|
||||
with {:ok, shout} <- Community.get_shout(id),
|
||||
{:ok, _removed} <-
|
||||
Community.remove_shout(socket.assigns.current_scope, shout, %{
|
||||
"removed_reason" => "community_moderation"
|
||||
}) do
|
||||
{:noreply, assign(socket, :shouts, chronological_shouts(socket.assigns.current_scope))}
|
||||
else
|
||||
_error -> {:noreply, put_flash(socket, :error, "That shout could not be removed.")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("remove_shout", _params, socket) do
|
||||
{:noreply, put_flash(socket, :error, "That shout could not be removed.")}
|
||||
end
|
||||
|
||||
defp observer_count do
|
||||
@presence_topic |> Presence.list() |> map_size()
|
||||
end
|
||||
|
||||
defp shout_time(%DateTime{} = datetime), do: Calendar.strftime(datetime, "%b %-d · %H:%M")
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue