diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 87e4455..8c29a98 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -44,7 +44,9 @@ defmodule BDS.Desktop.ShellLive do @impl true def mount(_params, _session, socket) do - if connected?(socket) do + connected = connected?(socket) + + if connected do :timer.send_interval(@refresh_interval, :refresh_task_status) end @@ -55,7 +57,8 @@ defmodule BDS.Desktop.ShellLive do |> assign(:page_title, ShellData.title()) |> assign(:page_language, ShellData.ui_language()) |> assign(:client_shortcuts, Commands.client_shortcuts()) - |> assign(:offline_mode, AI.airplane_mode?(true)) + |> assign(:offline_mode, if(connected, do: AI.airplane_mode?(true), else: true)) + |> assign(:handled_task_results, initial_handled_task_results()) |> assign(:assistant_prompt, "") |> assign(:assistant_messages, []) |> assign(:is_mac_ui, mac_ui?()) @@ -1003,19 +1006,30 @@ defmodule BDS.Desktop.ShellLive do @impl true def handle_info(:refresh_task_status, socket) do - task_status = BDS.Tasks.status_snapshot() + raw_task_status = BDS.Tasks.status_snapshot() - {:noreply, - socket - |> assign(:task_status, task_status) - |> assign(:editor_meta, ShellData.editor_meta(task_status)) - |> assign( - :status, - ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard, - ui_language: socket.assigns.page_language, - offline_mode: socket.assigns.offline_mode - ) - )} + case next_completed_task_result(socket, raw_task_status) do + nil -> + task_status = localize_task_status(raw_task_status, socket.assigns.page_language) + + {:noreply, + socket + |> assign(:task_status, task_status) + |> assign(:editor_meta, ShellData.editor_meta(task_status)) + |> assign( + :status, + ShellData.status_bar(socket.assigns.workbench, task_status, socket.assigns.dashboard, + ui_language: socket.assigns.page_language, + offline_mode: socket.assigns.offline_mode + ) + )} + + task -> + {:noreply, + socket + |> mark_task_result_handled(task.id) + |> apply_shell_command_result(task.result)} + end end @impl true @@ -1031,10 +1045,17 @@ defmodule BDS.Desktop.ShellLive do active_view_id = Atom.to_string(workbench.active_view) sidebar_data = ShellData.sidebar_view(projects.active_project_id, active_view_id, ShellSidebarState.current_filters(socket, active_view_id)) sidebar_data = ShellSidebarState.merge_ui_state(socket, active_view_id, sidebar_data) - task_status = BDS.Tasks.status_snapshot() + raw_task_status = BDS.Tasks.status_snapshot() activity_buttons = Workbench.activity_buttons(workbench, git_badge_count) page_language = socket.assigns[:page_language] || ShellData.ui_language() - offline_mode = Map.get(socket.assigns, :offline_mode, AI.airplane_mode?(true)) + offline_mode = + if connected?(socket) do + Map.get(socket.assigns, :offline_mode, AI.airplane_mode?(true)) + else + Map.get(socket.assigns, :offline_mode, true) + end + + task_status = localize_task_status(raw_task_status, page_language) socket |> assign(:workbench, workbench) @@ -1118,9 +1139,15 @@ defmodule BDS.Desktop.ShellLive do
<%= task.name %> - <%= task.status |> to_string() |> String.capitalize() %> + <%= Map.get(task, :status_label, task.status |> to_string() |> String.capitalize()) %>
<%= task.message || task.group_name || "" %> + <%= if is_number(task.progress) do %> +
+ + <%= Map.get(task, :progress_label, progress_percent(task.progress)) %> +
+ <% end %>
<% end %> @@ -1574,17 +1601,17 @@ defmodule BDS.Desktop.ShellLive do |> Workbench.set_panel_tab(String.to_existing_atom(panel_tab)) socket - |> append_output_entry(title, message) + |> append_output_entry(translate_for_socket(socket, title), translate_for_socket(socket, message)) |> reload_shell(workbench) end defp apply_shell_command_result(socket, %{kind: "output", title: title, message: message} = result) do socket - |> append_output_entry(title, message, Map.get(result, :details), Map.get(result, :level, "info")) + |> append_output_entry(translate_for_socket(socket, title), translate_for_socket(socket, message), Map.get(result, :details), Map.get(result, :level, "info")) end defp apply_shell_command_result(socket, %{kind: "open_url", title: title, message: message, url: url}) do - append_output_entry(socket, title, message, url) + append_output_entry(socket, translate_for_socket(socket, title), translate_for_socket(socket, message), url) end defp apply_shell_command_result(socket, %{kind: "open_editor", route: route, title: title, subtitle: subtitle} = result) do @@ -1594,12 +1621,12 @@ defmodule BDS.Desktop.ShellLive do tab_meta = Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{ - title: title, - subtitle: subtitle, + title: translate_for_socket(socket, title), + subtitle: translate_for_socket(socket, subtitle), action: Map.get(result, :action), payload: Map.get(result, :payload), project_id: Map.get(result, :project_id), - editor_meta: Map.get(result, :editorMeta, []) + editor_meta: translate_editor_meta(Map.get(result, :editorMeta, []), socket.assigns.page_language) }) socket @@ -1609,6 +1636,90 @@ defmodule BDS.Desktop.ShellLive do defp apply_shell_command_result(socket, _result), do: socket + defp initial_handled_task_results do + BDS.Tasks.status_snapshot() + |> Map.get(:tasks, []) + |> Enum.filter(fn task -> task.status == :completed and is_map(task.result) end) + |> Enum.map(& &1.id) + |> MapSet.new() + end + + defp next_completed_task_result(socket, task_status) do + handled = Map.get(socket.assigns, :handled_task_results, MapSet.new()) + + Enum.find(Map.get(task_status, :tasks, []), fn task -> + task.status == :completed and is_map(task.result) and not MapSet.member?(handled, task.id) + end) + end + + defp mark_task_result_handled(socket, task_id) do + handled = Map.get(socket.assigns, :handled_task_results, MapSet.new()) + assign(socket, :handled_task_results, MapSet.put(handled, task_id)) + end + + defp localize_task_status(task_status, locale) do + tasks = Enum.map(Map.get(task_status, :tasks, []), &localize_task(&1, locale)) + active = Enum.filter(tasks, &(&1.status in [:running, :pending])) + + task_status + |> Map.put(:tasks, tasks) + |> Map.put(:running_task_message, localized_running_task_message(active, locale)) + end + + defp localize_task(task, locale) do + progress = Map.get(task, :progress) + + task + |> Map.put(:name, ShellData.translate(task.name, %{}, locale)) + |> Map.put(:message, localize_task_message(Map.get(task, :message), locale)) + |> Map.put(:group_name, localize_task_group(Map.get(task, :group_name), locale)) + |> Map.put(:status_label, localize_task_status_label(task.status, locale)) + |> Map.put(:progress_label, if(is_number(progress), do: progress_percent(progress), else: nil)) + end + + defp localize_task_message(nil, _locale), do: nil + defp localize_task_message("", _locale), do: "" + defp localize_task_message(message, locale), do: ShellData.translate(message, %{}, locale) + + defp localize_task_group(nil, _locale), do: nil + defp localize_task_group(group, locale), do: ShellData.translate(group, %{}, locale) + + defp localize_task_status_label(status, locale) do + status + |> to_string() + |> String.capitalize() + |> ShellData.translate(%{}, locale) + end + + defp localized_running_task_message([], _locale), do: nil + + defp localized_running_task_message([task | _rest], locale) do + cond do + task.status == :pending -> ShellData.translate("Queued", %{}, locale) <> ": " <> task.name + is_binary(task.message) and task.message != "" -> task.name <> ": " <> task.message + true -> task.name + end + end + + defp translate_editor_meta(items, locale) do + Enum.map(items, fn item -> + item + |> Map.update(:label, nil, &ShellData.translate(&1, %{}, locale)) + |> Map.update(:value, nil, &translate_editor_meta_value(&1, locale)) + end) + end + + defp translate_editor_meta_value(value, locale) when is_binary(value), do: ShellData.translate(value, %{}, locale) + defp translate_editor_meta_value(value, _locale), do: value + + defp translate_for_socket(socket, text) when is_binary(text), do: ShellData.translate(text, %{}, socket.assigns.page_language) + defp translate_for_socket(_socket, text), do: text + + defp progress_percent(progress) when is_number(progress) do + percentage = progress |> Kernel.*(100) |> round() + Integer.to_string(percentage) <> "%" + end + defp command_title(action) do action |> to_string() diff --git a/priv/i18n/locales/de.json b/priv/i18n/locales/de.json index c7d80bc..748b107 100644 --- a/priv/i18n/locales/de.json +++ b/priv/i18n/locales/de.json @@ -172,7 +172,11 @@ "Menu": "Menü", "Metadata": "Metadaten", "Metadata Diff": "Metadaten-Diff", + "Metadata diff complete": "Metadaten-Diff abgeschlossen", "Metadata flush, diffing, and rebuild hooks still need editor wiring.": "Metadaten-Schreiben, Diffing und Rebuild-Hooks brauchen noch die Editor-Anbindung.", + "Comparing database and filesystem metadata": "Vergleicht Datenbank- und Dateisystem-Metadaten", + "Database state compared against filesystem metadata": "Datenbankstatus mit Dateisystem-Metadaten verglichen", + "Maintenance": "Wartung", "Missing": "Fehlend", "Missing Pages": "Fehlende Seiten", "Missing Translations": "Fehlende Übersetzungen", @@ -206,10 +210,14 @@ "Published": "Veröffentlicht", "Published Feb 10, 2026": "Veröffentlicht am 10. Feb. 2026", "Queued": "In Warteschlange", + "Pending": "Ausstehend", "Regenerate Calendar": "Kalender neu erzeugen", "Retrospective": "Rückblick", "Roadmap": "Fahrplan", "Running": "Läuft", + "Completed": "Abgeschlossen", + "Failed": "Fehlgeschlagen", + "Cancelled": "Abgebrochen", "Script": "Skript", "Scripts": "Skripte", "Select Project": "Projekt auswählen", @@ -217,7 +225,9 @@ "Settings": "Einstellungen", "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions": "Seitenleiste, Tabs, Panel und Assistentenbereiche sind als DOM-Regionen inspizierbar", "Site Validation": "Website-Validierung", + "Validation": "Validierung", "Source Control": "Quellcodeverwaltung", + "Embeddings": "Embeddings", "Stale": "Veraltet", "Stale Pages": "Veraltete Seiten", "Slug": "Slug", diff --git a/priv/i18n/locales/en.json b/priv/i18n/locales/en.json index 51d1fd3..811739b 100644 --- a/priv/i18n/locales/en.json +++ b/priv/i18n/locales/en.json @@ -172,7 +172,11 @@ "Menu": "Menu", "Metadata": "Metadata", "Metadata Diff": "Metadata Diff", + "Metadata diff complete": "Metadata diff complete", "Metadata flush, diffing, and rebuild hooks still need editor wiring.": "Metadata flush, diffing, and rebuild hooks still need editor wiring.", + "Comparing database and filesystem metadata": "Comparing database and filesystem metadata", + "Database state compared against filesystem metadata": "Database state compared against filesystem metadata", + "Maintenance": "Maintenance", "Missing": "Missing", "Missing Pages": "Missing Pages", "Missing Translations": "Missing Translations", @@ -206,10 +210,14 @@ "Published": "Published", "Published Feb 10, 2026": "Published Feb 10, 2026", "Queued": "Queued", + "Pending": "Pending", "Regenerate Calendar": "Regenerate Calendar", "Retrospective": "Retrospective", "Roadmap": "Roadmap", "Running": "Running", + "Completed": "Completed", + "Failed": "Failed", + "Cancelled": "Cancelled", "Script": "Script", "Scripts": "Scripts", "Select Project": "Select Project", @@ -217,7 +225,9 @@ "Settings": "Settings", "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions": "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions", "Site Validation": "Site Validation", + "Validation": "Validation", "Source Control": "Source Control", + "Embeddings": "Embeddings", "Stale": "Stale", "Stale Pages": "Stale Pages", "Slug": "Slug", diff --git a/priv/i18n/locales/es.json b/priv/i18n/locales/es.json index 258eca9..f0fae31 100644 --- a/priv/i18n/locales/es.json +++ b/priv/i18n/locales/es.json @@ -172,7 +172,11 @@ "Menu": "Menú", "Metadata": "Metadatos", "Metadata Diff": "Diff de metadatos", + "Metadata diff complete": "Diff de metadatos completado", "Metadata flush, diffing, and rebuild hooks still need editor wiring.": "El guardado de metadatos, el diff y los hooks de reconstrucción todavía necesitan la conexión del editor.", + "Comparing database and filesystem metadata": "Comparando metadatos de la base de datos y del sistema de archivos", + "Database state compared against filesystem metadata": "Estado de la base de datos comparado con los metadatos del sistema de archivos", + "Maintenance": "Mantenimiento", "Missing": "Faltante", "Missing Pages": "Páginas faltantes", "Missing Translations": "Traducciones faltantes", @@ -206,10 +210,14 @@ "Published": "Publicado", "Published Feb 10, 2026": "Publicado el 10 feb 2026", "Queued": "En cola", + "Pending": "Pendiente", "Regenerate Calendar": "Regenerar calendario", "Retrospective": "Retrospectiva", "Roadmap": "Hoja de ruta", "Running": "En ejecución", + "Completed": "Completado", + "Failed": "Fallido", + "Cancelled": "Cancelado", "Script": "Script", "Scripts": "Scripts", "Select Project": "Seleccionar proyecto", @@ -217,7 +225,9 @@ "Settings": "Configuración", "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions": "La barra lateral, las pestañas, el panel y el asistente son regiones DOM inspeccionables", "Site Validation": "Validación del sitio", + "Validation": "Validación", "Source Control": "Control de código fuente", + "Embeddings": "Embeddings", "Stale": "Desactualizado", "Stale Pages": "Páginas desactualizadas", "Slug": "Slug", diff --git a/priv/i18n/locales/fr.json b/priv/i18n/locales/fr.json index 346d360..39de281 100644 --- a/priv/i18n/locales/fr.json +++ b/priv/i18n/locales/fr.json @@ -172,7 +172,11 @@ "Menu": "Menu", "Metadata": "Métadonnées", "Metadata Diff": "Diff des métadonnées", + "Metadata diff complete": "Diff des métadonnées terminé", "Metadata flush, diffing, and rebuild hooks still need editor wiring.": "L’écriture des métadonnées, le diff et les hooks de reconstruction ont encore besoin du câblage de l’éditeur.", + "Comparing database and filesystem metadata": "Comparaison des métadonnées entre la base et le système de fichiers", + "Database state compared against filesystem metadata": "État de la base comparé aux métadonnées du système de fichiers", + "Maintenance": "Maintenance", "Missing": "Manquant", "Missing Pages": "Pages manquantes", "Missing Translations": "Traductions manquantes", @@ -206,10 +210,14 @@ "Published": "Publié", "Published Feb 10, 2026": "Publié le 10 févr. 2026", "Queued": "En file", + "Pending": "En attente", "Regenerate Calendar": "Régénérer le calendrier", "Retrospective": "Rétrospective", "Roadmap": "Feuille de route", "Running": "En cours", + "Completed": "Terminé", + "Failed": "Échec", + "Cancelled": "Annulé", "Script": "Script", "Scripts": "Scripts", "Select Project": "Sélectionner un projet", @@ -217,7 +225,9 @@ "Settings": "Paramètres", "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions": "La barre latérale, les onglets, le panneau et l’assistant sont des régions DOM inspectables", "Site Validation": "Validation du site", + "Validation": "Validation", "Source Control": "Contrôle de source", + "Embeddings": "Embeddings", "Stale": "Obsolète", "Stale Pages": "Pages obsolètes", "Slug": "Slug", diff --git a/priv/i18n/locales/it.json b/priv/i18n/locales/it.json index 4881032..e301021 100644 --- a/priv/i18n/locales/it.json +++ b/priv/i18n/locales/it.json @@ -172,7 +172,11 @@ "Menu": "Menu", "Metadata": "Metadati", "Metadata Diff": "Diff metadati", + "Metadata diff complete": "Diff metadati completato", "Metadata flush, diffing, and rebuild hooks still need editor wiring.": "Il salvataggio dei metadati, il diff e gli hook di ricostruzione hanno ancora bisogno del collegamento nell’editor.", + "Comparing database and filesystem metadata": "Confronto tra i metadati del database e del filesystem", + "Database state compared against filesystem metadata": "Stato del database confrontato con i metadati del filesystem", + "Maintenance": "Manutenzione", "Missing": "Mancante", "Missing Pages": "Pagine mancanti", "Missing Translations": "Traduzioni mancanti", @@ -206,10 +210,14 @@ "Published": "Pubblicato", "Published Feb 10, 2026": "Pubblicato il 10 feb 2026", "Queued": "In coda", + "Pending": "In attesa", "Regenerate Calendar": "Rigenera calendario", "Retrospective": "Retrospettiva", "Roadmap": "Roadmap", "Running": "In esecuzione", + "Completed": "Completato", + "Failed": "Fallito", + "Cancelled": "Annullato", "Script": "Script", "Scripts": "Script", "Select Project": "Seleziona progetto", @@ -217,7 +225,9 @@ "Settings": "Impostazioni", "Sidebar, tabs, panel, and assistant panes are inspectable DOM regions": "Barra laterale, schede, pannello e assistente sono regioni DOM ispezionabili", "Site Validation": "Validazione sito", + "Validation": "Validazione", "Source Control": "Controllo del codice sorgente", + "Embeddings": "Embeddings", "Stale": "Obsoleto", "Stale Pages": "Pagine obsolete", "Slug": "Slug", diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index fe2562d..3745116 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -839,6 +839,82 @@ defmodule BDS.Desktop.ShellLiveTest do assert html =~ ~s(class="task-list") or html =~ "No background tasks running" end + test "metadata diff tasks localize task text, show progress, and open the diff result in the UI" do + parent = self() + :ok = BDS.Tasks.clear_finished() + + {:ok, _task} = + BDS.Tasks.submit_task( + "Metadata Diff", + fn report -> + send(parent, {:metadata_diff_worker, self()}) + report.(0.35, "Comparing database and filesystem metadata") + + receive do + :finish -> + %{ + kind: "open_editor", + action: "metadata_diff", + project_id: "test-project", + route: "metadata_diff", + title: "Metadata Diff", + subtitle: "Database state compared against filesystem metadata", + editorMeta: [ + %{label: "Diffs", value: "1"}, + %{label: "Orphans", value: "1"} + ], + payload: %{ + summary: %{diff_count: 1, orphan_count: 1}, + diff_reports: [ + %{ + entity_type: "post", + entity_id: "post-1", + differences: [ + %{field: "slug", db_value: "hello-db", file_value: "hello-file"} + ] + } + ], + orphan_reports: [ + %{path: "posts/2026/04/orphan.md", entity_type: "post"} + ] + } + } + end + end, + %{group_name: "Maintenance"} + ) + + assert_receive {:metadata_diff_worker, worker_pid} + + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + _html = render_change(view, "change_ui_language", %{"ui_language" => "de"}) + + send(view.pid, :refresh_task_status) + + html = + view + |> element("[data-testid='status-task-button']") + |> render_click() + + assert html =~ "Metadaten-Diff" + assert html =~ "Vergleicht Datenbank- und Dateisystem-Metadaten" + assert html =~ "35%" + assert html =~ ~s(task-status-running) + + send(worker_pid, :finish) + send(view.pid, :refresh_task_status) + + html = render(view) + + assert html =~ ~s(data-tab-type="metadata_diff") + assert html =~ "Metadaten-Diff" + assert html =~ "slug" + assert html =~ "hello-db" + assert html =~ "hello-file" + assert html =~ "posts/2026/04/orphan.md" + 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"})