feat: headless server mode with ExRatatui SSH daemon (issue #26, phase 1)

This commit is contained in:
2026-07-14 19:17:06 +02:00
parent 6ec61610f1
commit 700ed79c43
15 changed files with 539 additions and 158 deletions

View File

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

View File

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

95
lib/bds/server.ex Normal file
View File

@@ -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: `<data dir>/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

38
lib/bds/tui.ex Normal file
View File

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

View File

@@ -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"},

View File

@@ -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"},

View File

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

View File

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

View File

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

View File

@@ -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 dimport"
msgid "Import failed: %{error}"
msgstr "Échec de limport : %{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 lassistant 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 dimporter 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."

View File

@@ -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 nellassistente 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."

View File

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

62
specs/server.allium Normal file
View File

@@ -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 -- <data dir>/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
}

View File

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

111
test/bds/server_test.exs Normal file
View File

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