From 88c689ee5563a41b43f3ab0beea8eb5d6c343602 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 9 May 2026 10:41:28 +0200 Subject: [PATCH] fix: fixed CSM-007 --- CODESMELL.md | 32 ++-- lib/bds/desktop/shell_live.ex | 183 ++++++++++++------ lib/bds/desktop/shell_live/bridges.ex | 24 ++- lib/bds/desktop/shell_live/sidebar_create.ex | 24 +-- lib/bds/desktop/shell_live/sidebar_delete.ex | 8 +- test/bds/csm007_reload_shell_test.exs | 191 +++++++++++++++++++ 6 files changed, 364 insertions(+), 98 deletions(-) create mode 100644 test/bds/csm007_reload_shell_test.exs diff --git a/CODESMELL.md b/CODESMELL.md index 6952881..1f17a38 100644 --- a/CODESMELL.md +++ b/CODESMELL.md @@ -131,18 +131,23 @@ --- -### CSM-007 — Monolithic State Rebuild ("God Function") -- **File:** `lib/bds/desktop/shell_live.ex:554-616` -- **What:** `reload_shell/2` rebuilds sidebar, dashboard, git badge, tasks, status bar, tab meta, and panel data on almost every event. This function is called from most `handle_event` and `handle_info` callbacks, even for trivial state changes like sidebar toggle. -- **Why it's bad:** Even a simple sidebar toggle triggers 5+ unrelated DB queries (project_snapshot, dashboard, git_badge_count, sidebar_view, task_status). The sidebar and dashboard data are rebuilt even when only the panel content changed. Output entries and editor meta are recalculated unnecessarily. -- **Fix:** Decompose into focused updaters, each only querying what it needs: - - `refresh_sidebar/2` — only queries sidebar data - - `refresh_dashboard/2` — only queries dashboard data - - `refresh_git_badge/2` — only queries git status - - `refresh_task_status/2` — only queries task state - - `refresh_tab_meta/2` — only syncs tab metadata - Each `handle_event` / `handle_info` should call only the relevant updaters. -- **Test:** Toggle sidebar; assert no dashboard or git queries are executed. Save a post; assert sidebar refreshes but dashboard does not. +### ~~CSM-007 — Monolithic State Rebuild ("God Function")~~ ✅ FIXED +- **Fixed:** 2026-05-09 +- **What was done:** + - Decomposed `reload_shell/2` into four focused updaters: + - `refresh_layout/2` — No DB queries. Recomputes workbench-derived assigns (activity_buttons, panel_tabs, current_tab, status_bar, sidebar_header, editor_meta) from existing socket.assigns. + - `refresh_sidebar/2` — Queries sidebar data only, then calls `refresh_layout`. + - `refresh_content/2` — Queries projects, dashboard, git badge, and sidebar data, then calls `refresh_layout`. + - `reload_shell/2` — Full refresh: tab_meta sync, task status, static data, then calls `refresh_content`. Kept for mount, project switch, session restore, and settings changes. + - Replaced all call sites with the minimal refresh needed: + - **Layout-only** (`refresh_layout`): toggle_sidebar, toggle_panel, toggle_assistant_sidebar, select_panel_tab, sync_layout, resize_panel, open_tasks_panel, select_tab, close_tab, toggle_offline_mode, layout menu actions (toggle, close_tab). + - **Sidebar** (`refresh_sidebar`): select_view, all sidebar filter events, sidebar menu actions (view_posts, view_media, edit_preferences, etc.), chat/import editor tab_meta updates. + - **Content** (`refresh_content`): entity_changed (CLI sync), tags_changed, sidebar create/delete. + - **Full reload** (`reload_shell`): mount, activate_project, restore_workbench_session, set_page_language, settings_changed. + - Updated Bridges callbacks to use focused refreshers: `refresh_layout` for toggle events and close_tab, `refresh_sidebar` for view switches and tab meta updates, `refresh_content` for entity/tag changes. + - Split `@local_menu_actions` into `@layout_menu_actions` and `@sidebar_menu_actions` for correct dispatch. + - Fixed `false || true` bug in `refresh_layout` where `offline_mode = assigns[:offline_mode] || true` incorrectly defaulted `false` to `true`. + - Added 7 tests in `test/bds/csm007_reload_shell_test.exs` using telemetry-based query counting: toggle_sidebar (0 queries), toggle_panel (0 queries), sync_layout (0 queries), select_panel_tab (0 queries), toggle_offline_mode (1 query — settings write only), select_view (sidebar queries but no dashboard/projects), sidebar_search (no dashboard queries). --- @@ -419,9 +424,10 @@ - CSM-004: Fixed. `attach_runner` moved to `handle_continue`, `terminate/2` added for cleanup, `restart: :temporary` set, JobStore `detach_runner` bug fixed. - [ ] All high-severity items (CSM-006 to CSM-010) have been addressed. - CSM-006: Fixed. Batch INSERT for reindexing, preloaded post records for rendering. + - CSM-007: Fixed. Decomposed into refresh_layout, refresh_sidebar, refresh_content, reload_shell. - [x] CSM-001 fix covers ALL 6 affected files, not just `import_definitions.ex`. - [x] CSM-003 fix covers ALL `Repo.delete!` call sites (posts, tags, scripts, media, projects, templates, translations). -- [ ] CSM-007 decomposition is the prerequisite for fixing CSM-008 (render-path queries). +- [x] CSM-007 decomposition is the prerequisite for fixing CSM-008 (render-path queries). - [x] Tests were written **before** implementation changes (Red → Green → Refactor). - [x] Full test suite passes: `mix test`. - [x] Dialyzer passes cleanly: `mix dialyzer` (zero warnings). diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index c081a97..d0e1c5f 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -82,18 +82,21 @@ defmodule BDS.Desktop.ShellLive do "load_more_sidebar" ] - @local_menu_actions MapSet.new([ - :toggle_sidebar, - :toggle_panel, - :toggle_assistant_sidebar, - :view_posts, - :view_media, - :edit_preferences, - :edit_menu, - :documentation, - :api_documentation, - :close_tab - ]) + @layout_menu_actions MapSet.new([ + :toggle_sidebar, + :toggle_panel, + :toggle_assistant_sidebar, + :close_tab + ]) + @sidebar_menu_actions MapSet.new([ + :view_posts, + :view_media, + :edit_preferences, + :edit_menu, + :documentation, + :api_documentation + ]) + @local_menu_actions MapSet.union(@layout_menu_actions, @sidebar_menu_actions) @socket_menu_actions MapSet.new([ :new_post, :import_media, @@ -174,15 +177,15 @@ defmodule BDS.Desktop.ShellLive do @impl true def handle_event("toggle_sidebar", _params, socket) do - {:noreply, reload_shell(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} + {:noreply, refresh_layout(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} end def handle_event("toggle_panel", _params, socket) do - {:noreply, reload_shell(socket, Workbench.toggle_panel(socket.assigns.workbench))} + {:noreply, refresh_layout(socket, Workbench.toggle_panel(socket.assigns.workbench))} end def handle_event("toggle_assistant_sidebar", _params, socket) do - {:noreply, reload_shell(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} + {:noreply, refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} end def handle_event("select_view", %{"view" => view_id}, socket) do @@ -192,7 +195,7 @@ defmodule BDS.Desktop.ShellLive do BoundedAtoms.sidebar_view(view_id, :posts) ) - {:noreply, reload_shell(socket, workbench)} + {:noreply, refresh_sidebar(socket, workbench)} end def handle_event("select_panel_tab", %{"tab" => tab}, socket) do @@ -201,7 +204,7 @@ defmodule BDS.Desktop.ShellLive do |> Workbench.set_panel_visible(true) |> Workbench.set_panel_tab(BoundedAtoms.panel_tab(tab, :tasks)) - {:noreply, reload_shell(socket, workbench)} + {:noreply, refresh_layout(socket, workbench)} end def handle_event("open_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do @@ -213,15 +216,15 @@ defmodule BDS.Desktop.ShellLive do end def handle_event("sync_layout", params, socket) do - {:noreply, reload_shell(socket, Layout.sync(socket.assigns.workbench, params))} + {:noreply, refresh_layout(socket, Layout.sync(socket.assigns.workbench, params))} end def handle_event("resize_panel", %{"target" => target, "width" => width}, socket) do - {:noreply, reload_shell(socket, Layout.resize(socket.assigns.workbench, target, width))} + {:noreply, refresh_layout(socket, Layout.resize(socket.assigns.workbench, target, width))} end def handle_event(event, params, socket) when event in @sidebar_filter_events do - SidebarEvents.handle(socket, event, params, &reload_shell/2) + SidebarEvents.handle(socket, event, params, &refresh_sidebar/2) end def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do @@ -248,7 +251,12 @@ defmodule BDS.Desktop.ShellLive do :preview ) - {:noreply, reload_shell(socket, workbench)} + tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{}) + + {:noreply, + socket + |> assign(:tab_meta, tab_meta) + |> refresh_layout(workbench)} end def handle_event("close_tab", %{"type" => type, "id" => id}, socket) do @@ -259,7 +267,7 @@ defmodule BDS.Desktop.ShellLive do {:noreply, socket |> assign(:tab_meta, tab_meta) - |> reload_shell(workbench)} + |> refresh_layout(workbench)} end def handle_event( @@ -283,7 +291,7 @@ defmodule BDS.Desktop.ShellLive do :ok = AI.set_airplane_mode(next_mode) socket = assign(socket, :offline_mode, next_mode) - {:noreply, reload_shell(socket, socket.assigns.workbench)} + {:noreply, refresh_layout(socket, socket.assigns.workbench)} end def handle_event("update_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do @@ -314,7 +322,7 @@ defmodule BDS.Desktop.ShellLive do |> Workbench.set_panel_visible(true) |> Workbench.set_panel_tab(:tasks) - {:noreply, reload_shell(socket, workbench)} + {:noreply, refresh_layout(socket, workbench)} end def handle_event("settings_shell_command", %{"action" => action}, socket) do @@ -551,12 +559,57 @@ defmodule BDS.Desktop.ShellLive do index(assigns) end - defp reload_shell(socket, workbench) do + defp refresh_layout(socket, workbench) do + git_badge_count = socket.assigns[:git_badge_count] || 0 + activity_buttons = Workbench.activity_buttons(workbench, git_badge_count) + task_status = socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil} + dashboard = socket.assigns[:dashboard] || BDS.UI.Dashboard.empty_snapshot() + page_language = socket.assigns[:page_language] || ShellData.ui_language() + offline_mode = Map.get(socket.assigns, :offline_mode, true) + sidebar_data = socket.assigns[:sidebar_data] || %{} + + socket + |> assign(:workbench, workbench) + |> assign(:activity_buttons, activity_buttons) + |> assign( + :sidebar_header, + active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data) + ) + |> assign(:panel_tabs, ShellData.panel_tabs(workbench)) + |> assign(:current_tab, current_tab(workbench)) + |> assign(:editor_meta, ShellData.editor_meta(task_status)) + |> assign( + :status, + ShellData.status_bar(workbench, task_status, dashboard, + ui_language: page_language, + offline_mode: offline_mode + ) + ) + end + + defp refresh_sidebar(socket, workbench) do + project_id = (socket.assigns[:projects] || %{})[:active_project_id] + active_view_id = Atom.to_string(workbench.active_view) + + sidebar_data = + ShellData.sidebar_view( + project_id, + active_view_id, + ShellSidebarState.current_filters(socket, active_view_id) + ) + + sidebar_data = ShellSidebarState.merge_ui_state(socket, active_view_id, sidebar_data) + + socket + |> assign(:sidebar_data, sidebar_data) + |> refresh_layout(workbench) + end + + defp refresh_content(socket, workbench) do projects = ShellData.project_snapshot() dashboard = ShellData.dashboard(projects.active_project_id) git_badge_count = ShellData.git_badge_count(projects.active_project_id) active_view_id = Atom.to_string(workbench.active_view) - tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{}) sidebar_data = ShellData.sidebar_view( @@ -566,8 +619,26 @@ defmodule BDS.Desktop.ShellLive do ) sidebar_data = ShellSidebarState.merge_ui_state(socket, active_view_id, sidebar_data) + + socket + |> assign(:projects, projects) + |> assign(:current_project, ShellData.current_project(projects)) + |> assign(:dashboard, dashboard) + |> assign(:dashboard_timeline_entries, Map.get(dashboard, :timeline_entries, [])) + |> assign(:dashboard_category_counts, Map.get(dashboard, :category_counts, [])) + |> assign(:dashboard_recent_posts, Map.get(dashboard, :recent_posts, [])) + |> assign( + :dashboard_tag_cloud_items, + ShellData.dashboard_tag_cloud_items(Map.get(dashboard, :tag_cloud_items, [])) + ) + |> assign(:git_badge_count, git_badge_count) + |> assign(:sidebar_data, sidebar_data) + |> refresh_layout(workbench) + end + + defp reload_shell(socket, workbench) do + tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{}) 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 = @@ -581,38 +652,13 @@ defmodule BDS.Desktop.ShellLive do socket |> assign(:tab_meta, tab_meta) - |> assign(:workbench, workbench) - |> assign(:projects, projects) - |> assign(:current_project, ShellData.current_project(projects)) - |> assign(:dashboard, dashboard) - |> assign(:dashboard_timeline_entries, Map.get(dashboard, :timeline_entries, [])) - |> assign(:dashboard_category_counts, Map.get(dashboard, :category_counts, [])) - |> assign(:dashboard_recent_posts, Map.get(dashboard, :recent_posts, [])) - |> assign( - :dashboard_tag_cloud_items, - ShellData.dashboard_tag_cloud_items(Map.get(dashboard, :tag_cloud_items, [])) - ) - |> assign(:sidebar_data, sidebar_data) - |> assign( - :sidebar_header, - active_sidebar_label(activity_buttons, workbench.active_view, sidebar_data) - ) - |> assign(:assistant_cards, ShellData.assistant_cards()) - |> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:task_status, task_status) - |> assign( - :status, - ShellData.status_bar(workbench, task_status, dashboard, - ui_language: page_language, - offline_mode: offline_mode - ) - ) - |> assign(:activity_buttons, activity_buttons) - |> assign(:panel_tabs, ShellData.panel_tabs(workbench)) + |> assign(:offline_mode, offline_mode) + |> assign(:assistant_cards, ShellData.assistant_cards()) |> assign(:supported_ui_languages, ShellData.supported_ui_languages()) |> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups()) |> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index]) - |> assign(:current_tab, current_tab(workbench)) + |> refresh_content(workbench) end defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts) @@ -657,6 +703,7 @@ defmodule BDS.Desktop.ShellLive do defp sidebar_create_callbacks do %{ reload: &reload_shell/2, + refresh_content: &refresh_content/2, open_sidebar: &open_sidebar_item/3, append_output: &append_output_entry/5 } @@ -681,9 +728,11 @@ defmodule BDS.Desktop.ShellLive do subtitle: Map.get(params, "subtitle", "") }) + tab_meta = TabHelpers.sync_tab_meta(workbench, tab_meta) + socket |> assign(:tab_meta, tab_meta) - |> reload_shell(workbench) + |> refresh_layout(workbench) end defp sidebar_create_action(view), do: SidebarCreate.action(view) @@ -752,8 +801,16 @@ defmodule BDS.Desktop.ShellLive do defp handle_menu_action(socket, action) when is_atom(action) do cond do - MapSet.member?(@local_menu_actions, action) -> - reload_shell(socket, MenuBar.execute(socket.assigns.workbench, action)) + MapSet.member?(@layout_menu_actions, action) -> + refresh_layout(socket, MenuBar.execute(socket.assigns.workbench, action)) + + MapSet.member?(@sidebar_menu_actions, action) -> + workbench = MenuBar.execute(socket.assigns.workbench, action) + tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{}) + + socket + |> assign(:tab_meta, tab_meta) + |> refresh_sidebar(workbench) MapSet.member?(@socket_menu_actions, action) -> handle_socket_menu_action(socket, action) @@ -842,14 +899,14 @@ defmodule BDS.Desktop.ShellLive do socket end - defp save_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench) + defp save_current_tab(socket), do: refresh_layout(socket, socket.assigns.workbench) defp publish_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do send_update(PostEditor, id: "post-editor-#{post_id}", action: :publish) socket end - defp publish_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench) + defp publish_current_tab(socket), do: refresh_layout(socket, socket.assigns.workbench) defp apply_shell_command(socket, action, params \\ %{}), do: ShellCommandRunner.execute(socket, action, params, shell_command_callbacks()) @@ -860,6 +917,7 @@ defmodule BDS.Desktop.ShellLive do defp shell_command_callbacks do %{ reload: &reload_shell/2, + refresh_content: &refresh_content/2, append_output: &append_output_entry/5 } end @@ -876,6 +934,7 @@ defmodule BDS.Desktop.ShellLive do defp overlay_callbacks, do: %{ reload: &reload_shell/2, + refresh_content: &refresh_content/2, append_output: &append_output_entry/5, execute_sidebar_delete: fn socket, route, id -> SidebarDelete.execute_delete(socket, route, id, sidebar_delete_callbacks()) @@ -885,12 +944,16 @@ defmodule BDS.Desktop.ShellLive do defp sidebar_delete_callbacks, do: %{ reload: &reload_shell/2, + refresh_content: &refresh_content/2, append_output: &append_output_entry/5 } defp bridges_callbacks, do: %{ reload: &reload_shell/2, + refresh_layout: &refresh_layout/2, + refresh_sidebar: &refresh_sidebar/2, + refresh_content: &refresh_content/2, append_output: &append_output_entry/5, open_sidebar: &open_sidebar_item/3, apply_shell_command: &apply_shell_command/3, diff --git a/lib/bds/desktop/shell_live/bridges.ex b/lib/bds/desktop/shell_live/bridges.ex index a844d2c..d272b5f 100644 --- a/lib/bds/desktop/shell_live/bridges.ex +++ b/lib/bds/desktop/shell_live/bridges.ex @@ -25,7 +25,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do {:noreply, socket |> assign(:tab_meta, tab_meta) - |> callbacks.reload.(socket.assigns.workbench)} + |> callbacks.refresh_sidebar.(socket.assigns.workbench)} end def handle_info({:chat_tool_call, conversation_id, tool_call}, socket, _callbacks) do @@ -82,7 +82,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do {:noreply, socket |> assign(:tab_meta, tab_meta) - |> callbacks.reload.(socket.assigns.workbench)} + |> callbacks.refresh_sidebar.(socket.assigns.workbench)} end def handle_info({:open_sidebar_item, params, intent}, socket, callbacks) do @@ -90,25 +90,30 @@ defmodule BDS.Desktop.ShellLive.Bridges do end def handle_info({:chat_editor_toggle_sidebar}, socket, callbacks) do - {:noreply, callbacks.reload.(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} + {:noreply, + callbacks.refresh_layout.(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} end def handle_info({:chat_editor_toggle_panel}, socket, callbacks) do - {:noreply, callbacks.reload.(socket, Workbench.toggle_panel(socket.assigns.workbench))} + {:noreply, + callbacks.refresh_layout.(socket, Workbench.toggle_panel(socket.assigns.workbench))} end def handle_info({:chat_editor_toggle_assistant_sidebar}, socket, callbacks) do {:noreply, - callbacks.reload.(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} + callbacks.refresh_layout.( + socket, + Workbench.toggle_assistant_sidebar(socket.assigns.workbench) + )} end def handle_info({:chat_editor_switch_view, view}, socket, callbacks) do {:noreply, - callbacks.reload.(socket, Workbench.click_activity(socket.assigns.workbench, view))} + callbacks.refresh_sidebar.(socket, Workbench.click_activity(socket.assigns.workbench, view))} end def handle_info({:entity_changed, payload}, socket, callbacks) when is_map(payload) do - {:noreply, CliSync.apply_entity_change(socket, payload, callbacks.reload)} + {:noreply, CliSync.apply_entity_change(socket, payload, callbacks.refresh_content)} end def handle_info(:refresh_task_status, socket, callbacks) do @@ -155,7 +160,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do end def handle_info(:tags_changed, socket, callbacks) do - {:noreply, callbacks.reload.(socket, socket.assigns.workbench)} + {:noreply, callbacks.refresh_content.(socket, socket.assigns.workbench)} end def handle_info({:settings_output, title, message, level}, socket, callbacks) do @@ -267,7 +272,8 @@ defmodule BDS.Desktop.ShellLive.Bridges do end def handle_info({:close_tab, type, id}, socket, callbacks) do - {:noreply, callbacks.reload.(socket, Workbench.close_tab(socket.assigns.workbench, type, id))} + {:noreply, + callbacks.refresh_layout.(socket, Workbench.close_tab(socket.assigns.workbench, type, id))} end def handle_info(_message, socket, _callbacks), do: {:noreply, socket} diff --git a/lib/bds/desktop/shell_live/sidebar_create.ex b/lib/bds/desktop/shell_live/sidebar_create.ex index d813e50..3ad45b3 100644 --- a/lib/bds/desktop/shell_live/sidebar_create.ex +++ b/lib/bds/desktop/shell_live/sidebar_create.ex @@ -19,7 +19,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do def create(socket, kind, callbacks) do case socket.assigns.projects.active_project_id do project_id when is_binary(project_id) -> create(socket, project_id, kind, callbacks) - _other -> callbacks.reload.(socket, socket.assigns.workbench) + _other -> callbacks.refresh_content.(socket, socket.assigns.workbench) end end @@ -32,12 +32,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do categories: [] }) do {:ok, _post} -> - callbacks.reload.(socket, socket.assigns.workbench) + callbacks.refresh_content.(socket, socket.assigns.workbench) {:error, reason} -> socket |> callbacks.append_output.(dgettext("ui", "New Post"), inspect(reason), nil, "error") - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -46,7 +46,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do {:ok, source_path} -> case BDS.Media.import_media(%{project_id: project_id, source_path: source_path}) do {:ok, _media} -> - callbacks.reload.(socket, socket.assigns.workbench) + callbacks.refresh_content.(socket, socket.assigns.workbench) {:error, reason} -> socket @@ -56,16 +56,16 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end :cancel -> - callbacks.reload.(socket, socket.assigns.workbench) + callbacks.refresh_content.(socket, socket.assigns.workbench) {:error, %{message: message}} -> socket |> callbacks.append_output.(dgettext("ui", "Import media"), message, nil, "error") - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -98,7 +98,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -130,7 +130,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -151,7 +151,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do {:error, reason} -> socket |> callbacks.append_output.(dgettext("ui", "New Chat"), inspect(reason), nil, "error") - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -180,12 +180,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end def create(socket, _project_id, _kind, callbacks), - do: callbacks.reload.(socket, socket.assigns.workbench) + do: callbacks.refresh_content.(socket, socket.assigns.workbench) def action(:posts), do: %{kind: "post", label: "sidebar.newPost"} def action(:media), do: %{kind: "media", label: "sidebar.importMedia"} diff --git a/lib/bds/desktop/shell_live/sidebar_delete.ex b/lib/bds/desktop/shell_live/sidebar_delete.ex index 53861f1..83834e0 100644 --- a/lib/bds/desktop/shell_live/sidebar_delete.ex +++ b/lib/bds/desktop/shell_live/sidebar_delete.ex @@ -31,7 +31,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do socket |> assign(:shell_overlay, nil) |> callbacks.append_output.(delete_title(route), inspect(reason), nil, "error") - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -74,7 +74,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end @@ -88,7 +88,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do socket |> assign(:shell_overlay, nil) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {type, id})) - |> callbacks.reload.(workbench) + |> callbacks.refresh_content.(workbench) {:error, reason} -> socket @@ -99,7 +99,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do nil, "error" ) - |> callbacks.reload.(socket.assigns.workbench) + |> callbacks.refresh_content.(socket.assigns.workbench) end end diff --git a/test/bds/csm007_reload_shell_test.exs b/test/bds/csm007_reload_shell_test.exs new file mode 100644 index 0000000..65fd0c3 --- /dev/null +++ b/test/bds/csm007_reload_shell_test.exs @@ -0,0 +1,191 @@ +defmodule BDS.CSM007ReloadShellTest do + use ExUnit.Case, async: false + + import Phoenix.ConnTest + import Phoenix.LiveViewTest + + @endpoint BDS.Desktop.Endpoint + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) + + temp_dir = + Path.join(System.tmp_dir!(), "bds-csm007-#{System.unique_integer([:positive])}") + + File.mkdir_p!(temp_dir) + on_exit(fn -> File.rm_rf(temp_dir) end) + + {:ok, project} = BDS.Projects.create_project(%{name: "CSM007", data_path: temp_dir}) + %{project: project} + end + + describe "toggle_sidebar" do + test "triggers no dashboard or git queries", %{project: _project} do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + query_count = count_queries(fn -> + render_click(view, "toggle_sidebar", %{}) + end) + + assert query_count == 0, + "Expected 0 DB queries for sidebar toggle, got #{query_count}" + end + end + + describe "toggle_panel" do + test "triggers no DB queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + query_count = count_queries(fn -> + render_click(view, "toggle_panel", %{}) + end) + + assert query_count == 0, + "Expected 0 DB queries for panel toggle, got #{query_count}" + end + end + + describe "sync_layout" do + test "triggers no DB queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + query_count = count_queries(fn -> + render_click(view, "sync_layout", %{ + "sidebar_width" => "300", + "sidebar_visible" => "true", + "panel_visible" => "false" + }) + end) + + assert query_count == 0, + "Expected 0 DB queries for sync_layout, got #{query_count}" + end + end + + describe "select_panel_tab" do + test "triggers no DB queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + query_count = count_queries(fn -> + render_click(view, "select_panel_tab", %{"tab" => "output"}) + end) + + assert query_count == 0, + "Expected 0 DB queries for select_panel_tab, got #{query_count}" + end + end + + describe "select_view" do + test "triggers sidebar query but not dashboard or git queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + {query_count, query_sources} = count_queries_with_sources(fn -> + render_click(view, "select_view", %{"view" => "media"}) + end) + + assert query_count > 0, "Expected at least 1 query for view change" + + refute "dashboard" in query_sources or "projects" in query_sources, + "View change should not query dashboard or projects, got: #{inspect(query_sources)}" + end + end + + describe "sidebar filter events" do + test "do not trigger dashboard or git queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + {_count, query_sources} = count_queries_with_sources(fn -> + render_click(view, "update_sidebar_search", %{ + "sidebar_filters" => %{"search" => "test"} + }) + end) + + refute "dashboard" in query_sources, + "Sidebar search should not query dashboard, got: #{inspect(query_sources)}" + end + end + + describe "toggle_offline_mode" do + test "triggers only the settings write, no refresh queries" do + {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) + + query_count = count_queries(fn -> + render_click(view, "toggle_offline_mode", %{}) + end) + + assert query_count == 1, + "Expected exactly 1 DB query (settings write) for offline mode toggle, got #{query_count}" + end + end + + defp count_queries(func) do + test_pid = self() + ref = make_ref() + handler_id = "csm007-query-counter-#{inspect(ref)}" + + :telemetry.attach( + handler_id, + [:bds, :repo, :query], + fn _event, _measurements, _metadata, _ -> + send(test_pid, {:query_executed, ref}) + end, + nil + ) + + func.() + + :telemetry.detach(handler_id) + count_messages(ref, 0) + end + + defp count_queries_with_sources(func) do + test_pid = self() + ref = make_ref() + handler_id = "csm007-query-sources-#{inspect(ref)}" + + :telemetry.attach( + handler_id, + [:bds, :repo, :query], + fn _event, _measurements, metadata, _ -> + source = metadata[:source] || extract_table(metadata[:query] || "") + send(test_pid, {:query_executed, ref, source}) + end, + nil + ) + + func.() + + :telemetry.detach(handler_id) + collect_query_sources(ref, 0, []) + end + + defp collect_query_sources(ref, count, sources) do + receive do + {:query_executed, ^ref, source} -> + collect_query_sources(ref, count + 1, [source | sources]) + after + 0 -> {count, Enum.uniq(sources)} + end + end + + defp extract_table(query) when is_binary(query) do + cond do + String.contains?(query, "dashboard") -> "dashboard" + String.contains?(query, "projects") -> "projects" + String.contains?(query, "posts") -> "posts" + String.contains?(query, "media") -> "media" + String.contains?(query, "tags") -> "tags" + true -> "other" + end + end + + defp extract_table(_), do: "unknown" + + defp count_messages(ref, acc) do + receive do + {:query_executed, ^ref} -> count_messages(ref, acc + 1) + after + 0 -> acc + end + end +end