defmodule BDS.UI.TaskGrouping do @moduledoc """ Groups task snapshot entries for the bottom panel's Tasks tab, mirroring the old app's `taskGrouping.ts`: tasks sharing a `group_id` collapse into one group entry (ordered by first appearance), everything else stays a single entry. Groups summarize to per-status counts plus an aggregate progress in the 0.0..1.0 range. """ @type task :: map() @type entry :: {:single, task()} | {:group, String.t(), String.t(), [task()]} @type summary :: %{ total: non_neg_integer(), running: non_neg_integer(), pending: non_neg_integer(), completed: non_neg_integer(), failed: non_neg_integer(), cancelled: non_neg_integer(), progress: float() } @spec build_task_entries([task()]) :: [entry()] def build_task_entries(tasks) when is_list(tasks) do {singles, groups} = tasks |> Enum.with_index() |> Enum.reduce({[], %{}}, fn {task, index}, {singles, groups} -> case Map.get(task, :group_id) do nil -> {[{index, task} | singles], groups} group_id -> groups = Map.update(groups, group_id, {index, Map.get(task, :group_name) || group_id, [task]}, fn {first_index, name, grouped} -> {first_index, name, [task | grouped]} end) {singles, groups} end end) single_entries = Enum.map(singles, fn {index, task} -> {index, {:single, task}} end) group_entries = Enum.map(groups, fn {group_id, {index, name, grouped}} -> {index, {:group, group_id, name, Enum.reverse(grouped)}} end) (single_entries ++ group_entries) |> Enum.sort_by(fn {index, _entry} -> index end) |> Enum.map(fn {_index, entry} -> entry end) end @spec summarize_task_group([task()]) :: summary() def summarize_task_group(tasks) when is_list(tasks) do base = %{total: 0, running: 0, pending: 0, completed: 0, failed: 0, cancelled: 0, progress: 0.0} summary = Enum.reduce(tasks, base, fn task, acc -> status = Map.get(task, :status) acc |> Map.update!(:total, &(&1 + 1)) |> count_status(status) |> Map.update!(:progress, &(&1 + progress_contribution(task))) end) if summary.total > 0 do %{summary | progress: summary.progress / summary.total} else summary end end defp count_status(acc, status) when status in [:running, :pending, :completed, :failed, :cancelled], do: Map.update!(acc, status, &(&1 + 1)) defp count_status(acc, _status), do: acc # Finished tasks count as fully done no matter their outcome, pending tasks # contribute nothing yet (old-app getProgressContribution). defp progress_contribution(%{status: status}) when status in [:completed, :failed, :cancelled], do: 1.0 defp progress_contribution(%{status: :pending}), do: 0.0 defp progress_contribution(%{progress: progress}) when is_number(progress), do: progress |> max(0.0) |> min(1.0) defp progress_contribution(_task), do: 0.0 end