Packages
A Fair Source multi-agent runtime with deterministic agent scoring and replayable run history.
Retired package: Deprecated - superseded — operator console moved to the Syntropy app
Current section
Files
Jump to
Current section
Files
lib/syntropy_web/live/history_live.ex
defmodule SyntropyWeb.HistoryLive do
use SyntropyWeb, :live_view
import SyntropyWeb.RuntimeViewHelpers
alias Syntropy.{HistoryStore, RecommendationStore, TaskScheduler}
@history_limit 20
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Syntropy.PubSub, "lattice:events")
end
{:ok,
assign(socket,
snapshots: [],
selected_snapshot_id: nil,
selected_snapshot: nil,
selected_task: nil,
selected_recommendation: nil
)}
end
@impl true
def handle_params(params, _uri, socket) do
{:noreply, load_history(socket, Map.get(params, "snapshot"))}
end
@impl true
def handle_info({:lattice_event, _event}, socket) do
{:noreply, load_history(socket, socket.assigns.selected_snapshot_id)}
end
@impl true
def render(assigns) do
~H"""
<main class="dashboard">
<section class="dashboard-header glass-panel p-4">
<div>
<h1>Replay history</h1>
<p class="body-copy">Retained lattice snapshots and their replay evidence.</p>
</div>
<nav class="dashboard-nav">
<.link navigate={~p"/"} class="text-link">Lattice</.link>
<span> · </span>
<.link navigate={~p"/tasks"} class="text-link">Tasks</.link>
<span> · </span>
<.link navigate={~p"/operations"} class="text-link">Operations</.link>
</nav>
</section>
<section class="two-column-grid mt-4">
<article class="glass-panel detail-panel" style="grid-column: span 1;">
<header class="section-heading">
<div>
<h2>Snapshots</h2>
</div>
</header>
<%= if @snapshots == [] do %>
<div class="empty-panel">
<p id="history-empty" class="body-copy">No snapshots recorded yet.</p>
</div>
<% else %>
<ul id="history-list" class="task-list">
<%= for snapshot <- @snapshots do %>
<li id={"snapshot-#{snapshot.id}"} class={"task-list-item " <> if @selected_snapshot_id == snapshot.id, do: "is-selected", else: ""}>
<div class="flex-row" style="justify-content: space-between; margin-bottom: 4px;">
<.link patch={~p"/history?snapshot=#{snapshot.id}"} class="text-link task-title">
<strong><%= snapshot.id %></strong>
</.link>
<span class="meta-copy">node <%= node_id_for(snapshot) %></span>
</div>
<div class="task-copy task-list-meta">
<span>trigger <%= snapshot.trigger_kind %></span>
<%= if snapshot.task_id do %>
<span> · task <%= snapshot.task_id %></span>
<% end %>
<%= if snapshot.proposal_id do %>
<span> · prop <%= snapshot.proposal_id %></span>
<% end %>
<span>
·
<.link navigate={graph_path(snapshot.id, List.first(snapshot.ranked_agent_ids))} class="text-link">
view in graph
</.link>
</span>
</div>
</li>
<% end %>
</ul>
<% end %>
</article>
<article class="glass-panel detail-panel" style="grid-column: span 1;">
<header class="section-heading">
<div>
<h2>Snapshot detail</h2>
</div>
</header>
<%= if @selected_snapshot do %>
<div id="snapshot-detail" class="flex-col">
<div class="two-column-grid mb-4">
<div class="meta-item">
<span class="meta-label">ID</span>
<strong><%= @selected_snapshot.id %></strong>
</div>
<div class="meta-item">
<span class="meta-label">Node</span>
<strong><%= node_id_for(@selected_snapshot) %></strong>
</div>
<div class="meta-item">
<span class="meta-label">Trigger</span>
<strong><%= @selected_snapshot.trigger_kind %></strong>
</div>
<%= if @selected_snapshot.task_id do %>
<div class="meta-item">
<span class="meta-label">Task</span>
<strong><%= @selected_snapshot.task_id %></strong>
</div>
<% end %>
<%= if @selected_snapshot.proposal_id do %>
<div class="meta-item">
<span class="meta-label">Proposal</span>
<strong><%= @selected_snapshot.proposal_id %></strong>
</div>
<% end %>
</div>
<%= if @selected_task do %>
<div class="mb-4">
<h3 class="meta-label">Requested vs resolved</h3>
<p class="body-copy">
<%= @selected_task.strategy %> -> <%= @selected_task.resolved_mode %>
· node <%= node_id_for(@selected_task) %>
</p>
</div>
<% end %>
<%= if @selected_recommendation do %>
<div class="mb-4">
<h3 class="meta-label">Recommendation</h3>
<p class="body-copy">
<%= @selected_recommendation.kind %>
· <%= @selected_recommendation.status %>
· node <%= node_id_for(@selected_recommendation) %>
</p>
</div>
<% end %>
<div class="mb-4">
<h3 class="meta-label">Ranked agents</h3>
<p class="body-copy"><%= Enum.join(@selected_snapshot.ranked_agent_ids, " > ") %></p>
</div>
<div class="mb-4">
<h3 class="meta-label">Graph</h3>
<p class="body-copy">
<.link
navigate={graph_path(@selected_snapshot.id, List.first(@selected_snapshot.ranked_agent_ids))}
class="text-link"
>
Open snapshot in mission control
</.link>
</p>
</div>
<div>
<h3 class="meta-label">Agents</h3>
<ul id="snapshot-agents" class="task-list">
<%= for agent <- @selected_snapshot.agents do %>
<li id={"snapshot-agent-#{agent.id}"} class="task-list-item flex-col" style="padding: 12px 16px;">
<div class="flex-row" style="justify-content: space-between; margin-bottom: 4px;">
<.link navigate={~p"/agent/#{agent.id}"} class="text-link"><strong><%= agent.name %></strong></.link>
<span class="meta-label">
<.link navigate={graph_path(@selected_snapshot.id, agent.id)} class="text-link">graph</.link>
</span>
</div>
<div class="task-copy task-list-meta">
<span>pos <%= format_score(agent.position) %></span>
<span> · <%= agent.perspective %></span>
<span><%= if agent.temporary, do: " · temporary", else: "" %></span>
</div>
<%= if agent.connections != [] do %>
<details class="mt-2">
<summary class="meta-label" style="cursor: pointer;">Connections</summary>
<ul class="body-copy mt-2" style="list-style-type: none; padding-left: 0;">
<%= for connection <- agent.connections do %>
<li style="margin-bottom: 4px;">
<span><%= connection.target_agent_id %></span>
<span> · weight <%= format_score(connection.weight) %></span>
<span> · <%= connection.interactions %> interactions</span>
</li>
<% end %>
</ul>
</details>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
<% else %>
<div class="empty-panel">
<p id="snapshot-detail-empty" class="body-copy">Select a snapshot to inspect it.</p>
</div>
<% end %>
</article>
</section>
</main>
"""
end
defp load_history(socket, selected_snapshot_id) do
snapshots = HistoryStore.recent(@history_limit)
selected_snapshot = pick_snapshot(snapshots, selected_snapshot_id)
assign(socket,
snapshots: snapshots,
selected_snapshot_id: selected_snapshot && selected_snapshot.id,
selected_snapshot: selected_snapshot,
selected_task: selected_snapshot && snapshot_task(selected_snapshot),
selected_recommendation: selected_snapshot && snapshot_recommendation(selected_snapshot)
)
end
defp pick_snapshot([], _selected_snapshot_id), do: nil
defp pick_snapshot(snapshots, nil), do: List.first(snapshots)
defp pick_snapshot(snapshots, selected_snapshot_id) do
Enum.find(snapshots, &(&1.id == selected_snapshot_id)) || List.first(snapshots)
end
defp snapshot_task(%{task_id: nil}), do: nil
defp snapshot_task(snapshot) do
Enum.find(TaskScheduler.recent(@history_limit), &(&1.task_id == snapshot.task_id))
end
defp snapshot_recommendation(%{proposal_id: nil}), do: nil
defp snapshot_recommendation(snapshot) do
RecommendationStore.get(snapshot.proposal_id)
end
defp graph_path(snapshot_id, agent_id) do
params =
%{}
|> maybe_put("snapshot", snapshot_id)
|> maybe_put("agent", agent_id)
~p"/?#{params}"
end
defp maybe_put(params, _key, nil), do: params
defp maybe_put(params, key, value), do: Map.put(params, key, value)
end