fix: fixed CSM-007

This commit is contained in:
2026-05-09 10:41:28 +02:00
parent e5429f7265
commit 88c689ee55
6 changed files with 364 additions and 98 deletions

View File

@@ -131,18 +131,23 @@
--- ---
### CSM-007 — Monolithic State Rebuild ("God Function") ### ~~CSM-007 — Monolithic State Rebuild ("God Function")~~ ✅ FIXED
- **File:** `lib/bds/desktop/shell_live.ex:554-616` - **Fixed:** 2026-05-09
- **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. - **What was done:**
- **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. - Decomposed `reload_shell/2` into four focused updaters:
- **Fix:** Decompose into focused updaters, each only querying what it needs: - `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`only queries sidebar data - `refresh_sidebar/2`Queries sidebar data only, then calls `refresh_layout`.
- `refresh_dashboard/2`only queries dashboard data - `refresh_content/2`Queries projects, dashboard, git badge, and sidebar data, then calls `refresh_layout`.
- `refresh_git_badge/2` — only queries git status - `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.
- `refresh_task_status/2` — only queries task state - Replaced all call sites with the minimal refresh needed:
- `refresh_tab_meta/2` — only syncs tab metadata - **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).
Each `handle_event` / `handle_info` should call only the relevant updaters. - **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.
- **Test:** Toggle sidebar; assert no dashboard or git queries are executed. Save a post; assert sidebar refreshes but dashboard does not. - **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. - 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. - [ ] 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-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-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). - [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] Tests were written **before** implementation changes (Red → Green → Refactor).
- [x] Full test suite passes: `mix test`. - [x] Full test suite passes: `mix test`.
- [x] Dialyzer passes cleanly: `mix dialyzer` (zero warnings). - [x] Dialyzer passes cleanly: `mix dialyzer` (zero warnings).

View File

@@ -82,18 +82,21 @@ defmodule BDS.Desktop.ShellLive do
"load_more_sidebar" "load_more_sidebar"
] ]
@local_menu_actions MapSet.new([ @layout_menu_actions MapSet.new([
:toggle_sidebar, :toggle_sidebar,
:toggle_panel, :toggle_panel,
:toggle_assistant_sidebar, :toggle_assistant_sidebar,
:view_posts, :close_tab
:view_media, ])
:edit_preferences, @sidebar_menu_actions MapSet.new([
:edit_menu, :view_posts,
:documentation, :view_media,
:api_documentation, :edit_preferences,
:close_tab :edit_menu,
]) :documentation,
:api_documentation
])
@local_menu_actions MapSet.union(@layout_menu_actions, @sidebar_menu_actions)
@socket_menu_actions MapSet.new([ @socket_menu_actions MapSet.new([
:new_post, :new_post,
:import_media, :import_media,
@@ -174,15 +177,15 @@ defmodule BDS.Desktop.ShellLive do
@impl true @impl true
def handle_event("toggle_sidebar", _params, socket) do 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 end
def handle_event("toggle_panel", _params, socket) do 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 end
def handle_event("toggle_assistant_sidebar", _params, socket) do 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 end
def handle_event("select_view", %{"view" => view_id}, socket) do 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) BoundedAtoms.sidebar_view(view_id, :posts)
) )
{:noreply, reload_shell(socket, workbench)} {:noreply, refresh_sidebar(socket, workbench)}
end end
def handle_event("select_panel_tab", %{"tab" => tab}, socket) do 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_visible(true)
|> Workbench.set_panel_tab(BoundedAtoms.panel_tab(tab, :tasks)) |> Workbench.set_panel_tab(BoundedAtoms.panel_tab(tab, :tasks))
{:noreply, reload_shell(socket, workbench)} {:noreply, refresh_layout(socket, workbench)}
end end
def handle_event("open_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do def handle_event("open_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do
@@ -213,15 +216,15 @@ defmodule BDS.Desktop.ShellLive do
end end
def handle_event("sync_layout", params, socket) do 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 end
def handle_event("resize_panel", %{"target" => target, "width" => width}, socket) do 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 end
def handle_event(event, params, socket) when event in @sidebar_filter_events do 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 end
def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do
@@ -248,7 +251,12 @@ defmodule BDS.Desktop.ShellLive do
:preview :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 end
def handle_event("close_tab", %{"type" => type, "id" => id}, socket) do def handle_event("close_tab", %{"type" => type, "id" => id}, socket) do
@@ -259,7 +267,7 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, {:noreply,
socket socket
|> assign(:tab_meta, tab_meta) |> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)} |> refresh_layout(workbench)}
end end
def handle_event( def handle_event(
@@ -283,7 +291,7 @@ defmodule BDS.Desktop.ShellLive do
:ok = AI.set_airplane_mode(next_mode) :ok = AI.set_airplane_mode(next_mode)
socket = assign(socket, :offline_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 end
def handle_event("update_assistant_prompt", %{"assistant" => %{"prompt" => prompt}}, socket) do 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_visible(true)
|> Workbench.set_panel_tab(:tasks) |> Workbench.set_panel_tab(:tasks)
{:noreply, reload_shell(socket, workbench)} {:noreply, refresh_layout(socket, workbench)}
end end
def handle_event("settings_shell_command", %{"action" => action}, socket) do def handle_event("settings_shell_command", %{"action" => action}, socket) do
@@ -551,12 +559,57 @@ defmodule BDS.Desktop.ShellLive do
index(assigns) index(assigns)
end 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() projects = ShellData.project_snapshot()
dashboard = ShellData.dashboard(projects.active_project_id) dashboard = ShellData.dashboard(projects.active_project_id)
git_badge_count = ShellData.git_badge_count(projects.active_project_id) git_badge_count = ShellData.git_badge_count(projects.active_project_id)
active_view_id = Atom.to_string(workbench.active_view) active_view_id = Atom.to_string(workbench.active_view)
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
sidebar_data = sidebar_data =
ShellData.sidebar_view( ShellData.sidebar_view(
@@ -566,8 +619,26 @@ defmodule BDS.Desktop.ShellLive do
) )
sidebar_data = ShellSidebarState.merge_ui_state(socket, active_view_id, sidebar_data) 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() 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() page_language = socket.assigns[:page_language] || ShellData.ui_language()
offline_mode = offline_mode =
@@ -581,38 +652,13 @@ defmodule BDS.Desktop.ShellLive do
socket socket
|> assign(:tab_meta, tab_meta) |> 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(:task_status, task_status)
|> assign( |> assign(:offline_mode, offline_mode)
:status, |> assign(:assistant_cards, ShellData.assistant_cards())
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(:supported_ui_languages, ShellData.supported_ui_languages()) |> assign(:supported_ui_languages, ShellData.supported_ui_languages())
|> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups()) |> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups())
|> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index]) |> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index])
|> assign(:current_tab, current_tab(workbench)) |> refresh_content(workbench)
end end
defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts) defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts)
@@ -657,6 +703,7 @@ defmodule BDS.Desktop.ShellLive do
defp sidebar_create_callbacks do defp sidebar_create_callbacks do
%{ %{
reload: &reload_shell/2, reload: &reload_shell/2,
refresh_content: &refresh_content/2,
open_sidebar: &open_sidebar_item/3, open_sidebar: &open_sidebar_item/3,
append_output: &append_output_entry/5 append_output: &append_output_entry/5
} }
@@ -681,9 +728,11 @@ defmodule BDS.Desktop.ShellLive do
subtitle: Map.get(params, "subtitle", "") subtitle: Map.get(params, "subtitle", "")
}) })
tab_meta = TabHelpers.sync_tab_meta(workbench, tab_meta)
socket socket
|> assign(:tab_meta, tab_meta) |> assign(:tab_meta, tab_meta)
|> reload_shell(workbench) |> refresh_layout(workbench)
end end
defp sidebar_create_action(view), do: SidebarCreate.action(view) 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 defp handle_menu_action(socket, action) when is_atom(action) do
cond do cond do
MapSet.member?(@local_menu_actions, action) -> MapSet.member?(@layout_menu_actions, action) ->
reload_shell(socket, MenuBar.execute(socket.assigns.workbench, 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) -> MapSet.member?(@socket_menu_actions, action) ->
handle_socket_menu_action(socket, action) handle_socket_menu_action(socket, action)
@@ -842,14 +899,14 @@ defmodule BDS.Desktop.ShellLive do
socket socket
end 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 defp publish_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do
send_update(PostEditor, id: "post-editor-#{post_id}", action: :publish) send_update(PostEditor, id: "post-editor-#{post_id}", action: :publish)
socket socket
end 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 \\ %{}), defp apply_shell_command(socket, action, params \\ %{}),
do: ShellCommandRunner.execute(socket, action, params, shell_command_callbacks()) do: ShellCommandRunner.execute(socket, action, params, shell_command_callbacks())
@@ -860,6 +917,7 @@ defmodule BDS.Desktop.ShellLive do
defp shell_command_callbacks do defp shell_command_callbacks do
%{ %{
reload: &reload_shell/2, reload: &reload_shell/2,
refresh_content: &refresh_content/2,
append_output: &append_output_entry/5 append_output: &append_output_entry/5
} }
end end
@@ -876,6 +934,7 @@ defmodule BDS.Desktop.ShellLive do
defp overlay_callbacks, defp overlay_callbacks,
do: %{ do: %{
reload: &reload_shell/2, reload: &reload_shell/2,
refresh_content: &refresh_content/2,
append_output: &append_output_entry/5, append_output: &append_output_entry/5,
execute_sidebar_delete: fn socket, route, id -> execute_sidebar_delete: fn socket, route, id ->
SidebarDelete.execute_delete(socket, route, id, sidebar_delete_callbacks()) SidebarDelete.execute_delete(socket, route, id, sidebar_delete_callbacks())
@@ -885,12 +944,16 @@ defmodule BDS.Desktop.ShellLive do
defp sidebar_delete_callbacks, defp sidebar_delete_callbacks,
do: %{ do: %{
reload: &reload_shell/2, reload: &reload_shell/2,
refresh_content: &refresh_content/2,
append_output: &append_output_entry/5 append_output: &append_output_entry/5
} }
defp bridges_callbacks, defp bridges_callbacks,
do: %{ do: %{
reload: &reload_shell/2, 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, append_output: &append_output_entry/5,
open_sidebar: &open_sidebar_item/3, open_sidebar: &open_sidebar_item/3,
apply_shell_command: &apply_shell_command/3, apply_shell_command: &apply_shell_command/3,

View File

@@ -25,7 +25,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do
{:noreply, {:noreply,
socket socket
|> assign(:tab_meta, tab_meta) |> assign(:tab_meta, tab_meta)
|> callbacks.reload.(socket.assigns.workbench)} |> callbacks.refresh_sidebar.(socket.assigns.workbench)}
end end
def handle_info({:chat_tool_call, conversation_id, tool_call}, socket, _callbacks) do def handle_info({:chat_tool_call, conversation_id, tool_call}, socket, _callbacks) do
@@ -82,7 +82,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do
{:noreply, {:noreply,
socket socket
|> assign(:tab_meta, tab_meta) |> assign(:tab_meta, tab_meta)
|> callbacks.reload.(socket.assigns.workbench)} |> callbacks.refresh_sidebar.(socket.assigns.workbench)}
end end
def handle_info({:open_sidebar_item, params, intent}, socket, callbacks) do def handle_info({:open_sidebar_item, params, intent}, socket, callbacks) do
@@ -90,25 +90,30 @@ defmodule BDS.Desktop.ShellLive.Bridges do
end end
def handle_info({:chat_editor_toggle_sidebar}, socket, callbacks) do 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 end
def handle_info({:chat_editor_toggle_panel}, socket, callbacks) do 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 end
def handle_info({:chat_editor_toggle_assistant_sidebar}, socket, callbacks) do def handle_info({:chat_editor_toggle_assistant_sidebar}, socket, callbacks) do
{:noreply, {:noreply,
callbacks.reload.(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))} callbacks.refresh_layout.(
socket,
Workbench.toggle_assistant_sidebar(socket.assigns.workbench)
)}
end end
def handle_info({:chat_editor_switch_view, view}, socket, callbacks) do def handle_info({:chat_editor_switch_view, view}, socket, callbacks) do
{:noreply, {:noreply,
callbacks.reload.(socket, Workbench.click_activity(socket.assigns.workbench, view))} callbacks.refresh_sidebar.(socket, Workbench.click_activity(socket.assigns.workbench, view))}
end end
def handle_info({:entity_changed, payload}, socket, callbacks) when is_map(payload) do 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 end
def handle_info(:refresh_task_status, socket, callbacks) do def handle_info(:refresh_task_status, socket, callbacks) do
@@ -155,7 +160,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do
end end
def handle_info(:tags_changed, socket, callbacks) do def handle_info(:tags_changed, socket, callbacks) do
{:noreply, callbacks.reload.(socket, socket.assigns.workbench)} {:noreply, callbacks.refresh_content.(socket, socket.assigns.workbench)}
end end
def handle_info({:settings_output, title, message, level}, socket, callbacks) do def handle_info({:settings_output, title, message, level}, socket, callbacks) do
@@ -267,7 +272,8 @@ defmodule BDS.Desktop.ShellLive.Bridges do
end end
def handle_info({:close_tab, type, id}, socket, callbacks) do 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 end
def handle_info(_message, socket, _callbacks), do: {:noreply, socket} def handle_info(_message, socket, _callbacks), do: {:noreply, socket}

View File

@@ -19,7 +19,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
def create(socket, kind, callbacks) do def create(socket, kind, callbacks) do
case socket.assigns.projects.active_project_id do case socket.assigns.projects.active_project_id do
project_id when is_binary(project_id) -> create(socket, project_id, kind, callbacks) 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
end end
@@ -32,12 +32,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
categories: [] categories: []
}) do }) do
{:ok, _post} -> {:ok, _post} ->
callbacks.reload.(socket, socket.assigns.workbench) callbacks.refresh_content.(socket, socket.assigns.workbench)
{:error, reason} -> {:error, reason} ->
socket socket
|> callbacks.append_output.(dgettext("ui", "New Post"), inspect(reason), nil, "error") |> callbacks.append_output.(dgettext("ui", "New Post"), inspect(reason), nil, "error")
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -46,7 +46,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
{:ok, source_path} -> {:ok, source_path} ->
case BDS.Media.import_media(%{project_id: project_id, source_path: source_path}) do case BDS.Media.import_media(%{project_id: project_id, source_path: source_path}) do
{:ok, _media} -> {:ok, _media} ->
callbacks.reload.(socket, socket.assigns.workbench) callbacks.refresh_content.(socket, socket.assigns.workbench)
{:error, reason} -> {:error, reason} ->
socket socket
@@ -56,16 +56,16 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
:cancel -> :cancel ->
callbacks.reload.(socket, socket.assigns.workbench) callbacks.refresh_content.(socket, socket.assigns.workbench)
{:error, %{message: message}} -> {:error, %{message: message}} ->
socket socket
|> callbacks.append_output.(dgettext("ui", "Import media"), message, nil, "error") |> callbacks.append_output.(dgettext("ui", "Import media"), message, nil, "error")
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -98,7 +98,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -130,7 +130,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -151,7 +151,7 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
{:error, reason} -> {:error, reason} ->
socket socket
|> callbacks.append_output.(dgettext("ui", "New Chat"), inspect(reason), nil, "error") |> callbacks.append_output.(dgettext("ui", "New Chat"), inspect(reason), nil, "error")
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -180,12 +180,12 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
def create(socket, _project_id, _kind, callbacks), 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(:posts), do: %{kind: "post", label: "sidebar.newPost"}
def action(:media), do: %{kind: "media", label: "sidebar.importMedia"} def action(:media), do: %{kind: "media", label: "sidebar.importMedia"}

View File

@@ -31,7 +31,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
socket socket
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> callbacks.append_output.(delete_title(route), inspect(reason), nil, "error") |> callbacks.append_output.(delete_title(route), inspect(reason), nil, "error")
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -74,7 +74,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end
@@ -88,7 +88,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
socket socket
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {type, id})) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {type, id}))
|> callbacks.reload.(workbench) |> callbacks.refresh_content.(workbench)
{:error, reason} -> {:error, reason} ->
socket socket
@@ -99,7 +99,7 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
nil, nil,
"error" "error"
) )
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.refresh_content.(socket.assigns.workbench)
end end
end end

View File

@@ -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