feat: more work on UI cleanup
This commit is contained in:
@@ -58,6 +58,20 @@ defmodule BDS.Desktop.MenuBar do
|
||||
{:noreply, menu}
|
||||
end
|
||||
|
||||
def handle_event("open_in_browser", menu) do
|
||||
case BDS.Desktop.ShellCommands.execute("open_in_browser") do
|
||||
{:ok, %{url: url}} -> OS.launch_default_browser(url)
|
||||
_other -> :ok
|
||||
end
|
||||
|
||||
{:noreply, menu}
|
||||
end
|
||||
|
||||
def handle_event("open_data_folder", menu) do
|
||||
_ = BDS.Desktop.ShellCommands.execute("open_data_folder")
|
||||
{:noreply, menu}
|
||||
end
|
||||
|
||||
def handle_event("report_issue", menu) do
|
||||
OS.launch_default_browser("https://github.com/rfc1437/bDS/issues")
|
||||
{:noreply, menu}
|
||||
|
||||
@@ -30,6 +30,21 @@ defmodule BDS.Desktop.Router do
|
||||
Plug.Conn.send_resp(conn, 200, "ok")
|
||||
end
|
||||
|
||||
get "/api/tasks" do
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.task_status_json())
|
||||
end
|
||||
|
||||
post "/api/commands" do
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
payload = if body == "", do: %{}, else: Jason.decode!(body)
|
||||
|
||||
conn
|
||||
|> Plug.Conn.put_resp_content_type("application/json")
|
||||
|> Plug.Conn.send_resp(200, BDS.Desktop.ShellController.command_json(payload))
|
||||
end
|
||||
|
||||
match _ do
|
||||
Plug.Conn.send_resp(conn, 404, "not found")
|
||||
end
|
||||
|
||||
342
lib/bds/desktop/shell_commands.ex
Normal file
342
lib/bds/desktop/shell_commands.ex
Normal file
@@ -0,0 +1,342 @@
|
||||
defmodule BDS.Desktop.ShellCommands do
|
||||
@moduledoc false
|
||||
|
||||
alias BDS.Embeddings
|
||||
alias BDS.Generation
|
||||
alias BDS.Maintenance
|
||||
alias BDS.Metadata
|
||||
alias BDS.Posts
|
||||
alias BDS.Preview
|
||||
alias BDS.Projects
|
||||
alias BDS.Publishing
|
||||
alias BDS.Search
|
||||
alias BDS.Tasks
|
||||
|
||||
@site_sections [:core, :single, :category, :tag, :date]
|
||||
|
||||
def execute(action, params \\ %{})
|
||||
|
||||
def execute(action, params) when is_atom(action) do
|
||||
execute(Atom.to_string(action), params)
|
||||
end
|
||||
|
||||
def execute(action, params) when is_binary(action) and is_map(params) do
|
||||
with {:ok, project} <- active_project() do
|
||||
dispatch(action, project, params)
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("open_in_browser", project, _params) do
|
||||
with {:ok, server} <- Preview.start_preview(project.id) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_url",
|
||||
action: "open_in_browser",
|
||||
title: "Open in Browser",
|
||||
message: "Preview server ready",
|
||||
project_id: project.id,
|
||||
url: preview_url(server)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("preview_post", project, _params) do
|
||||
with {:ok, server} <- Preview.start_preview(project.id) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_url",
|
||||
action: "preview_post",
|
||||
title: "Preview Post",
|
||||
message: "Preview server ready",
|
||||
project_id: project.id,
|
||||
url: preview_url(server)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("open_data_folder", project, _params) do
|
||||
path = Projects.project_data_dir(project)
|
||||
|
||||
case open_system_path(path) do
|
||||
:ok ->
|
||||
{:ok,
|
||||
%{
|
||||
kind: "output",
|
||||
action: "open_data_folder",
|
||||
title: "Open Data Folder",
|
||||
message: path,
|
||||
project_id: project.id,
|
||||
level: "info"
|
||||
}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %{action: "open_data_folder", message: "#{path}: #{inspect(reason)}"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("reindex_text", project, _params) do
|
||||
queue_task(project, "reindex_text", "Reindex Text", "Search", fn report ->
|
||||
report.(0.2, "Clearing and rebuilding text indexes")
|
||||
:ok = Search.reindex_project(project.id)
|
||||
report.(1.0, "Text indexes rebuilt")
|
||||
%{project_id: project.id}
|
||||
end)
|
||||
end
|
||||
|
||||
defp dispatch("rebuild_embedding_index", project, _params) do
|
||||
queue_task(project, "rebuild_embedding_index", "Rebuild Embedding Index", "Embeddings", fn report ->
|
||||
report.(0.2, "Rebuilding semantic index")
|
||||
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id)
|
||||
report.(1.0, "Embedding index rebuilt")
|
||||
%{project_id: project.id, rebuilt_post_ids: rebuilt_post_ids, rebuilt_count: length(rebuilt_post_ids)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp dispatch("rebuild_database", project, _params) do
|
||||
queue_task(project, "rebuild_database", "Rebuild Database", "Maintenance", fn report ->
|
||||
report.(0.1, "Rebuilding posts")
|
||||
{:ok, posts} = Maintenance.rebuild_from_filesystem(project.id, "post")
|
||||
report.(0.3, "Rebuilding media")
|
||||
{:ok, media} = Maintenance.rebuild_from_filesystem(project.id, "media")
|
||||
report.(0.5, "Rebuilding scripts")
|
||||
{:ok, scripts} = Maintenance.rebuild_from_filesystem(project.id, "script")
|
||||
report.(0.7, "Rebuilding templates")
|
||||
{:ok, templates} = Maintenance.rebuild_from_filesystem(project.id, "template")
|
||||
report.(0.9, "Rebuilding embeddings")
|
||||
{:ok, embeddings} = Maintenance.rebuild_from_filesystem(project.id, "embedding")
|
||||
report.(1.0, "Database rebuild complete")
|
||||
|
||||
%{
|
||||
project_id: project.id,
|
||||
counts: %{
|
||||
posts: length(posts),
|
||||
media: length(media),
|
||||
scripts: length(scripts),
|
||||
templates: length(templates),
|
||||
embeddings: length(embeddings)
|
||||
}
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp dispatch("generate_sitemap", project, _params) do
|
||||
queue_task(project, "generate_sitemap", "Generate Sitemap", "Generation", fn report ->
|
||||
report.(0.2, "Generating site output")
|
||||
{:ok, generation} = Generation.generate_site(project.id, @site_sections)
|
||||
report.(1.0, "Generated site output")
|
||||
%{project_id: project.id, sections: generation.sections, generated_count: length(generation.generated_files)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp dispatch("validate_site", project, _params) do
|
||||
with {:ok, report} <- Generation.validate_site(project.id, @site_sections) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_editor",
|
||||
action: "validate_site",
|
||||
project_id: project.id,
|
||||
route: "site_validation",
|
||||
title: "Site Validation",
|
||||
subtitle: "Generated output checked against expected site files",
|
||||
editorMeta: [
|
||||
%{label: "Missing", value: Integer.to_string(length(report.missing_pages))},
|
||||
%{label: "Extra", value: Integer.to_string(length(report.extra_pages))},
|
||||
%{label: "Stale", value: Integer.to_string(length(report.stale_pages))}
|
||||
],
|
||||
payload: normalize_site_validation(report)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("metadata_diff", project, _params) do
|
||||
with {:ok, report} <- Maintenance.metadata_diff(project.id) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_editor",
|
||||
action: "metadata_diff",
|
||||
project_id: project.id,
|
||||
route: "metadata_diff",
|
||||
title: "Metadata Diff",
|
||||
subtitle: "Database state compared against filesystem metadata",
|
||||
editorMeta: [
|
||||
%{label: "Diffs", value: Integer.to_string(length(report.diff_reports))},
|
||||
%{label: "Orphans", value: Integer.to_string(length(report.orphan_reports))}
|
||||
],
|
||||
payload: normalize_metadata_diff(report)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("validate_translations", project, _params) do
|
||||
with {:ok, report} <- Posts.validate_translations(project.id) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_editor",
|
||||
action: "validate_translations",
|
||||
project_id: project.id,
|
||||
route: "translation_validation",
|
||||
title: "Translation Validation",
|
||||
subtitle: "Published posts checked against required blog languages",
|
||||
editorMeta: [
|
||||
%{label: "Missing", value: Integer.to_string(length(report.missing))},
|
||||
%{label: "Orphans", value: Integer.to_string(length(report.orphan_files))},
|
||||
%{label: "Skipped", value: Integer.to_string(length(report.do_not_translate_posts))}
|
||||
],
|
||||
payload: normalize_translation_validation(report)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("find_duplicates", project, _params) do
|
||||
with {:ok, pairs} <- Embeddings.find_duplicates(project.id) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "open_editor",
|
||||
action: "find_duplicates",
|
||||
project_id: project.id,
|
||||
route: "find_duplicates",
|
||||
title: "Find Duplicates",
|
||||
subtitle: "Potential duplicate posts found via embeddings",
|
||||
editorMeta: [%{label: "Pairs", value: Integer.to_string(length(pairs))}],
|
||||
payload: normalize_duplicate_pairs(pairs)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch("upload_site", project, _params) do
|
||||
with {:ok, metadata} <- Metadata.get_project_metadata(project.id),
|
||||
{:ok, credentials} <- upload_credentials(metadata.publishing_preferences),
|
||||
{:ok, job} <- Publishing.upload_site(project.id, credentials) do
|
||||
{:ok,
|
||||
%{
|
||||
kind: "task_queued",
|
||||
action: "upload_site",
|
||||
title: "Upload Site",
|
||||
message: "Upload queued",
|
||||
project_id: project.id,
|
||||
task_id: job.task_id,
|
||||
publish_job_id: job.id,
|
||||
panel_tab: "tasks"
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch(action, _project, _params) do
|
||||
{:error, %{action: action, message: "Unsupported shell command"}}
|
||||
end
|
||||
|
||||
defp queue_task(project, action, title, group_name, work) do
|
||||
{:ok, task} =
|
||||
Tasks.submit_task(title, work, %{
|
||||
group_id: project.id,
|
||||
group_name: group_name
|
||||
})
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
kind: "task_queued",
|
||||
action: action,
|
||||
title: title,
|
||||
message: "#{title} queued",
|
||||
project_id: project.id,
|
||||
task_id: task.id,
|
||||
panel_tab: "tasks"
|
||||
}}
|
||||
end
|
||||
|
||||
defp active_project do
|
||||
case Projects.get_active_project() do
|
||||
nil -> {:error, %{message: "No active project selected"}}
|
||||
project -> {:ok, project}
|
||||
end
|
||||
end
|
||||
|
||||
defp preview_url(server) do
|
||||
"http://#{server.host}:#{server.port}/"
|
||||
end
|
||||
|
||||
defp normalize_site_validation(report) do
|
||||
%{
|
||||
summary: %{
|
||||
missing_count: length(report.missing_pages),
|
||||
extra_count: length(report.extra_pages),
|
||||
stale_count: length(report.stale_pages)
|
||||
},
|
||||
missing_pages: report.missing_pages,
|
||||
extra_pages: report.extra_pages,
|
||||
stale_pages: report.stale_pages,
|
||||
sections: Enum.map(report.sections, &to_string/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_metadata_diff(report) do
|
||||
%{
|
||||
summary: %{
|
||||
diff_count: length(report.diff_reports),
|
||||
orphan_count: length(report.orphan_reports)
|
||||
},
|
||||
diff_reports: Enum.map(report.diff_reports, &stringify_map/1),
|
||||
orphan_reports: Enum.map(report.orphan_reports, &stringify_map/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_translation_validation(report) do
|
||||
%{
|
||||
summary: %{
|
||||
missing_count: length(report.missing),
|
||||
orphan_count: length(report.orphan_files),
|
||||
do_not_translate_count: length(report.do_not_translate_posts)
|
||||
},
|
||||
missing: Enum.map(report.missing, &stringify_map/1),
|
||||
orphan_files: report.orphan_files,
|
||||
do_not_translate_posts: report.do_not_translate_posts
|
||||
}
|
||||
end
|
||||
|
||||
defp normalize_duplicate_pairs(pairs) do
|
||||
%{
|
||||
summary: %{pair_count: length(pairs)},
|
||||
pairs: Enum.map(pairs, &stringify_map/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp stringify_map(map) when is_map(map) do
|
||||
Map.new(map, fn {key, value} -> {to_string(key), stringify_value(value)} end)
|
||||
end
|
||||
|
||||
defp stringify_value(value) when is_map(value), do: stringify_map(value)
|
||||
defp stringify_value(value) when is_list(value), do: Enum.map(value, &stringify_value/1)
|
||||
defp stringify_value(value) when is_atom(value), do: Atom.to_string(value)
|
||||
defp stringify_value(value), do: value
|
||||
|
||||
defp upload_credentials(prefs) when is_map(prefs) do
|
||||
credentials = %{
|
||||
ssh_host: Map.get(prefs, "ssh_host"),
|
||||
ssh_user: Map.get(prefs, "ssh_user"),
|
||||
ssh_remote_path: Map.get(prefs, "ssh_remote_path"),
|
||||
ssh_mode: Map.get(prefs, "ssh_mode")
|
||||
}
|
||||
|
||||
if Enum.all?([credentials.ssh_host, credentials.ssh_user, credentials.ssh_remote_path], &is_binary/1) do
|
||||
{:ok, credentials}
|
||||
else
|
||||
{:error, %{action: "upload_site", message: "Publishing preferences are incomplete"}}
|
||||
end
|
||||
end
|
||||
|
||||
defp open_system_path(path) do
|
||||
{command, args} =
|
||||
case :os.type() do
|
||||
{:unix, :darwin} -> {"open", [path]}
|
||||
{:unix, _other} -> {"xdg-open", [path]}
|
||||
{:win32, _other} -> {"cmd", ["/c", "start", "", path]}
|
||||
end
|
||||
|
||||
case System.cmd(command, args, stderr_to_stdout: true) do
|
||||
{_output, 0} -> :ok
|
||||
{output, status} -> {:error, {status, String.trim(output)}}
|
||||
end
|
||||
rescue
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
||||
@@ -4,4 +4,21 @@ defmodule BDS.Desktop.ShellController do
|
||||
def index_html do
|
||||
BDS.UI.ShellPage.render()
|
||||
end
|
||||
|
||||
def task_status_json do
|
||||
Jason.encode!(BDS.Tasks.status_snapshot())
|
||||
end
|
||||
|
||||
def command_json(payload) when is_map(payload) do
|
||||
action = Map.get(payload, "action") || Map.get(payload, :action)
|
||||
params = Map.get(payload, "params") || Map.get(payload, :params) || %{}
|
||||
|
||||
case BDS.Desktop.ShellCommands.execute(action, params) do
|
||||
{:ok, result} -> Jason.encode!(%{status: "ok", result: result})
|
||||
{:error, error} -> Jason.encode!(%{status: "error", error: normalize_error(error)})
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_error(error) when is_map(error), do: error
|
||||
defp normalize_error(error), do: %{message: inspect(error)}
|
||||
end
|
||||
|
||||
@@ -17,6 +17,10 @@ defmodule BDS.Projects do
|
||||
Repo.all(from project in Project, order_by: [asc: project.created_at])
|
||||
end
|
||||
|
||||
def get_active_project do
|
||||
Repo.one(from project in Project, where: project.is_active == true, limit: 1)
|
||||
end
|
||||
|
||||
def get_project(id), do: Repo.get(Project, id)
|
||||
def get_project!(id), do: Repo.get!(Project, id)
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule BDS.Tasks do
|
||||
GenServer.call(__MODULE__, {:get_task, task_id})
|
||||
end
|
||||
|
||||
def status_snapshot do
|
||||
GenServer.call(__MODULE__, :status_snapshot)
|
||||
end
|
||||
|
||||
def cancel_task(task_id) when is_binary(task_id) do
|
||||
GenServer.call(__MODULE__, {:cancel_task, task_id})
|
||||
end
|
||||
@@ -67,6 +71,10 @@ defmodule BDS.Tasks do
|
||||
{:reply, state.tasks[task_id] && public_task(state.tasks[task_id]), state}
|
||||
end
|
||||
|
||||
def handle_call(:status_snapshot, _from, state) do
|
||||
{:reply, build_status_snapshot(state), state}
|
||||
end
|
||||
|
||||
def handle_call({:cancel_task, task_id}, _from, state) do
|
||||
cond do
|
||||
Map.has_key?(state.running, task_id) ->
|
||||
@@ -302,6 +310,46 @@ defmodule BDS.Tasks do
|
||||
Map.drop(task, [:last_reported_at])
|
||||
end
|
||||
|
||||
defp build_status_snapshot(state) do
|
||||
tasks = active_tasks(state)
|
||||
|
||||
%{
|
||||
active_count: length(tasks),
|
||||
running_count: Enum.count(tasks, &(&1.status == :running)),
|
||||
pending_count: Enum.count(tasks, &(&1.status == :pending)),
|
||||
running_task_message: running_task_message(tasks),
|
||||
running_task_overflow: running_task_overflow(tasks),
|
||||
tasks: Enum.map(tasks, &public_task/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp active_tasks(state) do
|
||||
state.tasks
|
||||
|> Map.values()
|
||||
|> Enum.filter(&(&1.status in [:running, :pending]))
|
||||
|> Enum.sort_by(&task_sort_key/1)
|
||||
end
|
||||
|
||||
defp task_sort_key(task) do
|
||||
{task_priority(task.status), task.started_at || task.created_at}
|
||||
end
|
||||
|
||||
defp task_priority(:running), do: 0
|
||||
defp task_priority(:pending), do: 1
|
||||
|
||||
defp running_task_message([]), do: nil
|
||||
|
||||
defp running_task_message([task | _rest]) do
|
||||
cond do
|
||||
task.status == :pending -> "Queued: #{task.name}"
|
||||
is_binary(task.message) and task.message != "" -> "#{task.name}: #{task.message}"
|
||||
true -> task.name
|
||||
end
|
||||
end
|
||||
|
||||
defp running_task_overflow([]), do: 0
|
||||
defp running_task_overflow(tasks), do: max(length(tasks) - 1, 0)
|
||||
|
||||
defp normalize_result({:ok, _value} = result), do: result
|
||||
defp normalize_result({:error, _reason} = result), do: result
|
||||
defp normalize_result(value), do: {:ok, value}
|
||||
|
||||
@@ -9,8 +9,12 @@ defmodule BDS.UI.Commands do
|
||||
|
||||
cond do
|
||||
primary and key == "b" -> MenuBar.execute(state, :toggle_sidebar)
|
||||
primary and key == "j" -> MenuBar.execute(state, :toggle_panel)
|
||||
primary and key == "1" -> MenuBar.execute(state, :view_posts)
|
||||
primary and key == "2" -> MenuBar.execute(state, :view_media)
|
||||
primary and key == "\\" -> MenuBar.execute(state, :toggle_assistant_sidebar)
|
||||
primary and key == "w" -> MenuBar.execute(state, :close_tab)
|
||||
true -> state
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,6 +82,8 @@ defmodule BDS.UI.MenuBar do
|
||||
def execute(state, :toggle_sidebar), do: Workbench.toggle_sidebar(state)
|
||||
def execute(state, :toggle_panel), do: Workbench.toggle_panel(state)
|
||||
def execute(state, :toggle_assistant_sidebar), do: Workbench.toggle_assistant_sidebar(state)
|
||||
def execute(state, :view_posts), do: %{state | active_view: :posts, sidebar_visible: true}
|
||||
def execute(state, :view_media), do: %{state | active_view: :media, sidebar_visible: true}
|
||||
|
||||
def execute(state, :close_tab) do
|
||||
case state.active_tab do
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule BDS.UI.ShellPage do
|
||||
@moduledoc false
|
||||
|
||||
alias BDS.I18n
|
||||
alias BDS.UI.MenuBar
|
||||
alias BDS.UI.Registry
|
||||
alias BDS.UI.Session
|
||||
@@ -49,9 +50,15 @@ defmodule BDS.UI.ShellPage do
|
||||
|
||||
defp bootstrap do
|
||||
workbench = Workbench.new()
|
||||
task_status = BDS.Tasks.status_snapshot()
|
||||
ui_language = I18n.current_ui_locale()
|
||||
|
||||
%{
|
||||
title: Application.get_env(:bds, :desktop)[:title] || "Blogging Desktop Server",
|
||||
i18n: %{
|
||||
ui_language: ui_language,
|
||||
supported_ui_languages: Enum.map(I18n.supported_languages(), &Map.take(&1, [:code, :flag]))
|
||||
},
|
||||
registry: %{
|
||||
sidebar_views: Enum.map(Registry.sidebar_views(), &encode_sidebar_view/1),
|
||||
editor_routes: Enum.map(Registry.editor_routes(), &encode_editor_route/1),
|
||||
@@ -59,21 +66,22 @@ defmodule BDS.UI.ShellPage do
|
||||
},
|
||||
menu_groups: Enum.map(MenuBar.default_groups(), &encode_menu_group/1),
|
||||
session: Session.serialize(workbench),
|
||||
task_status: task_status,
|
||||
content: %{
|
||||
sidebar: sidebar_content(),
|
||||
dashboard: dashboard_content(),
|
||||
dashboard: dashboard_content(task_status),
|
||||
assistant_cards: assistant_cards(),
|
||||
editor_meta: editor_meta()
|
||||
editor_meta: editor_meta(task_status)
|
||||
},
|
||||
status:
|
||||
Workbench.status_bar(workbench,
|
||||
post_count: 42,
|
||||
media_count: 18,
|
||||
theme_badge: "desktop-shell",
|
||||
ui_language: "en",
|
||||
ui_language: ui_language,
|
||||
offline_mode: true,
|
||||
running_task_message: "Desktop shell ready",
|
||||
running_task_overflow: 0,
|
||||
running_task_message: task_status.running_task_message,
|
||||
running_task_overflow: task_status.running_task_overflow,
|
||||
git_badge_count: 3
|
||||
)
|
||||
}
|
||||
@@ -180,14 +188,18 @@ defmodule BDS.UI.ShellPage do
|
||||
%{title: title, subtitle: subtitle, sections: [%{title: title, items: items}]}
|
||||
end
|
||||
|
||||
defp dashboard_content do
|
||||
defp dashboard_content(task_status) do
|
||||
%{
|
||||
title: "Dashboard",
|
||||
subtitle: "Desktop workbench shell wired through Elixir",
|
||||
summary_cards: [
|
||||
%{label: "Posts", value: "42", detail: "Across draft, published, and archive"},
|
||||
%{label: "Media", value: "18", detail: "Images and documents indexed"},
|
||||
%{label: "Tasks", value: "1", detail: "One background action visible in the status bar"}
|
||||
%{
|
||||
label: "Tasks",
|
||||
value: Integer.to_string(task_status.active_count),
|
||||
detail: task_summary_detail(task_status)
|
||||
}
|
||||
],
|
||||
checklist: [
|
||||
"Native menu groups mirror the old application shell",
|
||||
@@ -205,16 +217,28 @@ defmodule BDS.UI.ShellPage do
|
||||
]
|
||||
end
|
||||
|
||||
defp editor_meta do
|
||||
defp editor_meta(task_status) do
|
||||
%{
|
||||
dashboard: [
|
||||
%{label: "Status", value: "Workbench shell ready"},
|
||||
%{label: "Status", value: task_status.running_task_message || "Idle"},
|
||||
%{label: "Mode", value: "Offline"},
|
||||
%{label: "Main Language", value: "en"}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp task_summary_detail(%{active_count: 0}), do: "No active background tasks"
|
||||
|
||||
defp task_summary_detail(%{running_count: running, pending_count: pending}) do
|
||||
segments = []
|
||||
segments = if running > 0, do: ["#{running} running" | segments], else: segments
|
||||
segments = if pending > 0, do: ["#{pending} queued" | segments], else: segments
|
||||
|
||||
segments
|
||||
|> Enum.reverse()
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
|
||||
defp normalize_view_label(:chat, _label), do: "Chat"
|
||||
defp normalize_view_label(:git, _label), do: "Git"
|
||||
defp normalize_view_label(_id, label), do: label
|
||||
|
||||
Reference in New Issue
Block a user