chore: noise in tests

This commit is contained in:
2026-05-04 06:18:06 +02:00
parent 4de8492c4f
commit 43a4610ce7
48 changed files with 619 additions and 239 deletions

View File

@@ -3,6 +3,7 @@ import Config
config :bds, BDS.Repo, config :bds, BDS.Repo,
database: Path.expand("../priv/data/bds_test.db", __DIR__), database: Path.expand("../priv/data/bds_test.db", __DIR__),
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 5 pool_size: 5,
busy_timeout: 15_000
config :logger, level: :warning config :logger, level: :warning

View File

@@ -69,7 +69,9 @@ defmodule BDS.AI.Chat do
{:error, :not_found} {:error, :not_found}
%ChatConversation{} = conversation -> %ChatConversation{} = conversation ->
Repo.delete_all(from message in ChatMessage, where: message.conversation_id == ^conversation_id) Repo.delete_all(
from message in ChatMessage, where: message.conversation_id == ^conversation_id
)
case Repo.delete(conversation) do case Repo.delete(conversation) do
{:ok, _conversation} -> {:ok, :deleted} {:ok, _conversation} -> {:ok, :deleted}
@@ -375,7 +377,8 @@ defmodule BDS.AI.Chat do
opts, opts,
@chat_max_tool_rounds @chat_max_tool_rounds
), ),
{:ok, reply} <- maybe_generate_chat_title(conversation.id, user_message.content, reply, opts) do {:ok, reply} <-
maybe_generate_chat_title(conversation.id, user_message.content, reply, opts) do
{:ok, reply} {:ok, reply}
end end
end end
@@ -425,7 +428,8 @@ defmodule BDS.AI.Chat do
with {:ok, endpoint, model, mode} <- Runtime.resolve_target(:chat_title, opts), with {:ok, endpoint, model, mode} <- Runtime.resolve_target(:chat_title, opts),
:ok <- Runtime.validate_target(:chat_title, model, mode), :ok <- Runtime.validate_target(:chat_title, model, mode),
request <- build_chat_title_request(user_content, model), request <- build_chat_title_request(user_content, model),
{:ok, response} <- runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts) do {:ok, response} <-
runtime.generate(Runtime.endpoint_with_model(endpoint, model), request, opts) do
title = sanitize_chat_title(Map.get(response, :content)) title = sanitize_chat_title(Map.get(response, :content))
if title == "" do if title == "" do

View File

@@ -36,7 +36,9 @@ defmodule BDS.AI.ChatMessage do
:cache_read_tokens, :cache_read_tokens,
:cache_write_tokens, :cache_write_tokens,
:created_at :created_at
], empty_values: [nil]) ],
empty_values: [nil]
)
|> validate_required([:conversation_id, :role, :created_at]) |> validate_required([:conversation_id, :role, :created_at])
|> assoc_constraint(:conversation) |> assoc_constraint(:conversation)
end end

View File

@@ -60,7 +60,9 @@ defmodule BDS.AI.Model do
:interleaved, :interleaved,
:status, :status,
:updated_at :updated_at
], empty_values: [nil]) ],
empty_values: [nil]
)
|> validate_required([ |> validate_required([
:provider, :provider,
:model_id, :model_id,

View File

@@ -211,8 +211,14 @@ defmodule BDS.AI.OneShot do
model: model, model: model,
max_output_tokens: @default_max_output_tokens, max_output_tokens: @default_max_output_tokens,
messages: [ messages: [
%{"role" => "system", "content" => one_shot_system_prompt(operation, language, source_language)}, %{
%{"role" => "user", "content" => one_shot_user_content(operation, payload, language, source_language)} "role" => "system",
"content" => one_shot_system_prompt(operation, language, source_language)
},
%{
"role" => "user",
"content" => one_shot_user_content(operation, payload, language, source_language)
}
] ]
} }
end end
@@ -320,7 +326,8 @@ defmodule BDS.AI.OneShot do
[ [
%{ %{
"type" => "text", "type" => "text",
"text" => "Analyze this image and return title, alt text, and caption in #{language_name(language)}." "text" =>
"Analyze this image and return title, alt text, and caption in #{language_name(language)}."
}, },
%{"type" => "image_url", "image_url" => %{"url" => media.image_url}} %{"type" => "image_url", "image_url" => %{"url" => media.image_url}}
] ]
@@ -443,7 +450,11 @@ defmodule BDS.AI.OneShot do
defp resolve_image_data_url(%{image_url: "file://" <> path, mime_type: mime_type} = media) do defp resolve_image_data_url(%{image_url: "file://" <> path, mime_type: mime_type} = media) do
with {:ok, binary} <- File.read(path) do with {:ok, binary} <- File.read(path) do
data_url = "data:#{mime_type};base64," <> Base.encode64(binary) data_url = "data:#{mime_type};base64," <> Base.encode64(binary)
Logger.debug("AI analyze_image converted file://#{path} to data URL (#{byte_size(data_url)} chars)")
Logger.debug(
"AI analyze_image converted file://#{path} to data URL (#{byte_size(data_url)} chars)"
)
{:ok, %{media | image_url: data_url}} {:ok, %{media | image_url: data_url}}
else else
{:error, reason} -> {:error, reason} ->
@@ -452,7 +463,9 @@ defmodule BDS.AI.OneShot do
end end
end end
defp resolve_image_data_url(%{file_path: file_path, project_id: project_id, mime_type: mime_type} = media) defp resolve_image_data_url(
%{file_path: file_path, project_id: project_id, mime_type: mime_type} = media
)
when is_binary(file_path) and is_binary(project_id) do when is_binary(file_path) and is_binary(project_id) do
case Projects.get_project(project_id) do case Projects.get_project(project_id) do
nil -> nil ->
@@ -465,7 +478,11 @@ defmodule BDS.AI.OneShot do
case File.read(absolute_path) do case File.read(absolute_path) do
{:ok, binary} -> {:ok, binary} ->
data_url = "data:#{mime_type};base64," <> Base.encode64(binary) data_url = "data:#{mime_type};base64," <> Base.encode64(binary)
Logger.debug("AI analyze_image converted #{absolute_path} to data URL (#{byte_size(data_url)} chars)")
Logger.debug(
"AI analyze_image converted #{absolute_path} to data URL (#{byte_size(data_url)} chars)"
)
{:ok, %{media | image_url: data_url}} {:ok, %{media | image_url: data_url}}
{:error, reason} -> {:error, reason} ->

View File

@@ -53,7 +53,7 @@ defmodule BDS.AI.OpenAICompatibleRuntime do
case result do case result do
{:ok, %{json: nil, content: content}} when is_binary(content) -> {:ok, %{json: nil, content: content}} when is_binary(content) ->
Logger.warning( Logger.debug(
"AI OpenAI-compatible response parsed but content is not valid JSON. Content: #{String.slice(content, 0, 500)}" "AI OpenAI-compatible response parsed but content is not valid JSON. Content: #{String.slice(content, 0, 500)}"
) )

View File

@@ -39,7 +39,8 @@ defmodule BDS.AI.Runtime do
:ok :ok
capabilities.supports_attachment == false -> capabilities.supports_attachment == false ->
{:error, %{kind: :model_capability_missing, capability: :supports_attachment, model: model}} {:error,
%{kind: :model_capability_missing, capability: :supports_attachment, model: model}}
true -> true ->
:ok :ok

View File

@@ -16,17 +16,38 @@ defmodule BDS.Desktop.ShellData do
def activity_icon(id) do def activity_icon(id) do
case to_string(id) do case to_string(id) do
"posts" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"></path><path d="M8 12h8v2H8zm0 4h8v2H8z"></path></svg>) "posts" ->
"pages" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"></path></svg>) ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"></path><path d="M8 12h8v2H8zm0 4h8v2H8z"></path></svg>)
"media" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg>)
"scripts" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"></path></svg>) "pages" ->
"templates" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7zM5.5 5.5v4h4v-4h-4zm9 0v4h4v-4h-4zm-9 9v4h4v-4h-4zm9 0v4h4v-4h-4z"></path></svg>) ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h10v4h6v12H4V4zm10 1.5V9h4.5L14 5.5zM7 12h10v1.5H7V12zm0 3h10v1.5H7V15z"></path></svg>)
"tags" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"></path></svg>)
"chat" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"></path><circle cx="8" cy="10" r="1.5"></circle><circle cx="12" cy="10" r="1.5"></circle><circle cx="16" cy="10" r="1.5"></circle></svg>) "media" ->
"import" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"></path></svg>) ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg>)
"git" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M22 11.73L12.27 2a1 1 0 0 0-1.41 0L8.84 4.02l2.56 2.56a1.2 1.2 0 0 1 1.52 1.53l2.47 2.47a1.2 1.2 0 1 1-.72.67l-2.3-2.3v6.06a1.2 1.2 0 1 1-.85 0V8.9a1.2 1.2 0 0 1-.66-1.59L8.35 4.8 2 11.16a1 1 0 0 0 0 1.41L11.73 22a1 1 0 0 0 1.41 0L22 13.14a1 1 0 0 0 0-1.41z"></path></svg>)
"settings" -> ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg>) "scripts" ->
_other -> activity_icon("posts") ~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 3H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h7v2H8v2h8v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zM5 14V5h14v9H5zm2-7.5L9.5 9 7 11.5l1.4 1.4L12.3 9 8.4 5.1 7 6.5zm6.5 5.5h4v-2h-4v2z"></path></svg>)
"templates" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4h7v7H4V4zm9 0h7v7h-7V4zM4 13h7v7H4v-7zm9 0h7v7h-7v-7zM5.5 5.5v4h4v-4h-4zm9 0v4h4v-4h-4zm-9 9v4h4v-4h-4zm9 0v4h4v-4h-4z"></path></svg>)
"tags" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"></path></svg>)
"chat" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"></path><circle cx="8" cy="10" r="1.5"></circle><circle cx="12" cy="10" r="1.5"></circle><circle cx="16" cy="10" r="1.5"></circle></svg>)
"import" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"></path></svg>)
"git" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M22 11.73L12.27 2a1 1 0 0 0-1.41 0L8.84 4.02l2.56 2.56a1.2 1.2 0 0 1 1.52 1.53l2.47 2.47a1.2 1.2 0 1 1-.72.67l-2.3-2.3v6.06a1.2 1.2 0 1 1-.85 0V8.9a1.2 1.2 0 0 1-.66-1.59L8.35 4.8 2 11.16a1 1 0 0 0 0 1.41L11.73 22a1 1 0 0 0 1.41 0L22 13.14a1 1 0 0 0 0-1.41z"></path></svg>)
"settings" ->
~s(<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"></path></svg>)
_other ->
activity_icon("posts")
end end
end end
@@ -83,18 +104,28 @@ defmodule BDS.Desktop.ShellData do
def assistant_cards do def assistant_cards do
[ [
%{label: dgettext("ui", "Offline Gate"), text: dgettext("ui", "Automatic AI actions stay gated by airplane mode.")}, %{
label: dgettext("ui", "Offline Gate"),
text: dgettext("ui", "Automatic AI actions stay gated by airplane mode.")
},
%{ %{
label: dgettext("ui", "Filesystem Sync"), label: dgettext("ui", "Filesystem Sync"),
text: dgettext("ui", "Metadata flush, diffing, and rebuild hooks still need editor wiring.") text:
dgettext("ui", "Metadata flush, diffing, and rebuild hooks still need editor wiring.")
}, },
%{label: dgettext("ui", "Desktop Runtime"), text: dgettext("ui", "The app window is now served from LiveView state.")} %{
label: dgettext("ui", "Desktop Runtime"),
text: dgettext("ui", "The app window is now served from LiveView state.")
}
] ]
end end
def editor_meta(task_status) do def editor_meta(task_status) do
[ [
%{label: dgettext("ui", "Status"), value: task_status.running_task_message || dgettext("ui", "Idle")}, %{
label: dgettext("ui", "Status"),
value: task_status.running_task_message || dgettext("ui", "Idle")
},
%{label: dgettext("ui", "Mode"), value: dgettext("ui", "Offline")}, %{label: dgettext("ui", "Mode"), value: dgettext("ui", "Offline")},
%{label: dgettext("ui", "Main Language"), value: ui_language()} %{label: dgettext("ui", "Main Language"), value: ui_language()}
] ]
@@ -120,13 +151,25 @@ defmodule BDS.Desktop.ShellData do
def git_badge_count(project_id, opts) when is_binary(project_id) do def git_badge_count(project_id, opts) when is_binary(project_id) do
provider = Keyword.get(opts, :provider, git_remote_state_provider()) provider = Keyword.get(opts, :provider, git_remote_state_provider())
custom_provider? = provider != (&BDS.Git.remote_state/2)
try do try do
has_git =
custom_provider? ||
case BDS.Projects.get_project(project_id) do
nil -> false
project -> File.dir?(Path.join(BDS.Projects.project_data_dir(project), ".git"))
end
if has_git do
case provider.(project_id, []) do case provider.(project_id, []) do
{:ok, %{behind: behind}} when is_integer(behind) and behind > 0 -> behind {:ok, %{behind: behind}} when is_integer(behind) and behind > 0 -> behind
{:ok, %{behind: behind}} when is_binary(behind) -> parse_positive_count(behind) {:ok, %{behind: behind}} when is_binary(behind) -> parse_positive_count(behind)
_other -> 0 _other -> 0
end end
else
0
end
rescue rescue
error in [DBConnection.OwnershipError, Exqlite.Error] -> error in [DBConnection.OwnershipError, Exqlite.Error] ->
if match?(%Exqlite.Error{}, error) and if match?(%Exqlite.Error{}, error) and

View File

@@ -60,6 +60,9 @@ defmodule BDS.Desktop.ShellLive do
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@refresh_interval 1_500 @refresh_interval 1_500
def refresh_interval, do: @refresh_interval
@output_entry_limit 20 @output_entry_limit 20
@sidebar_filter_events [ @sidebar_filter_events [
"toggle_sidebar_filters", "toggle_sidebar_filters",
@@ -126,7 +129,9 @@ defmodule BDS.Desktop.ShellLive do
|> MapSet.union(MapSet.new([:open_in_browser, :open_data_folder])) |> MapSet.union(MapSet.new([:open_in_browser, :open_data_folder]))
|> MapSet.union(MapSet.new([:preview_post, :rebuild_database, :reindex_text])) |> MapSet.union(MapSet.new([:preview_post, :rebuild_database, :reindex_text]))
|> MapSet.union(MapSet.new([:rebuild_embedding_index, :metadata_diff, :regenerate_calendar])) |> MapSet.union(MapSet.new([:rebuild_embedding_index, :metadata_diff, :regenerate_calendar]))
|> MapSet.union(MapSet.new([:validate_translations, :fill_missing_translations, :find_duplicates])) |> MapSet.union(
MapSet.new([:validate_translations, :fill_missing_translations, :find_duplicates])
)
|> MapSet.union(MapSet.new([:generate_sitemap, :validate_site, :upload_site])) |> MapSet.union(MapSet.new([:generate_sitemap, :validate_site, :upload_site]))
end end
@@ -138,7 +143,7 @@ defmodule BDS.Desktop.ShellLive do
if connected do if connected do
Phoenix.PubSub.subscribe(BDS.PubSub, Watcher.topic()) Phoenix.PubSub.subscribe(BDS.PubSub, Watcher.topic())
:timer.send_interval(@refresh_interval, :refresh_task_status) Process.send_after(self(), :refresh_task_status, @refresh_interval)
end end
workbench = Workbench.new() workbench = Workbench.new()
@@ -262,7 +267,14 @@ defmodule BDS.Desktop.ShellLive do
%{"route" => route, "id" => id} = params, %{"route" => route, "id" => id} = params,
socket socket
) do ) do
{:noreply, SidebarDelete.request_delete(socket, route, id, Map.get(params, "title"), sidebar_delete_callbacks())} {:noreply,
SidebarDelete.request_delete(
socket,
route,
id,
Map.get(params, "title"),
sidebar_delete_callbacks()
)}
end end
def handle_event("toggle_offline_mode", _params, socket) do def handle_event("toggle_offline_mode", _params, socket) do
@@ -319,7 +331,8 @@ defmodule BDS.Desktop.ShellLive do
do: OverlayManager.handle_event("overlay_keydown", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_keydown", params, socket, overlay_callbacks())
def handle_event("overlay_toggle_ai_field", params, socket), def handle_event("overlay_toggle_ai_field", params, socket),
do: OverlayManager.handle_event("overlay_toggle_ai_field", params, socket, overlay_callbacks()) do:
OverlayManager.handle_event("overlay_toggle_ai_field", params, socket, overlay_callbacks())
def handle_event("overlay_set_search", params, socket), def handle_event("overlay_set_search", params, socket),
do: OverlayManager.handle_event("overlay_set_search", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_set_search", params, socket, overlay_callbacks())
@@ -334,22 +347,36 @@ defmodule BDS.Desktop.ShellLive do
do: OverlayManager.handle_event("overlay_select_result", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_select_result", params, socket, overlay_callbacks())
def handle_event("overlay_insert_external", params, socket), def handle_event("overlay_insert_external", params, socket),
do: OverlayManager.handle_event("overlay_insert_external", params, socket, overlay_callbacks()) do:
OverlayManager.handle_event("overlay_insert_external", params, socket, overlay_callbacks())
def handle_event("overlay_select_language", params, socket), def handle_event("overlay_select_language", params, socket),
do: OverlayManager.handle_event("overlay_select_language", params, socket, overlay_callbacks()) do:
OverlayManager.handle_event("overlay_select_language", params, socket, overlay_callbacks())
def handle_event("overlay_confirm", params, socket), def handle_event("overlay_confirm", params, socket),
do: OverlayManager.handle_event("overlay_confirm", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_confirm", params, socket, overlay_callbacks())
def handle_event("overlay_select_gallery_image", params, socket), def handle_event("overlay_select_gallery_image", params, socket),
do: OverlayManager.handle_event("overlay_select_gallery_image", params, socket, overlay_callbacks()) do:
OverlayManager.handle_event(
"overlay_select_gallery_image",
params,
socket,
overlay_callbacks()
)
def handle_event("overlay_close_lightbox", params, socket), def handle_event("overlay_close_lightbox", params, socket),
do: OverlayManager.handle_event("overlay_close_lightbox", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_close_lightbox", params, socket, overlay_callbacks())
def handle_event("overlay_lightbox_previous", params, socket), def handle_event("overlay_lightbox_previous", params, socket),
do: OverlayManager.handle_event("overlay_lightbox_previous", params, socket, overlay_callbacks()) do:
OverlayManager.handle_event(
"overlay_lightbox_previous",
params,
socket,
overlay_callbacks()
)
def handle_event("overlay_lightbox_next", params, socket), def handle_event("overlay_lightbox_next", params, socket),
do: OverlayManager.handle_event("overlay_lightbox_next", params, socket, overlay_callbacks()) do: OverlayManager.handle_event("overlay_lightbox_next", params, socket, overlay_callbacks())
@@ -484,7 +511,8 @@ defmodule BDS.Desktop.ShellLive do
next_socket = next_socket =
cond do cond do
Map.has_key?(socket.assigns.chat_editor_request_refs, ref) -> Map.has_key?(socket.assigns.chat_editor_request_refs, ref) ->
{conversation_id, remaining_refs} = Map.pop(socket.assigns.chat_editor_request_refs, ref) {conversation_id, remaining_refs} =
Map.pop(socket.assigns.chat_editor_request_refs, ref)
if reason == :normal do if reason == :normal do
assign(socket, :chat_editor_request_refs, remaining_refs) assign(socket, :chat_editor_request_refs, remaining_refs)
@@ -737,7 +765,13 @@ defmodule BDS.Desktop.ShellLive do
apply_shell_command(socket, Atom.to_string(action)) apply_shell_command(socket, Atom.to_string(action))
true -> true ->
append_output_entry(socket, "Menu", "Unsupported shell command", Atom.to_string(action), "error") append_output_entry(
socket,
"Menu",
"Unsupported shell command",
Atom.to_string(action),
"error"
)
end end
end end
@@ -871,5 +905,4 @@ defmodule BDS.Desktop.ShellLive do
pid -> send(pid, {:set_ui_locale, locale}) pid -> send(pid, {:set_ui_locale, locale})
end end
end end
end end

View File

@@ -2,7 +2,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do
@moduledoc false @moduledoc false
import Phoenix.Component, only: [assign: 3] import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [send_update: 2] import Phoenix.LiveView, only: [connected?: 1, send_update: 2]
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.{ChatEditor, PostEditor} alias BDS.Desktop.ShellLive.{ChatEditor, PostEditor}
@@ -103,7 +103,8 @@ defmodule BDS.Desktop.ShellLive.Bridges do
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, callbacks.reload.(socket, Workbench.click_activity(socket.assigns.workbench, view))} {:noreply,
callbacks.reload.(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
@@ -113,6 +114,7 @@ defmodule BDS.Desktop.ShellLive.Bridges do
def handle_info(:refresh_task_status, socket, callbacks) do def handle_info(:refresh_task_status, socket, callbacks) do
raw_task_status = BDS.Tasks.status_snapshot() raw_task_status = BDS.Tasks.status_snapshot()
socket =
case SessionUtil.next_completed_task_result(socket, raw_task_status) do case SessionUtil.next_completed_task_result(socket, raw_task_status) do
nil -> nil ->
task_status = task_status =
@@ -121,7 +123,6 @@ defmodule BDS.Desktop.ShellLive.Bridges do
socket.assigns.page_language socket.assigns.page_language
) )
{:noreply,
socket socket
|> assign(:task_status, task_status) |> assign(:task_status, task_status)
|> assign(:editor_meta, ShellData.editor_meta(task_status)) |> assign(:editor_meta, ShellData.editor_meta(task_status))
@@ -134,14 +135,19 @@ defmodule BDS.Desktop.ShellLive.Bridges do
ui_language: socket.assigns.page_language, ui_language: socket.assigns.page_language,
offline_mode: socket.assigns.offline_mode offline_mode: socket.assigns.offline_mode
) )
)} )
task -> task ->
{:noreply,
socket socket
|> SessionUtil.mark_task_result_handled(task.id) |> SessionUtil.mark_task_result_handled(task.id)
|> callbacks.apply_shell_command_result.(task.result)} |> callbacks.apply_shell_command_result.(task.result)
end end
if connected?(socket) do
Process.send_after(self(), :refresh_task_status, BDS.Desktop.ShellLive.refresh_interval())
end
{:noreply, socket}
end end
def handle_info({:tags_editor_output, title, message, level}, socket, callbacks) do def handle_info({:tags_editor_output, title, message, level}, socket, callbacks) do
@@ -210,7 +216,12 @@ defmodule BDS.Desktop.ShellLive.Bridges do
end end
def handle_info({:post_editor_insert_content, post_id, content}, socket, _callbacks) do def handle_info({:post_editor_insert_content, post_id, content}, socket, _callbacks) do
send_update(PostEditor, id: "post-editor-#{post_id}", action: :insert_content, content: content) send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :insert_content,
content: content
)
{:noreply, socket} {:noreply, socket}
end end
@@ -220,7 +231,8 @@ defmodule BDS.Desktop.ShellLive.Bridges do
end end
def handle_info({:post_editor_apply_ai_suggestions, post_id, fields}, socket, _callbacks) do def handle_info({:post_editor_apply_ai_suggestions, post_id, fields}, socket, _callbacks) do
send_update(PostEditor, id: "post-editor-#{post_id}", send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :apply_ai_suggestions, action: :apply_ai_suggestions,
fields: fields fields: fields
) )

View File

@@ -61,7 +61,8 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
def handle_event("toggle_chat_model_selector", _params, socket) do def handle_event("toggle_chat_model_selector", _params, socket) do
{:noreply, {:noreply,
assign(socket, :model_selector_open?, not socket.assigns.model_selector_open?) |> build_data()} assign(socket, :model_selector_open?, not socket.assigns.model_selector_open?)
|> build_data()}
end end
def handle_event("select_chat_model", %{"model" => model_id}, socket) do def handle_event("select_chat_model", %{"model" => model_id}, socket) do
@@ -101,7 +102,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
) do ) do
socket = socket =
socket socket
|> assign(:surface_tabs, Map.put(socket.assigns.surface_tabs, surface_id, parse_integer(index))) |> assign(
:surface_tabs,
Map.put(socket.assigns.surface_tabs, surface_id, parse_integer(index))
)
|> build_data() |> build_data()
{:noreply, socket} {:noreply, socket}
@@ -272,7 +276,10 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
assign(socket, :request, nil) |> build_data() assign(socket, :request, nil) |> build_data()
{:error, reason} -> {:error, reason} ->
notify_parent({:chat_editor_output, dgettext("ui", "Chat"), format_error(reason), "error"}) notify_parent(
{:chat_editor_output, dgettext("ui", "Chat"), format_error(reason), "error"}
)
assign(socket, :request, nil) |> build_data() assign(socket, :request, nil) |> build_data()
end end
end end

View File

@@ -215,7 +215,8 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do
persisted_markers = persisted_tool_markers_for_request(messages, request) persisted_markers = persisted_tool_markers_for_request(messages, request)
{remaining, _persisted_markers} = {remaining, _persisted_markers} =
Enum.reduce(tool_markers, {[], persisted_markers}, fn marker, {remaining, persisted_markers} -> Enum.reduce(tool_markers, {[], persisted_markers}, fn marker,
{remaining, persisted_markers} ->
case pop_matching_tool_marker(persisted_markers, marker) do case pop_matching_tool_marker(persisted_markers, marker) do
{nil, persisted_markers} -> {remaining ++ [marker], persisted_markers} {nil, persisted_markers} -> {remaining ++ [marker], persisted_markers}
{_matched, persisted_markers} -> {remaining, persisted_markers} {_matched, persisted_markers} -> {remaining, persisted_markers}

View File

@@ -30,9 +30,17 @@ defmodule BDS.Desktop.ShellLive.ChatSurface do
defp assistant_reply(socket) do defp assistant_reply(socket) do
if socket.assigns.offline_mode do if socket.assigns.offline_mode do
BDS.Gettext.lgettext(socket.assigns.page_language, "ui", "Automatic AI actions stay gated by airplane mode.") BDS.Gettext.lgettext(
socket.assigns.page_language,
"ui",
"Automatic AI actions stay gated by airplane mode."
)
else else
BDS.Gettext.lgettext(socket.assigns.page_language, "ui", "The assistant sidebar chat surface is ready, but model execution is not connected yet.") BDS.Gettext.lgettext(
socket.assigns.page_language,
"ui",
"The assistant sidebar chat surface is ready, but model execution is not connected yet."
)
end end
end end
end end

View File

@@ -31,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
] ]
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
import TaxonomyEditing, import TaxonomyEditing,
only: [ only: [
existing_taxonomy_terms: 1, existing_taxonomy_terms: 1,
@@ -344,7 +345,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
mapped_to <- Map.get(params, "mapped_to"), mapped_to <- Map.get(params, "mapped_to"),
normalized_value <- normalized_value <-
TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, mapped_to), TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, mapped_to),
updated_report <- TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value), updated_report <-
TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value),
{:ok, _definition} <- {:ok, _definition} <-
ImportDefinitions.update_definition(definition_id, %{ ImportDefinitions.update_definition(definition_id, %{
last_analysis_result: updated_report last_analysis_result: updated_report
@@ -375,7 +377,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
%{} = report <- ImportDefinitions.decode_analysis_result(definition), %{} = report <- ImportDefinitions.decode_analysis_result(definition),
normalized_value <- normalized_value <-
TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, ""), TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, ""),
updated_report <- TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value), updated_report <-
TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value),
{:ok, _definition} <- {:ok, _definition} <-
ImportDefinitions.update_definition(definition_id, %{ ImportDefinitions.update_definition(definition_id, %{
last_analysis_result: updated_report last_analysis_result: updated_report
@@ -409,7 +412,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
def handle_event("toggle_import_ai_model_selector", _params, socket) do def handle_event("toggle_import_ai_model_selector", _params, socket) do
{:noreply, {:noreply,
assign(socket, :model_selector_open?, not socket.assigns.model_selector_open?) |> build_data()} assign(socket, :model_selector_open?, not socket.assigns.model_selector_open?)
|> build_data()}
end end
def handle_event("select_import_ai_model", %{"model" => model_id}, socket) do def handle_event("select_import_ai_model", %{"model" => model_id}, socket) do
@@ -432,7 +436,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
if socket.assigns.offline_mode? do if socket.assigns.offline_mode? do
notify_output( notify_output(
dgettext("ui", "Import"), dgettext("ui", "Import"),
BDS.Gettext.lgettext(socket.assigns[:page_language] || ShellData.ui_language(), "ui", "Automatic AI actions stay gated by airplane mode."), BDS.Gettext.lgettext(
socket.assigns[:page_language] || ShellData.ui_language(),
"ui",
"Automatic AI actions stay gated by airplane mode."
),
"info" "info"
) )
@@ -485,7 +493,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
# ── handle_info for async tasks ──────────────────────────────────────────── # ── handle_info for async tasks ────────────────────────────────────────────
@spec handle_info({:import_analysis_progress, atom() | String.t(), String.t()}, Phoenix.LiveView.Socket.t()) :: @spec handle_info(
{:import_analysis_progress, atom() | String.t(), String.t()},
Phoenix.LiveView.Socket.t()
) ::
{:noreply, Phoenix.LiveView.Socket.t()} {:noreply, Phoenix.LiveView.Socket.t()}
def handle_info({:import_analysis_progress, step, detail}, socket) do def handle_info({:import_analysis_progress, step, detail}, socket) do
socket = socket =
@@ -551,7 +562,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|> assign(:analysis_state, default_analysis_state()) |> assign(:analysis_state, default_analysis_state())
|> notify_output(dgettext("ui", "Import"), message, "error") |> notify_output(dgettext("ui", "Import"), message, "error")
match?(%{ref: ^ref}, socket.assigns.execution_state) and reason not in [:normal, :shutdown] -> match?(%{ref: ^ref}, socket.assigns.execution_state) and
reason not in [:normal, :shutdown] ->
message = if is_binary(reason), do: reason, else: inspect(reason) message = if is_binary(reason), do: reason, else: inspect(reason)
socket socket
@@ -631,7 +643,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
notify_parent( notify_parent(
{:import_editor_tab_meta, socket.assigns.definition_id, title, {:import_editor_tab_meta, socket.assigns.definition_id, title,
dgettext("ui", "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.")} dgettext(
"ui",
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
)}
) )
socket socket

View File

@@ -265,9 +265,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do
seconds = div(ms, 1000) seconds = div(ms, 1000)
if seconds < 60 do if seconds < 60 do
dgettext("ui", "ETA: %{value}", dgettext("ui", "ETA: %{value}", value: dgettext("ui", "%{count}s", count: seconds))
value: dgettext("ui", "%{count}s", count: seconds)
)
else else
m = div(seconds, 60) m = div(seconds, 60)
s = rem(seconds, 60) s = rem(seconds, 60)

View File

@@ -86,7 +86,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do
socket socket
|> append_output.( |> append_output.(
dgettext("ui", "Import"), dgettext("ui", "Import"),
BDS.Gettext.lgettext(socket.assigns.page_language, "ui", "Automatic AI actions stay gated by airplane mode."), BDS.Gettext.lgettext(
socket.assigns.page_language,
"ui",
"Automatic AI actions stay gated by airplane mode."
),
nil, nil,
"info" "info"
) )

View File

@@ -124,7 +124,12 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> build_data() |> build_data()
notify_parent({:media_editor_dirty, media.id, false}) notify_parent({:media_editor_dirty, media.id, false})
notify_parent({:media_editor_tab_meta, media.id, display_title(updated_media), updated_media.original_name || updated_media.mime_type || ""})
notify_parent(
{:media_editor_tab_meta, media.id, display_title(updated_media),
updated_media.original_name || updated_media.mime_type || ""}
)
{:noreply, socket} {:noreply, socket}
{:ok, nil} -> {:ok, nil} ->
@@ -218,7 +223,11 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
{:noreply, socket} {:noreply, socket}
end end
def handle_event("change_media_post_picker", %{"media_post_picker" => %{"query" => query}}, socket) do def handle_event(
"change_media_post_picker",
%{"media_post_picker" => %{"query" => query}},
socket
) do
socket = socket =
socket socket
|> assign(:post_picker_query, to_string(query || "")) |> assign(:post_picker_query, to_string(query || ""))
@@ -351,7 +360,13 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
{:noreply, build_data(socket)} {:noreply, build_data(socket)}
{:error, reason} -> {:error, reason} ->
notify_output(socket, dgettext("ui", "Refresh Translation"), inspect(reason), "error") notify_output(
socket,
dgettext("ui", "Refresh Translation"),
inspect(reason),
"error"
)
{:noreply, build_data(socket)} {:noreply, build_data(socket)}
end end
@@ -469,7 +484,12 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> build_data() |> build_data()
notify_parent({:media_editor_dirty, media.id, false}) notify_parent({:media_editor_dirty, media.id, false})
notify_parent({:media_editor_tab_meta, media.id, display_title(updated_media), updated_media.original_name || updated_media.mime_type || ""})
notify_parent(
{:media_editor_tab_meta, media.id, display_title(updated_media),
updated_media.original_name || updated_media.mime_type || ""}
)
notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved")) notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved"))
socket socket
@@ -684,7 +704,6 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end end
end end
@spec media_editor_save_state_label(term()) :: term() @spec media_editor_save_state_label(term()) :: term()
def media_editor_save_state_label(:dirty), do: dgettext("ui", "Unsaved") def media_editor_save_state_label(:dirty), do: dgettext("ui", "Unsaved")
def media_editor_save_state_label(:saved), do: dgettext("ui", "Saved") def media_editor_save_state_label(:saved), do: dgettext("ui", "Saved")

View File

@@ -3,7 +3,6 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
use Phoenix.LiveComponent use Phoenix.LiveComponent
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
alias BDS.Desktop.ShellLive.MenuEditor.{ alias BDS.Desktop.ShellLive.MenuEditor.{
@@ -238,7 +237,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
tab_meta = tab_meta =
Map.put(socket.assigns.tab_meta, {:menu_editor, tab_id}, %{ Map.put(socket.assigns.tab_meta, {:menu_editor, tab_id}, %{
title: dgettext("ui", "Blog Menu"), title: dgettext("ui", "Blog Menu"),
subtitle: dgettext("ui", "Manage the central blog navigation outline and save it to meta/menu.opml.") subtitle:
dgettext(
"ui",
"Manage the central blog navigation outline and save it to meta/menu.opml."
)
}) })
socket socket
@@ -407,7 +410,6 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
""" """
end end
@spec row_label(term(), term()) :: term() @spec row_label(term(), term()) :: term()
def row_label(item, category_titles) do def row_label(item, category_titles) do
if item.kind == :category_archive do if item.kind == :category_archive do
@@ -430,8 +432,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
def editing_title(_menu_editor), do: dgettext("ui", "Select Page") def editing_title(_menu_editor), do: dgettext("ui", "Select Page")
@spec editing_hint(term()) :: term() @spec editing_hint(term()) :: term()
def editing_hint(%{draft: %{type: :category}}), do: dgettext("ui", "Select an existing category or press Enter to create a new archive entry") def editing_hint(%{draft: %{type: :category}}),
def editing_hint(_menu_editor), do: dgettext("ui", "Select a page below or press Enter to create a submenu") do: dgettext("ui", "Select an existing category or press Enter to create a new archive entry")
def editing_hint(_menu_editor),
do: dgettext("ui", "Select a page below or press Enter to create a submenu")
@spec editing_placeholder(term()) :: term() @spec editing_placeholder(term()) :: term()
def editing_placeholder(%{draft: %{type: :category}}), def editing_placeholder(%{draft: %{type: :category}}),

View File

@@ -31,7 +31,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
%{ %{
title: dgettext("ui", "Blog Menu Editor"), title: dgettext("ui", "Blog Menu Editor"),
description: dgettext("ui", "Manage the central blog navigation outline and save it to meta/menu.opml."), description:
dgettext(
"ui",
"Manage the central blog navigation outline and save it to meta/menu.opml."
),
items: state.items, items: state.items,
selected_id: state.selected_id, selected_id: state.selected_id,
draft: draft, draft: draft,

View File

@@ -87,7 +87,12 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
case Generation.apply_validation(project_id, report) do case Generation.apply_validation(project_id, report) do
{:ok, result} -> {:ok, result} ->
notify_output(dgettext("ui", "Site Validation"), dgettext("ui", "Validation changes applied"), inspect(result)) notify_output(
dgettext("ui", "Site Validation"),
dgettext("ui", "Validation changes applied"),
inspect(result)
)
notify_command("validate_site") notify_command("validate_site")
{:noreply, socket} {:noreply, socket}
end end
@@ -108,7 +113,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
notify_output( notify_output(
dgettext("ui", "Translation Validation"), dgettext("ui", "Translation Validation"),
dgettext("ui", "Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk", dgettext(
"ui",
"Deleted %{dbRows} DB rows and %{files} files, flushed %{flushed} translations to disk",
dbRows: result.deleted_database_rows, dbRows: result.deleted_database_rows,
files: result.deleted_files, files: result.deleted_files,
flushed: result.flushed_translations flushed: result.flushed_translations
@@ -193,7 +200,11 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
next_payload = Map.put(payload, :pairs, next_pairs) next_payload = Map.put(payload, :pairs, next_pairs)
notify_tab_meta(tab_type, tab_id, %{payload: next_payload}) notify_tab_meta(tab_type, tab_id, %{payload: next_payload})
notify_output(dgettext("ui", "Find Duplicates"), dgettext("ui", "Selected pairs dismissed"))
notify_output(
dgettext("ui", "Find Duplicates"),
dgettext("ui", "Selected pairs dismissed")
)
{:noreply, assign(socket, :selected_pairs, MapSet.new()) |> build_data()} {:noreply, assign(socket, :selected_pairs, MapSet.new()) |> build_data()}
@@ -242,13 +253,16 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
end end
def handle_event("open_duplicate_post", %{"id" => id, "title" => title}, socket) do def handle_event("open_duplicate_post", %{"id" => id, "title" => title}, socket) do
notify_open_sidebar_item(%{"route" => "post", "id" => id, "title" => title, "subtitle" => "draft"}, :preview) notify_open_sidebar_item(
%{"route" => "post", "id" => id, "title" => title, "subtitle" => "draft"},
:preview
)
{:noreply, socket} {:noreply, socket}
end end
# ── Public helper functions (used by template) ───────────────────────────── # ── Public helper functions (used by template) ─────────────────────────────
@spec misc_class(atom()) :: String.t() @spec misc_class(atom()) :: String.t()
def misc_class(:site_validation), do: "site-validation-view" def misc_class(:site_validation), do: "site-validation-view"
def misc_class(:metadata_diff), do: "metadata-diff-view" def misc_class(:metadata_diff), do: "metadata-diff-view"
@@ -430,7 +444,9 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
subtitle: Map.get(meta, :subtitle, ""), subtitle: Map.get(meta, :subtitle, ""),
summary: %{}, summary: %{},
summary_text: summary_text:
dgettext("ui", "Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}", dgettext(
"ui",
"Checked DB rows: %{dbRows} · Checked files: %{files} · Invalid DB rows: %{invalidDb} · Invalid files: %{invalidFiles}",
dbRows: report.checked_database_row_count, dbRows: report.checked_database_row_count,
files: report.checked_filesystem_file_count, files: report.checked_filesystem_file_count,
invalidDb: length(report.invalid_database_rows), invalidDb: length(report.invalid_database_rows),

View File

@@ -64,8 +64,6 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
def markdown_link(text, url), do: "[#{text}](#{url})" def markdown_link(text, url), do: "[#{text}](#{url})"
def project_metadata(nil), do: %{main_language: "en", blog_languages: []} def project_metadata(nil), do: %{main_language: "en", blog_languages: []}
def project_metadata(project_id) do def project_metadata(project_id) do

View File

@@ -148,8 +148,12 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
socket socket
result -> result ->
send(self(), {:post_editor_insert_content, post_id, send(
ShellOverlayComponents.markdown_link(result.title, result.canonical_url)}) self(),
{:post_editor_insert_content, post_id,
ShellOverlayComponents.markdown_link(result.title, result.canonical_url)}
)
socket socket
end end
@@ -233,13 +237,15 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
socket = socket =
case {socket.assigns[:shell_overlay], current_tab} do case {socket.assigns[:shell_overlay], current_tab} do
{%{kind: :confirm_delete, delete_action: %{source: :sidebar, route: route, id: id}}, {%{kind: :confirm_delete, delete_action: %{source: :sidebar, route: route, id: id}}, _tab} ->
_tab} ->
callbacks.execute_sidebar_delete.(socket, route, id) callbacks.execute_sidebar_delete.(socket, route, id)
{%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} -> {%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} ->
send(self(), {:post_editor_apply_ai_suggestions, post_id, send(
Overlay.selected_ai_fields(overlay)}) self(),
{:post_editor_apply_ai_suggestions, post_id, Overlay.selected_ai_fields(overlay)}
)
socket socket
{%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} -> {%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} ->
@@ -258,8 +264,10 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
socket socket
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> assign(:tab_meta, |> assign(
Map.delete(socket.assigns.tab_meta, {:media, media_id})) :tab_meta,
Map.delete(socket.assigns.tab_meta, {:media, media_id})
)
|> callbacks.reload.(workbench) |> callbacks.reload.(workbench)
{:error, reason} -> {:error, reason} ->
@@ -331,8 +339,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
} }
end end
assign(socket, :shell_overlay, assign(socket, :shell_overlay, Overlay.set_ai_suggestions(overlay, suggestions))
Overlay.set_ai_suggestions(overlay, suggestions))
else else
socket socket
end end
@@ -360,8 +367,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
inspect(reason) inspect(reason)
end end
assign(socket, :shell_overlay, assign(socket, :shell_overlay, Overlay.set_ai_suggestions_error(overlay, message))
Overlay.set_ai_suggestions_error(overlay, message))
else else
socket socket
end end
@@ -444,5 +450,4 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
rescue rescue
_error -> "en" _error -> "en"
end end
end end

View File

@@ -47,6 +47,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
] ]
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
import PostMetadata, import PostMetadata,
only: [ only: [
blank?: 1, blank?: 1,
@@ -589,7 +590,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
{:ok, %{language_code: language_code}} {:ok, %{language_code: language_code}}
when is_binary(language_code) and language_code != "" -> when is_binary(language_code) and language_code != "" ->
socket socket
|> put_component_draft_field("language", normalize_language(language_code, socket.assigns.canonical_language)) |> put_component_draft_field(
"language",
normalize_language(language_code, socket.assigns.canonical_language)
)
|> build_data() |> build_data()
{:error, reason} -> {:error, reason} ->
@@ -685,7 +689,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
socket socket
|> assign(:post, updated_post) |> assign(:post, updated_post)
|> assign(:project_metadata, metadata) |> assign(:project_metadata, metadata)
|> assign(:drafts, Map.put(socket.assigns.drafts, active_language, refreshed_form)) |> assign(
:drafts,
Map.put(socket.assigns.drafts, active_language, refreshed_form)
)
|> assign(:save_state, :dirty) |> assign(:save_state, :dirty)
|> assign(:dirty?, true) |> assign(:dirty?, true)
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
@@ -822,5 +829,4 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
@spec post_editor_mode_label(term()) :: term() @spec post_editor_mode_label(term()) :: term()
def post_editor_mode_label(:markdown), do: dgettext("ui", "Markdown") def post_editor_mode_label(:markdown), do: dgettext("ui", "Markdown")
def post_editor_mode_label(:preview), do: dgettext("ui", "Preview") def post_editor_mode_label(:preview), do: dgettext("ui", "Preview")
end end

View File

@@ -151,7 +151,10 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
socket socket
|> assign(:draft, nil) |> assign(:draft, nil)
|> build_data() |> build_data()
|> notify_output(dgettext("ui", "Scripts"), dgettext("ui", "Script published")) |> notify_output(
dgettext("ui", "Scripts"),
dgettext("ui", "Script published")
)
|> notify_reload() |> notify_reload()
{:error, reason} -> {:error, reason} ->

View File

@@ -118,13 +118,16 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
end end
def handle_event("save_settings_publishing", _params, socket) do def handle_event("save_settings_publishing", _params, socket) do
socket = PublishingSettings.save_publishing(socket, reload_callback(), append_output_callback()) socket =
PublishingSettings.save_publishing(socket, reload_callback(), append_output_callback())
notify_parent(:settings_changed) notify_parent(:settings_changed)
{:noreply, socket} {:noreply, socket}
end end
def handle_event("clear_settings_publishing", _params, socket) do def handle_event("clear_settings_publishing", _params, socket) do
{:noreply, PublishingSettings.clear_publishing(socket, reload_callback(), append_output_callback())} {:noreply,
PublishingSettings.clear_publishing(socket, reload_callback(), append_output_callback())}
end end
def handle_event("change_settings_new_category", %{"name" => name}, socket) do def handle_event("change_settings_new_category", %{"name" => name}, socket) do
@@ -138,25 +141,38 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
end end
def handle_event("reset_settings_categories", _params, socket) do def handle_event("reset_settings_categories", _params, socket) do
socket = ManagedCategories.reset_categories(socket, reload_callback(), append_output_callback()) socket =
ManagedCategories.reset_categories(socket, reload_callback(), append_output_callback())
notify_parent(:settings_changed) notify_parent(:settings_changed)
{:noreply, socket} {:noreply, socket}
end end
def handle_event("save_settings_category", %{"category_settings" => params}, socket) do def handle_event("save_settings_category", %{"category_settings" => params}, socket) do
socket = ManagedCategories.save_category(socket, params, reload_callback(), append_output_callback()) socket =
ManagedCategories.save_category(socket, params, reload_callback(), append_output_callback())
notify_parent(:settings_changed) notify_parent(:settings_changed)
{:noreply, socket} {:noreply, socket}
end end
def handle_event("remove_settings_category", %{"category" => category}, socket) do def handle_event("remove_settings_category", %{"category" => category}, socket) do
socket = ManagedCategories.remove_category(socket, category, reload_callback(), append_output_callback()) socket =
ManagedCategories.remove_category(
socket,
category,
reload_callback(),
append_output_callback()
)
notify_parent(:settings_changed) notify_parent(:settings_changed)
{:noreply, socket} {:noreply, socket}
end end
def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do
socket = MCPConfig.toggle_mcp_agent(socket, agent, reload_callback(), append_output_callback()) socket =
MCPConfig.toggle_mcp_agent(socket, agent, reload_callback(), append_output_callback())
notify_parent(:settings_changed) notify_parent(:settings_changed)
{:noreply, socket} {:noreply, socket}
end end
@@ -385,5 +401,4 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
defp section_matches?(query, keywords), defp section_matches?(query, keywords),
do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query))) do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query)))
end end

View File

@@ -27,10 +27,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
), ),
"online_title_model" => get_model_preference(:title), "online_title_model" => get_model_preference(:title),
"online_image_analysis_model" => get_model_preference(:image_analysis), "online_image_analysis_model" => get_model_preference(:image_analysis),
"online_chat_images" => "online_chat_images" => model_supports_images?(get_model_preference(:image_analysis)),
model_supports_images?(
get_model_preference(:image_analysis)
),
"offline_url" => Map.get(airplane_endpoint || %{}, :url, ""), "offline_url" => Map.get(airplane_endpoint || %{}, :url, ""),
"offline_api_key" => Map.get(airplane_endpoint || %{}, :api_key, ""), "offline_api_key" => Map.get(airplane_endpoint || %{}, :api_key, ""),
"offline_mode" => Map.get(assigns, :offline_mode, AI.airplane_mode?(true)), "offline_mode" => Map.get(assigns, :offline_mode, AI.airplane_mode?(true)),
@@ -48,9 +45,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do
"offline_title_model" => get_model_preference(:airplane_title), "offline_title_model" => get_model_preference(:airplane_title),
"offline_image_analysis_model" => get_model_preference(:airplane_image_analysis), "offline_image_analysis_model" => get_model_preference(:airplane_image_analysis),
"offline_chat_images" => "offline_chat_images" =>
model_supports_images?( model_supports_images?(get_model_preference(:airplane_image_analysis)),
get_model_preference(:airplane_image_analysis)
),
"system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || "" "system_prompt" => EditorSettings.get_global_setting("ai.system_prompt") || ""
} }
end end

View File

@@ -500,8 +500,8 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
""" """
end end
defp sidebar_deletable?(route),
defp sidebar_deletable?(route), do: route in ["post", "media", "scripts", "templates", "chat", "import"] do: route in ["post", "media", "scripts", "templates", "chat", "import"]
defp sidebar_delete_testid("post"), do: "sidebar-delete-post" defp sidebar_delete_testid("post"), do: "sidebar-delete-post"
defp sidebar_delete_testid("media"), do: "sidebar-delete-media" defp sidebar_delete_testid("media"), do: "sidebar-delete-media"
@@ -513,10 +513,19 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
defp sidebar_delete_title("chat"), do: dgettext("ui", "Delete conversation") defp sidebar_delete_title("chat"), do: dgettext("ui", "Delete conversation")
defp sidebar_delete_title("post"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Post") defp sidebar_delete_title("post"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Post")
defp sidebar_delete_title("media"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Media")
defp sidebar_delete_title("scripts"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Script") defp sidebar_delete_title("media"),
defp sidebar_delete_title("templates"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Template") do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Media")
defp sidebar_delete_title("import"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Import")
defp sidebar_delete_title("scripts"),
do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Script")
defp sidebar_delete_title("templates"),
do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Template")
defp sidebar_delete_title("import"),
do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Import")
defp sidebar_delete_title(_route), do: dgettext("ui", "Delete") defp sidebar_delete_title(_route), do: dgettext("ui", "Delete")
defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates" defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates"

View File

@@ -6,7 +6,13 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
alias BDS.{AI, ImportDefinitions, Media, Posts, Scripts, Templates} alias BDS.{AI, ImportDefinitions, Media, Posts, Scripts, Templates}
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@spec request_delete(Phoenix.LiveView.Socket.t(), String.t(), String.t(), String.t() | nil, map()) :: @spec request_delete(
Phoenix.LiveView.Socket.t(),
String.t(),
String.t(),
String.t() | nil,
map()
) ::
Phoenix.LiveView.Socket.t() Phoenix.LiveView.Socket.t()
def request_delete(socket, route, id, fallback_title, callbacks) do def request_delete(socket, route, id, fallback_title, callbacks) do
case delete_target(socket, route, id, fallback_title) do case delete_target(socket, route, id, fallback_title) do
@@ -43,9 +49,15 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
delete_entity(socket, :scripts, id, &Scripts.delete_script/1, callbacks) delete_entity(socket, :scripts, id, &Scripts.delete_script/1, callbacks)
"templates" -> "templates" ->
delete_entity(socket, :templates, id, fn tid -> delete_entity(
socket,
:templates,
id,
fn tid ->
Templates.delete_template(tid, force: true) Templates.delete_template(tid, force: true)
end, callbacks) end,
callbacks
)
"chat" -> "chat" ->
delete_entity(socket, :chat, id, &AI.delete_chat_conversation/1, callbacks) delete_entity(socket, :chat, id, &AI.delete_chat_conversation/1, callbacks)
@@ -56,7 +68,12 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
_other -> _other ->
socket socket
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> callbacks.append_output.(dgettext("ui", "Delete"), inspect(:unsupported_route), nil, "error") |> callbacks.append_output.(
dgettext("ui", "Delete"),
inspect(:unsupported_route),
nil,
"error"
)
|> callbacks.reload.(socket.assigns.workbench) |> callbacks.reload.(socket.assigns.workbench)
end end
end end
@@ -95,7 +112,9 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
"post" -> "post" ->
case Posts.get_post(id) do case Posts.get_post(id) do
%{project_id: ^active_project_id} = post -> %{project_id: ^active_project_id} = post ->
{:ok, present_title(fallback_title) || present_title(post.title) || present_title(post.slug) || id} {:ok,
present_title(fallback_title) || present_title(post.title) ||
present_title(post.slug) || id}
_other -> _other ->
{:error, :not_found} {:error, :not_found}
@@ -155,7 +174,10 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
defp delete_title("post"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Post") defp delete_title("post"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Post")
defp delete_title("media"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Media") defp delete_title("media"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Media")
defp delete_title("scripts"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Script") defp delete_title("scripts"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Script")
defp delete_title("templates"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Template")
defp delete_title("templates"),
do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Template")
defp delete_title("import"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Import") defp delete_title("import"), do: dgettext("ui", "Delete") <> " " <> dgettext("ui", "Import")
defp delete_title(_route), do: dgettext("ui", "Delete") defp delete_title(_route), do: dgettext("ui", "Delete")
@@ -168,5 +190,4 @@ defmodule BDS.Desktop.ShellLive.SidebarDelete do
end end
defp present_title(_value), do: nil defp present_title(_value), do: nil
end end

View File

@@ -3,7 +3,9 @@ defmodule BDS.Desktop.ShellLive.SidebarEvents do
alias BDS.Desktop.ShellLive.SidebarState, as: ShellSidebarState alias BDS.Desktop.ShellLive.SidebarState, as: ShellSidebarState
@spec handle(Phoenix.LiveView.Socket.t(), String.t(), map(), (Phoenix.LiveView.Socket.t(), term() -> Phoenix.LiveView.Socket.t())) :: @spec handle(Phoenix.LiveView.Socket.t(), String.t(), map(), (Phoenix.LiveView.Socket.t(),
term() ->
Phoenix.LiveView.Socket.t())) ::
{:noreply, Phoenix.LiveView.Socket.t()} {:noreply, Phoenix.LiveView.Socket.t()}
def handle(socket, event, params, reload) def handle(socket, event, params, reload)

View File

@@ -49,7 +49,8 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
end end
end end
defp default_tab_subtitle(_tab), do: "Desktop workbench content routed through the Elixir shell." defp default_tab_subtitle(_tab),
do: "Desktop workbench content routed through the Elixir shell."
def tab_route_label(nil), do: dgettext("ui", "Dashboard") def tab_route_label(nil), do: dgettext("ui", "Dashboard")
def tab_route_label(%{type: type}), do: ShellData.route_label(type) def tab_route_label(%{type: type}), do: ShellData.route_label(type)
@@ -168,7 +169,11 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
%{name: name} -> %{name: name} ->
%{ %{
title: blank_to_nil(name) || dgettext("ui", "Untitled Import"), title: blank_to_nil(name) || dgettext("ui", "Untitled Import"),
subtitle: dgettext("ui", "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.") subtitle:
dgettext(
"ui",
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
)
} }
_other -> _other ->
@@ -183,7 +188,11 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
defp derived_tab_meta(%{type: :menu_editor}) do defp derived_tab_meta(%{type: :menu_editor}) do
%{ %{
title: dgettext("ui", "Blog Menu"), title: dgettext("ui", "Blog Menu"),
subtitle: dgettext("ui", "Manage the central blog navigation outline and save it to meta/menu.opml.") subtitle:
dgettext(
"ui",
"Manage the central blog navigation outline and save it to meta/menu.opml."
)
} }
end end
@@ -212,7 +221,8 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
if is_binary(value), do: String.trim(value) != "", else: false if is_binary(value), do: String.trim(value) != "", else: false
end end
defp post_record_title(%Post{} = post), do: blank_to_nil(post.title) || blank_to_nil(post.slug) || post.id defp post_record_title(%Post{} = post),
do: blank_to_nil(post.title) || blank_to_nil(post.slug) || post.id
defp post_record_subtitle(%Post{} = post), do: Atom.to_string(post.status) defp post_record_subtitle(%Post{} = post), do: Atom.to_string(post.status)

View File

@@ -272,11 +272,13 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end), end),
selected: selected, selected: selected,
new_tag: new_tag:
Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:new_tag, %{"name" => "", "color" => ""}), Map.get(socket.assigns, :tags_editor, %{})
|> Map.get(:new_tag, %{"name" => "", "color" => ""}),
edit_draft: edit_draft, edit_draft: edit_draft,
templates: templates, templates: templates,
merge_target: merge_target:
Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:merge_target, List.first(selected) || ""), Map.get(socket.assigns, :tags_editor, %{})
|> Map.get(:merge_target, List.first(selected) || ""),
selected_section: selected_section selected_section: selected_section
} }

View File

@@ -1,7 +1,6 @@
defmodule BDS.Desktop.ShellLive.TaskLocalization do defmodule BDS.Desktop.ShellLive.TaskLocalization do
@moduledoc false @moduledoc false
def localize_task_status(task_status, locale) do def localize_task_status(task_status, locale) do
tasks = Enum.map(Map.get(task_status, :tasks, []), &localize_task(&1, locale)) tasks = Enum.map(Map.get(task_status, :tasks, []), &localize_task(&1, locale))
active = Enum.filter(tasks, &(&1.status in [:running, :pending])) active = Enum.filter(tasks, &(&1.status in [:running, :pending]))

View File

@@ -165,7 +165,11 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
%Template{} = template -> %Template{} = template ->
case MCP.validate_template(current_draft(socket.assigns, template)["content"] || "") do case MCP.validate_template(current_draft(socket.assigns, template)["content"] || "") do
{:ok, %{valid: true}} -> {:ok, %{valid: true}} ->
notify_output(socket, dgettext("ui", "Templates"), dgettext("ui", "Template syntax is valid")) notify_output(
socket,
dgettext("ui", "Templates"),
dgettext("ui", "Template syntax is valid")
)
{:ok, %{valid: false, errors: errors}} -> {:ok, %{valid: false, errors: errors}} ->
notify_output(socket, dgettext("ui", "Templates"), Enum.join(errors, "; "), "error") notify_output(socket, dgettext("ui", "Templates"), Enum.join(errors, "; "), "error")

View File

@@ -21,9 +21,7 @@ defmodule BDS.Desktop.Shutdown do
userData: self() userData: self()
) )
:wxFrame.connect(frame, :command_menu_selected, :wxFrame.connect(frame, :command_menu_selected, callback: &__MODULE__.command_menu_selected/2)
callback: &__MODULE__.command_menu_selected/2
)
:ok :ok
rescue rescue

View File

@@ -431,7 +431,8 @@ defmodule BDS.Generation.Validation do
_relative_path, _relative_path,
%{requires_fallback_section_render: true}, %{requires_fallback_section_render: true},
_main? _main?
), do: true ),
do: true
defp targeted_output_for_plan?(relative_path, plan, _main?) do defp targeted_output_for_plan?(relative_path, plan, _main?) do
cond do cond do

View File

@@ -65,7 +65,9 @@ defmodule BDS.Media.Rebuilder do
Sidecars.upsert_translation_from_sidecar( Sidecars.upsert_translation_from_sidecar(
project, project,
canonical_media_by_binary_path, canonical_media_by_binary_path,
sidecar, sync_search: false) sidecar,
sync_search: false
)
:ok = report_rebuild_progress(on_progress, index, total_files, "media files") :ok = report_rebuild_progress(on_progress, index, total_files, "media files")
end) end)

View File

@@ -113,7 +113,10 @@ defmodule BDS.Posts.AutoTranslation do
) )
{summary, completed} = {summary, completed} =
Enum.reduce(post_items, {empty_fill_summary(), 0}, fn %{post: post, language: language}, Enum.reduce(post_items, {empty_fill_summary(), 0}, fn %{
post: post,
language: language
},
{summary, completed} -> {summary, completed} ->
report_fill_item_progress( report_fill_item_progress(
on_progress, on_progress,
@@ -132,7 +135,10 @@ defmodule BDS.Posts.AutoTranslation do
end) end)
{summary, _completed} = {summary, _completed} =
Enum.reduce(media_items, {summary, completed}, fn %{media_id: media_id, language: language}, Enum.reduce(media_items, {summary, completed}, fn %{
media_id: media_id,
language: language
},
{summary, completed} -> {summary, completed} ->
report_fill_item_progress( report_fill_item_progress(
on_progress, on_progress,
@@ -392,7 +398,11 @@ defmodule BDS.Posts.AutoTranslation do
end end
with {:ok, translation} <- with {:ok, translation} <-
AI.translate_media(media_id, language, Keyword.put(ai_opts(), :source_language, source_language)), AI.translate_media(
media_id,
language,
Keyword.put(ai_opts(), :source_language, source_language)
),
{:ok, saved_translation} <- {:ok, saved_translation} <-
Media.upsert_media_translation(media_id, language, %{ Media.upsert_media_translation(media_id, language, %{
title: translation.title, title: translation.title,

View File

@@ -69,8 +69,7 @@ defmodule BDS.Rendering.Filters do
"macros/vimeo", "macros/vimeo",
%{ %{
"id" => Map.get(params, "id", ""), "id" => Map.get(params, "id", ""),
"title" => "title" => default_macro_title(Map.get(params, "title"), language, "Vimeo video")
default_macro_title(Map.get(params, "title"), language, "Vimeo video")
}, },
context context
) )

View File

@@ -97,7 +97,8 @@ defmodule BDS.Rendering.ListArchive do
post_data_json_by_id: post_data_json_by_id:
Enum.into(posts, %{}, fn post -> {post.id, PostRendering.post_data_json_value(post)} end), Enum.into(posts, %{}, fn post -> {post.id, PostRendering.post_data_json_value(post)} end),
day_blocks: day_blocks, day_blocks: day_blocks,
archive_month_name: Labels.month_name(Map.get(normalized_archive_context, :month), language), archive_month_name:
Labels.month_name(Map.get(normalized_archive_context, :month), language),
labels: Labels.for_language(language) labels: Labels.for_language(language)
} }
end end
@@ -148,7 +149,11 @@ defmodule BDS.Rendering.ListArchive do
Map.get( Map.get(
assigns, assigns,
"not_found_message", "not_found_message",
BDS.Gettext.lgettext(language, "render", "The requested preview page could not be found.") BDS.Gettext.lgettext(
language,
"render",
"The requested preview page could not be found."
)
) )
), ),
not_found_back_label: not_found_back_label:

View File

@@ -98,6 +98,7 @@ defmodule BDS.Scripts do
content_changed? = content_changed? =
has_attr?(attrs, :content) and attr(attrs, :content) != effective_script_content(script) has_attr?(attrs, :content) and attr(attrs, :content) != effective_script_content(script)
now = Persistence.now_ms() now = Persistence.now_ms()
updates = updates =
@@ -309,7 +310,9 @@ defmodule BDS.Scripts do
defp hydrate_script_content(%Script{} = script) do defp hydrate_script_content(%Script{} = script) do
case script do case script do
%Script{content: content} when is_binary(content) -> script %Script{content: content} when is_binary(content) ->
script
%Script{status: :published, file_path: file_path} when file_path not in [nil, ""] -> %Script{status: :published, file_path: file_path} when file_path not in [nil, ""] ->
%{script | content: published_script_body(script)} %{script | content: published_script_body(script)}

View File

@@ -102,7 +102,8 @@ defmodule BDS.Templates do
end end
content_changed? = content_changed? =
has_attr?(attrs, :content) and attr(attrs, :content) != effective_template_content(template) has_attr?(attrs, :content) and
attr(attrs, :content) != effective_template_content(template)
slug_changed? = next_slug != template.slug slug_changed? = next_slug != template.slug
now = Persistence.now_ms() now = Persistence.now_ms()
@@ -472,7 +473,9 @@ defmodule BDS.Templates do
defp hydrate_template_content(%Template{} = template) do defp hydrate_template_content(%Template{} = template) do
case template do case template do
%Template{content: content} when is_binary(content) -> template %Template{content: content} when is_binary(content) ->
template
%Template{status: :published, file_path: file_path} when file_path not in [nil, ""] -> %Template{status: :published, file_path: file_path} when file_path not in [nil, ""] ->
%{template | content: published_template_body(template)} %{template | content: published_template_body(template)}

View File

@@ -101,15 +101,40 @@ defmodule BDS.UI.Registry do
%{id: :chat, singleton: false, entity_tab: true, title: dgettext("ui", "Chat")}, %{id: :chat, singleton: false, entity_tab: true, title: dgettext("ui", "Chat")},
%{id: :import, singleton: false, entity_tab: true, title: dgettext("ui", "Import")}, %{id: :import, singleton: false, entity_tab: true, title: dgettext("ui", "Import")},
%{id: :menu_editor, singleton: true, entity_tab: false, title: dgettext("ui", "Menu")}, %{id: :menu_editor, singleton: true, entity_tab: false, title: dgettext("ui", "Menu")},
%{id: :metadata_diff, singleton: true, entity_tab: false, title: dgettext("ui", "Metadata Diff")}, %{
id: :metadata_diff,
singleton: true,
entity_tab: false,
title: dgettext("ui", "Metadata Diff")
},
%{id: :git_diff, singleton: false, entity_tab: true, title: dgettext("ui", "Git Diff")}, %{id: :git_diff, singleton: false, entity_tab: true, title: dgettext("ui", "Git Diff")},
%{id: :documentation, singleton: true, entity_tab: false, title: dgettext("ui", "Documentation")}, %{
id: :documentation,
singleton: true,
entity_tab: false,
title: dgettext("ui", "Documentation")
},
%{id: :api_documentation, singleton: true, entity_tab: false, title: dgettext("ui", "API")}, %{id: :api_documentation, singleton: true, entity_tab: false, title: dgettext("ui", "API")},
%{id: :site_validation, singleton: true, entity_tab: false, title: dgettext("ui", "Site Validation")}, %{
%{id: :translation_validation, singleton: true, entity_tab: false, title: dgettext("ui", "Translations")}, id: :site_validation,
singleton: true,
entity_tab: false,
title: dgettext("ui", "Site Validation")
},
%{
id: :translation_validation,
singleton: true,
entity_tab: false,
title: dgettext("ui", "Translations")
},
%{id: :scripts, singleton: false, entity_tab: true, title: dgettext("ui", "Script")}, %{id: :scripts, singleton: false, entity_tab: true, title: dgettext("ui", "Script")},
%{id: :templates, singleton: false, entity_tab: true, title: dgettext("ui", "Template")}, %{id: :templates, singleton: false, entity_tab: true, title: dgettext("ui", "Template")},
%{id: :find_duplicates, singleton: true, entity_tab: false, title: dgettext("ui", "Find Duplicates")} %{
id: :find_duplicates,
singleton: true,
entity_tab: false,
title: dgettext("ui", "Find Duplicates")
}
] ]
end end

View File

@@ -206,7 +206,8 @@ defmodule BDS.AITest do
{:ok, {:ok,
%{ %{
json: %{ json: %{
"title" => "Analyzed " <> (get_in(request.messages, [Access.at(1), "content"]) || ""), "title" =>
"Analyzed " <> (get_in(request.messages, [Access.at(1), "content"]) || ""),
"excerpt" => "Analyzed excerpt", "excerpt" => "Analyzed excerpt",
"slug" => "analyzed-slug" "slug" => "analyzed-slug"
}, },
@@ -386,6 +387,8 @@ defmodule BDS.AITest do
{:ok, {_address, port}} = ThousandIsland.listener_info(server) {:ok, {_address, port}} = ThousandIsland.listener_info(server)
log =
capture_log(fn ->
assert {:error, %{kind: :invalid_json_response, reason: %Jason.DecodeError{}}} = assert {:error, %{kind: :invalid_json_response, reason: %Jason.DecodeError{}}} =
BDS.AI.OpenAICompatibleRuntime.generate( BDS.AI.OpenAICompatibleRuntime.generate(
%{url: "http://127.0.0.1:#{port}/v1", api_key: nil}, %{url: "http://127.0.0.1:#{port}/v1", api_key: nil},
@@ -397,6 +400,9 @@ defmodule BDS.AITest do
}, },
[] []
) )
end)
assert log =~ "AI OpenAI-compatible response normalization failed"
end end
test "openai-compatible generation accepts title requests without tools" do test "openai-compatible generation accepts title requests without tools" do

View File

@@ -13,6 +13,26 @@ defmodule BDS.Desktop.ImportShellLiveTest do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
Enum.each(BDS.Tasks.list_running_tasks(), fn task ->
BDS.Tasks.cancel_task(task.id)
end)
if :ets.whereis(:bds_ai_in_flight) != :undefined do
Enum.each(:ets.tab2list(:bds_ai_in_flight), fn {_conversation_id, pid} ->
Process.exit(pid, :kill)
end)
end
for {_, pid, _, _} <- DynamicSupervisor.which_children(BDS.TCP.TaskSupervisor) do
DynamicSupervisor.terminate_child(BDS.TCP.TaskSupervisor, pid)
end
for {_, pid, _, _} <- DynamicSupervisor.which_children(BDS.Tasks.TaskSupervisor) do
DynamicSupervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid)
end
Process.sleep(100)
temp_dir = temp_dir =
Path.join(System.tmp_dir!(), "bds-import-shell-live-#{System.unique_integer([:positive])}") Path.join(System.tmp_dir!(), "bds-import-shell-live-#{System.unique_integer([:positive])}")

View File

@@ -241,7 +241,8 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert result.action == "fill_missing_translations" assert result.action == "fill_missing_translations"
assert is_binary(result.task_id) assert is_binary(result.task_id)
completed = wait_for_task(result.task_id, &(&1.status == :completed and is_map(&1.result)), 5_000) completed =
wait_for_task(result.task_id, &(&1.status == :completed and is_map(&1.result)), 5_000)
assert completed.group_name == "AI" assert completed.group_name == "AI"
assert completed.result.project_id == project.id assert completed.result.project_id == project.id
@@ -319,7 +320,9 @@ defmodule BDS.Desktop.ShellCommandsTest do
}) })
assert {:ok, result} = ShellCommands.execute("fill_missing_translations") assert {:ok, result} = ShellCommands.execute("fill_missing_translations")
completed = wait_for_task(result.task_id, &(&1.status == :completed and is_map(&1.result)), 5_000)
completed =
wait_for_task(result.task_id, &(&1.status == :completed and is_map(&1.result)), 5_000)
assert completed.result.translated_posts == 1 assert completed.result.translated_posts == 1
assert completed.result.translated_media == 1 assert completed.result.translated_media == 1
@@ -530,8 +533,10 @@ defmodule BDS.Desktop.ShellCommandsTest do
wait_for_task( wait_for_task(
result.task_id, result.task_id,
fn task -> fn task ->
task.status in [:running, :completed] and is_number(task.progress) and task.progress > 0.2 and task.status in [:running, :completed] and is_number(task.progress) and
Repo.get_by(BDS.Posts.Post, project_id: project.id, file_path: post_orphan_path) != nil and task.progress > 0.2 and
Repo.get_by(BDS.Posts.Post, project_id: project.id, file_path: post_orphan_path) !=
nil and
Repo.get_by(BDS.Posts.Translation, Repo.get_by(BDS.Posts.Translation,
project_id: project.id, project_id: project.id,
file_path: post_translation_orphan_path file_path: post_translation_orphan_path
@@ -545,6 +550,7 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert progressed.progress > 0.2 assert progressed.progress > 0.2
assert Repo.get_by(BDS.Posts.Post, project_id: project.id, file_path: post_orphan_path) assert Repo.get_by(BDS.Posts.Post, project_id: project.id, file_path: post_orphan_path)
assert Repo.get_by(BDS.Posts.Translation, assert Repo.get_by(BDS.Posts.Translation,
project_id: project.id, project_id: project.id,
file_path: post_translation_orphan_path file_path: post_translation_orphan_path

View File

@@ -1,6 +1,7 @@
defmodule BDS.Desktop.ShellLiveTest do defmodule BDS.Desktop.ShellLiveTest do
use ExUnit.Case, async: false use ExUnit.Case, async: false
import ExUnit.CaptureLog
import Phoenix.ConnTest import Phoenix.ConnTest
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
@@ -175,6 +176,26 @@ defmodule BDS.Desktop.ShellLiveTest do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
Enum.each(BDS.Tasks.list_running_tasks(), fn task ->
BDS.Tasks.cancel_task(task.id)
end)
if :ets.whereis(:bds_ai_in_flight) != :undefined do
Enum.each(:ets.tab2list(:bds_ai_in_flight), fn {_conversation_id, pid} ->
Process.exit(pid, :kill)
end)
end
for {_, pid, _, _} <- DynamicSupervisor.which_children(BDS.TCP.TaskSupervisor) do
DynamicSupervisor.terminate_child(BDS.TCP.TaskSupervisor, pid)
end
for {_, pid, _, _} <- DynamicSupervisor.which_children(BDS.Tasks.TaskSupervisor) do
DynamicSupervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid)
end
Process.sleep(100)
temp_dir = temp_dir =
Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}") Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}")
@@ -422,12 +443,16 @@ defmodule BDS.Desktop.ShellLiveTest do
_html = _html =
view view
|> element("#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Alpha']") |> element(
"#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Alpha']"
)
|> render_click() |> render_click()
_html = _html =
view view
|> element("#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Beta']") |> element(
"#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Beta']"
)
|> render_click() |> render_click()
_html = _html =
@@ -1328,7 +1353,9 @@ defmodule BDS.Desktop.ShellLiveTest do
html = html =
view view
|> element("#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='online']") |> element(
"#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='online']"
)
|> render_click() |> render_click()
assert html =~ ~s(<option value="gpt-4.1"></option>) assert html =~ ~s(<option value="gpt-4.1"></option>)
@@ -1336,7 +1363,9 @@ defmodule BDS.Desktop.ShellLiveTest do
html = html =
view view
|> element("#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='airplane']") |> element(
"#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='airplane']"
)
|> render_click() |> render_click()
assert html =~ ~s(<option value="llama3.3"></option>) assert html =~ ~s(<option value="llama3.3"></option>)
@@ -2717,7 +2746,12 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "ai-suggestions-modal" assert html =~ "ai-suggestions-modal"
_ =
capture_log(fn ->
send(view.pid, {:ai_suggestions_error, :post, post.id, :test_error}) send(view.pid, {:ai_suggestions_error, :post, post.id, :test_error})
render(view)
end)
html = render(view) html = render(view)
assert html =~ "ai-suggestions-modal" assert html =~ "ai-suggestions-modal"
@@ -3236,6 +3270,7 @@ defmodule BDS.Desktop.ShellLiveTest do
view view
|> element("[data-testid='chat-model-selector-button']") |> element("[data-testid='chat-model-selector-button']")
|> render_click() |> render_click()
assert selector_html =~ ~s(class="chat-model-selector-menu") assert selector_html =~ ~s(class="chat-model-selector-menu")
assert selector_html =~ ~s(data-testid="chat-model-selector-option") assert selector_html =~ ~s(data-testid="chat-model-selector-option")
assert selector_html =~ "llama-current" assert selector_html =~ "llama-current"

View File

@@ -25,7 +25,11 @@ defmodule BDS.DesktopTest do
defmodule FakeWindowLifecycle do defmodule FakeWindowLifecycle do
def hide(window_id) do def hide(window_id) do
send(Application.fetch_env!(:bds, :desktop_shutdown_test_pid), {:window_hide_requested, window_id}) send(
Application.fetch_env!(:bds, :desktop_shutdown_test_pid),
{:window_hide_requested, window_id}
)
:ok :ok
end end

View File

@@ -213,6 +213,8 @@ defmodule BDS.UI.SidebarTest do
Repo.update_all( Repo.update_all(
from(definition in BDS.ImportDefinitions.ImportDefinition, from(definition in BDS.ImportDefinitions.ImportDefinition,
where: definition.id == ^definition_id where: definition.id == ^definition_id
), set: [updated_at: updated_at]) ),
set: [updated_at: updated_at]
)
end end
end end