defmodule ArcanaWeb.MaintenanceLive do @moduledoc """ LiveView for maintenance operations in Arcana. """ use Phoenix.LiveView import Ecto.Query import ArcanaWeb.DashboardComponents @impl true def mount(_params, session, socket) do repo = get_repo_from_session(session) {:ok, socket |> assign(repo: repo) |> assign( reembed_running: false, reembed_progress: nil, reembed_collection: nil, embedding_info: get_embedding_info(), rebuild_graph_running: false, rebuild_graph_progress: nil, rebuild_graph_collection: nil, graph_info: get_graph_info(), detect_communities_running: false, detect_communities_progress: nil, detect_communities_collection: nil, collections: [], collections_for_assign: [], orphaned_stats: %{entities: 0, relationships: 0}, assign_orphans_collection: nil, stats: nil )} end @impl true def handle_params(_params, _uri, socket) do {:noreply, load_data(socket)} end defp load_data(socket) do repo = socket.assigns.repo collections = fetch_collections(repo) socket |> assign(stats: load_stats(repo)) |> assign(collections: Enum.map(collections, & &1.name)) |> assign(collections_for_assign: collections) |> assign(orphaned_stats: count_orphaned_graph_data(repo)) end defp fetch_collections(repo) do repo.all(from(c in Arcana.Collection, select: %{id: c.id, name: c.name}, order_by: c.name)) rescue _ -> [] end defp count_orphaned_graph_data(repo) do entities = repo.one( from(e in Arcana.Graph.Entity, where: is_nil(e.collection_id), select: count(e.id)) ) || 0 # Relationships don't have collection_id - count those connected to orphaned entities relationships = repo.one( from(r in Arcana.Graph.Relationship, join: source in Arcana.Graph.Entity, on: r.source_id == source.id, where: is_nil(source.collection_id), select: count(r.id) ) ) || 0 %{entities: entities, relationships: relationships} rescue _ -> %{entities: 0, relationships: 0} end defp get_embedding_info do Arcana.Maintenance.embedding_info() rescue _ -> %{type: :unknown, dimensions: nil} end defp get_graph_info do Arcana.Maintenance.graph_info() rescue _ -> %{enabled: false, extractor_type: :unknown} end @impl true def handle_event("select_reembed_collection", %{"collection" => collection}, socket) do collection = if collection == "", do: nil, else: collection {:noreply, assign(socket, reembed_collection: collection)} end def handle_event("reembed", _params, socket) do repo = socket.assigns.repo collection = socket.assigns.reembed_collection parent = self() socket = assign(socket, reembed_running: true, reembed_progress: %{current: 0, total: 0}) Arcana.TaskSupervisor.start_child(fn -> progress_fn = fn current, total -> send(parent, {:reembed_progress, current, total}) end opts = [batch_size: 50, progress: progress_fn] opts = if collection, do: Keyword.put(opts, :collection, collection), else: opts result = Arcana.Maintenance.reembed(repo, opts) send(parent, {:reembed_complete, result}) end) {:noreply, socket} end def handle_event("select_rebuild_collection", %{"collection" => collection}, socket) do collection = if collection == "", do: nil, else: collection {:noreply, assign(socket, rebuild_graph_collection: collection)} end def handle_event("rebuild_graph", _params, socket) do repo = socket.assigns.repo collection = socket.assigns.rebuild_graph_collection parent = self() socket = assign(socket, rebuild_graph_running: true, rebuild_graph_progress: %{current: 0, total: 0}) Arcana.TaskSupervisor.start_child(fn -> progress_fn = fn current, total -> send(parent, {:rebuild_graph_progress, current, total}) end opts = [progress: progress_fn] opts = if collection, do: Keyword.put(opts, :collection, collection), else: opts result = Arcana.Maintenance.rebuild_graph(repo, opts) send(parent, {:rebuild_graph_complete, result}) end) {:noreply, socket} end def handle_event("select_detect_communities_collection", %{"collection" => collection}, socket) do collection = if collection == "", do: nil, else: collection {:noreply, assign(socket, detect_communities_collection: collection)} end def handle_event("detect_communities", _params, socket) do repo = socket.assigns.repo collection = socket.assigns.detect_communities_collection parent = self() socket = assign(socket, detect_communities_running: true, detect_communities_progress: %{current: 0, total: 0} ) Arcana.TaskSupervisor.start_child(fn -> progress_fn = fn current, total -> send(parent, {:detect_communities_progress, current, total}) end opts = [progress: progress_fn] opts = if collection, do: Keyword.put(opts, :collection, collection), else: opts result = Arcana.Maintenance.detect_communities(repo, opts) send(parent, {:detect_communities_complete, result}) end) {:noreply, socket} end def handle_event("select_assign_collection", %{"collection" => collection}, socket) do collection = if collection == "", do: nil, else: collection {:noreply, assign(socket, assign_orphans_collection: collection)} end def handle_event("assign_orphans", _params, socket) do repo = socket.assigns.repo collection_name = socket.assigns.assign_orphans_collection if collection_name do collection = Enum.find(socket.assigns.collections_for_assign, &(&1.name == collection_name)) if collection do assign_orphaned_to_collection(repo, collection.id) socket = socket |> load_data() |> put_flash(:info, "Assigned orphaned graph data to #{collection_name}") {:noreply, socket} else {:noreply, put_flash(socket, :error, "Collection not found")} end else {:noreply, put_flash(socket, :error, "Please select a collection")} end end def handle_event("delete_orphans", _params, socket) do repo = socket.assigns.repo {entities_deleted, relationships_deleted} = delete_orphaned_graph_data(repo) socket = socket |> load_data() |> put_flash( :info, "Deleted #{entities_deleted} orphaned entities and #{relationships_deleted} orphaned relationships" ) {:noreply, socket} end defp assign_orphaned_to_collection(repo, collection_id) do # Only entities have collection_id - relationships reference entities repo.update_all( from(e in Arcana.Graph.Entity, where: is_nil(e.collection_id)), set: [collection_id: collection_id] ) end defp delete_orphaned_graph_data(repo) do # Get orphaned entity IDs first orphaned_entity_ids = repo.all( from(e in Arcana.Graph.Entity, where: is_nil(e.collection_id), select: e.id ) ) # Delete relationships connected to orphaned entities (must be done first) {rel_count, _} = if orphaned_entity_ids != [] do repo.delete_all( from(r in Arcana.Graph.Relationship, where: r.source_id in ^orphaned_entity_ids or r.target_id in ^orphaned_entity_ids ) ) else {0, nil} end # Then delete orphaned entities {entity_count, _} = repo.delete_all(from(e in Arcana.Graph.Entity, where: is_nil(e.collection_id))) {entity_count, rel_count} end @impl true def handle_info({:reembed_progress, current, total}, socket) do {:noreply, assign(socket, reembed_progress: %{current: current, total: total})} end def handle_info({:reembed_complete, result}, socket) do socket = case result do {:ok, %{reembedded: count}} -> socket |> assign(reembed_running: false, reembed_progress: nil) |> put_flash(:info, "Re-embedded #{count} chunks successfully!") {:error, reason} -> socket |> assign(reembed_running: false, reembed_progress: nil) |> put_flash(:error, "Re-embedding failed: #{inspect(reason)}") end {:noreply, socket} end def handle_info({:rebuild_graph_progress, current, total}, socket) do {:noreply, assign(socket, rebuild_graph_progress: %{current: current, total: total})} end def handle_info({:rebuild_graph_complete, result}, socket) do socket = case result do {:ok, %{entities: entities, relationships: relationships}} -> socket |> assign(rebuild_graph_running: false, rebuild_graph_progress: nil) |> load_data() |> put_flash( :info, "Rebuilt graph: #{entities} entities, #{relationships} relationships" ) {:error, reason} -> socket |> assign(rebuild_graph_running: false, rebuild_graph_progress: nil) |> put_flash(:error, "Rebuild graph failed: #{inspect(reason)}") end {:noreply, socket} end def handle_info({:detect_communities_progress, current, total}, socket) do {:noreply, assign(socket, detect_communities_progress: %{current: current, total: total})} end def handle_info({:detect_communities_complete, result}, socket) do socket = case result do {:ok, %{communities: communities}} -> socket |> assign(detect_communities_running: false, detect_communities_progress: nil) |> load_data() |> put_flash(:info, "Detected #{communities} communities successfully!") {:error, reason} -> socket |> assign(detect_communities_running: false, detect_communities_progress: nil) |> put_flash(:error, "Community detection failed: #{inspect(reason)}") end {:noreply, socket} end @impl true def render(assigns) do ~H""" <.dashboard_layout stats={@stats} current_tab={:maintenance}>
View embedding configuration and re-embed documents if settings change.
Re-embed chunks using the current embedding configuration. Use this after changing embedding models.
<%= if @reembed_running do %>Clear and rebuild the knowledge graph. Use this after changing graph extractor configuration or enabling relationship extraction.
<%= if @rebuild_graph_running do %>Run Leiden community detection on the knowledge graph. Communities enable global queries by grouping related entities.
<%= if @detect_communities_running do %>These entities and relationships don't belong to any collection. Assign them to a collection or delete them.