diff --git a/lib/bds/desktop/shell_commands.ex b/lib/bds/desktop/shell_commands.ex index cc6d76b..ed22501 100644 --- a/lib/bds/desktop/shell_commands.ex +++ b/lib/bds/desktop/shell_commands.ex @@ -119,7 +119,7 @@ defmodule BDS.Desktop.ShellCommands do defp dispatch("rebuild_posts_from_files", project, _params) do queue_task(project, "rebuild_posts_from_files", "Rebuild Posts From Files", "Maintenance", fn report -> - {:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report, rebuild_embeddings: false) + {:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post", on_progress: report) report.(1.0, "Post rebuild complete") %{project_id: project.id, counts: %{posts: length(posts)}} end) diff --git a/lib/bds/desktop/shell_live/misc_editor.ex b/lib/bds/desktop/shell_live/misc_editor.ex index 72c659d..33b1b7c 100644 --- a/lib/bds/desktop/shell_live/misc_editor.ex +++ b/lib/bds/desktop/shell_live/misc_editor.ex @@ -17,7 +17,11 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do def rerun(socket) do case meta(socket.assigns) do %{action: action} when is_binary(action) -> {:command, action} - _other -> {:noop, socket} + _other -> + case misc_route_action(socket.assigns.current_tab.type) do + nil -> {:noop, socket} + action -> {:command, action} + end end end @@ -464,7 +468,13 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do end end - defp metadata_diff_repairable_tab?(tab_id), do: tab_id in ["posts", "media", "scripts", "templates", "project"] + defp metadata_diff_repairable_tab?(tab_id), do: tab_id in ["posts", "media", "scripts", "templates", "project", "embeddings"] + + defp misc_route_action(:site_validation), do: "validate_site" + defp misc_route_action(:metadata_diff), do: "metadata_diff" + defp misc_route_action(:translation_validation), do: "validate_translations" + defp misc_route_action(:find_duplicates), do: "find_duplicates" + defp misc_route_action(_route), do: nil defp format_metadata_diff_value(nil), do: "-" defp format_metadata_diff_value(""), do: "-" diff --git a/lib/bds/embeddings.ex b/lib/bds/embeddings.ex index ddcf36b..300785e 100644 --- a/lib/bds/embeddings.ex +++ b/lib/bds/embeddings.ex @@ -20,6 +20,14 @@ defmodule BDS.Embeddings do def index_path(project_id), do: Index.path(project_id) def reindex_all(project_id), do: rebuild_project(project_id) + def refresh_snapshot(project_id) when is_binary(project_id) do + if enabled_for_project?(project_id) do + :ok = rebuild_snapshot(project_id) + end + + :ok + end + def get_indexing_progress(project_id) when is_binary(project_id) do indexed = Repo.one( @@ -105,7 +113,15 @@ defmodule BDS.Embeddings do if differences == [] do [] else - [%{entity_type: "embedding", entity_id: post.id, differences: differences}] + [ + %{ + entity_type: "embedding", + entity_id: post.id, + label: post.title || post.slug || post.id, + meta_label: Persistence.timestamp_to_iso8601(post.created_at), + differences: differences + } + ] end end) else diff --git a/lib/bds/maintenance.ex b/lib/bds/maintenance.ex index 9b1f328..4125522 100644 --- a/lib/bds/maintenance.ex +++ b/lib/bds/maintenance.ex @@ -664,6 +664,8 @@ defmodule BDS.Maintenance do {:db_to_file, "script"} -> BDS.Scripts.sync_published_script_file(entity_id) {:file_to_db, "template"} -> BDS.Templates.sync_template_from_file(entity_id) {:db_to_file, "template"} -> BDS.Templates.sync_published_template_file(entity_id) + {:file_to_db, "embedding"} -> BDS.Embeddings.sync_post(entity_id) + {:db_to_file, "embedding"} -> BDS.Embeddings.refresh_snapshot(project_id) _other -> {:error, :unsupported} end end diff --git a/test/bds/desktop/shell_commands_test.exs b/test/bds/desktop/shell_commands_test.exs index a091c5c..3c07c2e 100644 --- a/test/bds/desktop/shell_commands_test.exs +++ b/test/bds/desktop/shell_commands_test.exs @@ -99,6 +99,30 @@ defmodule BDS.Desktop.ShellCommandsTest do assert is_map(completed.result.payload.summary) end + test "rebuild_posts_from_files rebuilds embeddings for published posts when semantic similarity is enabled", %{project: project} do + assert {:ok, _metadata} = + BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true}) + + assert {:ok, post} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "Filesystem Embedding Source", + content: "space rocket orbit mission galaxy", + language: "en" + }) + + assert {:ok, published_post} = BDS.Posts.publish_post(post.id) + assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil + + BDS.Repo.delete_all(BDS.Embeddings.Key) + + assert {:ok, result} = ShellCommands.execute("rebuild_posts_from_files") + completed = wait_for_task(result.task_id, &(&1.status == :completed)) + + assert completed.group_name == "Maintenance" + assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil + end + test "repair_metadata_diff exposes live in-task progress from the repair worker", %{project: project} do original = Application.get_env(:bds, :tasks, []) diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 99bcf17..631e8ef 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -370,6 +370,34 @@ defmodule BDS.Desktop.ShellLiveTest do assert html =~ ~s(class="tab active transient") end + test "metadata diff refresh reruns after workbench session restore", %{project: project} do + :ok = BDS.Tasks.clear_finished() + + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + session_payload = + Workbench.new() + |> Workbench.open_tab(:metadata_diff, "metadata_diff", :pin) + |> Session.serialize() + + html = render_hook(view, "restore_workbench_session", %{"session" => session_payload}) + assert html =~ ~s(data-tab-type="metadata_diff") + + existing_ids = MapSet.new(Enum.map(BDS.Tasks.list_tasks(), & &1.id)) + + _html = + view + |> element("button[phx-click='rerun_misc_editor']") + |> render_click() + + refresh_task = new_task!(existing_ids, "Metadata Diff") + assert refresh_task.group_name == "Maintenance" + completed_task!(refresh_task.id) + send(view.pid, :refresh_task_status) + + assert render(view) =~ project.name + end + test "shell live renders the legacy git activity badge from remote behind count" do Application.put_env(:bds, :git_remote_state_provider, fn _project_id, _opts -> {:ok, %{local_branch: "main", upstream_branch: "origin/main", has_upstream: true, ahead: 0, behind: 7}} @@ -1163,6 +1191,59 @@ defmodule BDS.Desktop.ShellLiveTest do refute orphan_relative_path in Enum.map(diff.orphan_reports, & &1.file_path) end + test "metadata diff embeddings tab exposes repair actions and clears embedding drift", %{project: project} do + :ok = BDS.Tasks.clear_finished() + + assert {:ok, _metadata} = + Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true}) + + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Embedding Drift", + content: "space rocket orbit mission galaxy", + language: "en" + }) + + assert {:ok, published_post} = Posts.publish_post(post.id) + assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id) + + Repo.delete_all(BDS.Embeddings.Key) + + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + assert {:ok, queued} = BDS.Desktop.ShellCommands.execute("metadata_diff") + completed_task!(queued.task_id) + send(view.pid, :refresh_task_status) + + html = + view + |> element("[data-testid='metadata-diff-tab'][data-entity-tab='embeddings']") + |> render_click() + + assert html =~ "content_hash" + assert html =~ ~s(data-testid="metadata-diff-repair-button") + + existing_ids = MapSet.new(Enum.map(BDS.Tasks.list_tasks(), & &1.id)) + + html = + view + |> element("[data-testid='metadata-diff-repair-button'][data-direction='file_to_db'][data-field='content_hash']") + |> render_click() + + assert html =~ "Repair Metadata Diff" + + repair_task = new_task!(existing_ids, "Repair Metadata Diff") + completed_task!(repair_task.id) + send(view.pid, :refresh_task_status) + _html = render(view) + + assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil + + assert {:ok, diff} = BDS.Maintenance.metadata_diff(project.id) + refute Enum.any?(diff.diff_reports, &(&1.entity_type == "embedding" and &1.entity_id == published_post.id)) + end + test "post tabs render a real editor and drive save publish discard flows", %{project: project} do assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "alpha", color: "#112233"}) assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "beta", color: "#445566"})