defmodule SyntropyWeb.OperationsLive do @moduledoc """ Mission-control operations: agent deploy/remove, webhook administration, and runtime provider configuration — the same controls the REST API and platform cockpit expose, available directly in the LiveView. """ use SyntropyWeb, :live_view import SyntropyWeb.RuntimeViewHelpers alias Syntropy.{ ClusterInventory, Contract, EventRecorder, LatticeSupervisor, RuntimeConfig, TaskScheduler, Usage } alias Syntropy.Webhooks.Registry, as: WebhookRegistry alias SyntropyWeb.Api.AgentRequest @impl true def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(Syntropy.PubSub, "lattice:events") end {:ok, socket |> assign( perspective_presets: perspective_presets(), agent_feedback: nil, webhook_feedback: nil, new_webhook_secret: nil, provider_feedback: nil, provider_test: nil, limits_feedback: nil ) |> refresh_operations()} end @impl true def handle_info({:lattice_event, _event}, socket) do {:noreply, refresh_operations(socket)} end @impl true def handle_event("deploy_agent", %{"agent" => agent_params}, socket) do request = %{ "name" => Map.get(agent_params, "name"), "perspective" => Map.get(agent_params, "perspective"), "knowledge_items" => split_list(Map.get(agent_params, "knowledge_items", "")) } with {:ok, parsed} <- AgentRequest.parse(request), {:ok, agent_id} <- LatticeSupervisor.add_agent(parsed.name, parsed.perspective, parsed.opts) do agent = LatticeSupervisor.get_agent(agent_id) EventRecorder.record("agent_created", %{ agent_id: agent.id, runtime_id: agent.runtime_id, node_id: agent.node_id, agent_name: agent.name, perspective: agent.perspective, source: "cockpit" }) {:noreply, socket |> assign(agent_feedback: {:ok, "Deployed #{agent.name} (#{agent.perspective})."}) |> refresh_operations()} else {:error, errors} when is_list(errors) -> {:noreply, assign(socket, agent_feedback: {:error, agent_error_text(errors)})} {:error, _reason} -> {:noreply, assign(socket, agent_feedback: {:error, "The agent could not be started."})} end end @impl true def handle_event("remove_agent", %{"id" => agent_id}, socket) do case LatticeSupervisor.get_agent(agent_id) do nil -> {:noreply, assign(socket, agent_feedback: {:error, "Agent #{agent_id} was not found."})} agent -> if agent.runtime_id in TaskScheduler.active_agent_ids() do {:noreply, assign(socket, agent_feedback: {:error, "Agent #{agent_id} is executing an active run and cannot be removed."} )} else remove_agent(socket, agent) end end end @impl true def handle_event("create_webhook", %{"webhook" => webhook_params}, socket) do attrs = %{ "url" => Map.get(webhook_params, "url"), "events" => split_list(Map.get(webhook_params, "events", "")) } case WebhookRegistry.register(attrs) do {:ok, webhook} -> {:noreply, socket |> assign( webhook_feedback: {:ok, "Webhook registered. Copy the signing secret now:"}, new_webhook_secret: webhook.secret ) |> refresh_operations()} {:error, errors} -> {:noreply, assign(socket, webhook_feedback: {:error, agent_error_text(errors)}, new_webhook_secret: nil )} end end @impl true def handle_event("toggle_webhook", %{"id" => webhook_id, "active" => active}, socket) do case WebhookRegistry.set_active(webhook_id, active == "true") do {:ok, webhook} -> verb = if webhook.active, do: "resumed", else: "paused" {:noreply, socket |> assign(webhook_feedback: {:ok, "Deliveries #{verb} for #{webhook.url}."}) |> refresh_operations()} {:error, :not_found} -> {:noreply, assign(socket, webhook_feedback: {:error, "Webhook #{webhook_id} was not found."})} end end @impl true def handle_event("delete_webhook", %{"id" => webhook_id}, socket) do case WebhookRegistry.remove(webhook_id) do :ok -> {:noreply, socket |> assign(webhook_feedback: {:ok, "Webhook removed."}, new_webhook_secret: nil) |> refresh_operations()} {:error, :not_found} -> {:noreply, assign(socket, webhook_feedback: {:error, "Webhook #{webhook_id} was not found."})} end end @impl true def handle_event("apply_provider", %{"provider" => provider_params}, socket) do case RuntimeConfig.apply_provider(provider_params) do {:ok, _report} -> {:noreply, socket |> assign( provider_feedback: {:ok, "Provider configuration applied."}, provider_test: nil ) |> refresh_operations()} {:error, errors} -> {:noreply, assign(socket, provider_feedback: {:error, Enum.join(errors, " ")})} end end @impl true def handle_event("test_provider", %{"provider" => provider_params}, socket) do {:noreply, assign(socket, provider_test: RuntimeConfig.test_provider(provider_params))} end @impl true def handle_event("apply_budget", %{"limits" => %{"usage_budget_tokens" => tokens}}, socket) do case Usage.apply_budget(tokens) do {:ok, _budget} -> {:noreply, socket |> assign(limits_feedback: {:ok, "Usage budget updated."}) |> refresh_operations()} {:error, message} -> {:noreply, assign(socket, limits_feedback: {:error, message})} end end @impl true def render(assigns) do ~H"""

Operations

Agent deployment, webhooks, and runtime configuration.

Provider: <%= @runtime_report.provider.provider %> Config: <%= if @runtime_report.provider.valid, do: "valid", else: "invalid" %> Durability: <%= humanize_status(@runtime_report.persistence.status) %>

Agents

Deploy and remove lattice agents. Agents executing an active run cannot be removed.

<%= if @agent_feedback do %>

<%= feedback_text(@agent_feedback) %>

<% end %> <%= if @agents == [] do %>

No agents are deployed. Deploy your first agent above to start the lattice.

<% else %> <% end %>

Webhooks

Signed runtime-event deliveries. Use * to subscribe to every event.

<%= if @webhook_feedback do %>

<%= feedback_text(@webhook_feedback) %>

<% end %> <%= if @new_webhook_secret do %>

<%= @new_webhook_secret %> — shown once; deliveries are signed with this secret.

<% end %> <%= if @webhooks == [] do %>

No webhooks registered. Register one to receive signed runtime events.

<% else %> <% end %>

Runtime configuration

Applied changes take effect immediately for subsequent LLM calls. Scope: <%= @runtime_report.provider.applied_scope %>.

Provider <%= @runtime_report.provider.provider %>
Model <%= @runtime_report.provider.model || "—" %>
Base URL <%= @runtime_report.provider.base_url || "—" %>
API key <%= if @runtime_report.provider.api_key_configured, do: "configured", else: "not set" %>
<%= if @runtime_report.provider.validation_errors != [] do %>

<%= Enum.join(@runtime_report.provider.validation_errors, " ") %>

<% end %>
<%= if @provider_feedback do %>

<%= feedback_text(@provider_feedback) %>

<% end %> <%= if @provider_test do %>

Probe: <%= @provider_test.status %> · <%= @provider_test.provider %> <%= if @provider_test.model do %> · <%= @provider_test.model %> <% end %>

<% end %>
Env export for a permanent configuration
<%= @runtime_report.provider.env_export %>

Fair-use budget

<%= budget_summary(@runtime_report.limits.usage_budget) %>

<%= if @limits_feedback do %>

<%= feedback_text(@limits_feedback) %>

<% end %>
""" end defp refresh_operations(socket) do assign(socket, agents: ClusterInventory.cluster_agents(), busy_runtime_ids: ClusterInventory.busy_runtime_ids(TaskScheduler.active_tasks_snapshot()), webhooks: WebhookRegistry.list(), runtime_report: RuntimeConfig.report() ) end defp remove_agent(socket, agent) do case LatticeSupervisor.remove_agent(agent.id) do :ok -> EventRecorder.record("agent_removed", %{ agent_id: agent.id, runtime_id: agent.runtime_id, node_id: agent.node_id, agent_name: agent.name, perspective: agent.perspective, source: "cockpit" }) {:noreply, socket |> assign(agent_feedback: {:ok, "Removed #{agent.name}."}) |> refresh_operations()} {:error, :not_found} -> {:noreply, assign(socket, agent_feedback: {:error, "Agent #{agent.id} was not found."})} end end defp busy_agent?(agent, busy_runtime_ids) do runtime_id = Map.get(agent, :runtime_id) || agent.id MapSet.member?(busy_runtime_ids, runtime_id) end defp perspective_presets do Contract.perspectives_catalog() |> Map.get("presets", []) |> Enum.map(fn preset -> %{name: Map.fetch!(preset, "name"), perspective: Map.fetch!(preset, "perspective")} end) end defp split_list(value) when is_binary(value) do value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end defp split_list(_value), do: [] defp agent_error_text(errors) do Enum.map_join(errors, " ", fn %{field: field, message: message} -> "#{field} #{message}." %{"field" => field, "message" => message} -> "#{field} #{message}." other -> to_string(other) end) end defp feedback_style({:ok, _message}), do: "color: var(--ds-success);" defp feedback_style({:error, _message}), do: "color: var(--ds-danger);" defp feedback_text({_tone, message}), do: message defp budget_summary(%{limit_tokens: 0}) do "No token budget is set. Runs are uncapped." end defp budget_summary(budget) do used = Map.get(budget, :used_tokens, 0) limit = Map.get(budget, :limit_tokens, 0) "#{used} of #{limit} tokens used in the rolling 30-day window." end end