From 700ed79c430e711b22e711140dfc52dabcae7529 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Tue, 14 Jul 2026 19:17:06 +0200 Subject: [PATCH] feat: headless server mode with ExRatatui SSH daemon (issue #26, phase 1) --- config/config.exs | 2 + lib/bds/application.ex | 11 ++- lib/bds/server.ex | 95 +++++++++++++++++++++++++ lib/bds/tui.ex | 38 ++++++++++ mix.exs | 1 + mix.lock | 1 + priv/gettext/de/LC_MESSAGES/ui.po | 62 ++++++++++------- priv/gettext/en/LC_MESSAGES/ui.po | 62 ++++++++++------- priv/gettext/es/LC_MESSAGES/ui.po | 62 ++++++++++------- priv/gettext/fr/LC_MESSAGES/ui.po | 62 ++++++++++------- priv/gettext/it/LC_MESSAGES/ui.po | 62 ++++++++++------- priv/gettext/ui.pot | 62 ++++++++++------- specs/server.allium | 62 +++++++++++++++++ test/bds/scripting/job_test.exs | 4 +- test/bds/server_test.exs | 111 ++++++++++++++++++++++++++++++ 15 files changed, 539 insertions(+), 158 deletions(-) create mode 100644 lib/bds/server.ex create mode 100644 lib/bds/tui.ex create mode 100644 specs/server.allium create mode 100644 test/bds/server_test.exs diff --git a/config/config.exs b/config/config.exs index 77ddda6..b3fff8c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -25,6 +25,8 @@ config :bds, :desktop, window_min_size: {800, 600}, title: "Blogging Desktop Server" +config :bds, :server, ssh_port: 2222 + config :bds, BDS.Desktop.Endpoint, url: [host: "127.0.0.1"], adapter: Bandit.PhoenixAdapter, diff --git a/lib/bds/application.ex b/lib/bds/application.ex index dfdf003..8b0ffd7 100644 --- a/lib/bds/application.ex +++ b/lib/bds/application.ex @@ -5,6 +5,15 @@ defmodule BDS.Application do @compiled_env Application.compile_env(:bds, :current_env, Mix.env()) + # Children that depend on the boot mode (issue #26): desktop mode runs the + # wx window shell, server mode runs headless behind the SSH daemon. Both + # start the loopback HTTP endpoint and the CLI sync watcher. + def mode_children(:server, _env) do + [{BDS.Desktop.Server, []}, BDS.CliSync.Watcher, BDS.Server] + end + + def mode_children(:desktop, env), do: desktop_children(env) + def desktop_children(env \\ nil) def desktop_children(:test) do @@ -41,7 +50,7 @@ defmodule BDS.Application do {Task.Supervisor, name: BDS.Scripting.TaskSupervisor}, BDS.Scripting.JobSupervisor, BDS.Embeddings.Index - ] ++ embedding_children() ++ desktop_children(current_env()) + ] ++ embedding_children() ++ mode_children(BDS.Server.mode(), current_env()) opts = [strategy: :one_for_one, name: BDS.Supervisor] Supervisor.start_link(children, opts) diff --git a/lib/bds/server.ex b/lib/bds/server.ex new file mode 100644 index 0000000..a5e1a00 --- /dev/null +++ b/lib/bds/server.ex @@ -0,0 +1,95 @@ +defmodule BDS.Server do + @moduledoc """ + Headless server mode (issue #26). + + Boots the app without any wx window and serves clients over a single SSH + daemon: TUI sessions run server-side via `ExRatatui.SSH` (only terminal + cells cross the wire), and GUI clients tunnel to the loopback HTTP + endpoint through the same daemon (`tcpip_tunnel_out`). Authentication is + public-key only; host key and `authorized_keys` live in the private app + data dir next to the database — never in the repo or a priv dir. + """ + + @doc """ + Resolves the boot mode from the `BDS_MODE` environment variable. + Anything other than `"server"` (case-insensitive) is desktop mode. + """ + @spec mode(String.t() | nil) :: :desktop | :server + def mode(value \\ System.get_env("BDS_MODE")) + + def mode(value) when is_binary(value) do + if String.downcase(value) == "server", do: :server, else: :desktop + end + + def mode(nil), do: :desktop + + @doc "SSH key-material directory: `/ssh` next to the database." + @spec ssh_dir() :: Path.t() + def ssh_dir do + Application.get_env(:bds, BDS.Repo)[:database] + |> Path.dirname() + |> Path.join("ssh") + end + + @doc """ + Ensures the SSH directory exists with an `authorized_keys` file (created + empty and chmod 600 on first boot; existing keys are never touched). + """ + @spec ensure_ssh_dir!(Path.t()) :: Path.t() + def ensure_ssh_dir!(dir) do + File.mkdir_p!(dir) + keys_path = Path.join(dir, "authorized_keys") + + unless File.exists?(keys_path) do + File.write!(keys_path, "") + File.chmod!(keys_path, 0o600) + end + + dir + end + + @doc """ + Options for `ExRatatui.SSH.Daemon`: serves `BDS.TUI`, public-key auth + only, and allows TCP/IP tunneling out so GUI clients can reach the + loopback HTTP endpoint through the SSH connection. + """ + @spec daemon_opts(Path.t()) :: keyword() + def daemon_opts(dir \\ ssh_dir()) do + dir = ensure_ssh_dir!(dir) + system_dir = ExRatatui.SSH.Daemon.ensure_host_key!(dir) + + [ + mod: BDS.TUI, + port: ssh_port(), + system_dir: system_dir, + user_dir: String.to_charlist(dir), + auth_methods: ~c"publickey", + tcpip_tunnel_out: true + ] + end + + @doc "SSH listen port: `BDS_SSH_PORT` env or `:bds, :server` config." + @spec ssh_port() :: pos_integer() + def ssh_port do + case System.get_env("BDS_SSH_PORT") do + value when is_binary(value) and value != "" -> String.to_integer(value) + _other -> Application.get_env(:bds, :server)[:ssh_port] || 2222 + end + end + + @doc false + # Child spec that defers all filesystem work (key generation) to process + # start, so building a children list never touches the data dir. + def child_spec(_opts) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, []}, + type: :worker + } + end + + @doc false + def start_link do + ExRatatui.SSH.Daemon.start_link(daemon_opts()) + end +end diff --git a/lib/bds/tui.ex b/lib/bds/tui.ex new file mode 100644 index 0000000..c9a3a5f --- /dev/null +++ b/lib/bds/tui.ex @@ -0,0 +1,38 @@ +defmodule BDS.TUI do + @moduledoc """ + Terminal UI entry point (issue #26), served per SSH connection by + `ExRatatui.SSH.Daemon` in server mode and runnable locally. + + Placeholder shell for now: renders the app banner while the workbench + renderer lands in a later phase. The UI locale is the server-side UI + language setting, same as the GUI. + """ + + use ExRatatui.App + use Gettext, backend: BDS.Gettext + + alias BDS.Desktop.UILocale + alias ExRatatui.Layout.Rect + alias ExRatatui.Widgets.Paragraph + + @impl true + def mount(_opts) do + UILocale.put(BDS.Desktop.ShellData.ui_language()) + {:ok, %{}} + end + + @impl true + def render(_state, frame) do + text = + "bDS2 — " <> + dgettext("ui", "Blogging Desktop Server") <> + "\n\n" <> + dgettext("ui", "Press q to quit.") + + [{%Paragraph{text: text}, %Rect{x: 0, y: 0, width: frame.width, height: frame.height}}] + end + + @impl true + def handle_event(%ExRatatui.Event.Key{code: "q"}, state), do: {:stop, state} + def handle_event(_event, state), do: {:noreply, state} +end diff --git a/mix.exs b/mix.exs index a7c5020..3031920 100644 --- a/mix.exs +++ b/mix.exs @@ -40,6 +40,7 @@ defmodule BDS.MixProject do {:live_toast, "~> 0.8.0"}, {:saxy, "~> 1.4"}, {:desktop, "~> 1.5"}, + {:ex_ratatui, "~> 0.11"}, {:image, "~> 0.67"}, {:nx, "~> 0.10"}, {:exla, "~> 0.10"}, diff --git a/mix.lock b/mix.lock index 5eb3510..07f568d 100644 --- a/mix.lock +++ b/mix.lock @@ -25,6 +25,7 @@ "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"}, "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, + "ex_ratatui": {:hex, :ex_ratatui, "0.11.1", "f3cc5c9827d957bc326d3b3b48338a927716fedc2575311d65a3af0dd03ed951", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b37b02b33e1e8f49874497d7b7f8872417b3807ec77d4bc1526c07c05c7e4cc9"}, "ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"}, "ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"}, "exla": {:hex, :exla, "0.10.0", "93e7d75a774fbc06ce05b96de20c4b01bda413b315238cb3c727c09a05d2bc3a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.9.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "16fffdb64667d7f0a3bc683fdcd2792b143a9b345e4b1f1d5cd50330c63d8119"}, diff --git a/priv/gettext/de/LC_MESSAGES/ui.po b/priv/gettext/de/LC_MESSAGES/ui.po index f55ca14..adb7317 100644 --- a/priv/gettext/de/LC_MESSAGES/ui.po +++ b/priv/gettext/de/LC_MESSAGES/ui.po @@ -71,8 +71,8 @@ msgid "AI Assistant" msgstr "KI-Assistent" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "KI-Einstellungen" @@ -336,7 +336,7 @@ msgstr "Autor" msgid "Auto" msgstr "Automatisch" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -486,7 +486,7 @@ msgstr "Kategorie-Standards, Render-Flags und Template-Zuordnung" msgid "Category name is required" msgstr "Kategoriename ist erforderlich" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1063,7 +1063,7 @@ msgstr "Galerie" msgid "Generate Site" msgstr "Website generieren" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1077,7 +1077,7 @@ msgid "Git Diff" msgstr "Git-Diff" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1200,9 +1200,9 @@ msgstr "Importdefinitionen" msgid "Import failed: %{error}" msgstr "Import fehlgeschlagen: %{error}" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1784,7 +1784,7 @@ msgstr "Sonstige" msgid "Other (%{count})" msgstr "Andere (%{count})" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2538,7 +2538,7 @@ msgstr "Schlagwortname" msgid "Tags" msgstr "Tags" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3218,12 +3218,12 @@ msgstr "Willkommen beim KI-Assistenten" msgid "Comparing database and filesystem metadata" msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "%{count} Bilder zum Beitrag hinzugefügt" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "%{title} hinzugefügt" @@ -3243,18 +3243,18 @@ msgstr "Endbenutzer-Anleitung für redaktionelle Arbeitsabläufe, Medien, Vorlag msgid "Image Import Concurrency" msgstr "Gleichzeitige Bildimporte" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "Galerie-Bilder hinzufügen" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "%{path} konnte nicht verarbeitet werden: %{reason}" @@ -3445,12 +3445,12 @@ msgstr "umbenannt" msgid "untracked" msgstr "nicht verfolgt" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format msgid "Blogmark" msgstr "Blogmark" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "Öffnen Sie ein Projekt, bevor Sie ein Blogmark importieren." @@ -3491,7 +3491,7 @@ msgstr "Loeschen" msgid "Failed to copy bookmarklet to clipboard" msgstr "Bookmarklet konnte nicht in die Zwischenablage kopiert werden" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "Das Projekt, auf das dieses Blogmark verweist, existiert hier nicht." @@ -3578,12 +3578,12 @@ msgstr "KI-Einstellungen gespeichert." msgid "Could not load models" msgstr "Modelle konnten nicht geladen werden" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "KI-Einstellungen konnten nicht gespeichert werden" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "%{count} Modelle geladen." @@ -3593,7 +3593,17 @@ msgstr "%{count} Modelle geladen." msgid "OK" msgstr "OK" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "Der Endpunkt hat keine Modelle zurückgegeben. Sie können einen Modellnamen weiterhin manuell eingeben." + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "Blogging Desktop Server" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "Drücken Sie q zum Beenden." diff --git a/priv/gettext/en/LC_MESSAGES/ui.po b/priv/gettext/en/LC_MESSAGES/ui.po index dcc57d6..c99d896 100644 --- a/priv/gettext/en/LC_MESSAGES/ui.po +++ b/priv/gettext/en/LC_MESSAGES/ui.po @@ -71,8 +71,8 @@ msgid "AI Assistant" msgstr "" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "" @@ -336,7 +336,7 @@ msgstr "" msgid "Auto" msgstr "" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -486,7 +486,7 @@ msgstr "" msgid "Category name is required" msgstr "" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1063,7 +1063,7 @@ msgstr "" msgid "Generate Site" msgstr "" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1077,7 +1077,7 @@ msgid "Git Diff" msgstr "" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1200,9 +1200,9 @@ msgstr "" msgid "Import failed: %{error}" msgstr "" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1784,7 +1784,7 @@ msgstr "" msgid "Other (%{count})" msgstr "" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2538,7 +2538,7 @@ msgstr "" msgid "Tags" msgstr "" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3218,12 +3218,12 @@ msgstr "" msgid "Comparing database and filesystem metadata" msgstr "Comparing database and filesystem metadata" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "Added %{count} images to post" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "Added %{title}" @@ -3243,18 +3243,18 @@ msgstr "" msgid "Image Import Concurrency" msgstr "Image Import Concurrency" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "Add Gallery Images" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "Failed to process %{path}: %{reason}" @@ -3445,12 +3445,12 @@ msgstr "" msgid "untracked" msgstr "" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format, fuzzy msgid "Blogmark" msgstr "Blogmark" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "" @@ -3491,7 +3491,7 @@ msgstr "" msgid "Failed to copy bookmarklet to clipboard" msgstr "" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "" @@ -3578,12 +3578,12 @@ msgstr "" msgid "Could not load models" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "" @@ -3593,7 +3593,17 @@ msgstr "" msgid "OK" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "" + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "" diff --git a/priv/gettext/es/LC_MESSAGES/ui.po b/priv/gettext/es/LC_MESSAGES/ui.po index 1b86d55..de822ba 100644 --- a/priv/gettext/es/LC_MESSAGES/ui.po +++ b/priv/gettext/es/LC_MESSAGES/ui.po @@ -71,8 +71,8 @@ msgid "AI Assistant" msgstr "Asistente de IA" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "Configuración de IA" @@ -336,7 +336,7 @@ msgstr "Autor" msgid "Auto" msgstr "Automático" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -486,7 +486,7 @@ msgstr "Valores predeterminados de categoría, opciones de renderizado y conexi msgid "Category name is required" msgstr "El nombre de la categoría es obligatorio" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1063,7 +1063,7 @@ msgstr "Galeria" msgid "Generate Site" msgstr "Generar sitio" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1077,7 +1077,7 @@ msgid "Git Diff" msgstr "Diff de Git" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1200,9 +1200,9 @@ msgstr "Definiciones de importación" msgid "Import failed: %{error}" msgstr "La importación falló: %{error}" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1784,7 +1784,7 @@ msgstr "Otros" msgid "Other (%{count})" msgstr "Otros (%{count})" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2538,7 +2538,7 @@ msgstr "Nombre de la etiqueta" msgid "Tags" msgstr "Etiquetas" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3218,12 +3218,12 @@ msgstr "Bienvenido al asistente de IA" msgid "Comparing database and filesystem metadata" msgstr "Comparando metadatos de la base de datos y del sistema de archivos" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "%{count} imágenes añadidas a la publicación" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "%{title} añadido" @@ -3243,18 +3243,18 @@ msgstr "Guía del usuario para flujos editoriales, medios, plantillas, traducci msgid "Image Import Concurrency" msgstr "Importación simultánea de imágenes" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "Añadir imágenes a la galería" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "No se pudo procesar %{path}: %{reason}" @@ -3445,12 +3445,12 @@ msgstr "renombrado" msgid "untracked" msgstr "sin seguimiento" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format msgid "Blogmark" msgstr "Blogmark" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "Abre un proyecto antes de importar un blogmark." @@ -3491,7 +3491,7 @@ msgstr "Eliminar" msgid "Failed to copy bookmarklet to clipboard" msgstr "Error al copiar el bookmarklet al portapapeles" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "El proyecto al que apunta este blogmark no existe aquí." @@ -3578,12 +3578,12 @@ msgstr "Configuración de IA guardada." msgid "Could not load models" msgstr "No se pudieron cargar los modelos" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "No se pudo guardar la configuración de IA" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "%{count} modelos cargados." @@ -3593,7 +3593,17 @@ msgstr "%{count} modelos cargados." msgid "OK" msgstr "OK" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "El endpoint no devolvió ningún modelo. Aún puedes escribir el nombre de un modelo manualmente." + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "Blogging Desktop Server" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "Pulsa q para salir." diff --git a/priv/gettext/fr/LC_MESSAGES/ui.po b/priv/gettext/fr/LC_MESSAGES/ui.po index 9be6c82..f9a4f8e 100644 --- a/priv/gettext/fr/LC_MESSAGES/ui.po +++ b/priv/gettext/fr/LC_MESSAGES/ui.po @@ -71,8 +71,8 @@ msgid "AI Assistant" msgstr "Assistant IA" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "Paramètres IA" @@ -336,7 +336,7 @@ msgstr "Auteur" msgid "Auto" msgstr "Automatique" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -486,7 +486,7 @@ msgstr "Valeurs par défaut des catégories, options de rendu et liaison des mod msgid "Category name is required" msgstr "Le nom de la catégorie est requis" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1063,7 +1063,7 @@ msgstr "Galerie" msgid "Generate Site" msgstr "Générer le site" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1077,7 +1077,7 @@ msgid "Git Diff" msgstr "Diff Git" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1200,9 +1200,9 @@ msgstr "Définitions d’import" msgid "Import failed: %{error}" msgstr "Échec de l’import : %{error}" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1784,7 +1784,7 @@ msgstr "Autre" msgid "Other (%{count})" msgstr "Autres (%{count})" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2538,7 +2538,7 @@ msgstr "Nom du mot-clé" msgid "Tags" msgstr "Tags" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3218,12 +3218,12 @@ msgstr "Bienvenue dans l’assistant IA" msgid "Comparing database and filesystem metadata" msgstr "Comparaison des métadonnées entre la base et le système de fichiers" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "%{count} images ajoutées à l'article" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "%{title} ajouté" @@ -3243,18 +3243,18 @@ msgstr "Guide utilisateur pour les flux éditoriaux, médias, modèles, traducti msgid "Image Import Concurrency" msgstr "Importation simultanée d'images" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "Ajouter des images à la galerie" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "Impossible de traiter %{path} : %{reason}" @@ -3445,12 +3445,12 @@ msgstr "renommé" msgid "untracked" msgstr "non suivi" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format msgid "Blogmark" msgstr "Blogmark" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "Ouvrez un projet avant d’importer un blogmark." @@ -3491,7 +3491,7 @@ msgstr "Supprimer" msgid "Failed to copy bookmarklet to clipboard" msgstr "Échec de la copie du bookmarklet dans le presse-papiers" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "Le projet ciblé par ce blogmark n'existe pas ici." @@ -3578,12 +3578,12 @@ msgstr "Paramètres IA enregistrés." msgid "Could not load models" msgstr "Impossible de charger les modèles" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "Impossible d'enregistrer les paramètres IA" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "%{count} modèles chargés." @@ -3593,7 +3593,17 @@ msgstr "%{count} modèles chargés." msgid "OK" msgstr "OK" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "Le point de terminaison n'a renvoyé aucun modèle. Vous pouvez toujours saisir un nom de modèle manuellement." + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "Blogging Desktop Server" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "Appuyez sur q pour quitter." diff --git a/priv/gettext/it/LC_MESSAGES/ui.po b/priv/gettext/it/LC_MESSAGES/ui.po index 5c25575..35d592d 100644 --- a/priv/gettext/it/LC_MESSAGES/ui.po +++ b/priv/gettext/it/LC_MESSAGES/ui.po @@ -71,8 +71,8 @@ msgid "AI Assistant" msgstr "Assistente IA" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "Impostazioni IA" @@ -336,7 +336,7 @@ msgstr "Autore" msgid "Auto" msgstr "Automatico" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -486,7 +486,7 @@ msgstr "Valori predefiniti delle categorie, opzioni di rendering e collegamento msgid "Category name is required" msgstr "Il nome della categoria è obbligatorio" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1063,7 +1063,7 @@ msgstr "Galleria" msgid "Generate Site" msgstr "Genera sito" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1077,7 +1077,7 @@ msgid "Git Diff" msgstr "Diff Git" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1200,9 +1200,9 @@ msgstr "Definizioni di importazione" msgid "Import failed: %{error}" msgstr "Importazione non riuscita: %{error}" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1784,7 +1784,7 @@ msgstr "Altro" msgid "Other (%{count})" msgstr "Altro (%{count})" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2538,7 +2538,7 @@ msgstr "Nome del tag" msgid "Tags" msgstr "Tag" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3218,12 +3218,12 @@ msgstr "Benvenuto nell’assistente IA" msgid "Comparing database and filesystem metadata" msgstr "Confronto tra i metadati del database e del filesystem" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "%{count} immagini aggiunte al post" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "%{title} aggiunto" @@ -3243,18 +3243,18 @@ msgstr "Guida per l'utente finale per flussi editoriali, media, modelli, traduzi msgid "Image Import Concurrency" msgstr "Importazione simultanea immagini" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "Aggiungi immagini alla galleria" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "Impossibile elaborare %{path}: %{reason}" @@ -3445,12 +3445,12 @@ msgstr "rinominato" msgid "untracked" msgstr "non tracciato" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format msgid "Blogmark" msgstr "Blogmark" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "Apri un progetto prima di importare un blogmark." @@ -3491,7 +3491,7 @@ msgstr "Elimina" msgid "Failed to copy bookmarklet to clipboard" msgstr "Impossibile copiare il bookmarklet negli appunti" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "Il progetto a cui punta questo blogmark non esiste qui." @@ -3578,12 +3578,12 @@ msgstr "Impostazioni IA salvate." msgid "Could not load models" msgstr "Impossibile caricare i modelli" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "Impossibile salvare le impostazioni IA" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "Caricati %{count} modelli." @@ -3593,7 +3593,17 @@ msgstr "Caricati %{count} modelli." msgid "OK" msgstr "OK" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "L'endpoint non ha restituito alcun modello. Puoi comunque digitare manualmente il nome di un modello." + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "Blogging Desktop Server" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "Premi q per uscire." diff --git a/priv/gettext/ui.pot b/priv/gettext/ui.pot index 694053f..d1c5628 100644 --- a/priv/gettext/ui.pot +++ b/priv/gettext/ui.pot @@ -84,8 +84,8 @@ msgid "AI Assistant" msgstr "" #: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:93 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:208 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:193 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:211 #, elixir-autogen, elixir-format msgid "AI Settings" msgstr "" @@ -349,7 +349,7 @@ msgstr "" msgid "Auto" msgstr "" -#: lib/bds/desktop/shell_live.ex:448 +#: lib/bds/desktop/shell_live.ex:442 #: lib/bds/desktop/shell_live/chat_editor.ex:234 #: lib/bds/desktop/shell_live/media_editor.ex:171 #: lib/bds/desktop/shell_live/media_editor.ex:364 @@ -499,7 +499,7 @@ msgstr "" msgid "Category name is required" msgstr "" -#: lib/bds/desktop/shell_live.ex:771 +#: lib/bds/desktop/shell_live.ex:780 #: lib/bds/desktop/shell_live/chat_editor.ex:87 #: lib/bds/desktop/shell_live/chat_editor.ex:233 #: lib/bds/desktop/shell_live/chat_editor.ex:323 @@ -1076,7 +1076,7 @@ msgstr "" msgid "Generate Site" msgstr "" -#: lib/bds/desktop/shell_live.ex:772 +#: lib/bds/desktop/shell_live.ex:781 #: lib/bds/desktop/shell_live/socket_state.ex:109 #: lib/bds/ui/sidebar.ex:789 #, elixir-autogen, elixir-format @@ -1090,7 +1090,7 @@ msgid "Git Diff" msgstr "" #: lib/bds/desktop/shell_data.ex:226 -#: lib/bds/desktop/shell_live.ex:768 +#: lib/bds/desktop/shell_live.ex:777 #: lib/bds/desktop/shell_live/panel_renderer.ex:171 #, elixir-autogen, elixir-format msgid "Git Log" @@ -1213,9 +1213,9 @@ msgstr "" msgid "Import failed: %{error}" msgstr "" -#: lib/bds/desktop/shell_live.ex:598 -#: lib/bds/desktop/shell_live.ex:884 -#: lib/bds/desktop/shell_live.ex:890 +#: lib/bds/desktop/shell_live.ex:605 +#: lib/bds/desktop/shell_live.ex:893 +#: lib/bds/desktop/shell_live.ex:899 #: lib/bds/desktop/shell_live/sidebar_create.ex:47 #, elixir-autogen, elixir-format msgid "Import media" @@ -1797,7 +1797,7 @@ msgstr "" msgid "Other (%{count})" msgstr "" -#: lib/bds/desktop/shell_live.ex:767 +#: lib/bds/desktop/shell_live.ex:776 #: lib/bds/desktop/shell_live/panel_renderer.ex:83 #, elixir-autogen, elixir-format msgid "Output" @@ -2551,7 +2551,7 @@ msgstr "" msgid "Tags" msgstr "" -#: lib/bds/desktop/shell_live.ex:766 +#: lib/bds/desktop/shell_live.ex:775 #: lib/bds/desktop/shell_live/panel_renderer.ex:54 #, elixir-autogen, elixir-format msgid "Tasks" @@ -3231,12 +3231,12 @@ msgstr "" msgid "Comparing database and filesystem metadata" msgstr "" -#: lib/bds/desktop/shell_live.ex:674 +#: lib/bds/desktop/shell_live.ex:681 #, elixir-autogen, elixir-format msgid "Added %{count} images to post" msgstr "" -#: lib/bds/desktop/shell_live.ex:642 +#: lib/bds/desktop/shell_live.ex:649 #, elixir-autogen, elixir-format msgid "Added %{title}" msgstr "" @@ -3256,18 +3256,18 @@ msgstr "" msgid "Image Import Concurrency" msgstr "" -#: lib/bds/desktop/shell_live.ex:447 -#: lib/bds/desktop/shell_live.ex:460 -#: lib/bds/desktop/shell_live.ex:641 -#: lib/bds/desktop/shell_live.ex:673 -#: lib/bds/desktop/shell_live.ex:684 -#: lib/bds/desktop/shell_live.ex:695 +#: lib/bds/desktop/shell_live.ex:441 +#: lib/bds/desktop/shell_live.ex:454 +#: lib/bds/desktop/shell_live.ex:648 +#: lib/bds/desktop/shell_live.ex:680 +#: lib/bds/desktop/shell_live.ex:691 +#: lib/bds/desktop/shell_live.ex:702 #: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:420 #, elixir-autogen, elixir-format msgid "Add Gallery Images" msgstr "" -#: lib/bds/desktop/shell_live.ex:696 +#: lib/bds/desktop/shell_live.ex:703 #, elixir-autogen, elixir-format msgid "Failed to process %{path}: %{reason}" msgstr "" @@ -3458,12 +3458,12 @@ msgstr "" msgid "untracked" msgstr "" -#: lib/bds/desktop/shell_live.ex:792 +#: lib/bds/desktop/shell_live.ex:801 #, elixir-autogen, elixir-format msgid "Blogmark" msgstr "" -#: lib/bds/desktop/shell_live.ex:835 +#: lib/bds/desktop/shell_live.ex:844 #, elixir-autogen, elixir-format msgid "Open a project before importing a blogmark." msgstr "" @@ -3504,7 +3504,7 @@ msgstr "" msgid "Failed to copy bookmarklet to clipboard" msgstr "" -#: lib/bds/desktop/shell_live.ex:804 +#: lib/bds/desktop/shell_live.ex:813 #, elixir-autogen, elixir-format msgid "The project this blogmark targets does not exist here." msgstr "" @@ -3591,12 +3591,12 @@ msgstr "" msgid "Could not load models" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:187 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:190 #, elixir-autogen, elixir-format msgid "Could not save AI settings" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:182 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:184 #, elixir-autogen, elixir-format msgid "Loaded %{count} models." msgstr "" @@ -3606,7 +3606,17 @@ msgstr "" msgid "OK" msgstr "" -#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:173 +#: lib/bds/desktop/shell_live/settings_editor/ai_settings.ex:174 #, elixir-autogen, elixir-format msgid "The endpoint returned no models. You can still type a model name manually." msgstr "" + +#: lib/bds/tui.ex:28 +#, elixir-autogen, elixir-format +msgid "Blogging Desktop Server" +msgstr "" + +#: lib/bds/tui.ex:30 +#, elixir-autogen, elixir-format +msgid "Press q to quit." +msgstr "" diff --git a/specs/server.allium b/specs/server.allium new file mode 100644 index 0000000..eb4b157 --- /dev/null +++ b/specs/server.allium @@ -0,0 +1,62 @@ +-- allium: 1 +-- Headless Server Mode (issue #26, phase 1) +-- Scope: extension (client/server split — SSH transport for TUI and GUI) +-- Distilled from: lib/bds/server.ex, lib/bds/application.ex, lib/bds/tui.ex + +entity BootMode { + kind: desktop | server + -- Resolved from the BDS_MODE environment variable at application start. + -- Anything other than "server" (case-insensitive) is desktop. +} + +entity SshKeyMaterial { + dir: String -- /ssh, next to the database + host_key: String -- ssh_host_rsa_key, generated on first boot + authorized_keys: String -- created empty (mode 600) on first boot + + -- Invariant: key material lives in the private app data dir, + -- never in the repo or an application priv dir. +} + +surface ServerRuntimeSurface { + facing _: ServerRuntime + + provides: + ApplicationStarted(mode) + SshClientConnected() +} + +rule ModeResolution { + when: ApplicationStarted(mode) + -- BDS_MODE environment variable decides the mode once per boot + ensures: BootMode.created(kind: mode) +} + +rule DesktopBoot { + when: ApplicationStarted(mode: desktop) + -- Unchanged behavior: wx window shell, loopback HTTP endpoint. + ensures: DesktopWindowStarted() +} + +rule ServerBoot { + when: ApplicationStarted(mode: server) + -- Headless: no wx children at all. Works on macOS, Windows, Linux. + ensures: LoopbackEndpointStarted() + -- HTTP endpoint stays bound to 127.0.0.1; never exposed directly + ensures: CliSyncWatcherStarted() + ensures: SshDaemonStarted() + -- ExRatatui.SSH.Daemon: public-key auth only (no passwords), + -- tcpip_tunnel_out enabled so GUI clients tunnel to the loopback + -- endpoint through the same SSH connection and the same keys + ensures: SshKeyMaterial.created() + -- host key generated and authorized_keys touched on first boot only +} + +rule TuiSession { + when: SshClientConnected() + -- Each SSH client gets its own TUI app instance on the server; + -- only terminal cells cross the wire. + ensures: TuiAppMounted() + -- UI locale comes from the server-side UI language setting, + -- identical for every client +} diff --git a/test/bds/scripting/job_test.exs b/test/bds/scripting/job_test.exs index 3e21a20..0238c27 100644 --- a/test/bds/scripting/job_test.exs +++ b/test/bds/scripting/job_test.exs @@ -127,7 +127,9 @@ defmodule BDS.Scripting.JobTest do _running_job = wait_for_job(job.id, &(&1.status == :running)) assert :ok = BDS.Scripting.cancel_job(job.id) - assert_receive :job_cleanup_ran, 1_000 + # Generous timeout: cleanup runs in a separate shutdown path and 1s + # flakes when the full suite loads the machine. + assert_receive :job_cleanup_ran, 5_000 cancelled_job = wait_for_job(job.id, &(&1.status == :cancelled)) assert cancelled_job.finished_at != nil diff --git a/test/bds/server_test.exs b/test/bds/server_test.exs new file mode 100644 index 0000000..6291479 --- /dev/null +++ b/test/bds/server_test.exs @@ -0,0 +1,111 @@ +defmodule BDS.ServerTest do + use ExUnit.Case, async: false + + alias BDS.Server + + describe "mode/1" do + test "defaults to :desktop" do + assert Server.mode(nil) == :desktop + assert Server.mode("") == :desktop + end + + test "recognizes server mode" do + assert Server.mode("server") == :server + assert Server.mode("SERVER") == :server + end + + test "unknown values fall back to :desktop" do + assert Server.mode("garbage") == :desktop + end + end + + describe "ensure_ssh_dir!/1" do + setup do + dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}") + on_exit(fn -> File.rm_rf!(dir) end) + {:ok, dir: dir} + end + + test "creates the directory and an empty authorized_keys file", %{dir: dir} do + assert Server.ensure_ssh_dir!(dir) == dir + assert File.dir?(dir) + + keys_path = Path.join(dir, "authorized_keys") + assert File.exists?(keys_path) + assert File.read!(keys_path) == "" + + %File.Stat{mode: mode} = File.stat!(keys_path) + assert Bitwise.band(mode, 0o777) == 0o600 + end + + test "leaves an existing authorized_keys file untouched", %{dir: dir} do + File.mkdir_p!(dir) + keys_path = Path.join(dir, "authorized_keys") + File.write!(keys_path, "ssh-ed25519 AAAA... user@host\n") + + Server.ensure_ssh_dir!(dir) + + assert File.read!(keys_path) == "ssh-ed25519 AAAA... user@host\n" + end + end + + describe "daemon_opts/1" do + setup do + dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}") + on_exit(fn -> File.rm_rf!(dir) end) + {:ok, dir: dir} + end + + test "serves the TUI app over public-key auth only, with GUI tunneling", %{dir: dir} do + opts = Server.daemon_opts(dir) + + assert opts[:mod] == BDS.TUI + assert opts[:auth_methods] == ~c"publickey" + assert opts[:tcpip_tunnel_out] == true + refute Keyword.has_key?(opts, :pwdfun) + + assert opts[:system_dir] == String.to_charlist(dir) + assert opts[:user_dir] == String.to_charlist(dir) + end + + test "generates a host key on first use and reuses it", %{dir: dir} do + Server.daemon_opts(dir) + key_path = Path.join(dir, "ssh_host_rsa_key") + assert File.exists?(key_path) + first = File.read!(key_path) + + Server.daemon_opts(dir) + assert File.read!(key_path) == first + end + + test "uses the configured SSH port", %{dir: dir} do + assert Server.daemon_opts(dir)[:port] == Application.get_env(:bds, :server)[:ssh_port] + end + end + + describe "BDS.Application.mode_children/2" do + test "server mode starts endpoint, cli sync watcher, and the SSH daemon" do + children = BDS.Application.mode_children(:server, :prod) + + assert {BDS.Desktop.Server, []} in children + assert BDS.CliSync.Watcher in children + # BDS.Server wraps ExRatatui.SSH.Daemon, deferring key generation to + # process start so building this list never touches the data dir. + assert BDS.Server in children + end + + test "server mode starts no wx window children" do + children = BDS.Application.mode_children(:server, :prod) + + refute Enum.any?(children, fn + {Desktop.Window, _} -> true + _other -> false + end) + end + + test "desktop mode delegates to desktop_children" do + assert BDS.Application.mode_children(:desktop, :test) == + BDS.Application.desktop_children(:test) + end + end +end