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
450
lib/tarakan_web/live/review_task_live/show.ex
Normal file
450
lib/tarakan_web/live/review_task_live/show.ex
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
defmodule TarakanWeb.ReviewTaskLive.Show do
|
||||
use TarakanWeb, :live_view
|
||||
|
||||
alias Tarakan.Accounts
|
||||
alias Tarakan.Policy
|
||||
alias Tarakan.Scans
|
||||
alias Tarakan.Work
|
||||
alias Tarakan.Work.{Contribution, ReviewTask}
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
task =
|
||||
Work.get_visible_task(id, socket.assigns.current_scope) ||
|
||||
raise Ecto.NoResultsError, queryable: ReviewTask
|
||||
|
||||
if connected?(socket), do: Work.subscribe(task.repository_id)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, task.title)
|
||||
|> assign(:meta_description, task_meta_description(task))
|
||||
|> assign(:canonical_path, ~p"/jobs/#{task.id}")
|
||||
|> assign_task(task)
|
||||
|> assign(:decision_form, decision_form(task))
|
||||
|> assign(:disclosure_form, disclosure_form())}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({event, task_id}, %{assigns: %{task: current_task}} = socket)
|
||||
when event in [
|
||||
:review_task_updated,
|
||||
:review_task_published,
|
||||
:review_task_submitted,
|
||||
:review_task_accepted,
|
||||
:review_task_disclosed,
|
||||
:review_task_changes_requested,
|
||||
:review_task_rejected,
|
||||
:review_task_cancelled,
|
||||
:review_task_quarantined
|
||||
] do
|
||||
if task_id == current_task.id do
|
||||
case Work.get_visible_task(task_id, socket.assigns.current_scope) do
|
||||
nil -> {:noreply, push_navigate(socket, to: repository_path(current_task))}
|
||||
task -> {:noreply, assign_task(socket, task)}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("claim", _params, %{assigns: %{current_scope: scope, task: task}} = socket) do
|
||||
respond(socket, Work.claim_task(task, scope), "Task claimed.")
|
||||
end
|
||||
|
||||
def handle_event("release", _params, %{assigns: %{current_scope: scope, task: task}} = socket) do
|
||||
respond(socket, Work.release_task(task, scope), "Claim released.")
|
||||
end
|
||||
|
||||
def handle_event("validate_contribution", %{"contribution" => params}, socket) do
|
||||
form =
|
||||
%Contribution{}
|
||||
|> Work.change_contribution(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form(as: :contribution)
|
||||
|
||||
{:noreply, assign(socket, :contribution_form, form)}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"complete",
|
||||
%{"contribution" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
) do
|
||||
case Work.submit_task(task, scope, params) do
|
||||
{:ok, task} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_task(task)
|
||||
|> put_flash(:info, "Evidence submitted for independent review.")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :contribution_form, to_form(changeset, as: :contribution))}
|
||||
|
||||
error ->
|
||||
{:noreply, put_flash(socket, :error, error_message(error))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"complete_verification",
|
||||
%{"verification" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
) do
|
||||
case Work.submit_task(task, scope, params) do
|
||||
{:ok, task} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_task(task)
|
||||
|> put_flash(:info, "Finding check submitted.")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:verification_form, verification_form(task, params))
|
||||
|> put_flash(:error, changeset_error(changeset))}
|
||||
|
||||
error ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:verification_form, verification_form(task, params))
|
||||
|> put_flash(:error, error_message(error))}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"publish",
|
||||
%{"decision" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
) do
|
||||
with_recent_auth(socket, fn ->
|
||||
respond(
|
||||
socket,
|
||||
Work.publish_task(task, scope, params),
|
||||
"Task approved for the public queue."
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"review",
|
||||
%{"action" => action, "decision" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
)
|
||||
when action in ["accept", "request_changes", "reject"] do
|
||||
with_recent_auth(socket, fn ->
|
||||
{result, message} =
|
||||
case action do
|
||||
"accept" ->
|
||||
{Work.accept_task(task, scope, params),
|
||||
"Contribution accepted and held for a separate disclosure decision."}
|
||||
|
||||
"request_changes" ->
|
||||
{Work.request_changes(task, scope, params), "Changes requested."}
|
||||
|
||||
"reject" ->
|
||||
{Work.reject_task(task, scope, params), "Contribution rejected."}
|
||||
end
|
||||
|
||||
respond(socket, result, message)
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_event("review", _params, socket) do
|
||||
{:noreply, put_flash(socket, :error, "The requested review action is invalid.")}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"disclose",
|
||||
%{"visibility" => visibility, "disclosure" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
)
|
||||
when visibility in ["public_summary", "public"] do
|
||||
with_recent_auth(socket, fn ->
|
||||
message =
|
||||
if visibility == "public",
|
||||
do: "Full evidence disclosed after sensitive-data review.",
|
||||
else: "A redacted result summary is now public."
|
||||
|
||||
respond_disclosure(
|
||||
socket,
|
||||
Work.disclose_task(task, scope, visibility, params),
|
||||
message,
|
||||
params
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def handle_event("disclose", _params, socket) do
|
||||
{:noreply, put_flash(socket, :error, "The requested disclosure is invalid.")}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"cancel",
|
||||
%{"decision" => params},
|
||||
%{assigns: %{current_scope: scope, task: task}} = socket
|
||||
) do
|
||||
respond(socket, Work.cancel_task(task, scope, params), "Task cancelled.")
|
||||
end
|
||||
|
||||
defp respond(socket, {:ok, task}, message) do
|
||||
{:noreply, socket |> assign_task(task) |> put_flash(:info, message)}
|
||||
end
|
||||
|
||||
defp respond(socket, {:error, %Ecto.Changeset{} = changeset}, _message) do
|
||||
{:noreply, assign(socket, :decision_form, to_form(changeset, as: :decision))}
|
||||
end
|
||||
|
||||
defp respond(socket, error, _message) do
|
||||
{:noreply, put_flash(socket, :error, error_message(error))}
|
||||
end
|
||||
|
||||
defp respond_disclosure(socket, {:ok, task}, message, _params) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_task(task)
|
||||
|> assign(:disclosure_form, disclosure_form())
|
||||
|> put_flash(:info, message)}
|
||||
end
|
||||
|
||||
defp respond_disclosure(
|
||||
socket,
|
||||
{:error, %Ecto.Changeset{} = changeset},
|
||||
_message,
|
||||
params
|
||||
) do
|
||||
form =
|
||||
params
|
||||
|> Map.take(["reason", "sensitive_data_reviewed"])
|
||||
|> to_form(as: :disclosure, errors: changeset.errors)
|
||||
|
||||
{:noreply, assign(socket, :disclosure_form, form)}
|
||||
end
|
||||
|
||||
defp respond_disclosure(socket, error, _message, _params) do
|
||||
{:noreply, put_flash(socket, :error, error_message(error))}
|
||||
end
|
||||
|
||||
defp assign_task(socket, task) do
|
||||
socket
|
||||
|> assign(:task, task)
|
||||
|> assign(:visible_target_review, visible_target_review(socket.assigns.current_scope, task))
|
||||
|> assign(:provenance_options, provenance_options(task))
|
||||
|> assign(:verification_form, verification_form(task))
|
||||
|> assign(:decision_form, decision_form(task))
|
||||
|> assign(
|
||||
:contribution_form,
|
||||
to_form(
|
||||
Work.change_contribution(%Contribution{}, %{
|
||||
"provenance" => task |> allowed_provenances() |> List.first()
|
||||
}),
|
||||
as: :contribution
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp decision_form(%ReviewTask{status: "proposed"} = task) do
|
||||
to_form(%{"reason" => Work.default_publish_reason(task), "evidence" => ""}, as: :decision)
|
||||
end
|
||||
|
||||
defp decision_form(_task) do
|
||||
to_form(%{"reason" => "", "evidence" => ""}, as: :decision)
|
||||
end
|
||||
|
||||
defp disclosure_form do
|
||||
to_form(
|
||||
%{"reason" => "", "sensitive_data_reviewed" => "false"},
|
||||
as: :disclosure
|
||||
)
|
||||
end
|
||||
|
||||
defp verification_form(task, params \\ %{}) do
|
||||
defaults = %{
|
||||
"provenance" => task |> allowed_provenances() |> List.first(),
|
||||
"verdict" => "confirmed",
|
||||
"notes" => "",
|
||||
"evidence" => ""
|
||||
}
|
||||
|
||||
defaults
|
||||
|> Map.merge(params)
|
||||
|> to_form(as: :verification)
|
||||
end
|
||||
|
||||
defp visible_target_review(_scope, %{target_review_id: nil}), do: nil
|
||||
|
||||
defp visible_target_review(scope, %{target_review_id: review_id}) do
|
||||
case Scans.get_scan(scope, review_id) do
|
||||
{:ok, review} -> review
|
||||
{:error, :not_found} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp changeset_error(%Ecto.Changeset{errors: [{_field, {message, _opts}} | _]}),
|
||||
do: "Verification was not submitted: #{message}."
|
||||
|
||||
defp changeset_error(_changeset), do: "Verification was not submitted."
|
||||
|
||||
defp with_recent_auth(socket, fun) do
|
||||
account = socket.assigns.current_scope && socket.assigns.current_scope.account
|
||||
|
||||
if account && Accounts.sudo_mode?(account) do
|
||||
fun.()
|
||||
else
|
||||
return_to = ~p"/jobs/#{socket.assigns.task.id}"
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Confirm it's you with a magic link before changing the public record (sign-in older than 8 hours)."
|
||||
)
|
||||
|> push_navigate(to: TarakanWeb.AccountAuth.reauth_path(return_to))}
|
||||
end
|
||||
end
|
||||
|
||||
defp owns_claim?(task, %{account_id: account_id}) when is_integer(account_id) do
|
||||
task.claimed_by_id == account_id and ReviewTask.claim_active?(task)
|
||||
end
|
||||
|
||||
defp owns_claim?(_task, _scope), do: false
|
||||
|
||||
defp can_claim?(task, scope) do
|
||||
claimable_for_display?(task) and Policy.allowed?(scope, :claim_task, task)
|
||||
end
|
||||
|
||||
defp can_publish?(%{status: "proposed"} = task, %{account_id: account_id} = scope)
|
||||
when is_integer(account_id) do
|
||||
# Stewards/owners and moderators may publish, including their own proposals.
|
||||
Policy.allowed?(scope, :publish_task, task)
|
||||
end
|
||||
|
||||
defp can_publish?(_task, _scope), do: false
|
||||
|
||||
defp can_review?(%{status: "submitted"} = task, %{account_id: account_id} = scope)
|
||||
when is_integer(account_id) do
|
||||
task.created_by_id != account_id and task.claimed_by_id != account_id and
|
||||
not Enum.any?(task.contributions, &(&1.account_id == account_id)) and
|
||||
Policy.allowed?(scope, :review_contribution, task)
|
||||
end
|
||||
|
||||
defp can_review?(_task, _scope), do: false
|
||||
|
||||
defp can_disclose?(%{status: "accepted"} = task, scope) do
|
||||
Policy.allowed?(scope, :disclose_task, task)
|
||||
end
|
||||
|
||||
defp can_disclose?(_task, _scope), do: false
|
||||
|
||||
defp full_disclosure_allowed?(task) do
|
||||
task.repository.participation_mode in ["maintainer_verified", "curated"]
|
||||
end
|
||||
|
||||
defp can_cancel?(task, scope) do
|
||||
task.status in ["proposed", "open", "changes_requested"] and
|
||||
Policy.allowed?(scope, :cancel_task, task)
|
||||
end
|
||||
|
||||
defp created_by_current_account?(task, %{account_id: account_id})
|
||||
when is_integer(account_id),
|
||||
do: task.created_by_id == account_id
|
||||
|
||||
defp created_by_current_account?(_task, _scope), do: false
|
||||
|
||||
defp claimable_for_display?(%{status: "claimed"} = task),
|
||||
do: not ReviewTask.claim_active?(task)
|
||||
|
||||
defp claimable_for_display?(task), do: ReviewTask.claimable?(task)
|
||||
|
||||
defp error_message({:error, :own_task}), do: "That action is not allowed on this job."
|
||||
defp error_message({:error, :not_independent}), do: "An independent reviewer is required."
|
||||
defp error_message({:error, :already_claimed}), do: "This task is already claimed."
|
||||
defp error_message({:error, :claim_limit}), do: "You have reached your active claim limit."
|
||||
defp error_message({:error, :claim_expired}), do: "Your claim expired. Claim the task again."
|
||||
|
||||
defp error_message({:error, :capability_mismatch}),
|
||||
do: "The provenance does not match the task."
|
||||
|
||||
defp error_message({:error, :verdict_required}),
|
||||
do: "Choose confirmed or disputed and provide verification notes."
|
||||
|
||||
defp error_message({:error, :target_review_required}),
|
||||
do: "This check job has no target report."
|
||||
|
||||
defp error_message({:error, :target_review_mismatch}),
|
||||
do: "The selected report does not match this check job."
|
||||
|
||||
defp error_message({:error, :not_claimant}), do: "You do not hold this claim."
|
||||
defp error_message({:error, :closed}), do: "This task is closed."
|
||||
defp error_message({:error, :not_open}), do: "This task is not open for claims."
|
||||
defp error_message({:error, :invalid_state}), do: "That transition is no longer valid."
|
||||
defp error_message({:error, :active_work}), do: "Resolve active work before cancelling."
|
||||
defp error_message({:error, :invalid_visibility}), do: "That disclosure level is invalid."
|
||||
|
||||
defp error_message({:error, :full_disclosure_not_allowed}),
|
||||
do: "Full evidence requires a maintainer-verified or curated repository."
|
||||
|
||||
defp error_message({:error, :sensitive_data_review_required}),
|
||||
do: "Confirm that the evidence was checked for secrets and personal data."
|
||||
|
||||
defp error_message({:error, :identity_changed}),
|
||||
do: "The repository is no longer confirmed public, so this result cannot be disclosed."
|
||||
|
||||
defp error_message({:error, :claim_rate_limited}),
|
||||
do: "Too many claim changes. Wait a minute and try again."
|
||||
|
||||
defp error_message({:error, :unauthorized}), do: "You are not authorized for that action."
|
||||
defp error_message({:error, _reason}), do: "The action could not be completed."
|
||||
|
||||
defp task_meta_description(task) do
|
||||
repo =
|
||||
case task.repository do
|
||||
%{owner: owner, name: name} -> "#{owner}/#{name}"
|
||||
_ -> "open source"
|
||||
end
|
||||
|
||||
kind = task.kind |> to_string() |> String.replace("_", " ")
|
||||
status = task.status |> to_string() |> String.replace("_", " ")
|
||||
|
||||
desc =
|
||||
"Open security job on #{repo}: #{task.title}. " <>
|
||||
"#{String.capitalize(kind)} · #{status}. Claim on Tarakan."
|
||||
|
||||
String.slice(String.replace(desc, ~r/\s+/, " "), 0, 160)
|
||||
end
|
||||
|
||||
defp kind_label("code_review"), do: "Code review"
|
||||
defp kind_label("threat_model"), do: "Threat model"
|
||||
defp kind_label("privacy_review"), do: "Privacy review"
|
||||
defp kind_label("business_logic"), do: "Business logic"
|
||||
defp kind_label("verify_findings"), do: "Verify findings"
|
||||
defp kind_label("write_fix"), do: "Write a fix"
|
||||
|
||||
defp provenance_options(task) do
|
||||
Enum.map(allowed_provenances(task), &{provenance_label(&1), &1})
|
||||
end
|
||||
|
||||
defp agent_primary_path?(%{capability: "agent"}), do: true
|
||||
defp agent_primary_path?(_task), do: false
|
||||
|
||||
defp allowed_provenances(%{capability: "human"}), do: ["human", "hybrid"]
|
||||
defp allowed_provenances(%{capability: "agent"}), do: ["agent", "hybrid"]
|
||||
defp allowed_provenances(%{capability: "hybrid"}), do: ["hybrid"]
|
||||
|
||||
defp short_sha(sha), do: String.slice(sha, 0, 7)
|
||||
|
||||
defp task_status(%{status: "claimed"} = task) do
|
||||
if ReviewTask.claim_active?(task), do: "Claimed", else: "Open"
|
||||
end
|
||||
|
||||
defp task_status(%{status: "changes_requested"}), do: "Changes requested"
|
||||
defp task_status(%{status: status}), do: String.capitalize(status)
|
||||
|
||||
defp visibility_label("restricted"), do: "Restricted"
|
||||
defp visibility_label("public_summary"), do: "Public summary"
|
||||
defp visibility_label("public"), do: "Full evidence public"
|
||||
|
||||
defp repository_path(task),
|
||||
do: TarakanWeb.RepositoryPaths.repository_path(task.repository)
|
||||
end
|
||||
576
lib/tarakan_web/live/review_task_live/show.html.heex
Normal file
576
lib/tarakan_web/live/review_task_live/show.html.heex
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
|
||||
<Layouts.page>
|
||||
<.breadcrumbs>
|
||||
<:crumb navigate={~p"/"}>registry</:crumb>
|
||||
<:crumb navigate={TarakanWeb.RepositoryPaths.repository_path(@task.repository)}>
|
||||
{@task.repository.owner}/{@task.repository.name}
|
||||
</:crumb>
|
||||
<:crumb>job/{@task.id}</:crumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<main id="review-task" class="border-2 border-strong">
|
||||
<header class="border-b-2 border-strong bg-panel px-5 py-5 sm:px-8 sm:py-6">
|
||||
<div class="flex flex-wrap items-center gap-2 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<span id="review-task-status" class="border border-rule px-2 py-1 text-ink-muted">
|
||||
{task_status(@task)}
|
||||
</span>
|
||||
<span
|
||||
:if={@task.linked_review_id && @task.linked_review}
|
||||
id="review-task-linked-review"
|
||||
class="border border-phosphor/40 px-2 py-1 text-phosphor"
|
||||
>
|
||||
Review #{@task.linked_review.id} · {@task.linked_review.findings_count} findings · {@task.linked_review.review_status}
|
||||
</span>
|
||||
<span
|
||||
:if={
|
||||
@task.status in ["submitted", "accepted"] and is_nil(@task.linked_review_id) and
|
||||
@task.kind != "write_fix"
|
||||
}
|
||||
id="review-task-historical-badge"
|
||||
class="border border-rule px-2 py-1 text-ink-faint"
|
||||
>
|
||||
Historical notes only - no structured Review
|
||||
</span>
|
||||
<span
|
||||
:if={@task.status in ["submitted", "accepted"] and @task.kind == "write_fix"}
|
||||
id="review-task-fix-badge"
|
||||
class="border border-phosphor/40 px-2 py-1 text-phosphor"
|
||||
>
|
||||
Patch proposal
|
||||
</span>
|
||||
<span
|
||||
:if={@task.status == "accepted"}
|
||||
id="review-task-visibility"
|
||||
class="border border-rule px-2 py-1 text-ink-muted"
|
||||
>
|
||||
{visibility_label(@task.visibility)}
|
||||
</span>
|
||||
<span>{kind_label(@task.kind)}</span>
|
||||
<span>·</span>
|
||||
<span>{provenance_label(@task.capability)} required</span>
|
||||
<span>·</span>
|
||||
<span title={@task.commit_sha}>{short_sha(@task.commit_sha)}</span>
|
||||
</div>
|
||||
<h1
|
||||
id="review-task-title"
|
||||
class="mt-3 font-display text-2xl uppercase leading-tight tracking-[0.02em] text-ink sm:text-3xl"
|
||||
>
|
||||
{@task.title}
|
||||
</h1>
|
||||
<p
|
||||
:if={String.trim(@task.description || "") != ""}
|
||||
class="mt-3 max-w-3xl whitespace-pre-line text-sm leading-6 text-ink-muted"
|
||||
phx-no-format
|
||||
>{String.trim(@task.description)}</p>
|
||||
<p class="mt-3 font-mono text-[11px] text-ink-faint">
|
||||
Opened by <span class="text-ink-muted">@{@task.created_by.handle}</span>
|
||||
· commit <span title={@task.commit_sha}>{short_sha(@task.commit_sha)}</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section
|
||||
:if={@task.kind == "verify_findings"}
|
||||
id="review-task-target-report"
|
||||
class="border-b border-rule bg-ground px-5 py-5 sm:px-8"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<span>Target report #{@task.target_review_id}</span>
|
||||
<%= if @visible_target_review do %>
|
||||
<span>·</span>
|
||||
<span>{@visible_target_review.findings_count} findings</span>
|
||||
<span>·</span>
|
||||
<span>{@visible_target_review.review_status}</span>
|
||||
<span>·</span>
|
||||
<span title={@visible_target_review.commit_sha}>{short_sha(
|
||||
@visible_target_review.commit_sha
|
||||
)}</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm leading-6 text-ink-muted">
|
||||
Reproduce or dispute every visible finding against the target report's exact commit.
|
||||
</p>
|
||||
<ul
|
||||
:if={@visible_target_review && @visible_target_review.details_visible}
|
||||
class="mt-4 space-y-2"
|
||||
>
|
||||
<li
|
||||
:for={finding <- @visible_target_review.findings}
|
||||
class="border-l-2 border-rule pl-3 text-xs leading-5 text-ink-muted"
|
||||
>
|
||||
<span class="font-mono text-[10px] uppercase text-ink-faint">
|
||||
{finding.severity} · {finding.file_path}:{finding.line_start}
|
||||
</span>
|
||||
<span class="ml-2 font-semibold text-ink">{finding.title}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p
|
||||
:if={is_nil(@visible_target_review) || !@visible_target_review.details_visible}
|
||||
class="mt-3 text-xs leading-5 text-ink-faint"
|
||||
>
|
||||
Finding details are restricted until an authorized contributor claims the check.
|
||||
</p>
|
||||
<div
|
||||
:if={@visible_target_review && @visible_target_review.confirmations != []}
|
||||
id="review-task-target-confirmations"
|
||||
class="mt-5 border-t border-rule pt-4"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
Recorded checks
|
||||
</h3>
|
||||
<article
|
||||
:for={confirmation <- @visible_target_review.confirmations}
|
||||
class="mt-3 border-l-2 border-phosphor pl-4"
|
||||
>
|
||||
<p class="font-mono text-[11px] text-ink-muted">
|
||||
{confirmation.verdict} · {provenance_label(confirmation.provenance)}
|
||||
<span :if={confirmation.account}> · @{confirmation.account.handle}</span>
|
||||
</p>
|
||||
<p
|
||||
:if={confirmation.notes}
|
||||
class="mt-2 whitespace-pre-line text-xs leading-5 text-ink-muted"
|
||||
phx-no-format
|
||||
>{confirmation.notes}</p>
|
||||
<pre
|
||||
:if={confirmation.evidence}
|
||||
class="mt-2 overflow-x-auto whitespace-pre-wrap border border-rule bg-panel p-3 font-mono text-[11px] leading-5 text-ink"
|
||||
>{confirmation.evidence}</pre>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.status == "proposed"}
|
||||
id="review-task-proposal"
|
||||
class="px-5 py-6 sm:px-8"
|
||||
>
|
||||
<%= cond do %>
|
||||
<% can_publish?(@task, @current_scope) -> %>
|
||||
<p class="mb-5 max-w-2xl text-sm leading-6 text-ink-muted">
|
||||
Confirm the scope is safe and useful before it enters the public job queue.
|
||||
Stewards and moderators who open jobs from a repository page usually skip this step -
|
||||
it auto-publishes with a default reason.
|
||||
</p>
|
||||
<.form for={@decision_form} id="publish-review-task-form" phx-submit="publish">
|
||||
<.input
|
||||
field={@decision_form[:reason]}
|
||||
type="textarea"
|
||||
label="Approval reason (pre-filled - edit if you want)"
|
||||
placeholder="Why is this job safe, scoped, and useful?"
|
||||
/>
|
||||
<button
|
||||
id="publish-review-task-button"
|
||||
type="submit"
|
||||
class="clip-notch mt-5 bg-btn px-5 py-2.5 font-display text-xs uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90"
|
||||
>
|
||||
Publish job
|
||||
</button>
|
||||
</.form>
|
||||
<% created_by_current_account?(@task, @current_scope) -> %>
|
||||
<p id="review-task-proposed-notice" class="text-sm text-ink-muted">
|
||||
Awaiting a steward or moderator to publish this job.
|
||||
</p>
|
||||
<% true -> %>
|
||||
<p class="text-sm text-ink-muted">
|
||||
This proposal is awaiting a steward or moderator.
|
||||
</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.status in ["open", "claimed", "changes_requested"]}
|
||||
id="review-task-actions"
|
||||
class="px-5 py-6 sm:px-8"
|
||||
>
|
||||
<%= cond do %>
|
||||
<% owns_claim?(@task, @current_scope) -> %>
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-3 border-b border-rule pb-5">
|
||||
<p class="text-sm text-ink-muted">
|
||||
You hold this job until <span class="font-mono text-ink">{Calendar.strftime(@task.claim_expires_at, "%H:%M UTC")}</span>.
|
||||
</p>
|
||||
<button
|
||||
id="release-review-task-button"
|
||||
phx-click="release"
|
||||
class="font-mono text-xs text-ink-faint transition hover:text-signal"
|
||||
>
|
||||
Release claim
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%= cond do %>
|
||||
<% @task.kind == "verify_findings" and agent_primary_path?(@task) -> %>
|
||||
<div id="review-task-agent-path" class="space-y-4">
|
||||
<p class="text-sm leading-6 text-ink-muted">
|
||||
Run an independent agent check against every finding in the target report.
|
||||
</p>
|
||||
<div class="border border-strong bg-panel px-4 py-4 font-mono text-xs leading-6 text-ink">
|
||||
<p class="text-xs font-semibold text-ink-muted">CLI</p>
|
||||
<pre class="mt-2 overflow-x-auto whitespace-pre-wrap">tarakan worker --agent grok --once --jobs-only</pre>
|
||||
</div>
|
||||
</div>
|
||||
<% @task.kind == "write_fix" and agent_primary_path?(@task) -> %>
|
||||
<div id="review-task-agent-path" class="space-y-4">
|
||||
<p class="text-sm leading-6 text-ink-muted">
|
||||
The autonomous worker will produce a reviewable unified diff and test plan without modifying the repository.
|
||||
</p>
|
||||
<div class="border border-strong bg-panel px-4 py-4 font-mono text-xs leading-6 text-ink">
|
||||
<p class="text-xs font-semibold text-ink-muted">CLI</p>
|
||||
<pre class="mt-2 overflow-x-auto whitespace-pre-wrap">tarakan worker --agent grok --once --jobs-only</pre>
|
||||
</div>
|
||||
</div>
|
||||
<% agent_primary_path?(@task) -> %>
|
||||
<div id="review-task-agent-path" class="space-y-4">
|
||||
<p class="text-sm leading-6 text-ink-muted">
|
||||
This job requires <span class="font-semibold text-ink">agent</span> work.
|
||||
</p>
|
||||
<div class="border border-strong bg-panel px-4 py-4 font-mono text-xs leading-6 text-ink">
|
||||
<p class="text-xs font-semibold text-ink-muted">CLI</p>
|
||||
<pre class="mt-2 overflow-x-auto whitespace-pre-wrap">tarakan report --agent grok --job {@task.id} --yes
|
||||
# or: tarakan report --agent grok --job {@task.id} --interactive</pre>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-ink-faint">
|
||||
Agents: <span class="font-mono text-ink">grok</span>, <span class="font-mono text-ink">codex</span>, <span class="font-mono text-ink">claude</span>, <span class="font-mono text-ink">kimi</span>.
|
||||
</p>
|
||||
</div>
|
||||
<% @task.kind == "verify_findings" -> %>
|
||||
<.form
|
||||
for={@verification_form}
|
||||
id="review-task-verification-form"
|
||||
phx-submit="complete_verification"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<.input
|
||||
field={@verification_form[:provenance]}
|
||||
type="select"
|
||||
label="How was this check produced?"
|
||||
options={@provenance_options}
|
||||
/>
|
||||
<.input
|
||||
field={@verification_form[:verdict]}
|
||||
type="select"
|
||||
label="Overall check"
|
||||
options={[{"Confirmed", "confirmed"}, {"Disputed", "disputed"}]}
|
||||
/>
|
||||
<.input
|
||||
field={@verification_form[:notes]}
|
||||
type="textarea"
|
||||
label="Check notes"
|
||||
placeholder="Explain what was reproduced or why the report is incorrect."
|
||||
/>
|
||||
<.input
|
||||
field={@verification_form[:evidence]}
|
||||
type="textarea"
|
||||
label="Proof of concept or counter-evidence"
|
||||
placeholder="Commands, exact traces, observed output, and relevant file:line references"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="complete-review-task-button"
|
||||
type="submit"
|
||||
class="clip-notch mt-5 bg-btn px-5 py-2.5 font-display text-xs uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90 phx-submit-loading:opacity-60"
|
||||
>
|
||||
Submit check
|
||||
</button>
|
||||
</.form>
|
||||
<% true -> %>
|
||||
<.form
|
||||
for={@contribution_form}
|
||||
id="review-task-completion-form"
|
||||
phx-change="validate_contribution"
|
||||
phx-submit="complete"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<.input
|
||||
field={@contribution_form[:provenance]}
|
||||
type="select"
|
||||
label="How was this work produced?"
|
||||
options={@provenance_options}
|
||||
/>
|
||||
<.input
|
||||
field={@contribution_form[:summary]}
|
||||
type="textarea"
|
||||
label={
|
||||
if @task.kind == "write_fix", do: "Fix summary", else: "Result summary"
|
||||
}
|
||||
placeholder={
|
||||
if @task.kind == "write_fix",
|
||||
do: "What does the proposed patch change and why?",
|
||||
else: "What did you establish, reproduce, or refute?"
|
||||
}
|
||||
/>
|
||||
<.input
|
||||
field={@contribution_form[:evidence]}
|
||||
type="textarea"
|
||||
label={
|
||||
if @task.kind == "write_fix",
|
||||
do: "Proposed unified diff and test plan",
|
||||
else: "Evidence and reproduction notes"
|
||||
}
|
||||
placeholder={
|
||||
if @task.kind == "write_fix",
|
||||
do: "Paste a unified diff followed by tests that should prove the fix",
|
||||
else:
|
||||
"File paths, test commands, observations, and remaining uncertainty"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="complete-review-task-button"
|
||||
type="submit"
|
||||
class="clip-notch mt-5 bg-btn px-5 py-2.5 font-display text-xs uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90 phx-submit-loading:opacity-60"
|
||||
>
|
||||
Submit job result
|
||||
</button>
|
||||
</.form>
|
||||
<% end %>
|
||||
<% can_claim?(@task, @current_scope) -> %>
|
||||
<p class="max-w-2xl text-sm leading-6 text-ink-muted">
|
||||
Claiming reserves this job for two hours. Creators may claim and perform their own Jobs.
|
||||
</p>
|
||||
<button
|
||||
id="claim-review-task-button"
|
||||
phx-click="claim"
|
||||
class="clip-notch mt-4 bg-btn px-5 py-2.5 font-display text-xs uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90"
|
||||
>
|
||||
Claim job
|
||||
</button>
|
||||
<% @task.claimed_by && ReviewTask.claim_active?(@task) -> %>
|
||||
<p id="review-task-claimed-notice" class="text-sm text-ink-muted">
|
||||
Claimed by <span class="font-semibold text-ink">@{@task.claimed_by.handle}</span>.
|
||||
</p>
|
||||
<% true -> %>
|
||||
<.link
|
||||
navigate={~p"/accounts/log-in"}
|
||||
class="font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Sign in to claim this task.
|
||||
</.link>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.status == "submitted"}
|
||||
id="review-task-review"
|
||||
class="border-t border-rule px-5 py-6 sm:px-8"
|
||||
>
|
||||
<%= if can_review?(@task, @current_scope) do %>
|
||||
<h2 class="font-display text-xl uppercase tracking-[0.02em] text-ink">
|
||||
Independent review
|
||||
</h2>
|
||||
<.form
|
||||
for={@decision_form}
|
||||
id="review-task-decision-form"
|
||||
phx-submit="review"
|
||||
class="mt-5"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<.input field={@decision_form[:reason]} type="textarea" label="Decision reason" />
|
||||
<.input
|
||||
field={@decision_form[:evidence]}
|
||||
type="textarea"
|
||||
label="Independent evidence"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex flex-wrap gap-3">
|
||||
<button
|
||||
id="accept-review-task-button"
|
||||
type="submit"
|
||||
name="action"
|
||||
value="accept"
|
||||
class="clip-notch bg-btn px-4 py-2 font-display text-xs uppercase tracking-[0.12em] text-btn-fg"
|
||||
>
|
||||
Close request
|
||||
</button>
|
||||
<button
|
||||
id="request-changes-review-task-button"
|
||||
type="submit"
|
||||
name="action"
|
||||
value="request_changes"
|
||||
class="border border-rule px-4 py-2 font-display text-xs uppercase tracking-[0.12em] text-ink"
|
||||
>
|
||||
Request changes
|
||||
</button>
|
||||
<button
|
||||
id="reject-review-task-button"
|
||||
type="submit"
|
||||
name="action"
|
||||
value="reject"
|
||||
class="border border-signal px-4 py-2 font-display text-xs uppercase tracking-[0.12em] text-signal"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
<% else %>
|
||||
<p id="review-task-review-pending" class="text-sm text-ink-muted">
|
||||
Evidence submitted. Awaiting independent review.
|
||||
</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.status == "accepted"}
|
||||
id="review-task-disclosure"
|
||||
class="border-t border-rule px-5 py-6 sm:px-8"
|
||||
>
|
||||
<%= if can_disclose?(@task, @current_scope) do %>
|
||||
<h2 class="font-display text-xl uppercase tracking-[0.02em] text-ink">
|
||||
Disclosure decision
|
||||
</h2>
|
||||
<.form
|
||||
for={@disclosure_form}
|
||||
id="review-task-disclosure-form"
|
||||
phx-submit="disclose"
|
||||
class="mt-5"
|
||||
>
|
||||
<.input
|
||||
field={@disclosure_form[:reason]}
|
||||
type="textarea"
|
||||
label="Disclosure reason"
|
||||
placeholder="Why is this result safe and useful to disclose?"
|
||||
/>
|
||||
<div :if={full_disclosure_allowed?(@task)} class="mt-4">
|
||||
<.input
|
||||
field={@disclosure_form[:sensitive_data_reviewed]}
|
||||
type="checkbox"
|
||||
label="I checked the evidence for secrets, credentials, and personal data"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-5 flex flex-wrap gap-3">
|
||||
<button
|
||||
id="publish-review-task-summary-button"
|
||||
type="submit"
|
||||
name="visibility"
|
||||
value="public_summary"
|
||||
class="clip-notch bg-btn px-4 py-2 font-display text-xs uppercase tracking-[0.12em] text-btn-fg"
|
||||
>
|
||||
Publish summary
|
||||
</button>
|
||||
<button
|
||||
:if={full_disclosure_allowed?(@task)}
|
||||
id="publish-review-task-full-button"
|
||||
type="submit"
|
||||
name="visibility"
|
||||
value="public"
|
||||
class="border border-rule px-4 py-2 font-display text-xs uppercase tracking-[0.12em] text-ink"
|
||||
>
|
||||
Publish full evidence
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
<% else %>
|
||||
<p id="review-task-disclosure-pending" class="text-sm text-ink-muted">
|
||||
<%= if @task.visibility == "restricted" do %>
|
||||
Accepted. Awaiting a disclosure decision.
|
||||
<% else %>
|
||||
This result has an attributable disclosure decision.
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.linked_review}
|
||||
id="review-task-linked-review-detail"
|
||||
class="border-t border-rule px-5 py-6 sm:px-8 sm:py-8"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<span>Linked Review #{@task.linked_review.id}</span>
|
||||
<span>·</span>
|
||||
<span>{@task.linked_review.review_status}</span>
|
||||
<span>·</span>
|
||||
<span>{provenance_label(@task.linked_review.provenance)}</span>
|
||||
</div>
|
||||
<h2 class="mt-5 font-display text-xl uppercase tracking-[0.02em] text-ink">
|
||||
Structured findings
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-ink-muted">
|
||||
Request submitted. The linked report remains separately {@task.linked_review.review_status} until its findings are independently checked - closing this request does not accept the report.
|
||||
</p>
|
||||
<ul class="mt-5 space-y-3">
|
||||
<li
|
||||
:for={finding <- @task.linked_review.findings || []}
|
||||
class="border border-rule px-4 py-3"
|
||||
>
|
||||
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
{finding.severity} · {finding.file_path}:{finding.line_start}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-semibold text-ink">{finding.title}</div>
|
||||
<p class="mt-1 text-xs leading-5 text-ink-muted">{finding.description}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={@task.contribution}
|
||||
id="review-task-contribution"
|
||||
class="border-t border-rule px-5 py-6 sm:px-8 sm:py-8"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<span>
|
||||
<%= cond do %>
|
||||
<% @task.kind == "write_fix" -> %>
|
||||
Patch proposal v{@task.contribution.version} by @{@task.contribution.account.handle}
|
||||
<% @task.linked_review_id -> %>
|
||||
Notes v{@task.contribution.version} by @{@task.contribution.account.handle}
|
||||
<% true -> %>
|
||||
Historical notes v{@task.contribution.version} by @{@task.contribution.account.handle}
|
||||
<% end %>
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{provenance_label(@task.contribution.provenance)}</span>
|
||||
</div>
|
||||
<h2 class="mt-5 font-display text-xl uppercase tracking-[0.02em] text-ink">
|
||||
<%= cond do %>
|
||||
<% @task.kind == "write_fix" -> %>
|
||||
Proposed fix
|
||||
<% @task.linked_review_id -> %>
|
||||
Result
|
||||
<% true -> %>
|
||||
Historical notes
|
||||
<% end %>
|
||||
</h2>
|
||||
<p
|
||||
:if={is_nil(@task.linked_review_id) and @task.kind != "write_fix"}
|
||||
class="mt-2 text-xs text-ink-faint"
|
||||
>
|
||||
Free-text evidence only - no Findings on this submission.
|
||||
</p>
|
||||
<p :if={@task.kind == "write_fix"} class="mt-2 text-xs text-ink-faint">
|
||||
Reviewable patch artifact only - Tarakan did not modify or push to the repository.
|
||||
</p>
|
||||
<p class="mt-3 whitespace-pre-line text-sm leading-6 text-ink-muted" phx-no-format>{@task.contribution.summary}</p>
|
||||
<div :if={@task.contribution.evidence} class="mt-6 border-l-2 border-rule pl-5">
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
{if @task.kind == "write_fix", do: "Proposed patch and tests", else: "Evidence"}
|
||||
</h3>
|
||||
<pre
|
||||
class="mt-2 overflow-x-auto whitespace-pre-wrap font-mono text-xs leading-6 text-ink-muted"
|
||||
phx-no-format
|
||||
>{@task.contribution.evidence}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
:if={can_cancel?(@task, @current_scope)}
|
||||
id="review-task-cancellation"
|
||||
class="border-t border-rule px-5 py-6 sm:px-8"
|
||||
>
|
||||
<.form for={@decision_form} id="cancel-review-task-form" phx-submit="cancel">
|
||||
<.input
|
||||
field={@decision_form[:reason]}
|
||||
type="textarea"
|
||||
label="Cancellation reason"
|
||||
placeholder="Explain why this work should be withdrawn."
|
||||
/>
|
||||
<button
|
||||
id="cancel-review-task-button"
|
||||
type="submit"
|
||||
class="mt-4 font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Cancel task
|
||||
</button>
|
||||
</.form>
|
||||
</section>
|
||||
</main>
|
||||
</Layouts.page>
|
||||
</Layouts.app>
|
||||
Loading…
Add table
Add a link
Reference in a new issue