Initial commit on Forgejo
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s

Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:40 -04:00
commit af6077b9c3
455 changed files with 78366 additions and 0 deletions

View file

@ -0,0 +1,386 @@
defmodule TarakanWeb.BountyLive.New do
@moduledoc """
Post a new contract (a bounty internally).
This route lives in the public live session so `/bounties/:public_id` can
stay public; it enforces authentication in mount and redirects anonymous
visitors to the login page.
Targets are picked, not typed: repositories come from a live search and
infestations from a select, so `target_ref` always holds a reference the
market can resolve. `?target_type=&target_ref=` prefills the form, which is
how repository and infestation pages link into it.
"""
use TarakanWeb, :live_view
alias Tarakan.Credits
alias Tarakan.Infestations
alias Tarakan.Market
alias Tarakan.Repositories
alias Tarakan.Repositories.Repository
@target_types [
{"Repository", "repository"},
{"Infestation", "infestation"},
{"Finding", "finding"}
]
@fundings [{"Credits", "credits"}, {"Card (fiat escrow)", "fiat"}]
@search_limit 8
@infestation_limit 50
@impl true
def mount(params, _session, socket) do
scope = socket.assigns.current_scope
if scope && scope.account do
form_params = prefill(params)
{:ok,
socket
|> assign(:page_title, "Post a contract")
|> assign(:target_types, @target_types)
|> assign(:fundings, @fundings)
|> assign(:credit_balance, Credits.balance(scope.account))
|> assign(:infestation_options, infestation_options())
|> assign(:repo_results, [])
|> assign_form(form_params)}
else
{:ok,
socket
|> put_flash(:error, "You must log in to access this page.")
|> redirect(to: ~p"/accounts/log-in")}
end
end
defp prefill(params) do
%{
"target_type" => present_or(params["target_type"], "repository"),
"target_ref" => present_or(params["target_ref"], ""),
"funding" => present_or(params["funding"], "credits")
}
end
defp present_or(value, default) when is_binary(value) do
if String.trim(value) == "", do: default, else: value
end
defp present_or(_value, default), do: default
@impl true
def handle_event("validate", %{"bounty" => params}, socket) do
{:noreply,
socket
|> assign_form(params)
|> search_repositories(params)}
end
def handle_event("pick_repository", %{"ref" => ref}, socket) do
params =
socket.assigns.form.params
|> Map.put("target_ref", ref)
|> Map.put("repo_query", "")
{:noreply,
socket
|> assign_form(params)
|> assign(:repo_results, [])}
end
def handle_event("clear_repository", _params, socket) do
{:noreply,
socket
|> assign_form(Map.put(socket.assigns.form.params, "target_ref", ""))
|> assign(:repo_results, [])}
end
def handle_event("create", %{"bounty" => params}, socket) do
attrs = build_attrs(params)
case Market.create_bounty(socket.assigns.current_scope, attrs) do
{:ok, bounty} ->
{:noreply,
socket
|> put_flash(:info, "Contract posted - credits are held in escrow until settlement.")
|> push_navigate(to: ~p"/bounties/#{bounty.public_id}")}
{:ok, _bounty, checkout_url} ->
{:noreply, redirect(socket, external: checkout_url)}
{:error, reason} ->
{:noreply, put_flash(socket, :error, create_error_message(reason))}
end
end
# The repository search runs only while the repository target is selected and
# nothing has been picked yet - once a ref is set the results list is noise.
defp search_repositories(socket, params) do
query = params["repo_query"] || ""
results =
if params["target_type"] == "repository" and blank?(params["target_ref"]) do
Repositories.search_repositories(query, @search_limit)
else
[]
end
assign(socket, :repo_results, results)
end
defp assign_form(socket, params) do
params = Map.put_new(params, "target_type", "repository")
socket
|> assign(:form, to_form(params, as: :bounty))
|> assign(:target_type, params["target_type"])
|> assign(:funding, if(params["funding"] == "fiat", do: "fiat", else: "credits"))
|> assign(:selected_repository, selected_repository(params))
end
defp selected_repository(%{"target_type" => "repository", "target_ref" => ref})
when is_binary(ref) do
if String.trim(ref) == "", do: nil, else: ref
end
defp selected_repository(_params), do: nil
defp blank?(nil), do: true
defp blank?(value) when is_binary(value), do: String.trim(value) == ""
defp infestation_options do
Infestations.list_infestations(limit: @infestation_limit)
|> Enum.map(fn infestation ->
{"#{infestation.title} (#{infestation.repo_count} repos)", infestation.pattern_key}
end)
end
# Hosted repositories are addressed without a host; remote ones need theirs.
defp repository_ref(%Repository{} = repository) do
if Repository.hosted?(repository) do
"#{repository.owner}/#{repository.name}"
else
"#{repository.host}/#{repository.owner}/#{repository.name}"
end
end
defp build_attrs(params) do
funding = if params["funding"] == "fiat", do: "fiat", else: "credits"
base = %{
"target_type" => params["target_type"],
"target_ref" => params["target_ref"],
"title" => params["title"],
"description" => params["description"],
"funding" => funding,
"expires_days" => params["expires_days"]
}
case funding do
"fiat" -> Map.put(base, "amount_cents", dollars_to_cents(params["amount"]))
"credits" -> Map.put(base, "credit_amount", parse_integer(params["amount"]))
end
end
defp dollars_to_cents(nil), do: nil
defp dollars_to_cents(value) when is_binary(value) do
case Float.parse(String.trim(value)) do
{dollars, ""} -> round(dollars * 100)
_invalid -> nil
end
end
defp parse_integer(nil), do: nil
defp parse_integer(value) when is_binary(value) do
case Integer.parse(String.trim(value)) do
{integer, ""} -> integer
_invalid -> nil
end
end
defp create_error_message(:insufficient_credits),
do: "Not enough credits to fund this contract."
defp create_error_message(:target_not_found),
do: "That target was not found. Check the reference and try again."
defp create_error_message(:target_not_public),
do: "Contracts can only target publicly visible repositories and findings."
defp create_error_message(:unauthorized),
do: "Only active accounts can post contracts."
defp create_error_message(%Ecto.Changeset{} = changeset) do
details =
changeset
|> Ecto.Changeset.traverse_errors(fn {message, _meta} -> message end)
|> Enum.map_join("; ", fn {field, messages} -> "#{field} #{Enum.join(messages, ", ")}" end)
"The contract could not be created: #{details}."
end
defp create_error_message(_reason),
do: "The contract could not be created. Try again shortly."
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
<Layouts.page width={:focused}>
<div class="border-b-2 border-strong pb-6">
<h1 class="font-display text-3xl font-medium uppercase leading-none tracking-[0.02em] text-ink">
Post a contract
</h1>
</div>
<.form
for={@form}
id="bounty-form"
phx-change="validate"
phx-submit="create"
class="mt-6 space-y-5"
>
<.input
field={@form[:target_type]}
type="select"
label="Target type"
options={@target_types}
/>
<div :if={@target_type == "repository"}>
<div :if={@selected_repository} class="flex items-center gap-3">
<span class="font-mono text-xs uppercase tracking-[0.12em] text-ink-faint">Target</span>
<span
id="bounty-selected-repository"
class="min-w-0 flex-1 truncate border-2 border-strong bg-panel px-3 py-2 font-mono text-xs text-ink"
>
{@selected_repository}
</span>
<button
type="button"
phx-click="clear_repository"
class="cursor-pointer font-mono text-[11px] uppercase tracking-[0.12em] text-signal hover:underline"
>
Change
</button>
</div>
<div :if={is_nil(@selected_repository)}>
<.input
field={@form[:repo_query]}
type="text"
label="Repository"
placeholder="Search listed repositories"
phx-debounce="200"
autocomplete="off"
/>
<ul
:if={@repo_results != []}
id="bounty-repo-results"
class="mt-2 divide-y divide-rule border-2 border-strong bg-ground"
>
<li :for={repository <- @repo_results}>
<button
type="button"
phx-click="pick_repository"
phx-value-ref={repository_ref(repository)}
class="flex w-full cursor-pointer items-baseline gap-2 px-3 py-2 text-left transition hover:bg-panel"
>
<span class="min-w-0 flex-1 truncate font-mono text-xs text-ink">
{repository.owner}/{repository.name}
</span>
<span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
{repository.host}
</span>
</button>
</li>
</ul>
<p
:if={@repo_results == [] and not blank?(@form.params["repo_query"])}
class="mt-2 font-mono text-[11px] text-ink-faint"
>
No listed repository matches.
</p>
</div>
</div>
<.input
:if={@target_type == "infestation"}
field={@form[:target_ref]}
type="select"
label="Infestation"
options={@infestation_options}
prompt="Choose an infestation"
/>
<div :if={@target_type == "finding"}>
<.input
field={@form[:target_ref]}
type="text"
label="Finding"
placeholder="Public id from the finding page"
autocomplete="off"
/>
<p class="mt-2 font-mono text-[11px] leading-5 text-ink-faint">
The id in the finding's URL, <code>/findings/&lt;id&gt;</code>.
</p>
</div>
<input
:if={@target_type == "repository"}
type="hidden"
name="bounty[target_ref]"
value={@form.params["target_ref"] || ""}
/>
<.input field={@form[:title]} type="text" label="Title" required maxlength="200" />
<.input
field={@form[:description]}
type="textarea"
label="What should the hunter do?"
rows="5"
required
/>
<.input
field={@form[:funding]}
type="select"
label="Fund with"
options={@fundings}
/>
<.input
field={@form[:amount]}
type="text"
label={if @funding == "fiat", do: "Amount (USD)", else: "Amount (credits)"}
placeholder={if @funding == "fiat", do: "250", else: "500"}
required
/>
<p
:if={@funding == "credits"}
id="bounty-credit-balance"
class="-mt-3 font-mono text-[11px] leading-5 text-ink-faint"
>
Your balance: <span class="text-ink">{@credit_balance} credits</span>
· platform take rate applies at settlement.
</p>
<p
:if={@funding == "fiat"}
class="-mt-3 font-mono text-[11px] leading-5 text-ink-faint"
>
Held by Stripe until the work is accepted · platform take rate applies at settlement.
</p>
<button
id="bounty-create-button"
type="submit"
class="cursor-pointer border-2 border-signal px-5 py-2 font-display text-xs uppercase tracking-[0.12em] text-signal transition hover:bg-signal hover:text-ground"
>
Post contract
</button>
</.form>
</Layouts.page>
</Layouts.app>
"""
end
end