Compare commits

..

12 Commits

Author SHA1 Message Date
d03d033548 fix: fixed shutdown race 2026-05-29 16:16:33 +02:00
74ceaeb971 fix: force full re-embed on explicit rebuild and degrade gracefully when embedding model is unavailable 2026-05-29 15:49:53 +02:00
61ff2a77c0 perf: A1-14b replace O(n^2) embedding snapshot with hnswlib HNSW index and debounced persistence 2026-05-29 15:36:13 +02:00
744f7543d7 perf: batch CPU embedding inference and add A1-14c Apple GPU (EMLX) spec gap 2026-05-29 14:43:39 +02:00
a1004d72bf fix: A1-14 real neural embeddings via Bumblebee multilingual-e5-small with Float32 BLOB vector cache 2026-05-29 14:04:51 +02:00
489d787306 fix: A1-13 wire git sidebar to BDS.Git with branch, changes, history, and actions 2026-05-29 13:25:32 +02:00
babae1838d fix: A1-12 functional client-side search with real PagefindUI and fragment index 2026-05-29 10:29:42 +02:00
5b619f492a fix: A1-11 graceful preview shutdown drains inflight requests before stopping 2026-05-29 09:49:54 +02:00
b3434b3054 fix: A1-10 write template file to disk on create instead of leaving file_path empty 2026-05-29 09:43:18 +02:00
5b21dcb17d fix: A1-9 replace native color input with 17-preset colour picker popover + custom hex 2026-05-29 09:28:57 +02:00
1f645f6e5e fix: stabilize atom-leak test by checking specific keys instead of global atom count 2026-05-29 09:19:13 +02:00
99d36e6e2f fix: A1-8 add Liquid/Lua validation gates before template and script publish 2026-05-29 09:16:07 +02:00
71 changed files with 4743 additions and 1231 deletions

View File

@@ -17,13 +17,15 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update
| A1-5 | ~~Auto-save after 3000ms idle~~ | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | **Resolved:** 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added |
| A1-6 | ~~On-demand rendering in preview server~~ | preview.allium:53-93 | `Preview.Router` matches post/archive/home/language routes and renders on-demand via `Rendering` | **Resolved:** `Preview.Router` implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added |
| A1-7 | ~~Template lookup must use all 4 levels (post→tag→category→default)~~ | template_context.allium:267-277 | `resolve_post_template_slug/3` implements tag→category cascade; all callers (preview, generation) updated | **Resolved:** `resolve_post_template_slug/3` in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added |
| A1-8 | `ValidateLiquid`/`ValidateScript` before publish | template.allium:110, script.allium:165 | No validation gate before publish | Fix code: add validation step before publish |
| A1-9 | 17 preset colors + custom hex in tag picker | editor_tags.allium | Native `<input type="color">`, no preset palette | Fix code: implement preset color palette popover |
| A1-10 | Template file written on create | engine_side_effects.allium:151-153 | Draft templates have `file_path=""` | Fix code: write template file on create |
| A1-11 | Graceful shutdown with inflight request tracking | preview.allium:47-48 | Kills acceptor process, no inflight tracking | Fix code: track inflight requests, drain before shutdown |
| A1-12 | Real Pagefind integration for search | generation.allium:208 | Stub only: `pagefind-ui.js` is one-liner, `PagefindUI` never defined, search-runtime.js silently bails, client-side search non-functional | Fix code: bundle real Pagefind, build proper fragment index, wire PagefindUI |
| A1-13 | Git sidebar shows only "Working tree" placeholder | sidebar_views.allium:651-770 | `sidebar.ex:782-798` returns single entity_list item; `BDS.Git` has full status/diff/commit/history/fetch/pull/push/prune_lfs but sidebar doesn't use it | Fix code: wire sidebar `git_view/0` to `BDS.Git` — render branch, ahead/behind, status file list, commit input, history entries, action buttons per spec |
| A1-14 | Embedding uses TF-IDF hash projection instead of real neural model | embedding.allium:44-53, invariants ModelCaching/VectorCacheInDb | `backends/in_app.ex` hashes terms into sparse vectors via `:erlang.phash2`; no ONNX model, no `"query: "` prefix, no mean pooling, vectors stored as JSON text not Float32Array BLOB, snapshot-based neighbor lookup instead of USearch HNSW index | Fix code: (1) add Bumblebee + ONNX runtime deps to run `Xenova/multilingual-e5-small`, (2) implement lazy model download + cache in app data dir, (3) `"query: "` prefix + mean pooling + L2 norm in backend, (4) store vectors as binary BLOB (1536 bytes), (5) replace JSON snapshot with USearch HNSW index (cosine, M=16, ef=128/64, 5s debounce), (6) cross-language semantic similarity must work |
| A1-8 | ~~`ValidateLiquid`/`ValidateScript` before publish~~ | template.allium:110, script.allium:165 | `publish_template` validates Liquid via `Liquex.parse`, `publish_script` validates Lua via `BDS.Scripting.validate` | **Resolved:** validation gates added to `publish_template/1` and `publish_script/1`, invalid content returns `{:error, {:invalid_liquid|:invalid_script, reason}}`, 4 tests added |
| A1-9 | ~~17 preset colors + custom hex in tag picker~~ | editor_tags.allium | `ColourPicker` hook + popover with 17 preset swatches grid and custom hex input, wired to both create and edit forms | **Resolved:** replaced native `<input type="color">` with `ColourPickerPopover` component (17 presets, custom hex #RRGGBB, immediate selection), JS hook for click-away dismiss, 1 test added |
| A1-10 | ~~Template file written on create~~ | engine_side_effects.allium:151-153 | `create_template` now computes `file_path` and writes template file with YAML frontmatter on create | **Resolved:** `create_template/1` writes `templates/{slug}.liquid` on create, `next_template_file_path` always computes path, 1 test added |
| A1-11 | ~~Graceful shutdown with inflight request tracking~~ | preview.allium:47-48 | `stop_preview` now closes the listener, parks the reply, and drains monitored inflight request tasks before reporting stopped | **Resolved:** acceptor transfers socket ownership to each request task; GenServer monitors inflight tasks, `begin_graceful_stop` stops accepting and finalizes via `:DOWN`/`:drain_timeout` (5s force-kill cap), 1 test added |
| A1-12 | ~~Real Pagefind integration for search~~ | generation.allium:208 | Functional client-side search: `PagefindUI` defined in bundled `pagefind-ui.js`, fragment index records url/title/body-scoped text per page, search-runtime wires it up | **Resolved:** bundled real `PagefindUI` (fetch index, ranked full-text match, highlighted excerpts) + `pagefind-ui.css` as local assets read into `Pagefind`; index scoped to `data-pagefind-body` (unmarked pages excluded per PagefindHtmlMarking), title from `<title>`/`<h1>`; localized "No results found" label via `data-search-no-results` (de/fr/it/es); 3 unit tests added |
| A1-13 | ~~Git sidebar shows only "Working tree" placeholder~~ | sidebar_views.allium:651-770 | `git_view/1` now builds a full `layout: "git"` view from `BDS.Git` (repository/remote_state/status/history); `SidebarComponents` renders active + not_a_repo states | **Resolved:** `git_view/1` in sidebar.ex assembles branch/upstream/ahead/behind, status files, paginated history (20/page); `render_git_sidebar` renders branch header, sync legend, fetch/pull/push/prune-lfs buttons, commit form, clickable status files (open git_diff), history entries; shell_live wires `git_commit` (closes git_diff tabs), `git_fetch`/`git_pull`/`git_push`/`git_prune_lfs`, `git_initialize`; `BDS.Git.history` enriched with author/date, `BDS.Git.set_remote/2` added; i18n for de/fr/it/es; 3 shell tests + git author/date assertions added |
| A1-14 | ~~Embedding uses TF-IDF hash projection instead of real neural model~~ | embedding.allium:44-53, invariants RealNeuralModel/ModelCaching/VectorCacheInDb | `Backends.Neural` runs `intfloat/multilingual-e5-small` (e5 weights behind the Xenova id) via Bumblebee+EXLA | **Resolved (core):** added bumblebee/nx/exla deps; `Backends.Neural` is a lazily-loaded GenServer that builds the Bumblebee text-embedding serving on first request (`"query: "` prefix + mean pooling + L2 norm), downloads+caches the model under the app data dir (ModelCaching), and is wired into the supervision tree when configured; vectors now persisted as packed little-endian Float32 BLOB (384×4=1536 bytes) instead of JSON text (VectorCacheInDb) with migration recreating `embedding_keys.vector` as BLOB; `InApp` demoted to documented offline/test stub; test config uses the stub so the suite stays offline; spec EmbeddingModel clarified (Xenova id ↔ intfloat weights via Bumblebee); batched inference via optional `embed_many/2` backend callback (configurable `batch_size`/`sequence_length`; rebuild/index/repair embed in chunks instead of one post at a time) + `NativeAcceleratedExecution` invariant added to spec; 4 tests added (BLOB round-trip, batched-rebuild, Neural model_info/behaviour). **Deferred:** A1-14b USearch HNSW index, A1-14c Apple GPU (EMLX). |
| A1-14b | ~~USearch HNSW ANN index + debounced persistence not implemented~~ | embedding.allium config/FindSimilar/DebouncedPersistence | `Embeddings.Index` is now an HNSW (hnswlib) ANN index with debounced persistence | **Resolved:** rewrote `Embeddings.Index` as a DB-free GenServer wrapping an hnswlib HNSW graph (cosine, M=16, efConstruction=128, efSearch=64) — O(n·log n) build, O(log n) queries, replacing the O(n²) JSON cosine snapshot; per-project in-memory index + `label→post_id` map; 5s debounced `save_index` + `.meta.json` sidecar, force-save on project switch (`set_active_project`) and shutdown (`terminate`), `forget/1` on project delete; lazy reload from disk with rebuild-from-DB self-heal on miss; `find_similar`/`find_duplicates`/`compute_similarities` rewired (no brute-force fallback); USearch has no Elixir binding so hnswlib provides the identical HNSW algorithm/params (spec reconciled); supervision + dialyzer PLT updated; tests updated for debounced/binary persistence + self-heal. Follow-up hardening: explicit rebuild now forces re-embedding regardless of content_hash (ReindexAll), and model-unavailable errors propagate cleanly (post saves degrade to unindexed + log; rebuild/index return `{:error, reason}` surfaced as a failed task with a user-facing message instead of crashing). |
| A1-14c | Embedding model runs on CPU only; no Apple GPU acceleration | embedding.allium invariant NativeAcceleratedExecution | `Backends.Neural` uses Bumblebee+EXLA; on Apple Silicon XLA has no Metal backend so inference is native CPU (batched). Apple GPU/Neural Engine unused | Fix code: spike an EMLX (Apple MLX) Nx backend so the model executes on the Apple Silicon GPU; gate by platform/availability with EXLA-CPU fallback; verify Bumblebee serving + defn compiler compatibility and benchmark vs CPU batching |
| A1-15 | ~~Preview vs generation content source strategy undocumented~~ | preview.allium (no invariant), generation.allium (no invariant) | Generation uses only published .md file content (`Generation.Data` snapshots set `content: nil`); preview includes published+draft posts and prefers DB content over file (`Preview.Router` queries `:published`/`:draft`, uses `editor_body`) | **Resolved:** added `PreviewDraftOverlay` invariant to preview.allium and `GenerationPublishedOnly` invariant to generation.allium; both cross-reference each other; code already correct, 3 tests added for draft-in-preview behavior |
### A2. Spec Should Update (code is normative)
@@ -184,7 +186,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
## Priority Order for Resolution
1. **A1-1 through A1-14** — code must follow spec (includes auto-save, on-demand preview, template lookup, validation gates, real Pagefind, graceful shutdown, real embedding model)
1. **A1-1 through A1-14c** — code must follow spec (includes auto-save, on-demand preview, template lookup, validation gates, real Pagefind, graceful shutdown, real embedding model, HNSW ANN index; only A1-14c = Apple GPU/EMLX acceleration still open)
2. **D1-1 through D1-18** — untested invariants/guarantees
3. **C-1 through C-3** — internal spec inconsistencies (reconcile to code)
4. **B1-1 through B1-6** — major code behaviors missing from spec

View File

@@ -555,3 +555,88 @@
padding: 8px 12px;
}
}
/* Colour picker popover */
.colour-picker-wrap {
position: relative;
display: inline-flex;
}
.colour-picker-trigger {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid var(--vscode-input-border);
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
.colour-picker-trigger:hover {
opacity: 0.85;
}
.colour-picker-popover {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 30;
padding: 8px;
border: 1px solid var(--vscode-dropdown-border, var(--vscode-panel-border));
border-radius: 6px;
background: var(--vscode-dropdown-background, var(--vscode-sideBar-background));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
width: 196px;
}
.colour-picker-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
}
.colour-picker-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
border: 2px solid transparent;
cursor: pointer;
padding: 0;
transition: border-color 0.1s;
}
.colour-picker-swatch:hover {
border-color: var(--vscode-focusBorder);
}
.colour-picker-swatch.selected {
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}
.colour-picker-custom {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--vscode-panel-border);
}
.colour-picker-custom label {
font-size: 11px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
}
.colour-picker-custom input {
flex: 1;
min-width: 0;
font-size: 12px;
padding: 2px 6px;
border: 1px solid var(--vscode-input-border);
border-radius: 3px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: monospace;
}

View File

@@ -0,0 +1,45 @@
export const ColourPicker = {
mounted() {
this._onClickAway = (e) => {
if (!this.el.contains(e.target)) {
this.el.querySelector(".colour-picker-popover")?.classList.add("hidden");
}
};
document.addEventListener("mousedown", this._onClickAway);
this._setupCustomInput();
},
updated() {
this._setupCustomInput();
},
destroyed() {
document.removeEventListener("mousedown", this._onClickAway);
},
_setupCustomInput() {
const input = this.el.querySelector(".colour-picker-custom input");
if (!input || input._cpBound) return;
input._cpBound = true;
const pushColor = () => {
let val = input.value.trim();
if (val && !val.startsWith("#")) val = "#" + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
const event = this.el.dataset.pickEvent;
this.pushEventTo(this.el.dataset.target, event, { color: val });
this.el.querySelector(".colour-picker-popover")?.classList.add("hidden");
}
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
pushColor();
}
});
input.addEventListener("blur", pushColor);
}
};

View File

@@ -2,6 +2,7 @@ import { AppShell } from "./app_shell.js";
import { SidebarInteractions } from "./sidebar_interactions.js";
import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js";
import { ChatSurface } from "./chat_surface.js";
import { ColourPicker } from "./colour_picker.js";
import { MenuEditorTree } from "./menu_editor_tree.js";
import { MonacoEditor } from "./monaco_editor.js";
import { MonacoDiffEditor } from "./monaco_diff_editor.js";
@@ -12,6 +13,7 @@ export const Hooks = {
SettingsSectionScroll,
TagsSectionScroll,
ChatSurface,
ColourPicker,
MenuEditorTree,
MonacoEditor,
MonacoDiffEditor

View File

@@ -61,9 +61,18 @@ config :bds, :scripting,
job_max_reductions: :none
config :bds, :embeddings,
backend: BDS.Embeddings.Backends.InApp,
backend: BDS.Embeddings.Backends.Neural,
model_id: "Xenova/multilingual-e5-small",
dimensions: 384
model_repo: "intfloat/multilingual-e5-small",
dimensions: 384,
# Inference is batched: batch_size texts per compiled run, truncated to
# sequence_length tokens. Tuning these trades throughput against memory.
batch_size: 16,
sequence_length: 256
# Cache downloaded model files under the app data directory so they persist
# across sessions (ModelCaching invariant). Overridden at runtime in prod.
config :bumblebee, :cache_dir, Path.expand("../priv/data/models", __DIR__)
config :logger, :console,
format: "$time $metadata[$level] $message\n",

View File

@@ -8,4 +8,9 @@ if config_env() == :prod do
config :bds, BDS.Repo,
database: database_path,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1")
# Persist downloaded embedding model files alongside the database data dir.
config :bumblebee, :cache_dir,
System.get_env("BDS_MODEL_CACHE_DIR") ||
Path.join(Path.dirname(Path.expand(database_path)), "models")
end

View File

@@ -8,3 +8,13 @@ config :bds, BDS.Repo,
busy_timeout: 15_000
config :logger, level: :warning
# Tests use the deterministic lexical stub backend so the suite stays offline
# and never downloads the ~100 MB neural model.
config :bds, :embeddings,
backend: BDS.Embeddings.Backends.InApp,
model_id: "Xenova/multilingual-e5-small",
model_repo: "intfloat/multilingual-e5-small",
dimensions: 384,
batch_size: 16,
sequence_length: 256

View File

@@ -37,14 +37,24 @@ defmodule BDS.Application do
{Task.Supervisor, name: BDS.TCP.TaskSupervisor},
BDS.Scripting.JobStore,
{Task.Supervisor, name: BDS.Scripting.TaskSupervisor},
BDS.Scripting.JobSupervisor
| desktop_children(current_env())
]
BDS.Scripting.JobSupervisor,
BDS.Embeddings.Index
] ++ embedding_children() ++ desktop_children(current_env())
opts = [strategy: :one_for_one, name: BDS.Supervisor]
Supervisor.start_link(children, opts)
end
# The neural embedding backend runs as a supervised, lazily-initialised
# GenServer (it loads the model only on the first embedding request). Only
# start it when it is the configured backend.
defp embedding_children do
case Application.get_env(:bds, :embeddings, [])[:backend] do
BDS.Embeddings.Backends.Neural -> [BDS.Embeddings.Backends.Neural]
_other -> []
end
end
defp current_env do
Application.get_env(:bds, :current_env_override) || @compiled_env
end

View File

@@ -120,16 +120,7 @@ defmodule BDS.Desktop.ShellCommands do
"rebuild_embedding_index",
"Rebuild Embedding Index",
"Embeddings",
fn report ->
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
report.(1.0, "Embedding index rebuilt")
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
end
fn report -> rebuild_embedding_index_work(project, report) end
)
end
@@ -524,20 +515,39 @@ defmodule BDS.Desktop.ShellCommands do
},
%{
name: "Rebuild Embedding Index",
work: fn report ->
{:ok, rebuilt_post_ids} = Embeddings.rebuild_project(project.id, on_progress: report)
report.(1.0, "Embedding index rebuilt")
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
end
work: fn report -> rebuild_embedding_index_work(project, report) end
}
]
end
defp rebuild_embedding_index_work(project, report) do
case Embeddings.rebuild_project(project.id, on_progress: report) do
{:ok, rebuilt_post_ids} ->
report.(1.0, "Embedding index rebuilt")
%{
project_id: project.id,
rebuilt_post_ids: rebuilt_post_ids,
rebuilt_count: length(rebuilt_post_ids)
}
{:error, reason} ->
{:error, embedding_error_message(reason)}
end
end
defp embedding_error_message(reason) do
detail =
case reason do
message when is_binary(message) -> message
{:embedding_backend_unavailable, _inner} -> "the embedding service did not start"
other -> inspect(other)
end
"Could not build the embedding index: #{detail}. The model is downloaded on first use, " <>
"so check your internet connection — or turn off semantic similarity in Settings."
end
defp run_rebuild_sequence(_group_id, _attrs, []), do: :ok
defp run_rebuild_sequence(group_id, attrs, [step | remaining_steps]) do

View File

@@ -84,6 +84,8 @@ defmodule BDS.Desktop.ShellLive do
"load_more_sidebar"
]
@git_action_events ["git_fetch", "git_pull", "git_push", "git_prune_lfs"]
@layout_menu_actions MapSet.new([
:toggle_sidebar,
:toggle_panel,
@@ -192,7 +194,8 @@ defmodule BDS.Desktop.ShellLive do
end
def handle_event("toggle_assistant_sidebar", _params, socket) do
{:noreply, refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
{:noreply,
refresh_layout(socket, Workbench.toggle_assistant_sidebar(socket.assigns.workbench))}
end
def handle_event("select_view", %{"view" => view_id}, socket) do
@@ -237,6 +240,20 @@ defmodule BDS.Desktop.ShellLive do
SidebarEvents.handle(socket, event, params, &refresh_sidebar/2)
end
def handle_event(event, _params, socket) when event in @git_action_events do
{:noreply, run_git_action(socket, event)}
end
def handle_event("git_commit", params, socket) do
message = params |> get_in(["git", "message"]) |> to_string() |> String.trim()
{:noreply, commit_git(socket, message)}
end
def handle_event("git_initialize", params, socket) do
remote_url = params |> get_in(["git", "remote_url"]) |> normalize_git_remote_url()
{:noreply, initialize_git(socket, remote_url)}
end
def handle_event("create_sidebar_item", %{"kind" => kind}, socket) do
{:noreply, create_sidebar_item(socket, kind)}
end
@@ -424,7 +441,9 @@ defmodule BDS.Desktop.ShellLive do
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
case FilePicker.choose_files(dgettext("ui", "Add Gallery Images"),
image_only: true, multiple: true) do
image_only: true,
multiple: true
) do
{:ok, paths} when is_list(paths) and paths != [] ->
GalleryImport.start(paths, project_id, post_id, language, concurrency_limit, parent)
@@ -623,7 +642,13 @@ defmodule BDS.Desktop.ShellLive do
def handle_info({:add_image_processed, title}, socket) do
{:noreply,
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), dgettext("ui", "Added %{title}", title: title), nil, "info")}
append_output_entry(
socket,
dgettext("ui", "Add Gallery Images"),
dgettext("ui", "Added %{title}", title: title),
nil,
"info"
)}
end
def handle_info({:add_images_complete, count}, socket) do
@@ -660,7 +685,13 @@ defmodule BDS.Desktop.ShellLive do
def handle_info({:add_images_error, reason}, socket) do
{:noreply,
append_output_entry(socket, dgettext("ui", "Add Gallery Images"), inspect(reason), nil, "error")}
append_output_entry(
socket,
dgettext("ui", "Add Gallery Images"),
inspect(reason),
nil,
"error"
)}
end
def handle_info({:add_image_error, path, reason}, socket) do
@@ -668,7 +699,10 @@ defmodule BDS.Desktop.ShellLive do
append_output_entry(
socket,
dgettext("ui", "Add Gallery Images"),
dgettext("ui", "Failed to process %{path}: %{reason}", path: Path.basename(path), reason: inspect(reason)),
dgettext("ui", "Failed to process %{path}: %{reason}",
path: Path.basename(path),
reason: inspect(reason)
),
nil,
"error"
)}
@@ -696,13 +730,17 @@ defmodule BDS.Desktop.ShellLive do
defp refresh_layout(socket, workbench) do
git_badge_count = socket.assigns[:git_badge_count] || 0
activity_buttons = Workbench.activity_buttons(workbench, git_badge_count)
task_status = socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
task_status =
socket.assigns[:task_status] || %{running_task_message: nil, running_task_overflow: nil}
dashboard = socket.assigns[:dashboard] || BDS.UI.Dashboard.empty_snapshot()
page_language = socket.assigns[:page_language] || ShellData.ui_language()
offline_mode = Map.get(socket.assigns, :offline_mode, true)
sidebar_data = socket.assigns[:sidebar_data] || %{}
current_tab = current_tab(workbench)
prev_tab = socket.assigns[:current_tab]
prev_panel_tab =
case socket.assigns[:workbench] do
%Workbench{panel: %{active_tab: tab}} -> tab
@@ -1017,6 +1055,122 @@ defmodule BDS.Desktop.ShellLive do
|> push_url_state()
end
defp run_git_action(socket, event) do
project_id = current_project_id(socket)
{label, result} =
case event do
"git_fetch" -> {dgettext("ui", "Fetch"), git_call(project_id, &BDS.Git.fetch/1)}
"git_pull" -> {dgettext("ui", "Pull"), git_call(project_id, &BDS.Git.pull/1)}
"git_push" -> {dgettext("ui", "Push"), git_call(project_id, &BDS.Git.push/1)}
"git_prune_lfs" -> {dgettext("ui", "Prune LFS"), prune_lfs(project_id)}
end
socket
|> append_git_result(label, result)
|> refresh_sidebar(socket.assigns.workbench)
end
defp commit_git(socket, "") do
socket
|> append_output_entry(
dgettext("ui", "Commit"),
dgettext("ui", "Commit message is required"),
nil,
"error"
)
|> refresh_sidebar(socket.assigns.workbench)
end
defp commit_git(socket, message) do
case git_call(current_project_id(socket), &BDS.Git.commit_all(&1, message)) do
{:ok, _result} ->
workbench = close_git_diff_tabs(socket.assigns.workbench)
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
socket
|> assign(:tab_meta, tab_meta)
|> append_output_entry(dgettext("ui", "Commit"), message)
|> refresh_sidebar(workbench)
|> push_url_state()
{:error, reason} ->
socket
|> append_output_entry(dgettext("ui", "Commit"), format_git_error(reason), nil, "error")
|> refresh_sidebar(socket.assigns.workbench)
end
end
defp initialize_git(socket, remote_url) do
project_id = current_project_id(socket)
case git_call(project_id, &BDS.Git.initialize_repo/1) do
{:ok, _repo} ->
_ = maybe_set_git_remote(project_id, remote_url)
socket
|> append_output_entry(
dgettext("ui", "Initialize Git"),
dgettext("ui", "Repository initialized")
)
|> refresh_sidebar(socket.assigns.workbench)
{:error, reason} ->
socket
|> append_output_entry(
dgettext("ui", "Initialize Git"),
format_git_error(reason),
nil,
"error"
)
|> refresh_sidebar(socket.assigns.workbench)
end
end
defp git_call(nil, _fun), do: {:error, :no_project}
defp git_call("default", _fun), do: {:error, :no_project}
defp git_call(project_id, fun) when is_binary(project_id), do: fun.(project_id)
defp prune_lfs(nil), do: {:error, :no_project}
defp prune_lfs("default"), do: {:error, :no_project}
defp prune_lfs(project_id) when is_binary(project_id),
do: BDS.Git.prune_lfs_cache(project_id, 10)
defp maybe_set_git_remote(_project_id, nil), do: :ok
defp maybe_set_git_remote(project_id, remote_url),
do: BDS.Git.set_remote(project_id, remote_url)
defp append_git_result(socket, label, {:ok, _result}) do
append_output_entry(socket, label, dgettext("ui", "Done"))
end
defp append_git_result(socket, label, {:error, reason}) do
append_output_entry(socket, label, format_git_error(reason), nil, "error")
end
defp format_git_error(:no_project), do: dgettext("ui", "No active project")
defp format_git_error(%{message: message}) when is_binary(message), do: message
defp format_git_error(%{guidance: guidance}) when is_binary(guidance), do: guidance
defp format_git_error({:git_failed, message}) when is_binary(message), do: message
defp format_git_error(reason), do: inspect(reason)
defp close_git_diff_tabs(workbench) do
workbench.tabs
|> Enum.filter(&(&1.type == :git_diff))
|> Enum.reduce(workbench, fn tab, wb -> Workbench.close_tab(wb, :git_diff, tab.id) end)
end
defp current_project_id(socket), do: (socket.assigns[:projects] || %{})[:active_project_id]
defp normalize_git_remote_url(value) do
case value |> to_string() |> String.trim() do
"" -> nil
url -> url
end
end
defp sidebar_create_action(view), do: SidebarCreate.action(view)
defp set_page_language(socket, language) do

View File

@@ -257,6 +257,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
"media_grid" -> render_media_sidebar(assigns)
"entity_list" -> render_entity_sidebar(assigns)
"nav_list" -> render_nav_sidebar(assigns)
"git" -> render_git_sidebar(assigns)
_other -> render_default_sidebar(assigns)
end
end
@@ -483,6 +484,141 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
"""
end
defp render_git_sidebar(assigns) do
assigns = assign(assigns, :git_state, Map.get(assigns.sidebar_data, :git_state, "not_a_repo"))
~H"""
<div class="git-sidebar">
<%= if @git_state == "active" do %>
<%= render_git_active(assigns) %>
<% else %>
<%= render_git_not_a_repo(assigns) %>
<% end %>
</div>
"""
end
defp render_git_not_a_repo(assigns) do
~H"""
<section class="git-section git-not-a-repo">
<p class="git-empty-hint"><%= dgettext("ui", "This project is not a Git repository yet.") %></p>
<form class="git-init-form flex flex-col gap-2" data-testid="git-init-form" phx-submit="git_initialize">
<input
type="text"
name="git[remote_url]"
placeholder={dgettext("ui", "Remote URL (optional)")}
value={Map.get(@sidebar_data, :remote_url) || ""}
/>
<button class="git-action-button" data-testid="git-initialize" type="submit">
<%= dgettext("ui", "Initialize Git") %>
</button>
</form>
</section>
"""
end
defp render_git_active(assigns) do
~H"""
<header class="git-header">
<div class="git-branch-row flex items-center gap-2">
<span class="git-branch-icon">⎇</span>
<span class="git-branch" data-testid="git-branch"><%= @sidebar_data.branch %></span>
<%= if @sidebar_data.upstream do %>
<span class="git-upstream" data-testid="git-upstream"><%= @sidebar_data.upstream %></span>
<% end %>
</div>
<div class="git-tracking flex items-center gap-3">
<span class="git-ahead" data-testid="git-ahead" title={dgettext("ui", "Ahead")}>↑ <%= @sidebar_data.ahead %></span>
<span class="git-behind" data-testid="git-behind" title={dgettext("ui", "Behind")}>↓ <%= @sidebar_data.behind %></span>
</div>
<div class="git-sync-legend flex items-center gap-3">
<span class="git-legend-item"><span class="git-sync-dot git-sync-synced"></span><%= dgettext("ui", "Synced") %></span>
<span class="git-legend-item"><span class="git-sync-dot git-sync-local_only"></span><%= dgettext("ui", "Local only") %></span>
<span class="git-legend-item"><span class="git-sync-dot git-sync-remote_only"></span><%= dgettext("ui", "Remote only") %></span>
</div>
</header>
<div class="git-actions flex items-center gap-2">
<button class="git-action-button" data-testid="git-action-fetch" type="button" phx-click="git_fetch" title={dgettext("ui", "Fetch")}><%= dgettext("ui", "Fetch") %></button>
<button class="git-action-button" data-testid="git-action-pull" type="button" phx-click="git_pull" title={dgettext("ui", "Pull")}><%= dgettext("ui", "Pull") %></button>
<button class="git-action-button" data-testid="git-action-push" type="button" phx-click="git_push" title={dgettext("ui", "Push")}><%= dgettext("ui", "Push") %></button>
<button class="git-action-button" data-testid="git-action-prune-lfs" type="button" phx-click="git_prune_lfs" title={dgettext("ui", "Prune LFS")}><%= dgettext("ui", "Prune LFS") %></button>
</div>
<section class="git-section git-changes">
<div class="git-section-title">
<span><%= dgettext("ui", "Changes") %></span>
<span class="git-section-count"><%= length(@sidebar_data.status_files) %></span>
</div>
<form class="git-commit-form flex flex-col gap-2" data-testid="git-commit-form" phx-submit="git_commit">
<input type="text" name="git[message]" placeholder={dgettext("ui", "Commit message")} />
<button class="git-action-button" data-testid="git-commit" type="submit"><%= dgettext("ui", "Commit") %></button>
</form>
<%= if Enum.any?(@sidebar_data.status_files) do %>
<div class="git-status-list flex flex-col">
<%= for file <- @sidebar_data.status_files do %>
<button
class="git-status-file flex items-center justify-between gap-2"
data-testid="git-status-file"
data-route="git_diff"
type="button"
title={"#{file.label}: #{file.path}"}
phx-click="open_sidebar_item"
phx-value-route="git_diff"
phx-value-id={"git-diff:" <> file.path}
phx-value-title={file.path}
phx-value-subtitle={file.label}
>
<span class="git-status-path"><%= file.path %></span>
<span class={"git-status-badge git-status-#{file.status}"}><%= file.code %></span>
</button>
<% end %>
</div>
<% else %>
<p class="git-empty-hint"><%= dgettext("ui", "No changes") %></p>
<% end %>
</section>
<section class="git-section git-history">
<div class="git-section-title">
<span><%= dgettext("ui", "History") %></span>
</div>
<%= if Enum.any?(@sidebar_data.history_entries) do %>
<div class="git-history-list flex flex-col">
<%= for entry <- @sidebar_data.history_entries do %>
<button
class="git-history-entry flex flex-col"
data-testid="git-history-entry"
data-route="git_diff"
type="button"
phx-click="open_sidebar_item"
phx-value-route="git_diff"
phx-value-id={"git-diff:commit:" <> entry.short_hash}
phx-value-title={entry.short_hash}
phx-value-subtitle={entry.subject || ""}
>
<span class="git-history-subject"><%= entry.subject %></span>
<span class="git-history-meta flex items-center gap-2">
<span class={"git-sync-dot git-sync-#{entry.sync_status}"}></span>
<span class="git-history-hash"><%= entry.short_hash %></span>
<%= if entry.author do %><span class="git-history-author"><%= entry.author %></span><% end %>
<%= if entry.date do %><span class="git-history-date"><%= entry.date %></span><% end %>
</span>
</button>
<% end %>
</div>
<%= if @sidebar_data.has_more_history do %>
<p class="git-history-more"><%= dgettext("ui", "Older history available") %></p>
<% end %>
<% else %>
<p class="git-empty-hint"><%= dgettext("ui", "No commits yet") %></p>
<% end %>
</section>
"""
end
defp render_default_sidebar(assigns) do
~H"""
<%= for section <- Map.get(@sidebar_data, :sections, []) do %>

View File

@@ -16,6 +16,16 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
@tags_sections ~w(cloud manage merge)
@colour_presets ~w(
#ef4444 #f97316 #f59e0b #eab308 #84cc16
#22c55e #10b981 #14b8a6 #06b6d4 #0ea5e9
#3b82f6 #6366f1 #8b5cf6 #a855f7 #d946ef
#ec4899 #64748b
)
@spec colour_presets() :: [String.t()]
def colour_presets, do: @colour_presets
@spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
@impl true
def update(%{action: :save} = assigns, socket) do
@@ -107,6 +117,26 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
{:noreply, assign(socket, :tags_editor, tags_editor)}
end
def handle_event("pick_new_tag_color", %{"color" => color}, socket) do
tags_editor =
Map.put(socket.assigns.tags_editor, :new_tag, %{
socket.assigns.tags_editor.new_tag
| "color" => color
})
{:noreply, assign(socket, :tags_editor, tags_editor)}
end
def handle_event("pick_edit_tag_color", %{"color" => color}, socket) do
tags_editor =
Map.put(socket.assigns.tags_editor, :edit_draft, %{
socket.assigns.tags_editor.edit_draft
| "color" => color
})
{:noreply, assign(socket, :tags_editor, tags_editor)}
end
def handle_event("save_tag_editor", _params, socket) do
{:noreply, do_save(socket)}
end
@@ -241,6 +271,55 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
end
end
attr :color, :string, default: nil
attr :presets, :list, required: true
attr :pick_event, :string, required: true
attr :target, :any, required: true
defp colour_picker(assigns) do
~H"""
<div
class="colour-picker-wrap"
id={"cp-#{@pick_event}"}
phx-hook="ColourPicker"
data-pick-event={@pick_event}
data-target={if @target, do: @target.cid}
>
<button
type="button"
class="colour-picker-trigger"
style={"background-color: #{if @color in [nil, ""], do: "#3b82f6", else: @color}"}
phx-click={Phoenix.LiveView.JS.toggle(to: "#cp-#{@pick_event} .colour-picker-popover")}
/>
<div class="colour-picker-popover hidden">
<div class="colour-picker-grid">
<%= for preset <- @presets do %>
<button
type="button"
class={"colour-picker-swatch#{if normalize_hex(@color) == normalize_hex(preset), do: " selected", else: ""}"}
style={"background-color: #{preset}"}
phx-click={Phoenix.LiveView.JS.push(@pick_event, value: %{color: preset}, target: if(@target, do: @target.cid)) |> Phoenix.LiveView.JS.add_class("hidden", to: "#cp-#{@pick_event} .colour-picker-popover")}
/>
<% end %>
</div>
<div class="colour-picker-custom">
<label>#</label>
<input
type="text"
maxlength="7"
placeholder="RRGGBB"
value={if @color in [nil, ""], do: "", else: @color}
/>
</div>
</div>
</div>
"""
end
defp normalize_hex(nil), do: nil
defp normalize_hex(""), do: nil
defp normalize_hex(hex), do: String.downcase(hex)
defp load_data(socket) do
project_id = socket.assigns.project_id
@@ -280,7 +359,8 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
merge_target:
Map.get(socket.assigns, :tags_editor, %{})
|> Map.get(:merge_target, List.first(selected) || ""),
selected_section: selected_section
selected_section: selected_section,
colour_presets: @colour_presets
}
assign(socket, :tags_editor, data)

View File

@@ -38,7 +38,13 @@
<form class="tag-create-form" phx-change="change_new_tag_editor" phx-target={@myself}>
<div class="tag-form-row flex flex-wrap items-center gap-3">
<input class="ui-input" type="text" name="new_tag[name]" value={@tags_editor.new_tag["name"]} placeholder={dgettext("ui", "Tag name")} />
<input type="color" name="new_tag[color]" value={if(@tags_editor.new_tag["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.new_tag["color"])} />
<input type="hidden" name="new_tag[color]" value={@tags_editor.new_tag["color"] || ""} />
<.colour_picker
color={@tags_editor.new_tag["color"]}
presets={@tags_editor.colour_presets}
pick_event="pick_new_tag_color"
target={@myself}
/>
<button class="primary ui-button ui-button-primary" type="button" phx-click="create_tag_editor" phx-target={@myself}><%= dgettext("ui", "Create") %></button>
</div>
</form>
@@ -47,7 +53,13 @@
<form class="tag-edit-form" phx-change="change_edit_tag_editor" phx-target={@myself}>
<div class="tag-form-row flex flex-wrap items-center gap-3">
<input class="ui-input" type="text" name="edit_tag[name]" value={@tags_editor.edit_draft["name"]} />
<input type="color" name="edit_tag[color]" value={if(@tags_editor.edit_draft["color"] in [nil, ""], do: "#3b82f6", else: @tags_editor.edit_draft["color"])} />
<input type="hidden" name="edit_tag[color]" value={@tags_editor.edit_draft["color"] || ""} />
<.colour_picker
color={@tags_editor.edit_draft["color"]}
presets={@tags_editor.colour_presets}
pick_event="pick_edit_tag_color"
target={@myself}
/>
<select class="ui-input" name="edit_tag[post_template_slug]">
<option value=""><%= dgettext("ui", "No Template") %></option>
<%= for template <- @tags_editor.templates do %>

View File

@@ -61,9 +61,26 @@ defmodule BDS.Desktop.Shutdown do
def command_menu_selected(_event, _command_event), do: :ok
@doc """
Terminate the OS process directly with SIGKILL.
`Desktop.Window.quit/0` routes through `System.halt/1`, which calls the libc
`exit()` and runs the wxWidgets C++ static destructors on the way out. On
macOS that races the still-running wx event loop on the main thread and
segfaults (`wxMenu::~wxMenu` vs `wxAppBase::ProcessIdle`). A SIGKILL is a
kernel-level termination that skips those destructors entirely, so the app
exits cleanly without producing a crash report.
"""
@spec quit() :: :ok
def quit do
kill_heart()
kill_beam()
:ok
end
defp start_shutdown_task do
Task.start(fn ->
MainWindow.persist_now()
persist_safely()
maybe_hide_window()
Process.sleep(50)
quit_module().quit()
@@ -72,6 +89,57 @@ defmodule BDS.Desktop.Shutdown do
:ok
end
defp persist_safely do
MainWindow.persist_now()
:ok
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
# heart, when present, would relaunch the app after we kill the BEAM, so it
# has to be terminated first. When the app is started without heart (e.g. via
# `mix`) there is nothing to do here.
defp kill_heart do
with heart when is_pid(heart) <- Process.whereis(:heart),
{:links, links} <- Process.info(heart, :links),
port when is_port(port) <- Enum.find(links, &is_port/1),
{:os_pid, heart_pid} <- Port.info(port, :os_pid) do
os_kill(heart_pid)
else
_ -> :ok
end
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
defp kill_beam do
os_kill(:os.getpid())
end
defp os_kill(os_pid) do
os_kill_fun().(os_pid)
:ok
rescue
_error -> :ok
catch
:exit, _reason -> :ok
end
defp os_kill_fun do
Application.get_env(:bds, :desktop_os_kill_fun, &__MODULE__.hard_kill/1)
end
@doc false
@spec hard_kill(charlist() | integer() | String.t()) :: :ok
def hard_kill(os_pid) do
System.cmd("kill", ["-9", to_string(os_pid)], stderr_to_stdout: true)
:ok
end
defp maybe_hide_window do
module = window_module()
@@ -86,8 +154,10 @@ defmodule BDS.Desktop.Shutdown do
:exit, _reason -> :ok
end
defp quit_module do
Application.get_env(:bds, :desktop_window_quit_module, Window)
@doc false
@spec quit_module() :: module()
def quit_module do
Application.get_env(:bds, :desktop_window_quit_module, __MODULE__)
end
defp window_module do

View File

@@ -2,6 +2,7 @@ defmodule BDS.Embeddings do
@moduledoc false
import Ecto.Query
require Logger
alias BDS.Persistence
alias BDS.Embeddings.DismissedDuplicatePair
@@ -75,25 +76,16 @@ defmodule BDS.Embeddings do
)
existing_keys = preload_keys_by_post_id(project_id, Enum.map(posts, & &1.id))
base_label = max_label_value()
{rows, _next_label} =
Enum.reduce(posts, {[], base_label + 1}, fn post, {acc, next_label} ->
existing_key = Map.get(existing_keys, post.id)
case build_key_rows(posts, existing_keys, max_label_value(), nil, false) do
{:ok, rows} ->
batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id)
{:ok, Enum.map(posts, & &1.id)}
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id)
{:ok, Enum.map(posts, & &1.id)}
{:error, _reason} = error ->
error
end
else
{:ok, []}
end
@@ -113,9 +105,6 @@ defmodule BDS.Embeddings do
)
post_ids = Enum.map(posts, & &1.id)
total_posts = length(posts)
:ok = report_rebuild_started(on_progress, total_posts, "embedding entries")
Repo.delete_all(
from key in Key,
@@ -123,30 +112,19 @@ defmodule BDS.Embeddings do
)
existing_keys = preload_keys_by_post_id(project_id)
base_label = max_label_value()
{rows, _next_label} =
posts
|> Enum.with_index(1)
|> Enum.reduce({[], base_label + 1}, fn {post, index}, {acc, next_label} ->
:ok = report_rebuild_progress(on_progress, index, total_posts, "embedding entries")
existing_key = Map.get(existing_keys, post.id)
# An explicit rebuild re-embeds every post from scratch (ReindexAll),
# ignoring the content_hash skip optimisation.
case build_key_rows(posts, existing_keys, max_label_value(), on_progress, true) do
{:ok, rows} ->
batch_upsert_keys(rows)
:ok = report_rebuild_phase(on_progress, 0.99, "Persisting embedding snapshot")
:ok = rebuild_snapshot(project_id)
{:ok, post_ids}
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
batch_upsert_keys(rows)
:ok = report_rebuild_phase(on_progress, 0.99, "Persisting embedding snapshot")
:ok = rebuild_snapshot(project_id)
{:ok, post_ids}
{:error, _reason} = error ->
error
end
else
{:ok, []}
end
@@ -200,32 +178,42 @@ defmodule BDS.Embeddings do
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
%Key{content_hash: ^content_hash} ->
if Keyword.get(opts, :refresh_index, true) and
snapshot_content_hash(post.project_id, post.id) != content_hash do
:ok = rebuild_snapshot(post.project_id)
end
# Embedding is already current. The HNSW index self-heals on query
# (find_similar/find_duplicates rebuild when no index is loaded), so
# there is nothing to refresh here.
:ok
existing_key ->
label = existing_key_label(existing_key) || next_label()
{:ok, vector} = embed_text(raw_text, post.language)
case embed_text(raw_text, post.language) do
{:ok, vector} ->
label = existing_key_label(existing_key) || next_label()
(existing_key || %Key{})
|> Key.changeset(%{
label: label,
post_id: post.id,
project_id: post.project_id,
content_hash: content_hash,
vector: Jason.encode!(vector)
})
|> Repo.insert_or_update()
(existing_key || %Key{})
|> Key.changeset(%{
label: label,
post_id: post.id,
project_id: post.project_id,
content_hash: content_hash,
vector: encode_vector(vector)
})
|> Repo.insert_or_update()
if Keyword.get(opts, :refresh_index, true) do
:ok = rebuild_snapshot(post.project_id)
if Keyword.get(opts, :refresh_index, true) do
:ok = rebuild_snapshot(post.project_id)
end
:ok
{:error, reason} ->
# Embedding is best-effort on post save: if the model is unavailable
# (e.g. offline first-use download), leave the post unindexed rather
# than failing the save. An explicit reindex surfaces the error.
Logger.warning(
"Embedding unavailable for post #{post.id}: #{inspect(reason)}; left unindexed"
)
:ok
end
:ok
end
end
@@ -246,17 +234,100 @@ defmodule BDS.Embeddings do
Repo.one(from key in Key, select: max(key.label)) || 0
end
defp compute_key_data(%Post{} = post, existing_key, next_label) do
body = resolve_post_body(post)
raw_text = compose_embedding_source(post.title, body)
content_hash = hash_text(raw_text)
# Builds the upsert rows for a batch of posts. Unless `force?` is set, posts
# whose content_hash is unchanged are skipped (ContentHashSkipsUnchanged); the
# rest are embedded in batches (see embed_pending/2) so model inference is not
# serialised one post at a time. Labels keep their existing value or take the
# next free integer. Returns `{:error, reason}` if the model is unavailable.
defp build_key_rows(posts, existing_keys, base_label, on_progress, force?) do
prepared =
Enum.map(posts, fn post ->
raw_text = compose_embedding_source(post.title, resolve_post_body(post))
existing = Map.get(existing_keys, post.id)
content_hash = hash_text(raw_text)
if existing_key && existing_key.content_hash == content_hash do
:skip
else
{:ok, vector} = embed_text(raw_text, post.language)
label = if existing_key, do: existing_key.label, else: next_label
{:upsert, [label, post.id, post.project_id, content_hash, Jason.encode!(vector)]}
%{
post: post,
existing: existing,
raw_text: raw_text,
content_hash: content_hash,
needs_embed?: force? or is_nil(existing) or existing.content_hash != content_hash
}
end)
pending = Enum.filter(prepared, & &1.needs_embed?)
:ok = report_rebuild_started(on_progress, length(pending), "embedding entries")
case embed_pending(pending, on_progress) do
{:ok, vectors_by_post_id} -> {:ok, collect_rows(prepared, vectors_by_post_id, base_label)}
{:error, _reason} = error -> error
end
end
defp collect_rows(prepared, vectors_by_post_id, base_label) do
{rows, _next_label} =
Enum.reduce(prepared, {[], base_label + 1}, fn entry, {acc, next_label} ->
if entry.needs_embed? do
vector = Map.fetch!(vectors_by_post_id, entry.post.id)
label = if entry.existing, do: entry.existing.label, else: next_label
bump = if entry.existing, do: 0, else: 1
row = [
label,
entry.post.id,
entry.post.project_id,
entry.content_hash,
encode_vector(vector)
]
{[row | acc], next_label + bump}
else
{acc, next_label}
end
end)
rows
end
defp embed_pending([], _on_progress), do: {:ok, %{}}
defp embed_pending(pending, on_progress) do
total = length(pending)
batch = batch_size()
pending
# Group by language so the lexical stub stems consistently; the neural
# backend is multilingual and ignores the language hint.
|> Enum.group_by(& &1.post.language)
|> Enum.reduce_while({%{}, 0}, fn {language, group}, acc ->
group
|> Enum.chunk_every(batch)
|> Enum.reduce_while(acc, fn chunk, {vectors, done} ->
case embed_many(Enum.map(chunk, & &1.raw_text), language) do
{:ok, chunk_vectors} ->
vectors =
chunk
|> Enum.zip(chunk_vectors)
|> Enum.reduce(vectors, fn {entry, vector}, acc ->
Map.put(acc, entry.post.id, vector)
end)
done = done + length(chunk)
:ok = report_rebuild_progress(on_progress, done, total, "embedding entries")
{:cont, {vectors, done}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end)
|> case do
{:error, reason} -> {:halt, {:error, reason}}
accumulator -> {:cont, accumulator}
end
end)
|> case do
{:error, reason} -> {:error, reason}
{vectors, _done} -> {:ok, vectors}
end
end
@@ -308,29 +379,20 @@ defmodule BDS.Embeddings do
)
existing_keys = preload_keys_by_post_id(project_id)
base_label = max_label_value()
{rows, _next_label} =
Enum.reduce(posts, {[], base_label + 1}, fn post, {acc, next_label} ->
existing_key = Map.get(existing_keys, post.id)
case build_key_rows(posts, existing_keys, max_label_value(), nil, false) do
{:ok, rows} ->
batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id)
case compute_key_data(post, existing_key, next_label) do
:skip ->
{acc, next_label}
indexed =
Repo.all(from key in Key, where: key.project_id == ^project_id, select: key.post_id)
{:upsert, row} ->
bump = if existing_key, do: 0, else: 1
{[row | acc], next_label + bump}
end
end)
{:ok, indexed}
batch_upsert_keys(rows)
:ok = rebuild_snapshot(project_id)
indexed =
Repo.all(from key in Key, where: key.project_id == ^project_id, select: key.post_id)
{:ok, indexed}
{:error, _reason} = error ->
error
end
else
{:ok, []}
end
@@ -344,28 +406,28 @@ defmodule BDS.Embeddings do
{:error, :not_found} ->
{:ok, []}
{:ok, post, source_vector} ->
similar =
case Index.neighbors(post.project_id, post.id, limit) do
{:ok, neighbors} ->
neighbors
{:ok, _post, nil} ->
{:ok, []}
{:error, :missing} ->
Repo.all(
from key in Key,
where: key.project_id == ^post.project_id and key.post_id != ^post.id
)
|> Enum.map(fn key ->
%{
post_id: key.post_id,
score: cosine_similarity(source_vector, decode_vector(key.vector))
}
end)
|> Enum.sort_by(& &1.score, :desc)
|> Enum.take(max(limit, 0))
end
{:ok, post, %Key{} = key} ->
{:ok, query_similar(post.project_id, key, limit)}
end
end
{:ok, similar}
# Queries the HNSW index for a post's neighbours, rebuilding the index from
# the DB vectors if it is not currently loaded (e.g. after a restart).
defp query_similar(project_id, %Key{} = key, limit) do
case Index.neighbors(project_id, key.label, key.vector, limit) do
{:ok, neighbors} ->
neighbors
{:error, :missing} ->
:ok = rebuild_snapshot(project_id)
case Index.neighbors(project_id, key.label, key.vector, limit) do
{:ok, neighbors} -> neighbors
{:error, :missing} -> []
end
end
end
@@ -378,8 +440,12 @@ defmodule BDS.Embeddings do
{:error, :not_found} ->
{:ok, %{}}
{:ok, post, source_vector} ->
{:ok, _post, nil} ->
{:ok, %{}}
{:ok, post, %Key{} = source_key} ->
target_ids = Enum.uniq(target_post_ids)
source_vector = decode_vector(source_key.vector)
scores =
Repo.all(
@@ -435,46 +501,18 @@ defmodule BDS.Embeddings do
if enabled_for_project?(project_id) do
on_progress = progress_callback(opts)
dismissed = dismissed_pair_keys(project_id)
entries = load_index_entries(project_id)
pairs =
case duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
{:ok, pairs} -> pairs
{:error, :missing} -> []
end
duplicates =
case Index.duplicate_pairs(project_id, @duplicate_threshold, on_progress: on_progress) do
{:ok, pairs} ->
pairs
|> Enum.reject(fn pair -> pair_key(pair.post_id_a, pair.post_id_b) in dismissed end)
|> enrich_duplicate_pairs(project_id)
{:error, :missing} ->
keys =
Repo.all(
from key in Key,
where: key.project_id == ^project_id,
order_by: [asc: key.post_id]
)
total_keys = length(keys)
:ok = report_rebuild_started(on_progress, total_keys, "embedding entries")
keys
|> Enum.with_index(1)
|> Enum.flat_map(fn {left, index} ->
:ok = report_rebuild_progress(on_progress, index, total_keys, "embedding entries")
for right <- keys,
left.post_id < right.post_id,
pair_key(left.post_id, right.post_id) not in dismissed,
similarity =
cosine_similarity(decode_vector(left.vector), decode_vector(right.vector)),
similarity >= @duplicate_threshold do
%{
post_id_a: left.post_id,
post_id_b: right.post_id,
score: similarity
}
end
end)
|> enrich_duplicate_pairs(project_id)
end
pairs
|> Enum.reject(fn pair -> pair_key(pair.post_id_a, pair.post_id_b) in dismissed end)
|> enrich_duplicate_pairs(project_id)
:ok = report_rebuild_phase(on_progress, 0.99, "Resolving duplicate candidates")
{:ok, duplicates}
@@ -538,17 +576,33 @@ defmodule BDS.Embeddings do
with {:ok, post} <- fetch_post(post_id) do
if enabled_for_project?(post.project_id) do
:ok = ensure_key(post)
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
nil -> {:ok, post, []}
key -> {:ok, post, decode_vector(key.vector)}
end
{:ok, post, Repo.get_by(Key, post_id: post.id, project_id: post.project_id)}
else
{:disabled, post.project_id}
end
end
end
defp duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
case Index.duplicate_pairs(project_id, entries, @duplicate_threshold, on_progress: on_progress) do
{:ok, pairs} ->
{:ok, pairs}
{:error, :missing} ->
:ok = rebuild_snapshot(project_id)
Index.duplicate_pairs(project_id, entries, @duplicate_threshold, on_progress: on_progress)
end
end
defp load_index_entries(project_id) do
Repo.all(
from key in Key,
where: key.project_id == ^project_id,
order_by: [asc: key.post_id]
)
|> Enum.map(fn key -> %{label: key.label, post_id: key.post_id, vector: key.vector} end)
end
defp ensure_key(%Post{} = post) do
case Repo.get_by(Key, post_id: post.id, project_id: post.project_id) do
nil -> sync_post(post)
@@ -655,11 +709,42 @@ defmodule BDS.Embeddings do
end
defp embed_text(raw_text, language) do
configured_backend().embed("query: " <> raw_text, language: language)
# Per-backend preprocessing (e5 "query: " prefix, pooling, normalisation)
# is the backend's responsibility — see BDS.Embeddings.Backends.Neural.
configured_backend().embed(raw_text, language: language)
end
# Embeds a batch of texts in one shot. Backends that implement the optional
# embed_many/2 callback (e.g. the neural backend, which feeds them through the
# model as a single batched inference run) handle the whole list; others fall
# back to sequential single embeds.
defp embed_many(texts, language) do
backend = configured_backend()
if function_exported?(backend, :embed_many, 2) do
backend.embed_many(texts, language: language)
else
Enum.reduce_while(texts, {:ok, []}, fn text, {:ok, acc} ->
case backend.embed(text, language: language) do
{:ok, vector} -> {:cont, {:ok, [vector | acc]}}
{:error, _reason} = error -> {:halt, error}
end
end)
|> case do
{:ok, vectors} -> {:ok, Enum.reverse(vectors)}
{:error, _reason} = error -> error
end
end
end
defp batch_size do
Application.get_env(:bds, :embeddings, [])
|> Keyword.get(:batch_size, 16)
|> max(1)
end
defp rebuild_snapshot(project_id) do
Index.rebuild(project_id, model_id: model_id(), dimensions: dimensions())
Index.put(project_id, dimensions(), load_index_entries(project_id))
end
defp progress_callback(opts), do: ProgressReporter.callback(opts)
@@ -684,13 +769,6 @@ defmodule BDS.Embeddings do
defp report_rebuild_phase(callback, value, label),
do: ProgressReporter.report_phase(callback, value, label)
defp snapshot_content_hash(project_id, post_id) do
case Index.read(project_id) do
{:ok, snapshot} -> get_in(snapshot, ["entries", post_id, "content_hash"])
_other -> nil
end
end
defp current_embedding_status(nil, _expected_hash), do: "missing"
defp current_embedding_status(%Key{vector: vector}, _expected_hash) when vector in [nil, ""],
@@ -726,8 +804,22 @@ defmodule BDS.Embeddings do
defp hash_text(text), do: :crypto.hash(:sha256, text) |> Base.encode16(case: :lower)
# Vectors are persisted as a packed little-endian Float32 BLOB
# (`dimensions` * 4 bytes; 1536 bytes for multilingual-e5-small) per the
# VectorCacheInDb invariant in specs/embedding.allium.
defp encode_vector(values) when is_list(values) do
for value <- values, into: <<>>, do: <<float32(value)::float-32-little>>
end
defp float32(value) when is_float(value), do: value
defp float32(value) when is_integer(value), do: value * 1.0
defp decode_vector(nil), do: []
defp decode_vector(vector), do: Jason.decode!(vector)
defp decode_vector(<<>>), do: []
defp decode_vector(binary) when is_binary(binary) do
for <<value::float-32-little <- binary>>, do: value
end
defp cosine_similarity([], _other), do: 0.0
defp cosine_similarity(_vector, []), do: 0.0

View File

@@ -3,4 +3,15 @@ defmodule BDS.Embeddings.Backend do
@callback model_info() :: %{model_id: String.t(), dimensions: pos_integer()}
@callback embed(String.t(), keyword()) :: {:ok, [number()]} | {:error, term()}
@doc """
Embeds a list of texts in a single call.
Backends that can amortise work across inputs (e.g. running the neural model
on a batched tensor) should implement this. The result list is aligned with
the input list. Optional — callers fall back to repeated `embed/2`.
"""
@callback embed_many([String.t()], keyword()) :: {:ok, [[number()]]} | {:error, term()}
@optional_callbacks embed_many: 2
end

View File

@@ -1,5 +1,13 @@
defmodule BDS.Embeddings.Backends.InApp do
@moduledoc false
@moduledoc """
Deterministic lexical embedding stub.
This backend does NOT satisfy the `RealNeuralModel` invariant — it projects
stemmed tokens and bigrams into a sparse hashed vector. It exists only as an
offline, dependency-free fallback for tests and environments where the neural
model (see `BDS.Embeddings.Backends.Neural`) cannot be loaded. Production and
development use the neural backend.
"""
@behaviour BDS.Embeddings.Backend
@@ -29,6 +37,17 @@ defmodule BDS.Embeddings.Backends.InApp do
{:ok, vector}
end
@impl true
def embed_many(texts, opts) when is_list(texts) and is_list(opts) do
vectors =
Enum.map(texts, fn text ->
{:ok, vector} = embed(text, opts)
vector
end)
{:ok, vectors}
end
defp tokenize(text) do
Regex.scan(~r/[[:alnum:]]+/u, String.downcase(text))
|> List.flatten()

View File

@@ -0,0 +1,152 @@
defmodule BDS.Embeddings.Backends.Neural do
@moduledoc """
Real on-device neural embedding backend.
Implements the `RealNeuralModel` and `ModelCaching` invariants from
`specs/embedding.allium`: embeddings are produced by the actual
multilingual-e5-small transformer (the `intfloat/multilingual-e5-small`
weights behind the `Xenova/multilingual-e5-small` identifier) via
Bumblebee + EXLA, never by a lexical approximation.
* Lazy-loaded — the model pipeline is built on the first embedding
request, not at application startup.
* Model files (~100 MB) are downloaded from the Hugging Face Hub on
first use and cached on disk (Bumblebee cache dir), persisting across
sessions and project switches.
* Text preprocessing follows the e5 convention: every input is prefixed
with `"query: "`, pooled with mean pooling over the attention mask, and
L2-normalised. This is what makes cross-language semantic similarity
work.
* Inference is batched. `embed_many/2` runs the model on `batch_size`
texts per compiled inference run instead of one at a time, which is the
dominant cost when (re)indexing large numbers of posts. The serving is
compiled for a fixed `batch_size`/`sequence_length` (configurable);
shorter sequences mean less wasted transformer compute.
EXLA on Apple Silicon runs on the CPU — XLA has no Metal/GPU backend. See
SPECGAPS A1-14c for the planned EMLX (Apple GPU via MLX) acceleration path.
"""
@behaviour BDS.Embeddings.Backend
use GenServer
@query_prefix "query: "
@embed_timeout :timer.minutes(10)
@default_model_id "Xenova/multilingual-e5-small"
@default_model_repo "intfloat/multilingual-e5-small"
@default_dimensions 384
@default_batch_size 16
@default_sequence_length 256
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl BDS.Embeddings.Backend
def model_info do
config = config()
%{
model_id: Keyword.get(config, :model_id, @default_model_id),
dimensions: Keyword.get(config, :dimensions, @default_dimensions)
}
end
@impl BDS.Embeddings.Backend
def embed(text, _opts) when is_binary(text) do
case run([@query_prefix <> text]) do
{:ok, [vector]} -> {:ok, vector}
{:ok, _other} -> {:error, :unexpected_embedding_result}
{:error, _reason} = error -> error
end
end
@impl BDS.Embeddings.Backend
def embed_many([], _opts), do: {:ok, []}
def embed_many(texts, _opts) when is_list(texts) do
run(Enum.map(texts, &(@query_prefix <> &1)))
end
defp run(prefixed_texts) do
GenServer.call(__MODULE__, {:embed, prefixed_texts}, @embed_timeout)
catch
:exit, reason -> {:error, {:embedding_backend_unavailable, reason}}
end
@impl GenServer
def init(_opts), do: {:ok, %{serving: nil}}
@impl GenServer
def handle_call({:embed, texts}, _from, state) do
case ensure_serving(state) do
{:ok, %{serving: serving} = next_state} ->
vectors =
texts
|> Enum.chunk_every(batch_size())
|> Enum.flat_map(&run_chunk(serving, &1))
{:reply, {:ok, vectors}, next_state}
{:error, _reason} = error ->
{:reply, error, state}
end
rescue
exception ->
{:reply, {:error, Exception.message(exception)}, state}
end
defp run_chunk(serving, [single]) do
%{embedding: tensor} = Nx.Serving.run(serving, single)
[Nx.to_flat_list(tensor)]
end
defp run_chunk(serving, chunk) do
serving
|> Nx.Serving.run(chunk)
|> Enum.map(fn %{embedding: tensor} -> Nx.to_flat_list(tensor) end)
end
defp ensure_serving(%{serving: nil} = state) do
case build_serving() do
{:ok, serving} -> {:ok, %{state | serving: serving}}
{:error, _reason} = error -> error
end
end
defp ensure_serving(state), do: {:ok, state}
defp build_serving do
repo = {:hf, Keyword.get(config(), :model_repo, @default_model_repo)}
with {:ok, model_info} <- Bumblebee.load_model(repo),
{:ok, tokenizer} <- Bumblebee.load_tokenizer(repo) do
serving =
Bumblebee.Text.text_embedding(model_info, tokenizer,
output_pool: :mean_pooling,
output_attribute: :hidden_state,
embedding_processor: :l2_norm,
compile: [batch_size: batch_size(), sequence_length: sequence_length()],
defn_options: [compiler: EXLA]
)
{:ok, serving}
end
end
defp batch_size do
config() |> Keyword.get(:batch_size, @default_batch_size) |> max(1)
end
defp sequence_length do
config() |> Keyword.get(:sequence_length, @default_sequence_length) |> max(1)
end
defp config, do: Application.get_env(:bds, :embeddings, [])
end

View File

@@ -1,214 +1,342 @@
defmodule BDS.Embeddings.Index do
@moduledoc false
@moduledoc """
Per-project approximate-nearest-neighbour index over post embeddings.
import Ecto.Query
Backed by an HNSW graph (hnswlib) per the A1-14b / `specs/embedding.allium`
requirement — cosine space, connectivity M=16, efConstruction=128,
efSearch=64. This replaces the previous O(n²) brute-force cosine snapshot:
building is O(n·log n) and queries are O(log n).
The process is intentionally **database-free**: callers (running in their own
process, e.g. under the test SQL sandbox) read embedding vectors from the DB
and hand them in. This GenServer owns only the in-memory HNSW graphs, the
`label → post_id` maps, and file persistence.
Persistence (DebouncedPersistence invariant): the index file
(`embeddings.usearch`) plus a small sidecar holding the dimension and the
label→post_id map are written behind a 5s debounce, and force-saved on
project switch / shutdown. On a cold query the index is lazily reloaded from
those files; if they are absent the caller rebuilds from the DB vectors.
"""
use GenServer
alias BDS.Persistence
alias BDS.Embeddings.Key
alias BDS.Projects
alias BDS.ProgressReporter
alias BDS.Repo
@neighbor_limit 21
@debounce_ms 5_000
@space :cosine
@m 16
@ef_construction 128
@ef_search 64
# ─── Public API ─────────────────────────────────────────────
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "On-disk path of the HNSW index file for a project."
def path(project_id) when is_binary(project_id) do
Path.join(Projects.project_cache_dir(project_id), "embeddings.usearch")
end
def rebuild(project_id, opts) when is_binary(project_id) and is_list(opts) do
model_id = Keyword.fetch!(opts, :model_id)
dimensions = Keyword.fetch!(opts, :dimensions)
keys =
Repo.all(
from key in Key,
where: key.project_id == ^project_id,
order_by: [asc: key.post_id]
)
entries =
keys
|> Enum.map(fn key ->
vector = decode_vector(key.vector)
{key.post_id,
%{
"label" => key.label,
"content_hash" => key.content_hash,
"neighbors" => neighbor_entries(keys, key, vector)
}}
end)
|> Map.new()
payload = %{
"project_id" => project_id,
"model_id" => model_id,
"dimensions" => dimensions,
"updated_at" => Persistence.now_ms(),
"entries" => entries
}
write_snapshot(path(project_id), payload, project_id)
@doc """
(Re)builds the index for a project from the given entries and schedules a
debounced save. `entries` is a list of `%{label:, post_id:, vector:}` where
`vector` is the packed little-endian Float32 BLOB.
"""
def put(project_id, dimensions, entries)
when is_binary(project_id) and is_integer(dimensions) and is_list(entries) do
GenServer.call(__MODULE__, {:put, project_id, dimensions, entries}, :infinity)
end
def read(project_id) when is_binary(project_id) do
project_id
|> candidate_paths()
|> read_snapshot_paths()
@doc """
Returns up to `limit` nearest neighbours of `query_vector` (the post's packed
BLOB), excluding `query_label`. `{:error, :missing}` if no index is available.
"""
def neighbors(project_id, query_label, query_vector, limit)
when is_binary(project_id) and is_integer(query_label) and is_binary(query_vector) do
GenServer.call(__MODULE__, {:neighbors, project_id, query_label, query_vector, limit}, :infinity)
end
def neighbors(project_id, post_id, limit) when is_binary(project_id) and is_binary(post_id) do
with {:ok, snapshot} <- read(project_id),
%{} = entry <- get_in(snapshot, ["entries", post_id]) do
entry
|> Map.get("neighbors", [])
|> Enum.take(max(limit, 0))
|> Enum.map(fn neighbor ->
%{
post_id: neighbor["post_id"],
score: neighbor["score"]
}
end)
|> then(&{:ok, &1})
else
_ -> {:error, :missing}
@doc """
Finds near-duplicate pairs at/above `threshold` by querying the HNSW graph for
each entry's neighbours. `{:error, :missing}` if no index is available.
"""
def duplicate_pairs(project_id, entries, threshold, opts \\ [])
when is_binary(project_id) and is_list(entries) and is_number(threshold) do
GenServer.call(
__MODULE__,
{:duplicate_pairs, project_id, entries, threshold, opts},
:infinity
)
end
@doc "Forces a pending save for a project to disk now (e.g. on project switch)."
def flush(project_id) when is_binary(project_id) do
GenServer.call(__MODULE__, {:flush, project_id}, :infinity)
end
@doc "Forces all pending saves to disk now (e.g. on shutdown)."
def flush_all do
GenServer.call(__MODULE__, :flush_all, :infinity)
end
@doc "Drops the in-memory index for a project (e.g. on project deletion)."
def forget(project_id) when is_binary(project_id) do
GenServer.call(__MODULE__, {:forget, project_id}, :infinity)
end
# ─── GenServer ──────────────────────────────────────────────
@impl true
def init(_opts) do
Process.flag(:trap_exit, true)
{:ok, %{}}
end
@impl true
def handle_call({:put, project_id, dimensions, entries}, _from, state) do
entry = build_entry(dimensions, entries)
state = state |> Map.put(project_id, entry) |> schedule_save(project_id)
{:reply, :ok, state}
end
def handle_call({:neighbors, project_id, query_label, query_vector, limit}, _from, state) do
case ensure_loaded(state, project_id) do
{:ok, %{index: nil}, state} ->
{:reply, {:error, :missing}, state}
{:ok, entry, state} ->
{:reply, {:ok, query_neighbors(entry, query_label, query_vector, limit)}, state}
{:missing, state} ->
{:reply, {:error, :missing}, state}
end
end
def duplicate_pairs(project_id, threshold, opts \\ []) when is_binary(project_id) do
with {:ok, snapshot} <- read(project_id) do
entries = Map.get(snapshot, "entries", %{})
entry_count = map_size(entries)
on_progress = progress_callback(opts)
def handle_call({:duplicate_pairs, project_id, entries, threshold, opts}, _from, state) do
case ensure_loaded(state, project_id) do
{:ok, %{index: nil}, state} ->
{:reply, {:error, :missing}, state}
:ok = report_scan_started(on_progress, entry_count, "embedding entries")
{:ok, entry, state} ->
{:reply, {:ok, scan_duplicates(entry, entries, threshold, opts)}, state}
pairs =
entries
|> Enum.with_index(1)
|> Enum.flat_map(fn {{post_id, entry}, index} ->
:ok = report_scan_progress(on_progress, index, entry_count, "embedding entries")
entry
|> Map.get("neighbors", [])
|> Enum.filter(&(&1["score"] >= threshold))
|> Enum.map(fn neighbor ->
{post_id_a, post_id_b} = sort_pair(post_id, neighbor["post_id"])
{{post_id_a, post_id_b},
%{
post_id_a: post_id_a,
post_id_b: post_id_b,
score: neighbor["score"]
}}
end)
end)
|> Map.new()
|> Map.values()
|> Enum.sort_by(& &1.score, :desc)
{:ok, pairs}
else
_ -> {:error, :missing}
{:missing, state} ->
{:reply, {:error, :missing}, state}
end
end
defp neighbor_entries(keys, current_key, current_vector) do
keys
|> Enum.reject(&(&1.post_id == current_key.post_id))
|> Enum.map(fn other_key ->
%{
"post_id" => other_key.post_id,
"label" => other_key.label,
"score" => cosine_similarity(current_vector, decode_vector(other_key.vector))
}
end)
|> Enum.sort_by(& &1["score"], :desc)
|> Enum.take(@neighbor_limit)
def handle_call({:flush, project_id}, _from, state) do
{:reply, :ok, save_now(state, project_id)}
end
defp write_snapshot(snapshot_path, payload, project_id) do
:ok = Persistence.atomic_write(snapshot_path, Jason.encode!(payload))
legacy_path = legacy_path(snapshot_path)
def handle_call(:flush_all, _from, state) do
state = Enum.reduce(Map.keys(state), state, &save_now(&2, &1))
{:reply, :ok, state}
end
if File.exists?(legacy_path) do
File.rm(legacy_path)
def handle_call({:forget, project_id}, _from, state) do
case Map.get(state, project_id) do
%{timer: timer} when is_reference(timer) -> Process.cancel_timer(timer)
_other -> :ok
end
cleanup_legacy_project_snapshots(project_id, snapshot_path)
{:reply, :ok, Map.delete(state, project_id)}
end
@impl true
def handle_info({:save, project_id}, state) do
{:noreply, save_now(state, project_id)}
end
def handle_info(_message, state), do: {:noreply, state}
@impl true
def terminate(_reason, state) do
Enum.each(Map.keys(state), &save_now(state, &1))
:ok
end
defp candidate_paths(project_id) do
current_snapshot_path = path(project_id)
legacy_project_snapshot_path = legacy_project_snapshot_path(project_id)
# ─── Build / query ──────────────────────────────────────────
[
current_snapshot_path,
legacy_path(current_snapshot_path),
legacy_project_snapshot_path,
legacy_project_snapshot_path && legacy_path(legacy_project_snapshot_path)
]
|> Enum.filter(&is_binary/1)
|> Enum.uniq()
defp build_entry(dimensions, []), do: %{index: nil, labels: %{}, dim: dimensions, timer: nil}
defp build_entry(dimensions, entries) do
count = length(entries)
{:ok, index} = HNSWLib.Index.new(@space, dimensions, count, m: @m, ef_construction: @ef_construction)
:ok = HNSWLib.Index.set_ef(index, @ef_search)
tensor =
entries
|> Enum.map(& &1.vector)
|> IO.iodata_to_binary()
|> Nx.from_binary(:f32)
|> Nx.reshape({count, dimensions})
:ok = HNSWLib.Index.add_items(index, tensor, ids: Enum.map(entries, & &1.label))
%{
index: index,
labels: Map.new(entries, &{&1.label, &1.post_id}),
dim: dimensions,
timer: nil
}
end
defp read_snapshot_paths([]), do: {:error, :missing}
defp query_neighbors(%{index: index, labels: labels}, query_label, query_vector, limit) do
case query(index, query_vector, limit + 1) do
[] ->
[]
defp read_snapshot_paths([snapshot_path | rest]) do
case File.read(snapshot_path) do
{:ok, contents} -> {:ok, Jason.decode!(contents)}
{:error, :enoent} -> read_snapshot_paths(rest)
{:error, reason} -> {:error, reason}
results ->
results
|> Enum.reject(fn {label, _score} -> label == query_label end)
|> Enum.map(fn {label, score} -> %{post_id: Map.get(labels, label), score: score} end)
|> Enum.reject(&is_nil(&1.post_id))
|> Enum.take(max(limit, 0))
end
end
defp cleanup_legacy_project_snapshots(project_id, snapshot_path) do
current_paths = [snapshot_path, legacy_path(snapshot_path)]
defp scan_duplicates(%{index: index, labels: labels}, entries, threshold, opts) do
on_progress = ProgressReporter.callback(opts)
total = length(entries)
:ok = report_scan_started(on_progress, total, "embedding entries")
project_id
|> legacy_project_snapshot_path()
|> then(fn legacy_snapshot_path ->
[legacy_snapshot_path, legacy_snapshot_path && legacy_path(legacy_snapshot_path)]
end)
|> Enum.filter(&is_binary/1)
|> Enum.reject(&(&1 in current_paths))
|> Enum.each(fn legacy_snapshot_path ->
if File.exists?(legacy_snapshot_path) do
File.rm(legacy_snapshot_path)
end
entries
|> Enum.with_index(1)
|> Enum.flat_map(fn {entry, position} ->
:ok = report_scan_progress(on_progress, position, total, "embedding entries")
index
|> query(entry.vector, @neighbor_limit)
|> Enum.reject(fn {label, _score} -> label == entry.label end)
|> Enum.map(fn {label, score} -> {Map.get(labels, label), score} end)
|> Enum.filter(fn {post_id, score} -> not is_nil(post_id) and score >= threshold end)
|> Enum.map(fn {other_post_id, score} ->
{post_id_a, post_id_b} = sort_pair(entry.post_id, other_post_id)
{{post_id_a, post_id_b}, %{post_id_a: post_id_a, post_id_b: post_id_b, score: score}}
end)
end)
|> Map.new()
|> Map.values()
|> Enum.sort_by(& &1.score, :desc)
end
defp legacy_project_snapshot_path(project_id) do
case Projects.get_project(project_id) do
nil -> nil
project -> Path.join(Projects.project_data_dir(project), "embeddings.usearch")
# Runs a knn query and returns [{label, similarity}] sorted by descending
# similarity. Cosine distance is converted to similarity as max(0, 1 - d).
defp query(index, query_vector, k) do
case HNSWLib.Index.get_current_count(index) do
{:ok, count} when count > 0 ->
clamped = min(k, count)
case HNSWLib.Index.knn_query(index, query_vector, k: clamped) do
{:ok, labels, distances} ->
Enum.zip(
Nx.to_flat_list(labels),
Enum.map(Nx.to_flat_list(distances), fn distance -> max(0.0, 1.0 - distance) end)
)
{:error, _reason} ->
[]
end
_other ->
[]
end
end
defp legacy_path(snapshot_path) do
Path.join(Path.dirname(snapshot_path), "embeddings.index.json")
# ─── Persistence ────────────────────────────────────────────
defp schedule_save(state, project_id) do
entry = Map.fetch!(state, project_id)
if is_reference(entry.timer), do: Process.cancel_timer(entry.timer)
timer = Process.send_after(self(), {:save, project_id}, @debounce_ms)
Map.put(state, project_id, %{entry | timer: timer})
end
defp decode_vector(nil), do: []
defp decode_vector(vector), do: Jason.decode!(vector)
defp save_now(state, project_id) do
case Map.get(state, project_id) do
nil ->
state
defp cosine_similarity([], _other), do: 0.0
defp cosine_similarity(_vector, []), do: 0.0
defp cosine_similarity(left, right) do
Enum.zip(left, right)
|> Enum.reduce(0.0, fn {left_value, right_value}, acc -> acc + left_value * right_value end)
|> max(0.0)
entry ->
if is_reference(entry.timer), do: Process.cancel_timer(entry.timer)
persist(project_id, entry)
Map.put(state, project_id, %{entry | timer: nil})
end
end
defp persist(_project_id, %{index: nil}), do: :ok
defp persist(project_id, %{index: index, labels: labels, dim: dim}) do
index_path = path(project_id)
File.mkdir_p!(Path.dirname(index_path))
HNSWLib.Index.save_index(index, index_path)
write_meta(index_path, dim, labels)
:ok
rescue
_exception -> :ok
end
defp write_meta(index_path, dim, labels) do
payload = %{
"dim" => dim,
"labels" => Enum.map(labels, fn {label, post_id} -> [label, post_id] end)
}
File.write(meta_path(index_path), Jason.encode!(payload))
end
defp ensure_loaded(state, project_id) do
case Map.get(state, project_id) do
nil ->
case load_from_disk(project_id) do
{:ok, entry} -> {:ok, entry, Map.put(state, project_id, entry)}
:error -> {:missing, state}
end
entry ->
{:ok, entry, state}
end
end
defp load_from_disk(project_id) do
index_path = path(project_id)
with {:ok, %{dim: dim, labels: labels}} <- read_meta(index_path),
true <- File.exists?(index_path),
{:ok, index} <- HNSWLib.Index.load_index(@space, dim, index_path) do
:ok = HNSWLib.Index.set_ef(index, @ef_search)
{:ok, %{index: index, labels: labels, dim: dim, timer: nil}}
else
_other -> :error
end
rescue
_exception -> :error
end
defp read_meta(index_path) do
with {:ok, contents} <- File.read(meta_path(index_path)),
{:ok, %{"dim" => dim, "labels" => labels}} <- Jason.decode(contents) do
{:ok,
%{
dim: dim,
labels: Map.new(labels, fn [label, post_id] -> {label, post_id} end)
}}
else
_other -> :error
end
end
defp meta_path(index_path), do: index_path <> ".meta.json"
defp sort_pair(post_id_a, post_id_b) when post_id_a <= post_id_b, do: {post_id_a, post_id_b}
defp sort_pair(post_id_a, post_id_b), do: {post_id_b, post_id_a}
defp progress_callback(opts), do: ProgressReporter.callback(opts)
defp report_scan_started(callback, total, label) do
ProgressReporter.report_count_started(callback, total, label,
verb: "Scanning",

View File

@@ -12,7 +12,9 @@ defmodule BDS.Embeddings.Key do
belongs_to :project, BDS.Projects.Project, type: :string
field :content_hash, :string
field :vector, :string
# Packed little-endian Float32 BLOB (dimensions * 4 bytes), per the
# VectorCacheInDb invariant in specs/embedding.allium.
field :vector, :binary
end
def changeset(key, attrs) do

View File

@@ -7,10 +7,25 @@ defmodule BDS.Generation.Pagefind do
@typedoc "A (relative_path, content) generated file tuple."
@type generated_file :: {String.t(), String.t()}
@assets_dir Application.app_dir(:bds, "priv/preview_assets/assets")
@ui_js_path Path.join(@assets_dir, "pagefind-ui.js")
@ui_css_path Path.join(@assets_dir, "pagefind-ui.css")
@external_resource @ui_js_path
@external_resource @ui_css_path
@ui_js File.read!(@ui_js_path)
@ui_css File.read!(@ui_css_path)
@doc """
Build the per-language Pagefind index outputs (`pagefind/index.json`,
`pagefind/pagefind-ui.js`, `pagefind/pagefind-ui.css`) for every blog
language declared on the plan.
The fragment index records one entry per indexable page, where indexable
means the page carries a `data-pagefind-body` region. Each entry stores the
page URL, its title, and the body text scoped to that region — mirroring
Pagefind's behaviour of ignoring content outside `data-pagefind-body`.
"""
@spec build_outputs(map(), [html_output()]) :: [generated_file()]
def build_outputs(plan, html_outputs) do
@@ -31,8 +46,8 @@ defmodule BDS.Generation.Pagefind do
[
{Path.join(prefix ++ ["index.json"]),
Jason.encode!(%{"language" => language, "pages" => pages})},
{Path.join(prefix ++ ["pagefind-ui.js"]), ui_js(language)},
{Path.join(prefix ++ ["pagefind-ui.css"]), ui_css()}
{Path.join(prefix ++ ["pagefind-ui.js"]), @ui_js},
{Path.join(prefix ++ ["pagefind-ui.css"]), @ui_css}
]
end)
end
@@ -43,11 +58,14 @@ defmodule BDS.Generation.Pagefind do
String.ends_with?(relative_path, ".html") and
language_match?(relative_path, route_language, other_prefixes)
end)
|> Enum.map(fn {relative_path, content} ->
%{
"url" => "/" <> relative_path,
"text" => text(content)
}
|> Enum.flat_map(fn {relative_path, content} ->
case body_text(content) do
nil ->
[]
text ->
[%{"url" => "/" <> relative_path, "title" => title(content), "text" => text}]
end
end)
end
@@ -60,19 +78,94 @@ defmodule BDS.Generation.Pagefind do
defp language_match?(relative_path, route_language, _other_prefixes),
do: String.starts_with?(relative_path, route_language <> "/")
defp text(content) do
content
# Extract the indexable body text scoped to the data-pagefind-body element.
# Returns nil when the page is not marked, so unmarked pages are excluded
# from the index entirely (matching Pagefind semantics).
defp body_text(content) do
case Regex.run(~r/<([a-zA-Z0-9]+)[^>]*\bdata-pagefind-body\b[^>]*>/, content,
return: :index
) do
[{open_start, open_len}, {tag_start, tag_len}] ->
tag = binary_part(content, tag_start, tag_len)
region = scoped_region(content, tag, open_start + open_len)
plain_text(region)
_no_match ->
nil
end
end
# Capture the inner HTML of the marked element by balancing same-tag
# open/close pairs from the opening tag onward.
defp scoped_region(content, tag, body_start) do
rest = binary_part(content, body_start, byte_size(content) - body_start)
open_re = Regex.compile!("<#{tag}\\b", "i")
close_re = Regex.compile!("</#{tag}\\s*>", "i")
events =
(Regex.scan(open_re, rest, return: :index) ++ Regex.scan(close_re, rest, return: :index))
|> Enum.map(fn [{pos, _len}] -> pos end)
|> Enum.map(fn pos -> {pos, event_kind(rest, pos, tag)} end)
|> Enum.sort_by(&elem(&1, 0))
close_at = balanced_close(events, 0)
case close_at do
nil -> rest
pos -> binary_part(rest, 0, pos)
end
end
defp event_kind(rest, pos, tag) do
if String.starts_with?(binary_part(rest, pos, min(2 + byte_size(tag), byte_size(rest) - pos)), "</") do
:close
else
:open
end
end
defp balanced_close([], _depth), do: nil
defp balanced_close([{pos, :close} | _rest], 0), do: pos
defp balanced_close([{_pos, :close} | rest], depth),
do: balanced_close(rest, depth - 1)
defp balanced_close([{_pos, :open} | rest], depth),
do: balanced_close(rest, depth + 1)
defp title(content) do
tag_text(content, ~r/<title[^>]*>(.*?)<\/title>/si) ||
tag_text(content, ~r/<h1[^>]*>(.*?)<\/h1>/si) ||
""
end
defp tag_text(content, regex) do
case Regex.run(regex, content) do
[_full, raw] -> raw |> plain_text() |> nil_if_blank()
_no_match -> nil
end
end
defp nil_if_blank(""), do: nil
defp nil_if_blank(value), do: value
defp plain_text(html) do
html
|> String.replace(~r/<[^>]+>/, " ")
|> decode_entities()
|> String.replace(~r/\s+/u, " ")
|> String.trim()
end
defp ui_js(language) do
"window.bDSPagefind = { language: #{Jason.encode!(language)} };\n"
end
defp ui_css do
".pagefind-ui{display:block;}\n"
defp decode_entities(text) do
text
|> String.replace("&amp;", "&")
|> String.replace("&lt;", "<")
|> String.replace("&gt;", ">")
|> String.replace("&quot;", "\"")
|> String.replace("&#39;", "'")
|> String.replace("&nbsp;", " ")
end
defp route_language(main_language, language) when main_language == language, do: nil

View File

@@ -114,10 +114,19 @@ defmodule BDS.Git do
def history(project_id, branch, opts \\ [])
when is_binary(project_id) and is_binary(branch) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id),
{:ok, local_log} <- run_git(project_dir, ["log", "--format=%H%x09%s", branch], opts),
{:ok, remote_log} <-
run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do
local_commits = parse_local_history(local_log)
{:ok, local_log} <-
run_git(
project_dir,
["log", "--date=short", "--format=%H%x09%an%x09%ad%x09%s", branch],
opts
) do
remote_log =
case run_git(project_dir, ["log", "--format=%H", "origin/#{branch}"], opts) do
{:ok, output} -> output
{:error, {:git_failed, _message}} -> ""
end
local_commits = parse_history_log(local_log)
remote_hashes = MapSet.new(parse_remote_history(remote_log))
local_hashes = MapSet.new(Enum.map(local_commits, & &1.hash))
@@ -126,7 +135,7 @@ defmodule BDS.Git do
|> MapSet.difference(local_hashes)
|> MapSet.to_list()
|> Enum.map(fn hash ->
%{hash: hash, subject: nil, sync_status: %{kind: :remote_only}}
%{hash: hash, subject: nil, author: nil, date: nil, sync_status: %{kind: :remote_only}}
end)
commits =
@@ -204,6 +213,22 @@ defmodule BDS.Git do
end
end
def set_remote(project_id, remote_url, opts \\ [])
when is_binary(project_id) and is_binary(remote_url) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id) do
case run_git(project_dir, ["remote", "add", "origin", remote_url], opts) do
{:ok, _output} ->
{:ok, %{remote_url: remote_url}}
{:error, {:git_failed, _message}} ->
with {:ok, _output} <-
run_git(project_dir, ["remote", "set-url", "origin", remote_url], opts) do
{:ok, %{remote_url: remote_url}}
end
end
end
end
def remote_state(project_id, opts \\ []) when is_binary(project_id) and is_list(opts) do
with {:ok, project_dir} <- project_dir(project_id),
{:ok, local_branch} <- current_branch(project_dir, opts) do
@@ -380,6 +405,23 @@ defmodule BDS.Git do
end)
end
defp parse_history_log(output) do
output
|> String.split("\n", trim: true)
|> Enum.map(fn line ->
case String.split(line, "\t", parts: 4) do
[hash, author, date, subject] ->
%{hash: hash, author: author, date: date, subject: subject}
[hash, author, date] ->
%{hash: hash, author: author, date: date, subject: nil}
[hash | _rest] ->
%{hash: hash, author: nil, date: nil, subject: nil}
end
end)
end
defp parse_remote_history(output) do
String.split(output, "\n", trim: true)
end

View File

@@ -101,11 +101,18 @@ defmodule BDS.Maintenance.Repair do
:file_to_db ->
post_ids = Enum.map(items, &metadata_diff_item_entity_id/1)
{:ok, repaired_post_ids} = Embeddings.repair_posts(project_id, post_ids)
repaired_post_ids = MapSet.new(repaired_post_ids)
# If the embedding model is unavailable, every item is reported as
# failed rather than crashing the repair task.
repaired =
case Embeddings.repair_posts(project_id, post_ids) do
{:ok, repaired_post_ids} -> repaired_post_ids
{:error, _reason} -> []
end
repaired_set = MapSet.new(repaired)
build_batch_repair_result(items, total, on_progress, fn item ->
MapSet.member?(repaired_post_ids, metadata_diff_item_entity_id(item))
MapSet.member?(repaired_set, metadata_diff_item_entity_id(item))
end)
:db_to_file ->

View File

@@ -1,6 +1,8 @@
defmodule BDS.Metadata do
@moduledoc false
require Logger
alias BDS.Embeddings
alias BDS.I18n
alias BDS.Persistence
@@ -653,7 +655,17 @@ defmodule BDS.Metadata do
) do
if previous_state.semantic_similarity_enabled != true and
project_metadata.semantic_similarity_enabled == true do
{:ok, _indexed_post_ids} = Embeddings.index_unindexed(project_id)
# Backfill is best-effort: if the embedding model is unavailable, keep the
# setting enabled and log it rather than failing the metadata update.
case Embeddings.index_unindexed(project_id) do
{:ok, _indexed_post_ids} ->
:ok
{:error, reason} ->
Logger.warning(
"Embedding backfill skipped for project #{project_id}: #{inspect(reason)}"
)
end
end
result

View File

@@ -14,6 +14,10 @@ defmodule BDS.Preview do
@host "127.0.0.1"
@port 4123
# Max time to wait for inflight requests to finish during graceful shutdown
# before remaining request tasks are forcibly terminated.
@drain_timeout 5_000
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@@ -56,7 +60,7 @@ defmodule BDS.Preview do
@impl true
def init(_state) do
{:ok, %{current: nil}}
{:ok, %{current: nil, stopping: nil}}
end
@impl true
@@ -78,15 +82,12 @@ defmodule BDS.Preview do
{:reply, reply, next_state}
end
def handle_call({:stop_preview, project_id}, _from, state) do
next_state =
if match?(%{project_id: ^project_id}, state.current) do
stop_current_server(state)
else
state
end
{:reply, :ok, next_state}
def handle_call({:stop_preview, project_id}, from, state) do
if match?(%{project_id: ^project_id}, state.current) do
begin_graceful_stop(state, from)
else
{:reply, :ok, state}
end
end
def handle_call({:request, project_id, request_path, query_params}, _from, state) do
@@ -141,6 +142,25 @@ defmodule BDS.Preview do
end
@impl true
def handle_cast({:track_request, pid}, %{current: %{} = current} = state) when is_pid(pid) do
ref = Process.monitor(pid)
inflight = Map.put(current.inflight, ref, pid)
{:noreply, %{state | current: %{current | inflight: inflight}}}
end
def handle_cast({:track_request, _pid}, state), do: {:noreply, state}
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{current: %{} = current} = state) do
inflight = Map.delete(current.inflight, ref)
state = %{state | current: %{current | inflight: inflight}}
{:noreply, maybe_finalize_stop(state)}
end
def handle_info(:drain_timeout, state) do
{:noreply, force_finalize_stop(state)}
end
def handle_info(_msg, state) do
{:noreply, state}
end
@@ -287,9 +307,18 @@ defmodule BDS.Preview do
defp accept_loop(listener, project_id) do
case :gen_tcp.accept(listener) do
{:ok, socket} ->
Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
serve_client(socket, project_id)
end)
case Task.Supervisor.start_child(BDS.TCP.TaskSupervisor, fn ->
serve_client(socket, project_id)
end) do
{:ok, pid} ->
# Hand the socket to the request task so an inflight request survives
# the acceptor being shut down (it would otherwise close the socket).
_ = :gen_tcp.controlling_process(socket, pid)
GenServer.cast(__MODULE__, {:track_request, pid})
_other ->
:ok
end
accept_loop(listener, project_id)
@@ -412,14 +441,58 @@ defmodule BDS.Preview do
end
end
defp stop_current_server(%{current: %{listener: listener, acceptor_pid: acceptor_pid}} = state) do
_ = :gen_tcp.close(listener)
if is_pid(acceptor_pid), do: Process.exit(acceptor_pid, :normal)
# Graceful shutdown: stop accepting new connections, then wait for inflight
# request tasks to finish before reporting the server stopped. The stop call
# is parked (no immediate reply) and finalized from the :DOWN handlers, so the
# GenServer stays available to serve the requests it is draining.
defp begin_graceful_stop(%{current: current} = state, from) do
_ = :gen_tcp.close(current.listener)
if is_pid(current.acceptor_pid), do: Process.exit(current.acceptor_pid, :normal)
if map_size(current.inflight) == 0 do
{:reply, :ok, %{state | current: nil, stopping: nil}}
else
timer = Process.send_after(self(), :drain_timeout, @drain_timeout)
{:noreply, %{state | stopping: %{from: from, timer: timer}}}
end
end
defp maybe_finalize_stop(
%{stopping: %{from: from, timer: timer}, current: %{inflight: inflight}} = state
)
when map_size(inflight) == 0 do
if is_reference(timer), do: Process.cancel_timer(timer)
GenServer.reply(from, :ok)
%{state | current: nil, stopping: nil}
end
defp maybe_finalize_stop(state), do: state
defp force_finalize_stop(%{stopping: %{from: from}, current: %{inflight: inflight}} = state) do
kill_inflight(inflight)
GenServer.reply(from, :ok)
%{state | current: nil, stopping: nil}
end
defp force_finalize_stop(state), do: state
# Hard stop used when restarting the server in place (no graceful drain).
defp stop_current_server(%{current: %{} = current} = state) do
_ = :gen_tcp.close(current.listener)
if is_pid(current.acceptor_pid), do: Process.exit(current.acceptor_pid, :normal)
kill_inflight(current.inflight)
%{state | current: nil}
end
defp stop_current_server(state), do: state
defp kill_inflight(inflight) do
Enum.each(inflight, fn {ref, pid} ->
Process.demonitor(ref, [:flush])
if is_pid(pid), do: Process.exit(pid, :kill)
end)
end
defp start_server(state, project_id, data_dir, owner_pid) do
state = stop_current_server(state)
maybe_allow_repo(owner_pid)
@@ -442,7 +515,8 @@ defmodule BDS.Preview do
port: @port,
is_running: true,
listener: listener,
acceptor_pid: acceptor_pid
acceptor_pid: acceptor_pid,
inflight: %{}
}
{{:ok, public_server(server)}, %{state | current: server}}

View File

@@ -148,6 +148,9 @@ defmodule BDS.Projects do
project ->
now = Persistence.now_ms()
previous_active_id =
Repo.one(from p in Project, where: p.is_active == true, select: p.id)
Repo.transaction(fn ->
Repo.update_all(
from(p in Project, where: p.is_active == true),
@@ -159,8 +162,16 @@ defmodule BDS.Projects do
|> Repo.update!()
end)
|> case do
{:ok, active_project} -> {:ok, active_project}
{:error, reason} -> {:error, reason}
{:ok, active_project} ->
# Force-save the outgoing project's embedding index (DebouncedPersistence).
if is_binary(previous_active_id) and previous_active_id != active_project.id do
BDS.Embeddings.Index.flush(previous_active_id)
end
{:ok, active_project}
{:error, reason} ->
{:error, reason}
end
end
end
@@ -194,6 +205,8 @@ defmodule BDS.Projects do
end)
|> case do
{:ok, deleted_project} ->
BDS.Embeddings.Index.forget(deleted_project.id)
Enum.each(cleanup_dirs, fn dir ->
_ = File.rm_rf(dir)
end)

View File

@@ -27,6 +27,7 @@ defmodule BDS.Rendering.Labels do
language_switcher_label: dgettext("render", "Language"),
site_search_label: dgettext("render", "Site search"),
search_placeholder: dgettext("render", "Search..."),
search_no_results: dgettext("render", "No results found"),
not_found_message: dgettext("render", "The requested preview page could not be found."),
not_found_back_label: dgettext("render", "Back to preview home"),
youtube_video: dgettext("render", "YouTube video"),

View File

@@ -57,28 +57,35 @@ defmodule BDS.Scripts do
{:error, :not_found}
script ->
file_path = script_file_path(script.slug)
full_path = full_file_path(script.project_id, file_path)
updated_at = Persistence.now_ms()
content = script.content || ""
:ok =
Persistence.atomic_write(
full_path,
serialize_script_file(
%{script | status: :published, file_path: file_path, updated_at: updated_at},
content
)
)
case validate_script(content) do
:ok ->
file_path = script_file_path(script.slug)
full_path = full_file_path(script.project_id, file_path)
updated_at = Persistence.now_ms()
script
|> Script.changeset(%{
status: :published,
file_path: file_path,
content: nil,
updated_at: updated_at
})
|> Repo.update()
:ok =
Persistence.atomic_write(
full_path,
serialize_script_file(
%{script | status: :published, file_path: file_path, updated_at: updated_at},
content
)
)
script
|> Script.changeset(%{
status: :published,
file_path: file_path,
content: nil,
updated_at: updated_at
})
|> Repo.update()
{:error, reason} ->
{:error, {:invalid_script, reason}}
end
end
end
@@ -254,6 +261,13 @@ defmodule BDS.Scripts do
not Repo.exists?(scoped_query)
end
defp validate_script(source) do
case BDS.Scripting.validate(source) do
:ok -> :ok
{:error, reason} -> {:error, reason}
end
end
defp script_file_path(slug), do: Path.join(["scripts", "#{slug}.lua"])
defp full_file_path(project_id, relative_path) do

View File

@@ -28,23 +28,38 @@ defmodule BDS.Templates do
now = Persistence.now_ms()
project_id = attr(attrs, :project_id)
title = attr(attrs, :title) || ""
slug = unique_slug(project_id, Slug.slugify(title), "template")
file_path = template_file_path(slug)
%Template{}
|> Template.changeset(%{
id: Ecto.UUID.generate(),
project_id: project_id,
slug: unique_slug(project_id, Slug.slugify(title), "template"),
title: title,
kind: attr(attrs, :kind),
enabled: true,
version: 1,
file_path: "",
status: :draft,
content: attr(attrs, :content),
created_at: now,
updated_at: now
})
|> Repo.insert()
changeset =
%Template{}
|> Template.changeset(%{
id: Ecto.UUID.generate(),
project_id: project_id,
slug: slug,
title: title,
kind: attr(attrs, :kind),
enabled: true,
version: 1,
file_path: file_path,
status: :draft,
content: attr(attrs, :content),
created_at: now,
updated_at: now
})
with {:ok, template} <- Repo.insert(changeset) do
full_path = full_file_path(template.project_id, file_path)
File.mkdir_p!(Path.dirname(full_path))
:ok =
Persistence.atomic_write(
full_path,
serialize_template_file(template, template.content || "")
)
{:ok, template}
end
end
@spec get_template(String.t()) :: Template.t() | nil
@@ -62,28 +77,35 @@ defmodule BDS.Templates do
{:error, :not_found}
template ->
file_path = template_file_path(template.slug)
full_path = full_file_path(template.project_id, file_path)
updated_at = Persistence.now_ms()
content = template.content || ""
:ok =
Persistence.atomic_write(
full_path,
serialize_template_file(
%{template | status: :published, file_path: file_path, updated_at: updated_at},
content
)
)
case validate_liquid(content) do
:ok ->
file_path = template_file_path(template.slug)
full_path = full_file_path(template.project_id, file_path)
updated_at = Persistence.now_ms()
template
|> Template.changeset(%{
status: :published,
file_path: file_path,
content: nil,
updated_at: updated_at
})
|> Repo.update()
:ok =
Persistence.atomic_write(
full_path,
serialize_template_file(
%{template | status: :published, file_path: file_path, updated_at: updated_at},
content
)
)
template
|> Template.changeset(%{
status: :published,
file_path: file_path,
content: nil,
updated_at: updated_at
})
|> Repo.update()
{:error, reason} ->
{:error, {:invalid_liquid, reason}}
end
end
end
@@ -327,6 +349,13 @@ defmodule BDS.Templates do
not Repo.exists?(scoped_query)
end
defp validate_liquid(source) do
case Liquex.parse(source) do
{:ok, _ast} -> :ok
{:error, reason, line} -> {:error, "#{reason} at line #{line}"}
end
end
defp template_file_path(slug), do: Path.join(["templates", "#{slug}.liquid"])
defp full_file_path(project_id, relative_path) do
@@ -334,7 +363,6 @@ defmodule BDS.Templates do
Path.join(Projects.project_data_dir(project), relative_path)
end
defp next_template_file_path(%Template{file_path: ""}, _next_slug), do: ""
defp next_template_file_path(%Template{}, next_slug), do: template_file_path(next_slug)
defp serialize_template_file(template, content) do

View File

@@ -35,7 +35,7 @@ defmodule BDS.UI.Sidebar do
"import",
list_import_definitions(project_id)
),
"git" => git_view(),
"git" => git_view(project_id),
"settings" => settings_nav_view()
}
end
@@ -94,7 +94,7 @@ defmodule BDS.UI.Sidebar do
)
"git" ->
git_view()
git_view(project_id)
"settings" ->
settings_nav_view()
@@ -139,13 +139,17 @@ defmodule BDS.UI.Sidebar do
"import",
[]
),
"git" => git_view(),
"git" => git_view(nil),
"settings" => settings_nav_view()
}
end
defp empty_view("posts"), do: build_posts_view([], %{}, false, empty_filter_params(), %{}, [], [], [])
defp empty_view("pages"), do: build_posts_view([], %{}, true, empty_filter_params(), %{}, [], [], [])
defp empty_view("posts"),
do: build_posts_view([], %{}, false, empty_filter_params(), %{}, [], [], [])
defp empty_view("pages"),
do: build_posts_view([], %{}, true, empty_filter_params(), %{}, [], [], [])
defp empty_view("media"), do: build_media_view([], empty_filter_params(), %{}, [], [], 0)
defp empty_view("scripts"),
@@ -186,7 +190,7 @@ defmodule BDS.UI.Sidebar do
[]
)
defp empty_view("git"), do: git_view()
defp empty_view("git"), do: git_view(nil)
defp empty_view("settings"), do: settings_nav_view()
defp empty_view(_other),
@@ -563,7 +567,14 @@ defmodule BDS.UI.Sidebar do
build_media_view(limited_media, filters, tag_colors, year_months, avail_tags, total_count)
end
defp build_media_view(limited_media, filters, tag_colors, year_month_counts, available_tags, total_count) do
defp build_media_view(
limited_media,
filters,
tag_colors,
year_month_counts,
available_tags,
total_count
) do
loaded_count = length(limited_media)
%{
@@ -779,24 +790,115 @@ defmodule BDS.UI.Sidebar do
}
end
defp git_view do
%{
@git_history_page_size 20
defp git_view(project_id) do
base = %{
title: dgettext("ui", "Git"),
subtitle: dgettext("ui", "Working tree and history"),
layout: "entity_list",
empty_message: dgettext("ui", "No items"),
items: [
%{
id: "git-working-tree",
title: dgettext("ui", "Working tree"),
meta: dgettext("ui", "Working tree and history"),
route: "git_diff",
updated_at: nil
}
]
layout: "git",
empty_message: dgettext("ui", "No items")
}
if git_repo?(project_id) do
Map.merge(base, active_git_view(project_id))
else
Map.merge(base, %{git_state: "not_a_repo", remote_url: nil})
end
end
defp git_repo?(nil), do: false
defp git_repo?(project_id) when is_binary(project_id) do
case BDS.Projects.get_project(project_id) do
nil -> false
project -> File.dir?(Path.join(BDS.Projects.project_data_dir(project), ".git"))
end
end
defp active_git_view(project_id) do
repo =
case BDS.Git.repository(project_id) do
{:ok, repo} -> repo
_other -> %{current_branch: nil, remote_url: nil}
end
branch = repo[:current_branch]
remote =
case BDS.Git.remote_state(project_id) do
{:ok, state} -> state
_other -> %{upstream_branch: nil, ahead: 0, behind: 0}
end
status_files =
case BDS.Git.status(project_id) do
{:ok, %{files: files}} -> Enum.map(files, &git_status_file/1)
_other -> []
end
commits =
if is_binary(branch) do
case BDS.Git.history(project_id, branch) do
{:ok, %{commits: commits}} -> commits
_other -> []
end
else
[]
end
%{
git_state: "active",
branch: branch,
upstream: remote[:upstream_branch],
ahead: remote[:ahead] || 0,
behind: remote[:behind] || 0,
status_files: status_files,
history_entries:
commits |> Enum.take(@git_history_page_size) |> Enum.map(&git_history_entry/1),
has_more_history: length(commits) > @git_history_page_size,
remote_url: repo[:remote_url]
}
end
defp git_status_file(%{status: status} = file) do
%{
path: Map.get(file, :path, ""),
status: to_string(status),
code: git_status_code(status),
label: git_status_label(status)
}
end
defp git_status_code(:added), do: "A"
defp git_status_code(:deleted), do: "D"
defp git_status_code(:modified), do: "M"
defp git_status_code(:renamed), do: "R"
defp git_status_code(:untracked), do: "U"
defp git_status_code(_other), do: "M"
defp git_status_label(:added), do: dgettext("ui", "added")
defp git_status_label(:deleted), do: dgettext("ui", "deleted")
defp git_status_label(:modified), do: dgettext("ui", "modified")
defp git_status_label(:renamed), do: dgettext("ui", "renamed")
defp git_status_label(:untracked), do: dgettext("ui", "untracked")
defp git_status_label(_other), do: dgettext("ui", "modified")
defp git_history_entry(commit) do
%{
short_hash: commit |> Map.get(:hash, "") |> String.slice(0, 7),
subject: Map.get(commit, :subject),
author: Map.get(commit, :author),
date: Map.get(commit, :date),
sync_status: git_sync_status(get_in(commit, [:sync_status, :kind]))
}
end
defp git_sync_status(:both), do: "synced"
defp git_sync_status(:local_only), do: "local_only"
defp git_sync_status(:remote_only), do: "remote_only"
defp git_sync_status(_other), do: "synced"
defp entity_list_view(title, subtitle, route, items) do
%{
title: title,

View File

@@ -33,7 +33,11 @@ defmodule BDS.MixProject do
{:plug, "~> 1.18"},
{:bandit, "~> 1.5"},
{:desktop, "~> 1.5"},
{:image, "~> 0.65"},
{:image, "~> 0.67"},
{:nx, "~> 0.10"},
{:exla, "~> 0.10"},
{:bumblebee, "~> 0.6.3"},
{:hnswlib, "~> 0.1.7"},
{:stemex, "~> 0.2.1"},
{:gettext, "~> 0.24"},
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
@@ -60,7 +64,7 @@ defmodule BDS.MixProject do
env = Mix.env()
[
plt_add_apps: [:mix, :inets, :ssl],
plt_add_apps: [:mix, :inets, :ssl, :nx, :exla, :bumblebee, :hnswlib],
paths: ["_build/#{env}/lib/bds/ebin"]
]
end

View File

@@ -1,12 +1,16 @@
%{
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
"axon": {:hex, :axon, "0.7.0", "2e2c6d93b4afcfa812566b8922204fa022b60081e86ebd411df4db7ea30f5457", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "ee9857a143c9486597ceff434e6ca833dc1241be6158b01025b8217757ed1036"},
"bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
"bumblebee": {:hex, :bumblebee, "0.6.3", "c0028643c92de93258a9804da1d4d48797eaf7911b702464b3b3dd2cc7f938f1", [:mix], [{:axon, "~> 0.7.0", [hex: :axon, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.9.0 or ~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.2.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:progress_bar, "~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:safetensors, "~> 0.1.3", [hex: :safetensors, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.4", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "~> 0.12.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "c619197787561f8e5fb2ffba269c341654accaec9d591999b7fddd55761dd079"},
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"color": {:hex, :color, "0.12.0", "f59f9bb6452a460760d44116ec0c1cf86f9d7707c8756c01f83c6d8fe042ae67", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "1e17768919dad0bd44f48d0daf294d24bdd5a615bbfe0b4e01a51312203bd294"},
"color": {:hex, :color, "0.13.0", "068110e5397ac5d3c9f97658282e0f4ab9a32468be6d7a2a91a8804e67b228d7", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "de127946869931d418bac2d82dc29feae1a8f5f729f135922fbccf0059a58ab2"},
"complex": {:hex, :complex, "0.7.0", "695632ef9487517aa5d57edd1697801079d622414cb2e1a7cf538b1f9a50f205", [:mix], [], "hexpm", "0ee39c0803129f546e7f3f640da8f021c9e659402bf59da6f7f2c4848f068f8d"},
"date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"},
"debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"decimal": {:hex, :decimal, "2.4.1", "6c0fbede12fb122ba685e9ab41c6a40c129e322b3aa192f9e072e61f3a6ffaf2", [:mix], [], "hexpm", "7e618897933a8455f19a727d7c5e50a2c071a544b700e5e724298ecb4340187f"},
"desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
@@ -19,15 +23,17 @@
"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_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"},
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
"exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"},
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"hnswlib": {:hex, :hnswlib, "0.1.7", "784afdbfbc9af53e64d4b6da3f685c07039e472636a98fa954ffae5292ad6cc4", [:make, :mix], [{:cc_precompiler, "~> 0.1.0", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fb43bb675facc8bb1ef0f4f8fec92479fc23317ed0f35c7160b2f95aff3e4742"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.4.4", "271455b4d300d5d53a5d92b5bd1c00ad14c5abf1c9ff87be069af5736496515c", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "12e1754204e7db5df1750df0a5dba1bbdf89260800019ab081f2b046596be56b"},
"image": {:hex, :image, "0.65.0", "44908233a1a0dcdbb6ae873ec09fd9ae533d1840d300d8b0b1b186d586b935e6", [:mix], [{:color, "~> 0.4", [hex: :color, repo: "hexpm", optional: false]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "0.11.0", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.11.0", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}, {:xav, "~> 0.10", [hex: :xav, repo: "hexpm", optional: true]}], "hexpm", "d2060e08d0f42564f49de1ea97a82a5d237f9ac91edb141dece51f1238dd8b4a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"image": {:hex, :image, "0.67.0", "886325f45bd39f3d705d32f223680163f3eaba142526d34f7f871c2232577e64", [:mix], [{:color, "~> 0.13", [hex: :color, repo: "hexpm", optional: false]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.10", [hex: :exla, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.10", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}, {:xav, "~> 0.10", [hex: :xav, repo: "hexpm", optional: true]}], "hexpm", "401c3e13137af8932eee377ad8bc8a8ae1a8343894266543e8bedd36b414c999"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"},
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
"liquex": {:hex, :liquex, "0.13.1", "49f90d0b85fb2908f2558f35cd49d78497fe77a895eb55b360889940e1d7afb9", [:mix], [{:date_time_parser, "~> 1.2", [hex: :date_time_parser, repo: "hexpm", optional: false]}, {:html_entities, "~> 0.5.2", [hex: :html_entities, repo: "hexpm", optional: false]}, {:html_sanitize_ex, "~> 1.4.3", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fbea5b9db264c1758a69bfafdcc8aaebcd56e168365bb9575392cd55d800108f"},
@@ -35,23 +41,35 @@
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"},
"nx_image": {:hex, :nx_image, "0.1.2", "0c6e3453c1dc30fc80c723a54861204304cebc8a89ed3b806b972c73ee5d119d", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "9161863c42405ddccb6dbbbeae078ad23e30201509cc804b3b3a7c9e98764b81"},
"nx_signal": {:hex, :nx_signal, "0.2.0", "e1ca0318877b17c81ce8906329f5125f1e2361e4c4235a5baac8a95ee88ea98e", [:mix], [{:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "7247e5e18a177a59c4cb5355952900c62fdeadeb2bad02a9a34237b68744e2bb"},
"oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"},
"progress_bar": {:hex, :progress_bar, "3.0.0", "f54ff038c2ac540cfbb4c2bfe97c75e7116ead044f3c2b10c9f212452194b5cd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6981c2b25ab24aecc91a2dc46623658e1399c21a2ae24db986b90d678530f2b7"},
"rustler": {:hex, :rustler, "0.32.1", "f4cf5a39f9e85d182c0a3f75fa15b5d0add6542ab0bf9ceac6b4023109ebd3fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "b96be75526784f86f6587f051bc8d6f4eaff23d6e0f88dbcfe4d5871f52946f7"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"safetensors": {:hex, :safetensors, "0.1.3", "7ff3c22391e213289c713898481d492c9c28a49ab1d0705b72630fb8360426b2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fe50b53ea59fde4e723dd1a2e31cfdc6013e69343afac84c6be86d6d7c562c14"},
"saxy": {:hex, :saxy, "1.4.0", "c7203ad20001f72eaaad07d08f82be063fa94a40924e6bb39d93d55f979abcba", [:mix], [], "hexpm", "3fe790354d3f2234ad0b5be2d99822a23fa2d4e8ccd6657c672901dac172e9a9"},
"stemex": {:hex, :stemex, "0.2.1", "47017c6b10cdd6926a0d523ccf1f801c5f3faf5a0a9c862f49304e07f9b5584f", [:mix], [], "hexpm", "dbfc76d27adfa31d831d183979c595942884e6530a4496714aa5b70d0964c2e4"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"tokenizers": {:hex, :tokenizers, "0.5.1", "b0975d92b4ee5b18e8f47b5d65b9d5f1e583d9130189b1a2620401af4e7d4b35", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5f08d97cc7f2ed3d71d370d68120da6d3de010948ccf676c9c0eb591ba4bacc9"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
"unzip": {:hex, :unzip, "0.12.0", "beed92238724732418b41eba77dcb7f51e235b707406c05b1732a3052d1c0f36", [:mix], [], "hexpm", "95655b72db368e5a84951f0bed586ac053b55ee3815fd96062fce10ce4fc998d"},
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"xla": {:hex, :xla, "0.9.1", "cca0040ff94902764007a118871bfc667f1a0085d4a5074533a47d6b58bec61e", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb5e443ae5391b1953f253e051f2307bea183b59acee138053a9300779930daf"},
}

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div>
</div>
</nav>
@@ -35,7 +35,7 @@
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div>
</div>
{% endif %}

View File

@@ -5,7 +5,7 @@
msgid "Archive"
msgstr "Archiv"
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr "Apr."
@@ -15,7 +15,7 @@ msgstr "Apr."
msgid "Archive calendar"
msgstr "Archiv"
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr "Aug."
@@ -35,27 +35,27 @@ msgstr "Kalenderdaten konnten nicht geladen werden."
msgid "Close calendar"
msgstr "Kalender schließen"
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr "Dezember"
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr "Februar"
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr "Januar"
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr "Juli"
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr "Juni"
@@ -75,22 +75,22 @@ msgstr "Verlinkt von"
msgid "Loading calendar…"
msgstr "Kalender wird geladen …"
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr "März"
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr "Mai"
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr "Nov."
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr "Oktober"
@@ -110,7 +110,7 @@ msgstr "Seitennummerierung"
msgid "Search..."
msgstr "Suchen..."
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr "Sept."
@@ -135,22 +135,27 @@ msgstr "neuer"
msgid "older"
msgstr "älter"
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr "Zurück zur Vorschau-Startseite"
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr "Die angeforderte Vorschauseite konnte nicht gefunden werden."
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr "Vimeo-Video"
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr "YouTube-Video"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Keine Ergebnisse gefunden"

View File

@@ -79,7 +79,7 @@ msgstr "KI-Einstellungen"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatisch"
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Automatische KI-Aktionen bleiben durch den Flugmodus gesperrt."
@@ -404,7 +404,7 @@ msgstr "Kategorie-Standards, Render-Flags und Template-Zuordnung"
msgid "Category name is required"
msgstr "Kategoriename ist erforderlich"
#: lib/bds/desktop/shell_live.ex:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "MCP-Konfiguration %{path} konnte nicht gelesen werden: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}"
msgstr "MCP-Konfiguration %{path} konnte nicht geschrieben werden: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr "Erstellen"
@@ -636,12 +636,12 @@ msgstr "Standard-Bearbeitungsmodus und Diff-Darstellung"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Standard-Bearbeitungsmodus und Diff-Darstellung"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Medium loeschen"
msgid "Delete Translation"
msgstr "Übersetzung löschen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Erkennen"
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr "Sprache erkennen"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Änderungen verwerfen und veröffentlichte Version wiederherstellen"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr "Entdecken"
@@ -991,8 +991,8 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Website generieren"
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Git-Diff"
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr "Leerlauf"
@@ -1075,7 +1075,7 @@ msgstr "Bilder und Dateien"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Importdefinitionen"
msgid "Import failed: %{error}"
msgstr "Import fehlgeschlagen: %{error}"
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Sprache"
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr "Spracherkennung fehlgeschlagen."
@@ -1288,7 +1288,7 @@ msgstr "Zuordnen zu..."
msgid "Mapped"
msgstr "Zugeordnet"
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Maximale Beiträge pro Seite"
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Medium gespeichert"
msgid "Menu"
msgstr "Menü"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr "Zusammenführen"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Neues Untermenü"
msgid "New Template"
msgstr "Neue Vorlage"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr "Kein Template"
@@ -1453,12 +1453,12 @@ msgstr "Noch keine Git-Historie"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr "Keine Einträge"
@@ -1721,7 +1721,7 @@ msgstr "Sonstige"
msgid "Other (%{count})"
msgstr "Andere (%{count})"
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Die erkannte Sprache für dieses Medium speichern"
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Beitragsvorlage"
msgid "Post is marked as do-not-translate but has translations"
msgstr "Beitrag ist als nicht-übersetzen markiert, hat aber Übersetzungen"
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr "Beitrag veröffentlicht"
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr "Beitrag gespeichert"
@@ -1854,7 +1854,7 @@ msgstr "Beiträge (%{count})"
msgid "Preferences"
msgstr "Einstellungen"
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Ausgewähltes veröffentlichen"
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Lösung"
msgid "Result"
msgstr "Ergebnis"
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr "Zurückgesetzt"
@@ -2161,7 +2161,7 @@ msgstr "SSH-Modus"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Übersetzung speichern"
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr "Gespeichert"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Einträge werden gescannt..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Untermenü"
msgid "Switch project"
msgstr "Projekt wechseln"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr "Synchronisieren"
@@ -2468,11 +2468,11 @@ msgstr "Schlagwortname"
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Schlagwortname"
msgid "Tags"
msgstr "Tags"
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Technik"
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Seitenleiste umschalten"
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Verknüpfung mit Beitrag aufheben"
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Nicht gespeichert"
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr "Ohne Titel"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr "Arbeitsverzeichnis"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr "Arbeitsverzeichnis und Verlauf"
@@ -3179,12 +3177,12 @@ msgstr "Willkommen beim KI-Assistenten"
msgid "Comparing database and filesystem metadata"
msgstr "Vergleicht Datenbank- und Dateisystem-Metadaten"
#: lib/bds/desktop/shell_live.ex:650
#: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} Bilder zum Beitrag hinzugefügt"
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} hinzugefügt"
@@ -3204,18 +3202,18 @@ msgstr "Endbenutzer-Anleitung für redaktionelle Arbeitsabläufe, Medien, Vorlag
msgid "Image Import Concurrency"
msgstr "Gleichzeitige Bildimporte"
#: lib/bds/desktop/shell_live.ex:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Galerie-Bilder hinzufügen"
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "%{path} konnte nicht verarbeitet werden: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivieren"
msgid "Move this post to the archive"
msgstr "Diesen Beitrag ins Archiv verschieben"
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr "Beitrag archiviert"
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr "Beitrag wiederhergestellt"
@@ -3249,3 +3247,157 @@ msgstr "Diesen Beitrag als Entwurf wiederherstellen"
#, elixir-autogen, elixir-format
msgid "Unarchive"
msgstr "Wiederherstellen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "Voraus"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Zurück"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Änderungen"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Commit-Nachricht"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Commit-Nachricht erforderlich"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fertig"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Abrufen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Verlauf"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Git initialisieren"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Nur lokal"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Kein aktives Projekt"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Keine Änderungen"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Noch keine Commits"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Ältere Einträge verfügbar"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "LFS bereinigen"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "Remote-URL (optional)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Nur remote"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repository initialisiert"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Synchronisiert"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Dieses Projekt ist noch kein Git-Repository."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "hinzugefügt"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "gelöscht"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "geändert"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "umbenannt"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "nicht verfolgt"

View File

@@ -5,7 +5,7 @@
msgid "Archive"
msgstr ""
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr ""
@@ -15,7 +15,7 @@ msgstr ""
msgid "Archive calendar"
msgstr ""
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr ""
@@ -35,27 +35,27 @@ msgstr ""
msgid "Close calendar"
msgstr ""
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr ""
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr ""
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr ""
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr ""
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr ""
@@ -75,22 +75,22 @@ msgstr ""
msgid "Loading calendar…"
msgstr ""
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr ""
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr ""
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr ""
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr ""
@@ -110,7 +110,7 @@ msgstr ""
msgid "Search..."
msgstr ""
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr ""
@@ -135,22 +135,27 @@ msgstr ""
msgid "older"
msgstr ""
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr ""
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr ""
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr ""
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr ""
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr ""

View File

@@ -79,7 +79,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr ""
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr ""
@@ -404,7 +404,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr ""
msgid "Could not write MCP config %{path}: %{reason}"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr ""
@@ -636,12 +636,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -665,7 +665,7 @@ msgstr ""
msgid "Delete Translation"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr ""
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr ""
@@ -991,8 +991,8 @@ msgstr ""
msgid "Generate Site"
msgstr ""
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr ""
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr ""
@@ -1075,7 +1075,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr ""
@@ -1288,7 +1288,7 @@ msgstr ""
msgid "Mapped"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr ""
msgid "Menu"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr ""
msgid "New Template"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr ""
@@ -1453,12 +1453,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr ""
@@ -1721,7 +1721,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr ""
msgid "Post is marked as do-not-translate but has translations"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr ""
@@ -1854,7 +1854,7 @@ msgstr ""
msgid "Preferences"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr ""
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr ""
msgid "Result"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr ""
@@ -2161,7 +2161,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr ""
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr ""
msgid "Switch project"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr ""
@@ -2468,11 +2468,11 @@ msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr ""
msgid "Tags"
msgstr ""
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr ""
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr ""
@@ -3179,12 +3177,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr "Comparing database and filesystem metadata"
#: lib/bds/desktop/shell_live.ex:650
#: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "Added %{count} images to post"
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "Added %{title}"
@@ -3204,18 +3202,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr "Image Import Concurrency"
#: lib/bds/desktop/shell_live.ex:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Add Gallery Images"
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Failed to process %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr ""
msgid "Move this post to the archive"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format, fuzzy
msgid "Post archived"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format, fuzzy
msgid "Post unarchived"
msgstr ""
@@ -3249,3 +3247,157 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy
msgid "Unarchive"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format, fuzzy
msgid "Done"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format, fuzzy
msgid "No commits yet"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format, fuzzy
msgid "Push"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format, fuzzy
msgid "Synced"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr ""
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format, fuzzy
msgid "added"
msgstr ""
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format, fuzzy
msgid "deleted"
msgstr ""
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr ""
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format, fuzzy
msgid "renamed"
msgstr ""
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr ""

View File

@@ -5,7 +5,7 @@
msgid "Archive"
msgstr "Archivo"
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr "abril"
@@ -15,7 +15,7 @@ msgstr "abril"
msgid "Archive calendar"
msgstr "Archivo"
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr "agosto"
@@ -35,27 +35,27 @@ msgstr "No se pudieron cargar los datos del calendario."
msgid "Close calendar"
msgstr "Cerrar calendario"
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr "diciembre"
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr "febrero"
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr "enero"
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr "julio"
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr "junio"
@@ -75,22 +75,22 @@ msgstr "Enlazado desde"
msgid "Loading calendar…"
msgstr "Cargando calendario…"
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr "marzo"
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr "mayo"
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr "noviembre"
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr "octubre"
@@ -110,7 +110,7 @@ msgstr "Paginación"
msgid "Search..."
msgstr "Buscar..."
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr "septiembre"
@@ -135,22 +135,27 @@ msgstr "más reciente"
msgid "older"
msgstr "más antiguo"
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr "Volver al inicio de vista previa"
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr "No se pudo encontrar la página de vista previa solicitada."
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr "Vídeo de Vimeo"
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr "Vídeo de YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "No se encontraron resultados"

View File

@@ -79,7 +79,7 @@ msgstr "Configuración de IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automático"
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Las acciones automáticas de IA siguen bloqueadas por el modo avión."
@@ -404,7 +404,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:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "No se pudo leer la configuración MCP %{path}: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}"
msgstr "No se pudo escribir la configuración MCP %{path}: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr "Crear"
@@ -636,12 +636,12 @@ msgstr "Modo de edición predeterminado y presentación de diff"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Modo de edición predeterminado y presentación de diff"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Eliminar medio"
msgid "Delete Translation"
msgstr "Eliminar traducción"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Detectar"
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr "Detectar idioma"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Descartar cambios y restaurar la versión publicada"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr "Descubrir"
@@ -991,8 +991,8 @@ msgstr "Galeria"
msgid "Generate Site"
msgstr "Generar sitio"
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff de Git"
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr "Inactivo"
@@ -1075,7 +1075,7 @@ msgstr "Imágenes y archivos"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Definiciones de importación"
msgid "Import failed: %{error}"
msgstr "La importación falló: %{error}"
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Idioma"
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr "La detección de idioma falló."
@@ -1288,7 +1288,7 @@ msgstr "Mapear a..."
msgid "Mapped"
msgstr "Mapeado"
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Máximo de publicaciones por página"
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Medio guardado"
msgid "Menu"
msgstr "Menú"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr "Fusionar"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nuevo submenú"
msgid "New Template"
msgstr "Nueva plantilla"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr "Sin plantilla"
@@ -1453,12 +1453,12 @@ msgstr "Aún no hay historial de Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr "No hay elementos"
@@ -1721,7 +1721,7 @@ msgstr "Otros"
msgid "Other (%{count})"
msgstr "Otros (%{count})"
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Guardar el idioma detectado para este medio"
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Plantilla de publicación"
msgid "Post is marked as do-not-translate but has translations"
msgstr "La entrada está marcada como no-traducir pero tiene traducciones"
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr "Artículo publicado"
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr "Artículo guardado"
@@ -1854,7 +1854,7 @@ msgstr "Publicaciones (%{count})"
msgid "Preferences"
msgstr "Preferencias"
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Publicar seleccionados"
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Resolución"
msgid "Result"
msgstr "Resultado"
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr "Revertido"
@@ -2161,7 +2161,7 @@ msgstr "Modo SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Guardar traducción"
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr "Guardado"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Escaneando entradas..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Submenú"
msgid "Switch project"
msgstr "Cambiar proyecto"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr "Sincronizar"
@@ -2468,11 +2468,11 @@ msgstr "Nombre de la etiqueta"
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nombre de la etiqueta"
msgid "Tags"
msgstr "Etiquetas"
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Tecnología"
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Alternar barra lateral"
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Desvincular del artículo"
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Sin guardar"
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr "Sin título"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr "Árbol de trabajo"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr "Árbol de trabajo e historial"
@@ -3179,12 +3177,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:650
#: lib/bds/desktop/shell_live.ex:671
#, 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:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} añadido"
@@ -3204,18 +3202,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:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Añadir imágenes a la galería"
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "No se pudo procesar %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivar"
msgid "Move this post to the archive"
msgstr "Mover este artículo al archivo"
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr "Artículo archivado"
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr "Artículo restaurado"
@@ -3249,3 +3247,157 @@ msgstr "Restaurar este artículo como borrador"
#, elixir-autogen, elixir-format
msgid "Unarchive"
msgstr "Desarchivar"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "Adelante"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Atrás"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Cambios"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Mensaje de commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Se requiere un mensaje de commit"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Listo"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Obtener"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Historial"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Inicializar Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Solo local"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Sin proyecto activo"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Sin cambios"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Aún no hay commits"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Hay historial más antiguo disponible"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Limpiar LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL remota (opcional)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Solo remoto"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repositorio inicializado"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Sincronizado"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Este proyecto aún no es un repositorio Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "añadido"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "eliminado"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modificado"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "renombrado"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "sin seguimiento"

View File

@@ -5,7 +5,7 @@
msgid "Archive"
msgstr "Archives"
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr "avril"
@@ -15,7 +15,7 @@ msgstr "avril"
msgid "Archive calendar"
msgstr "Archives"
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr "août"
@@ -35,27 +35,27 @@ msgstr "Impossible de charger les données du calendrier."
msgid "Close calendar"
msgstr "Fermer le calendrier"
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr "décembre"
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr "février"
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr "janvier"
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr "juillet"
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr "juin"
@@ -75,22 +75,22 @@ msgstr "Lié depuis"
msgid "Loading calendar…"
msgstr "Chargement du calendrier…"
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr "mars"
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr "mai"
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr "novembre"
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr "octobre"
@@ -110,7 +110,7 @@ msgstr "Navigation paginée"
msgid "Search..."
msgstr "Rechercher..."
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr "septembre"
@@ -135,22 +135,27 @@ msgstr "plus récent"
msgid "older"
msgstr "plus ancien"
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr "Retour à laccueil de laperçu"
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr "La page daperçu demandée est introuvable."
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr "Vidéo Vimeo"
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr "Vidéo YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Aucun résultat trouvé"

View File

@@ -79,7 +79,7 @@ msgstr "Paramètres IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatique"
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Les actions IA automatiques restent bloquées par le mode avion."
@@ -404,7 +404,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:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "Impossible de lire la configuration MCP %{path} : %{reason}"
msgid "Could not write MCP config %{path}: %{reason}"
msgstr "Impossible d'écrire la configuration MCP %{path} : %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr "Créer"
@@ -636,12 +636,12 @@ msgstr "Mode dédition par défaut et présentation des diffs"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Mode dédition par défaut et présentation des diffs"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Supprimer le media"
msgid "Delete Translation"
msgstr "Supprimer la traduction"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Détecter"
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr "Détecter la langue"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Annuler les modifications et restaurer la version publiée"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr "Découvrir"
@@ -991,8 +991,8 @@ msgstr "Galerie"
msgid "Generate Site"
msgstr "Générer le site"
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Hôte"
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr "Inactif"
@@ -1075,7 +1075,7 @@ msgstr "Images et fichiers"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Définitions dimport"
msgid "Import failed: %{error}"
msgstr "Échec de limport : %{error}"
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Langue"
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr "La détection de la langue a échoué."
@@ -1288,7 +1288,7 @@ msgstr "Mapper vers..."
msgid "Mapped"
msgstr "Mappé"
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Nombre maximal darticles par page"
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Média enregistré"
msgid "Menu"
msgstr "Menu"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr "Fusionner"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nouveau sous-menu"
msgid "New Template"
msgstr "Nouveau modèle"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr "Aucun template"
@@ -1453,12 +1453,12 @@ msgstr "Pas encore d'historique Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr "Aucun élément"
@@ -1721,7 +1721,7 @@ msgstr "Autre"
msgid "Other (%{count})"
msgstr "Autres (%{count})"
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Enregistrer la langue détectée pour ce média"
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Modèle darticle"
msgid "Post is marked as do-not-translate but has translations"
msgstr "L'article est marqué ne-pas-traduire mais a des traductions"
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr "Article publié"
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr "Article enregistré"
@@ -1854,7 +1854,7 @@ msgstr "Articles (%{count})"
msgid "Preferences"
msgstr "Préférences"
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Publier la sélection"
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Résolution"
msgid "Result"
msgstr "Résultat"
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr "Restauré"
@@ -2161,7 +2161,7 @@ msgstr "Mode SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Enregistrer la traduction"
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr "Enregistré"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Analyse des entrées..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Sous-menu"
msgid "Switch project"
msgstr "Changer de projet"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr "Synchroniser"
@@ -2468,11 +2468,11 @@ msgstr "Nom du mot-clé"
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nom du mot-clé"
msgid "Tags"
msgstr "Tags"
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Technologie"
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Afficher ou masquer la barre latérale"
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Dissocier de l'article"
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Non enregistré"
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr "Sans titre"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr "Arbre de travail"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr "Arbre de travail et historique"
@@ -3179,12 +3177,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:650
#: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} images ajoutées à l'article"
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} ajouté"
@@ -3204,18 +3202,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:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Ajouter des images à la galerie"
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossible de traiter %{path} : %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archiver"
msgid "Move this post to the archive"
msgstr "Déplacer cet article dans les archives"
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr "Article archivé"
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr "Article désarchivé"
@@ -3249,3 +3247,157 @@ msgstr "Restaurer cet article en brouillon"
#, elixir-autogen, elixir-format
msgid "Unarchive"
msgstr "Désarchiver"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "En avance"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "En retard"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Modifications"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Message de commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Le message de commit est requis"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Terminé"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Récupérer"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Historique"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Initialiser Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Local uniquement"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Aucun projet actif"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Aucune modification"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Aucun commit pour linstant"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Historique plus ancien disponible"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Nettoyer LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL distante (facultatif)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Distant uniquement"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Dépôt initialisé"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Synchronisé"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Ce projet nest pas encore un dépôt Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "ajouté"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "supprimé"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modifié"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "renommé"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "non suivi"

View File

@@ -5,7 +5,7 @@
msgid "Archive"
msgstr "Archivio"
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr "aprile"
@@ -15,7 +15,7 @@ msgstr "aprile"
msgid "Archive calendar"
msgstr "Archivio"
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr "agosto"
@@ -35,27 +35,27 @@ msgstr "Impossibile caricare i dati del calendario."
msgid "Close calendar"
msgstr "Chiudi calendario"
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr "dicembre"
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr "febbraio"
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr "gennaio"
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr "luglio"
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr "giugno"
@@ -75,22 +75,22 @@ msgstr "Collegato da"
msgid "Loading calendar…"
msgstr "Caricamento calendario…"
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr "marzo"
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr "maggio"
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr "novembre"
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr "ottobre"
@@ -110,7 +110,7 @@ msgstr "Paginazione"
msgid "Search..."
msgstr "Cerca..."
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr "settembre"
@@ -135,22 +135,27 @@ msgstr "più recente"
msgid "older"
msgstr "più vecchio"
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr "Torna alla home di anteprima"
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr "La pagina di anteprima richiesta non è stata trovata."
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr "Video Vimeo"
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr "Video YouTube"
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr "Nessun risultato trovato"

View File

@@ -79,7 +79,7 @@ msgstr "Impostazioni IA"
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -257,14 +257,14 @@ msgid "Auto"
msgstr "Automatico"
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr "Le azioni IA automatiche restano bloccate dalla modalità aereo."
@@ -404,7 +404,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:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -537,7 +537,7 @@ msgstr "Impossibile leggere la configurazione MCP %{path}: %{reason}"
msgid "Could not write MCP config %{path}: %{reason}"
msgstr "Impossibile scrivere la configurazione MCP %{path}: %{reason}"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr "Crea"
@@ -636,12 +636,12 @@ msgstr "Modalità di modifica predefinita e presentazione dei diff"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -649,7 +649,7 @@ msgstr "Modalità di modifica predefinita e presentazione dei diff"
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -665,7 +665,7 @@ msgstr "Elimina media"
msgid "Delete Translation"
msgstr "Elimina traduzione"
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -706,9 +706,9 @@ msgstr "Rileva"
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr "Rileva lingua"
@@ -749,7 +749,7 @@ msgid "Discard changes and restore the published version"
msgstr "Annulla le modifiche e ripristina la versione pubblicata"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr "Scopri"
@@ -991,8 +991,8 @@ msgstr "Galleria"
msgid "Generate Site"
msgstr "Genera sito"
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr "Git"
@@ -1004,7 +1004,7 @@ msgid "Git Diff"
msgstr "Diff Git"
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1033,7 +1033,7 @@ msgstr "Host"
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr "Inattivo"
@@ -1075,7 +1075,7 @@ msgstr "Immagini e file"
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1127,9 +1127,9 @@ msgstr "Definizioni di importazione"
msgid "Import failed: %{error}"
msgstr "Importazione non riuscita: %{error}"
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1204,7 +1204,7 @@ msgid "Language"
msgstr "Lingua"
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr "Rilevamento della lingua non riuscito."
@@ -1288,7 +1288,7 @@ msgstr "Mappa a..."
msgid "Mapped"
msgstr "Mappato"
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1304,7 +1304,7 @@ msgstr "Numero massimo di post per pagina"
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1328,12 +1328,12 @@ msgstr "Media salvato"
msgid "Menu"
msgstr "Menu"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr "Unisci"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1424,7 +1424,7 @@ msgstr "Nuovo sottomenu"
msgid "New Template"
msgstr "Nuovo modello"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr "Nessun template"
@@ -1453,12 +1453,12 @@ msgstr "Nessuna cronologia Git"
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr "Nessun elemento"
@@ -1721,7 +1721,7 @@ msgstr "Altro"
msgid "Other (%{count})"
msgstr "Altro (%{count})"
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1786,17 +1786,17 @@ msgid "Persist the detected language for this media item"
msgstr "Salva la lingua rilevata per questo media"
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1825,12 +1825,12 @@ msgstr "Template del post"
msgid "Post is marked as do-not-translate but has translations"
msgstr "Il post è contrassegnato come non-tradurre ma ha traduzioni"
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr "Articolo pubblicato"
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr "Articolo salvato"
@@ -1854,7 +1854,7 @@ msgstr "Articoli (%{count})"
msgid "Preferences"
msgstr "Preferenze"
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1923,7 +1923,7 @@ msgid "Publish Selected"
msgstr "Pubblica selezionati"
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2121,7 +2121,7 @@ msgstr "Risoluzione"
msgid "Result"
msgstr "Risultato"
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr "Ripristinato"
@@ -2161,7 +2161,7 @@ msgstr "Modalità SSH"
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2173,7 +2173,7 @@ msgid "Save Translation"
msgstr "Salva traduzione"
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr "Salvato"
@@ -2184,7 +2184,7 @@ msgid "Scanning entries..."
msgstr "Scansione delle voci..."
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2431,7 +2431,7 @@ msgstr "Sottomenu"
msgid "Switch project"
msgstr "Cambia progetto"
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr "Sincronizza"
@@ -2468,11 +2468,11 @@ msgstr "Nome del tag"
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2483,7 +2483,7 @@ msgstr "Nome del tag"
msgid "Tags"
msgstr "Tag"
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2497,7 +2497,7 @@ msgstr "Tecnologia"
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2648,9 +2648,9 @@ msgstr "Attiva/disattiva barra laterale"
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2727,7 +2727,7 @@ msgstr "Scollega dall'articolo"
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2735,7 +2735,7 @@ msgstr "Non salvato"
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr "Senza titolo"
@@ -2829,14 +2829,12 @@ msgid "WYSIWYG"
msgstr "WYSIWYG"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr "Working tree"
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr "Working tree e cronologia"
@@ -3179,12 +3177,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:650
#: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr "%{count} immagini aggiunte al post"
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr "%{title} aggiunto"
@@ -3204,18 +3202,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:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr "Aggiungi immagini alla galleria"
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr "Impossibile elaborare %{path}: %{reason}"
@@ -3230,12 +3228,12 @@ msgstr "Archivia"
msgid "Move this post to the archive"
msgstr "Sposta questo articolo nell'archivio"
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr "Articolo archiviato"
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr "Articolo ripristinato"
@@ -3249,3 +3247,157 @@ msgstr "Ripristina questo articolo come bozza"
#, elixir-autogen, elixir-format
msgid "Unarchive"
msgstr "Ripristina"
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr "In avanti"
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr "Indietro"
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr "Modifiche"
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr "Commit"
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr "Messaggio di commit"
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr "Il messaggio di commit è obbligatorio"
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr "Fatto"
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr "Recupera"
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr "Cronologia"
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr "Inizializza Git"
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr "Solo locale"
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr "Nessun progetto attivo"
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr "Nessuna modifica"
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr "Nessun commit ancora"
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr "Cronologia precedente disponibile"
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr "Pulisci LFS"
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr "Pull"
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr "Push"
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr "URL remota (opzionale)"
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr "Solo remoto"
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr "Repository inizializzato"
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr "Sincronizzato"
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr "Questo progetto non è ancora un repository Git."
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr "aggiunto"
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr "eliminato"
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr "modificato"
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr "rinominato"
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr "non tracciato"

View File

@@ -18,7 +18,7 @@ msgstr ""
msgid "Archive"
msgstr ""
#: lib/bds/rendering/labels.ex:54
#: lib/bds/rendering/labels.ex:55
#, elixir-autogen, elixir-format
msgid "April"
msgstr ""
@@ -28,7 +28,7 @@ msgstr ""
msgid "Archive calendar"
msgstr ""
#: lib/bds/rendering/labels.ex:70
#: lib/bds/rendering/labels.ex:71
#, elixir-autogen, elixir-format
msgid "August"
msgstr ""
@@ -48,27 +48,27 @@ msgstr ""
msgid "Close calendar"
msgstr ""
#: lib/bds/rendering/labels.ex:86
#: lib/bds/rendering/labels.ex:87
#, elixir-autogen, elixir-format
msgid "December"
msgstr ""
#: lib/bds/rendering/labels.ex:46
#: lib/bds/rendering/labels.ex:47
#, elixir-autogen, elixir-format
msgid "February"
msgstr ""
#: lib/bds/rendering/labels.ex:42
#: lib/bds/rendering/labels.ex:43
#, elixir-autogen, elixir-format
msgid "January"
msgstr ""
#: lib/bds/rendering/labels.ex:66
#: lib/bds/rendering/labels.ex:67
#, elixir-autogen, elixir-format
msgid "July"
msgstr ""
#: lib/bds/rendering/labels.ex:62
#: lib/bds/rendering/labels.ex:63
#, elixir-autogen, elixir-format
msgid "June"
msgstr ""
@@ -88,22 +88,22 @@ msgstr ""
msgid "Loading calendar…"
msgstr ""
#: lib/bds/rendering/labels.ex:50
#: lib/bds/rendering/labels.ex:51
#, elixir-autogen, elixir-format
msgid "March"
msgstr ""
#: lib/bds/rendering/labels.ex:58
#: lib/bds/rendering/labels.ex:59
#, elixir-autogen, elixir-format
msgid "May"
msgstr ""
#: lib/bds/rendering/labels.ex:82
#: lib/bds/rendering/labels.ex:83
#, elixir-autogen, elixir-format
msgid "November"
msgstr ""
#: lib/bds/rendering/labels.ex:78
#: lib/bds/rendering/labels.ex:79
#, elixir-autogen, elixir-format
msgid "October"
msgstr ""
@@ -123,7 +123,7 @@ msgstr ""
msgid "Search..."
msgstr ""
#: lib/bds/rendering/labels.ex:74
#: lib/bds/rendering/labels.ex:75
#, elixir-autogen, elixir-format
msgid "September"
msgstr ""
@@ -148,22 +148,27 @@ msgstr ""
msgid "older"
msgstr ""
#: lib/bds/rendering/labels.ex:31
#: lib/bds/rendering/labels.ex:32
#, elixir-autogen, elixir-format
msgid "Back to preview home"
msgstr ""
#: lib/bds/rendering/labels.ex:30
#: lib/bds/rendering/labels.ex:31
#, elixir-autogen, elixir-format
msgid "The requested preview page could not be found."
msgstr ""
#: lib/bds/rendering/labels.ex:33
#: lib/bds/rendering/labels.ex:34
#, elixir-autogen, elixir-format
msgid "Vimeo video"
msgstr ""
#: lib/bds/rendering/labels.ex:32
#: lib/bds/rendering/labels.ex:33
#, elixir-autogen, elixir-format
msgid "YouTube video"
msgstr ""
#: lib/bds/rendering/labels.ex:30
#, elixir-autogen, elixir-format
msgid "No results found"
msgstr ""

View File

@@ -92,7 +92,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:42
#: lib/bds/desktop/shell_live/overlay_manager.ex:72
#: lib/bds/desktop/shell_live/post_editor.ex:776
#: lib/bds/desktop/shell_live/post_editor.ex:781
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:43
#, elixir-autogen, elixir-format
msgid "AI Suggestions"
@@ -270,14 +270,14 @@ msgid "Auto"
msgstr ""
#: lib/bds/desktop/shell_data.ex:98
#: lib/bds/desktop/shell_live.ex:409
#: lib/bds/desktop/shell_live.ex:430
#: lib/bds/desktop/shell_live/chat_editor.ex:231
#: lib/bds/desktop/shell_live/media_editor.ex:156
#: lib/bds/desktop/shell_live/media_editor.ex:349
#: lib/bds/desktop/shell_live/media_editor.ex:538
#: lib/bds/desktop/shell_live/overlay_manager.ex:73
#: lib/bds/desktop/shell_live/post_editor.ex:643
#: lib/bds/desktop/shell_live/post_editor.ex:692
#: lib/bds/desktop/shell_live/post_editor.ex:648
#: lib/bds/desktop/shell_live/post_editor.ex:697
#, elixir-autogen, elixir-format
msgid "Automatic AI actions stay gated by airplane mode."
msgstr ""
@@ -417,7 +417,7 @@ msgstr ""
msgid "Category name is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:932
#: lib/bds/desktop/shell_live.ex:953
#: lib/bds/desktop/shell_live/chat_editor.ex:87
#: lib/bds/desktop/shell_live/chat_editor.ex:230
#: lib/bds/desktop/shell_live/chat_editor.ex:318
@@ -550,7 +550,7 @@ msgstr ""
msgid "Could not write MCP config %{path}: %{reason}"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:42
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:48
#, elixir-autogen, elixir-format
msgid "Create"
msgstr ""
@@ -649,12 +649,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:166
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:116
#: lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex:16
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:529
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_components.ex:665
#: lib/bds/desktop/shell_live/sidebar_delete.ex:72
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
@@ -662,7 +662,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/desktop/shell_live/sidebar_delete.ex:182
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:58
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:70
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:15
#, elixir-autogen, elixir-format
msgid "Delete"
@@ -678,7 +678,7 @@ msgstr ""
msgid "Delete Translation"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:514
#: lib/bds/desktop/shell_live/sidebar_components.ex:650
#: lib/bds/desktop/shell_live/sidebar_delete.ex:173
#, elixir-autogen, elixir-format
msgid "Delete conversation"
@@ -719,9 +719,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:199
#: lib/bds/desktop/shell_live/media_editor.ex:205
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:59
#: lib/bds/desktop/shell_live/post_editor.ex:642
#: lib/bds/desktop/shell_live/post_editor.ex:671
#: lib/bds/desktop/shell_live/post_editor.ex:677
#: lib/bds/desktop/shell_live/post_editor.ex:647
#: lib/bds/desktop/shell_live/post_editor.ex:676
#: lib/bds/desktop/shell_live/post_editor.ex:682
#, elixir-autogen, elixir-format
msgid "Detect Language"
msgstr ""
@@ -762,7 +762,7 @@ msgid "Discard changes and restore the published version"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:21
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:84
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:96
#, elixir-autogen, elixir-format
msgid "Discover"
msgstr ""
@@ -1004,8 +1004,8 @@ msgstr ""
msgid "Generate Site"
msgstr ""
#: lib/bds/desktop/shell_live.ex:933
#: lib/bds/ui/sidebar.ex:784
#: lib/bds/desktop/shell_live.ex:954
#: lib/bds/ui/sidebar.ex:786
#, elixir-autogen, elixir-format
msgid "Git"
msgstr ""
@@ -1017,7 +1017,7 @@ msgid "Git Diff"
msgstr ""
#: lib/bds/desktop/shell_data.ex:244
#: lib/bds/desktop/shell_live.ex:929
#: lib/bds/desktop/shell_live.ex:950
#: lib/bds/desktop/shell_live/panel_renderer.ex:171
#, elixir-autogen, elixir-format
msgid "Git Log"
@@ -1046,7 +1046,7 @@ msgstr ""
#: lib/bds/desktop/shell_data.ex:116
#: lib/bds/desktop/shell_live/index.html.heex:666
#: lib/bds/desktop/shell_live/media_editor.ex:703
#: lib/bds/desktop/shell_live/post_editor.ex:894
#: lib/bds/desktop/shell_live/post_editor.ex:899
#, elixir-autogen, elixir-format
msgid "Idle"
msgstr ""
@@ -1088,7 +1088,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:127
#: lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex:137
#: lib/bds/desktop/shell_live/misc_editor_html/misc_editor.html.heex:182
#: lib/bds/desktop/shell_live/sidebar_components.ex:527
#: lib/bds/desktop/shell_live/sidebar_components.ex:663
#: lib/bds/desktop/shell_live/sidebar_delete.ex:181
#: lib/bds/ui/registry.ex:70
#: lib/bds/ui/registry.ex:105
@@ -1140,9 +1140,9 @@ msgstr ""
msgid "Import failed: %{error}"
msgstr ""
#: lib/bds/desktop/shell_live.ex:580
#: lib/bds/desktop/shell_live.ex:970
#: lib/bds/desktop/shell_live.ex:976
#: lib/bds/desktop/shell_live.ex:601
#: lib/bds/desktop/shell_live.ex:991
#: lib/bds/desktop/shell_live.ex:997
#: lib/bds/desktop/shell_live/sidebar_create.ex:47
#, elixir-autogen, elixir-format
msgid "Import media"
@@ -1217,7 +1217,7 @@ msgid "Language"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:206
#: lib/bds/desktop/shell_live/post_editor.ex:678
#: lib/bds/desktop/shell_live/post_editor.ex:683
#, elixir-autogen, elixir-format
msgid "Language detection failed."
msgstr ""
@@ -1301,7 +1301,7 @@ msgstr ""
msgid "Mapped"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor.ex:902
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:120
#, elixir-autogen, elixir-format
msgid "Markdown"
@@ -1317,7 +1317,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:494
#: lib/bds/desktop/shell_live/misc_editor.ex:744
#: lib/bds/desktop/shell_live/misc_editor.ex:771
#: lib/bds/desktop/shell_live/sidebar_components.ex:518
#: lib/bds/desktop/shell_live/sidebar_components.ex:654
#: lib/bds/desktop/shell_live/sidebar_delete.ex:175
#: lib/bds/ui/registry.ex:30
#: lib/bds/ui/registry.ex:100
@@ -1341,12 +1341,12 @@ msgstr ""
msgid "Menu"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:75
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:87
#, elixir-autogen, elixir-format
msgid "Merge"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:66
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:78
#: lib/bds/ui/sidebar.ex:747
#, elixir-autogen, elixir-format
msgid "Merge Tags"
@@ -1437,7 +1437,7 @@ msgstr ""
msgid "New Template"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:52
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:64
#, elixir-autogen, elixir-format
msgid "No Template"
msgstr ""
@@ -1466,12 +1466,12 @@ msgstr ""
#: lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex:72
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:308
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:320
#: lib/bds/desktop/shell_live/sidebar_components.ex:380
#: lib/bds/desktop/shell_live/sidebar_components.ex:454
#: lib/bds/desktop/shell_live/sidebar_components.ex:321
#: lib/bds/desktop/shell_live/sidebar_components.ex:381
#: lib/bds/desktop/shell_live/sidebar_components.ex:455
#: lib/bds/ui/sidebar.ex:198
#: lib/bds/ui/sidebar.ex:787
#: lib/bds/ui/sidebar.ex:805
#: lib/bds/ui/sidebar.ex:789
#: lib/bds/ui/sidebar.ex:896
#, elixir-autogen, elixir-format
msgid "No items"
msgstr ""
@@ -1734,7 +1734,7 @@ msgstr ""
msgid "Other (%{count})"
msgstr ""
#: lib/bds/desktop/shell_live.ex:928
#: lib/bds/desktop/shell_live.ex:949
#: lib/bds/desktop/shell_live/panel_renderer.ex:83
#, elixir-autogen, elixir-format
msgid "Output"
@@ -1799,17 +1799,17 @@ msgid "Persist the detected language for this media item"
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:742
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:478
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:517
#: lib/bds/desktop/shell_live/post_editor.ex:552
#: lib/bds/desktop/shell_live/post_editor.ex:567
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:599
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:632
#: lib/bds/desktop/shell_live/sidebar_components.ex:515
#: lib/bds/desktop/shell_live/post_editor.ex:479
#: lib/bds/desktop/shell_live/post_editor.ex:483
#: lib/bds/desktop/shell_live/post_editor.ex:518
#: lib/bds/desktop/shell_live/post_editor.ex:522
#: lib/bds/desktop/shell_live/post_editor.ex:557
#: lib/bds/desktop/shell_live/post_editor.ex:572
#: lib/bds/desktop/shell_live/post_editor.ex:601
#: lib/bds/desktop/shell_live/post_editor.ex:604
#: lib/bds/desktop/shell_live/post_editor.ex:634
#: lib/bds/desktop/shell_live/post_editor.ex:637
#: lib/bds/desktop/shell_live/sidebar_components.ex:651
#: lib/bds/desktop/shell_live/sidebar_delete.ex:174
#: lib/bds/ui/registry.ex:99
#, elixir-autogen, elixir-format
@@ -1838,12 +1838,12 @@ msgstr ""
msgid "Post is marked as do-not-translate but has translations"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:513
#: lib/bds/desktop/shell_live/post_editor.ex:518
#, elixir-autogen, elixir-format
msgid "Post published"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:474
#: lib/bds/desktop/shell_live/post_editor.ex:479
#, elixir-autogen, elixir-format
msgid "Post saved"
msgstr ""
@@ -1867,7 +1867,7 @@ msgstr ""
msgid "Preferences"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:898
#: lib/bds/desktop/shell_live/post_editor.ex:903
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:121
#, elixir-autogen, elixir-format
msgid "Preview"
@@ -1936,7 +1936,7 @@ msgid "Publish Selected"
msgstr ""
#: lib/bds/desktop/shell_data.ex:181
#: lib/bds/desktop/shell_live/post_editor.ex:892
#: lib/bds/desktop/shell_live/post_editor.ex:897
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:456
#: lib/bds/ui/sidebar.ex:320
#, elixir-autogen, elixir-format
@@ -2134,7 +2134,7 @@ msgstr ""
msgid "Result"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:893
#: lib/bds/desktop/shell_live/post_editor.ex:898
#, elixir-autogen, elixir-format
msgid "Reverted"
msgstr ""
@@ -2174,7 +2174,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:312
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:329
#: lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex:342
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:57
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:69
#: lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex:13
#, elixir-autogen, elixir-format
msgid "Save"
@@ -2186,7 +2186,7 @@ msgid "Save Translation"
msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:702
#: lib/bds/desktop/shell_live/post_editor.ex:891
#: lib/bds/desktop/shell_live/post_editor.ex:896
#, elixir-autogen, elixir-format
msgid "Saved"
msgstr ""
@@ -2197,7 +2197,7 @@ msgid "Scanning entries..."
msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:746
#: lib/bds/desktop/shell_live/sidebar_components.ex:521
#: lib/bds/desktop/shell_live/sidebar_components.ex:657
#: lib/bds/desktop/shell_live/sidebar_delete.ex:176
#: lib/bds/ui/registry.ex:133
#, elixir-autogen, elixir-format
@@ -2444,7 +2444,7 @@ msgstr ""
msgid "Switch project"
msgstr ""
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:82
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:94
#, elixir-autogen, elixir-format
msgid "Sync"
msgstr ""
@@ -2481,11 +2481,11 @@ msgstr ""
#: lib/bds/desktop/shell_live/index.html.heex:325
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:161
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:158
#: lib/bds/desktop/shell_live/tags_editor.ex:94
#: lib/bds/desktop/shell_live/tags_editor.ex:136
#: lib/bds/desktop/shell_live/tags_editor.ex:189
#: lib/bds/desktop/shell_live/tags_editor.ex:203
#: lib/bds/desktop/shell_live/tags_editor.ex:234
#: lib/bds/desktop/shell_live/tags_editor.ex:104
#: lib/bds/desktop/shell_live/tags_editor.ex:166
#: lib/bds/desktop/shell_live/tags_editor.ex:219
#: lib/bds/desktop/shell_live/tags_editor.ex:233
#: lib/bds/desktop/shell_live/tags_editor.ex:264
#: lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex:11
#: lib/bds/ui/registry.ex:54
#: lib/bds/ui/registry.ex:103
@@ -2496,7 +2496,7 @@ msgstr ""
msgid "Tags"
msgstr ""
#: lib/bds/desktop/shell_live.ex:927
#: lib/bds/desktop/shell_live.ex:948
#: lib/bds/desktop/shell_live/panel_renderer.ex:54
#, elixir-autogen, elixir-format
msgid "Tasks"
@@ -2510,7 +2510,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/misc_editor.ex:747
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:286
#: lib/bds/desktop/shell_live/sidebar_components.ex:524
#: lib/bds/desktop/shell_live/sidebar_components.ex:660
#: lib/bds/desktop/shell_live/sidebar_delete.ex:179
#: lib/bds/ui/registry.ex:134
#, elixir-autogen, elixir-format
@@ -2661,9 +2661,9 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:558
#: lib/bds/desktop/shell_live/media_editor.ex:563
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:76
#: lib/bds/desktop/shell_live/post_editor.ex:691
#: lib/bds/desktop/shell_live/post_editor.ex:720
#: lib/bds/desktop/shell_live/post_editor.ex:696
#: lib/bds/desktop/shell_live/post_editor.ex:725
#: lib/bds/desktop/shell_live/post_editor.ex:730
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:60
#, elixir-autogen, elixir-format
msgid "Translate"
@@ -2740,7 +2740,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/media_editor.ex:701
#: lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex:10
#: lib/bds/desktop/shell_live/post_editor.ex:890
#: lib/bds/desktop/shell_live/post_editor.ex:895
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:7
#, elixir-autogen, elixir-format
msgid "Unsaved"
@@ -2748,7 +2748,7 @@ msgstr ""
#: lib/bds/desktop/shell_live/import_editor.ex:867
#: lib/bds/desktop/shell_live/post_editor/post_metadata.ex:166
#: lib/bds/ui/sidebar.ex:1014
#: lib/bds/ui/sidebar.ex:1105
#, elixir-autogen, elixir-format
msgid "Untitled"
msgstr ""
@@ -2842,14 +2842,12 @@ msgid "WYSIWYG"
msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:791
#, elixir-autogen, elixir-format
msgid "Working tree"
msgstr ""
#: lib/bds/desktop/shell_live/tab_helpers.ex:191
#: lib/bds/ui/sidebar.ex:785
#: lib/bds/ui/sidebar.ex:792
#: lib/bds/ui/sidebar.ex:787
#, elixir-autogen, elixir-format
msgid "Working tree and history"
msgstr ""
@@ -3192,12 +3190,12 @@ msgstr ""
msgid "Comparing database and filesystem metadata"
msgstr ""
#: lib/bds/desktop/shell_live.ex:650
#: lib/bds/desktop/shell_live.ex:671
#, elixir-autogen, elixir-format
msgid "Added %{count} images to post"
msgstr ""
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:642
#, elixir-autogen, elixir-format
msgid "Added %{title}"
msgstr ""
@@ -3217,18 +3215,18 @@ msgstr ""
msgid "Image Import Concurrency"
msgstr ""
#: lib/bds/desktop/shell_live.ex:408
#: lib/bds/desktop/shell_live.ex:421
#: lib/bds/desktop/shell_live.ex:621
#: lib/bds/desktop/shell_live.ex:649
#: lib/bds/desktop/shell_live.ex:658
#: lib/bds/desktop/shell_live.ex:665
#: lib/bds/desktop/shell_live.ex:429
#: lib/bds/desktop/shell_live.ex:442
#: lib/bds/desktop/shell_live.ex:642
#: lib/bds/desktop/shell_live.ex:670
#: lib/bds/desktop/shell_live.ex:679
#: lib/bds/desktop/shell_live.ex:686
#: lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex:407
#, elixir-autogen, elixir-format
msgid "Add Gallery Images"
msgstr ""
#: lib/bds/desktop/shell_live.ex:666
#: lib/bds/desktop/shell_live.ex:687
#, elixir-autogen, elixir-format
msgid "Failed to process %{path}: %{reason}"
msgstr ""
@@ -3243,12 +3241,12 @@ msgstr ""
msgid "Move this post to the archive"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:596
#: lib/bds/desktop/shell_live/post_editor.ex:601
#, elixir-autogen, elixir-format
msgid "Post archived"
msgstr ""
#: lib/bds/desktop/shell_live/post_editor.ex:629
#: lib/bds/desktop/shell_live/post_editor.ex:634
#, elixir-autogen, elixir-format
msgid "Post unarchived"
msgstr ""
@@ -3262,3 +3260,157 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Unarchive"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:531
#, elixir-autogen, elixir-format
msgid "Ahead"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:532
#, elixir-autogen, elixir-format
msgid "Behind"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:550
#, elixir-autogen, elixir-format
msgid "Changes"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1055
#: lib/bds/desktop/shell_live.ex:1071
#: lib/bds/desktop/shell_live.ex:1077
#: lib/bds/desktop/shell_live/sidebar_components.ex:556
#, elixir-autogen, elixir-format
msgid "Commit"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:555
#, elixir-autogen, elixir-format
msgid "Commit message"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1056
#, elixir-autogen, elixir-format
msgid "Commit message is required"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1120
#, elixir-autogen, elixir-format
msgid "Done"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1041
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#: lib/bds/desktop/shell_live/sidebar_components.ex:542
#, elixir-autogen, elixir-format
msgid "Fetch"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:586
#, elixir-autogen, elixir-format
msgid "History"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1091
#: lib/bds/desktop/shell_live.ex:1099
#: lib/bds/desktop/shell_live/sidebar_components.ex:513
#, elixir-autogen, elixir-format
msgid "Initialize Git"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:536
#, elixir-autogen, elixir-format
msgid "Local only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1127
#, elixir-autogen, elixir-format
msgid "No active project"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:580
#, elixir-autogen, elixir-format
msgid "No changes"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:616
#, elixir-autogen, elixir-format
msgid "No commits yet"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:613
#, elixir-autogen, elixir-format
msgid "Older history available"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1044
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#: lib/bds/desktop/shell_live/sidebar_components.ex:545
#, elixir-autogen, elixir-format
msgid "Prune LFS"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1042
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#: lib/bds/desktop/shell_live/sidebar_components.ex:543
#, elixir-autogen, elixir-format
msgid "Pull"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1043
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#: lib/bds/desktop/shell_live/sidebar_components.ex:544
#, elixir-autogen, elixir-format
msgid "Push"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:509
#, elixir-autogen, elixir-format
msgid "Remote URL (optional)"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:537
#, elixir-autogen, elixir-format
msgid "Remote only"
msgstr ""
#: lib/bds/desktop/shell_live.ex:1092
#, elixir-autogen, elixir-format
msgid "Repository initialized"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:535
#, elixir-autogen, elixir-format
msgid "Synced"
msgstr ""
#: lib/bds/desktop/shell_live/sidebar_components.ex:504
#, elixir-autogen, elixir-format
msgid "This project is not a Git repository yet."
msgstr ""
#: lib/bds/ui/sidebar.ex:869
#, elixir-autogen, elixir-format
msgid "added"
msgstr ""
#: lib/bds/ui/sidebar.ex:870
#, elixir-autogen, elixir-format
msgid "deleted"
msgstr ""
#: lib/bds/ui/sidebar.ex:871
#: lib/bds/ui/sidebar.ex:874
#, elixir-autogen, elixir-format
msgid "modified"
msgstr ""
#: lib/bds/ui/sidebar.ex:872
#, elixir-autogen, elixir-format
msgid "renamed"
msgstr ""
#: lib/bds/ui/sidebar.ex:873
#, elixir-autogen, elixir-format
msgid "untracked"
msgstr ""

View File

@@ -0,0 +1,65 @@
/* Styling for the self-contained PagefindUI search widget. */
.pagefind-ui {
display: block;
font-size: 0.95rem;
}
.pagefind-ui__form {
margin: 0 0 0.75rem 0;
}
.pagefind-ui__search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--pico-form-element-border-color, #ccc);
border-radius: 0.375rem;
font: inherit;
box-sizing: border-box;
}
.pagefind-ui__results {
display: block;
}
.pagefind-ui__message {
margin: 0.5rem 0;
opacity: 0.7;
}
.pagefind-ui__result-list {
list-style: none;
margin: 0;
padding: 0;
}
.pagefind-ui__result {
padding: 0.5rem 0;
border-bottom: 1px solid var(--pico-muted-border-color, #eee);
}
.pagefind-ui__result:last-child {
border-bottom: none;
}
.pagefind-ui__result-link {
display: block;
font-weight: 600;
text-decoration: none;
}
.pagefind-ui__result-link:hover {
text-decoration: underline;
}
.pagefind-ui__result-excerpt {
margin: 0.25rem 0 0 0;
opacity: 0.8;
line-height: 1.4;
}
.pagefind-ui__result-excerpt mark {
background: var(--pico-mark-background-color, #ffe08a);
color: inherit;
padding: 0 0.1em;
border-radius: 0.15em;
}

View File

@@ -0,0 +1,272 @@
/*
* Self-contained client-side search UI for generated blog output.
*
* Exposes a global `PagefindUI` constructor that the bundled
* `search-runtime.js` instantiates. It fetches the per-language fragment
* index (`index.json`) co-located with this script, performs full-text
* matching over the indexed post fragments, and renders ranked results.
*
* No external/CDN dependencies — everything needed ships in this file.
*/
(function () {
"use strict";
// Resolve the sibling index.json relative to this script's own URL so the
// same bundle works for every language directory (e.g. /pagefind/,
// /de/pagefind/) without baking the path in at generation time.
var scriptSrc = (document.currentScript && document.currentScript.src) || "";
function resolveIndexUrl(src) {
try {
return new URL("index.json", src).href;
} catch (err) {
return "index.json";
}
}
function tokenize(value) {
return (value || "")
.toLowerCase()
.split(/[^\p{L}\p{N}]+/u)
.filter(function (token) {
return token.length > 0;
});
}
function PagefindUI(options) {
options = options || {};
this.element =
typeof options.element === "string"
? document.querySelector(options.element)
: options.element;
if (!this.element) {
return;
}
var translations = options.translations || {};
this.placeholder = translations.placeholder || "Search";
this.zeroResults = translations.zero_results || "No results found";
this.indexUrl = options.indexUrl || resolveIndexUrl(scriptSrc);
this.pages = null;
this.loadPromise = null;
this.render();
}
PagefindUI.prototype.render = function () {
var self = this;
this.element.classList.add("pagefind-ui");
this.element.innerHTML = "";
var form = document.createElement("form");
form.className = "pagefind-ui__form";
form.setAttribute("role", "search");
form.addEventListener("submit", function (event) {
event.preventDefault();
});
var input = document.createElement("input");
input.type = "search";
input.className = "pagefind-ui__search-input";
input.setAttribute("autocomplete", "off");
input.placeholder = this.placeholder;
input.setAttribute("aria-label", this.placeholder);
var results = document.createElement("div");
results.className = "pagefind-ui__results";
form.appendChild(input);
this.element.appendChild(form);
this.element.appendChild(results);
this.input = input;
this.results = results;
var debounce = null;
input.addEventListener("input", function () {
window.clearTimeout(debounce);
debounce = window.setTimeout(function () {
self.search(input.value);
}, 120);
});
};
PagefindUI.prototype.load = function () {
if (this.pages) {
return Promise.resolve(this.pages);
}
if (this.loadPromise) {
return this.loadPromise;
}
var self = this;
this.loadPromise = fetch(this.indexUrl, { credentials: "same-origin" })
.then(function (response) {
if (!response.ok) {
throw new Error("pagefind index request failed: " + response.status);
}
return response.json();
})
.then(function (data) {
self.pages = (data && data.pages) || [];
return self.pages;
})
.catch(function () {
self.pages = [];
return self.pages;
});
return this.loadPromise;
};
PagefindUI.prototype.search = function (query) {
var self = this;
var terms = tokenize(query);
if (terms.length === 0) {
this.results.innerHTML = "";
return;
}
this.load().then(function (pages) {
var matches = [];
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
var title = (page.title || "").toLowerCase();
var body = (page.text || "").toLowerCase();
var score = 0;
var matchedAll = true;
for (var t = 0; t < terms.length; t++) {
var term = terms[t];
var bodyHits = body.split(term).length - 1;
var titleHits = title.split(term).length - 1;
if (bodyHits === 0 && titleHits === 0) {
matchedAll = false;
break;
}
// Title matches are weighted more heavily than body matches.
score += bodyHits + titleHits * 5;
}
if (matchedAll) {
matches.push({ page: page, score: score });
}
}
matches.sort(function (a, b) {
return b.score - a.score;
});
self.renderResults(matches.slice(0, 10), terms);
});
};
PagefindUI.prototype.renderResults = function (matches, terms) {
this.results.innerHTML = "";
if (matches.length === 0) {
var message = document.createElement("p");
message.className = "pagefind-ui__message";
message.textContent = this.zeroResults;
this.results.appendChild(message);
return;
}
var list = document.createElement("ol");
list.className = "pagefind-ui__result-list";
for (var i = 0; i < matches.length; i++) {
var page = matches[i].page;
var item = document.createElement("li");
item.className = "pagefind-ui__result";
var link = document.createElement("a");
link.className = "pagefind-ui__result-link";
link.href = page.url;
link.textContent = page.title || page.url;
var excerpt = document.createElement("p");
excerpt.className = "pagefind-ui__result-excerpt";
buildExcerpt(excerpt, page.text || "", terms);
item.appendChild(link);
item.appendChild(excerpt);
list.appendChild(item);
}
this.results.appendChild(list);
};
// Build an excerpt around the first matching term and highlight all terms
// with <mark>, using safe text nodes (never innerHTML on untrusted text).
function buildExcerpt(target, text, terms) {
var lower = text.toLowerCase();
var firstHit = -1;
for (var t = 0; t < terms.length; t++) {
var idx = lower.indexOf(terms[t]);
if (idx !== -1 && (firstHit === -1 || idx < firstHit)) {
firstHit = idx;
}
}
var start = firstHit === -1 ? 0 : Math.max(0, firstHit - 60);
var snippet = text.slice(start, start + 220);
if (start > 0) {
snippet = "…" + snippet;
}
if (start + 220 < text.length) {
snippet = snippet + "…";
}
highlightInto(target, snippet, terms);
}
function highlightInto(target, snippet, terms) {
var escaped = terms
.map(function (term) {
return term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
})
.filter(function (term) {
return term.length > 0;
});
if (escaped.length === 0) {
target.appendChild(document.createTextNode(snippet));
return;
}
var pattern = new RegExp("(" + escaped.join("|") + ")", "giu");
var lastIndex = 0;
var match;
while ((match = pattern.exec(snippet)) !== null) {
if (match.index > lastIndex) {
target.appendChild(
document.createTextNode(snippet.slice(lastIndex, match.index))
);
}
var mark = document.createElement("mark");
mark.textContent = match[0];
target.appendChild(mark);
lastIndex = match.index + match[0].length;
// Guard against zero-length matches looping forever.
if (match.index === pattern.lastIndex) {
pattern.lastIndex++;
}
}
if (lastIndex < snippet.length) {
target.appendChild(document.createTextNode(snippet.slice(lastIndex)));
}
}
window.PagefindUI = PagefindUI;
})();

View File

@@ -15,11 +15,12 @@
}
initialized = true;
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
var zeroResults = root.getAttribute('data-search-no-results') || 'No results found';
new PagefindUI({
element: root,
showSubResults: true,
showImages: false,
translations: { placeholder: placeholder }
translations: { placeholder: placeholder, zero_results: zeroResults }
});
var input = root.querySelector('input');
if (input) {

View File

@@ -0,0 +1,33 @@
defmodule BDS.Repo.Migrations.ConvertEmbeddingVectorToBlob do
use Ecto.Migration
# Embedding vectors are now persisted as a packed little-endian Float32 BLOB
# (VectorCacheInDb invariant) instead of JSON text. The table is a rebuildable
# cache and the previous lexical vectors are incompatible with the neural
# model, so we drop and recreate it; rows are re-embedded on next index.
def up do
drop table(:embedding_keys)
create_embedding_keys(:binary)
end
def down do
drop table(:embedding_keys)
create_embedding_keys(:text)
end
defp create_embedding_keys(vector_type) do
create table(:embedding_keys, primary_key: false) do
add :label, :integer, primary_key: true
add :post_id, references(:posts, column: :id, type: :string, on_delete: :delete_all),
null: false
add :project_id, references(:projects, type: :string, on_delete: :delete_all), null: false
add :content_hash, :string, null: false
add :vector, vector_type
end
create index(:embedding_keys, [:post_id])
create index(:embedding_keys, [:project_id])
end
end

View File

@@ -15,7 +15,7 @@
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div>
</div>
</nav>
@@ -35,7 +35,7 @@
</svg>
</button>
<div class="blog-search-panel" data-blog-search-panel hidden>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}"></div>
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ labels.search_placeholder }}" data-search-no-results="{{ labels.search_no_results }}"></div>
</div>
</div>
{% endif %}

View File

@@ -48,6 +48,9 @@ value EmbeddingModel {
-- Lazy-loaded: pipeline created on first embedding request, not at startup
-- Text preprocessing: prefix all input with "query: " (e5 convention)
-- Pooling: mean pooling + L2 normalization
-- Loaded on-device via Bumblebee+EXLA; the canonical e5 weights come from
-- the "intfloat/multilingual-e5-small" repository, surfaced under the
-- "Xenova/multilingual-e5-small" model_id identifier.
model_id: String -- "Xenova/multilingual-e5-small"
dimensions: Integer -- 384
}
@@ -60,7 +63,7 @@ value EmbeddingVector {
-- ─── Entities ───────────────────────────────────────────────
entity EmbeddingKey {
label: Integer -- HNSW label for USearch
label: Integer -- HNSW node label / id
post: post/Post
content_hash: String -- SHA-256 of "{title}\n\n{content}"
vector: EmbeddingVector
@@ -72,9 +75,11 @@ entity DismissedDuplicatePair {
-- IDs stored in canonical order (sorted) for dedup
}
-- ─── USearch HNSW Index ─────────────────────────────────────
-- ─── HNSW Index ─────────────────────────────────────────────
config {
-- HNSW approximate-nearest-neighbour index (hnswlib). USearch has no Elixir
-- binding; hnswlib provides the same HNSW algorithm and parameters.
model_id: String = "Xenova/multilingual-e5-small"
embedding_dimensions: Integer = 384
hnsw_metric: String = "cosine"
@@ -83,7 +88,10 @@ config {
hnsw_expansion_search: Integer = 64 -- efSearch
debounce_persist: Duration = 5.seconds
-- Index file: {userData}/projects/{projectId}/embeddings.usearch
-- Key mapping is persisted alongside the embedding records
-- Key mapping (label → post_id) persisted in a sidecar (.meta.json) next
-- to the index file, plus the source-of-truth rows in embedding_keys
batch_size: Integer = 16 -- texts per batched inference run
sequence_length: Integer = 256 -- max tokens per input (truncated)
}
-- ─── Gating ─────────────────────────────────────────────────
@@ -107,7 +115,7 @@ rule EmbedPost {
let existing = EmbeddingKey{post: post}
if not exists existing or existing.content_hash != hash:
-- Compute embedding vector via local model
-- Upsert into USearch index + embedding_keys DB table
-- Upsert into HNSW index + embedding_keys DB table
-- Debounced index save (5s)
ensures: EmbeddingKeyUpdated(post)
}
@@ -146,9 +154,9 @@ rule IndexUnindexed {
rule FindSimilar {
when: FindSimilarRequested(post, limit)
requires: semantic_similarity_enabled
-- HNSW approximate nearest neighbor search via USearch
-- HNSW approximate nearest neighbor search (hnswlib)
-- Searches index for (limit + 1) neighbors, excludes self
-- Converts USearch cosine distance to similarity: max(0, 1 - distance)
-- Converts HNSW cosine distance to similarity: max(0, 1 - distance)
-- Returns ranked list sorted by descending similarity
ensures: SimilarPostsResult(post, ranked_matches)
}
@@ -157,7 +165,7 @@ rule ComputeSimilarities {
when: ComputeSimilaritiesRequested(source_post, target_post_ids)
requires: semantic_similarity_enabled
-- Exact pairwise cosine similarity between source vector and each target vector
-- Uses in-memory vector cache, NOT USearch search
-- Uses in-memory vector cache, NOT the HNSW index
-- Returns map of post_id -> similarity score
-- Used by InsertPostLinkModal to rank FTS search results
ensures: SimilarityScoresResult(source_post, scores)
@@ -202,7 +210,7 @@ invariant ContentHashSkipsUnchanged {
}
invariant DebouncedPersistence {
-- USearch index persistence is debounced at 5 seconds
-- HNSW index persistence is debounced at 5 seconds
-- Prevents excessive disk I/O during bulk operations
-- Index also force-saved on project switch and app shutdown
}
@@ -221,6 +229,19 @@ invariant RealNeuralModel {
-- This is only achievable with the trained multilingual transformer model.
}
invariant NativeAcceleratedExecution {
-- Model execution MUST use the platform's native hardware acceleration
-- where available (GPU/Metal/Neural Engine on Apple Silicon, CUDA on
-- NVIDIA, etc.), and otherwise fall back to optimised native CPU execution.
-- Inference MUST be batched: batch_size inputs are run per compiled
-- inference pass and inputs are truncated to a bounded sequence_length, so
-- (re)indexing many posts is not serialised one document at a time.
-- Current implementation: Bumblebee + EXLA, which is native CPU on Apple
-- Silicon (XLA has no Metal backend); neighbour search is HNSW (hnswlib).
-- Apple GPU acceleration via EMLX/MLX is tracked as a follow-up
-- (SPECGAPS A1-14c).
}
invariant ModelCaching {
-- Model files (~100 MB) downloaded from Hugging Face Hub on first use
-- Cached in app data directory, persists across sessions
@@ -228,7 +249,7 @@ invariant ModelCaching {
}
invariant ProjectIsolation {
-- Each project has its own USearch index file and embedding_keys rows
-- Each project has its own HNSW index file and embedding_keys rows
-- On project switch: save current index, load new project's index
-- Model pipeline shared across projects (not reloaded)
}

View File

@@ -286,15 +286,16 @@ defmodule BDS.AITest do
end
test "model_capabilities does not create atoms from unknown capability keys" do
before = :erlang.system_info(:atom_count)
fictive_key = "csm001_fictive_#{:erlang.unique_integer()}"
another_key = "another_unknown_#{:erlang.unique_integer()}"
malicious_caps =
Jason.encode!(%{
"supports_attachment" => true,
"supports_tool_calls" => false,
"disables_reasoning" => false,
"csm001_fictive_#{:erlang.unique_integer()}" => true,
"another_unknown_#{:erlang.unique_integer()}" => 42
fictive_key => true,
another_key => 42
})
:ok = BDS.AI.SettingsStore.put_setting("ai.model_capabilities.csm-test-model", malicious_caps)
@@ -303,8 +304,9 @@ defmodule BDS.AITest do
assert result.supports_attachment == true
assert result.supports_tool_calls == false
after_count = :erlang.system_info(:atom_count)
assert after_count == before
# Unknown keys must remain as strings, not converted to atoms
assert_raise ArgumentError, fn -> String.to_existing_atom(fictive_key) end
assert_raise ArgumentError, fn -> String.to_existing_atom(another_key) end
end
test "put_endpoint, get_endpoint, and delete_endpoint manage encrypted endpoint settings" do

View File

@@ -31,7 +31,7 @@ defmodule BDS.CSM013BangRenderingTest do
assert {:error, _reason} = result
end
test "render_post_page returns {:error, _} for broken template source", %{project: project} do
test "publish_template rejects broken template source", %{project: project} do
{:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -40,30 +40,7 @@ defmodule BDS.CSM013BangRenderingTest do
content: "{% if true %}unclosed if"
})
{:ok, published_template} = BDS.Templates.publish_template(template.id)
{:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Test Post",
content: "Body",
language: "en",
template_slug: published_template.slug
})
{:ok, published_post} = BDS.Posts.publish_post(post.id)
result =
Rendering.render_post_page(project.id, published_template.slug, %{
id: published_post.id,
title: published_post.title,
content: published_post.content || "",
slug: published_post.slug,
language: "en",
template_slug: published_post.template_slug
})
assert {:error, _reason} = result
assert {:error, {:invalid_liquid, _reason}} = BDS.Templates.publish_template(template.id)
end
end

View File

@@ -7,7 +7,7 @@ defmodule BDS.CSM025HardcodedLanguagesTest do
%{language: main, blog_languages: languages}
end
defp html(path), do: {path, "<p>hello</p>"}
defp html(path), do: {path, "<article data-pagefind-body><p>hello</p></article>"}
describe "build_outputs/2 derives language prefixes from plan" do
test "no hardcoded language prefixes in source" do

View File

@@ -34,9 +34,13 @@ defmodule BDS.CSM033BatchInsertsTest do
"expected ON CONFLICT upsert clause"
end
test "compute_key_data is used instead of individual Repo.insert_or_update", %{source: source} do
assert source =~ "compute_key_data(post, existing_key, next_label)",
"expected compute_key_data helper for row computation"
test "build_key_rows computes rows for batched upsert instead of individual Repo.insert_or_update",
%{source: source} do
assert source =~ "build_key_rows(posts, existing_keys",
"expected build_key_rows helper for batched row computation"
assert source =~ "embed_many(",
"expected batched embedding via embed_many"
end
end

View File

@@ -44,32 +44,32 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.click(session, "[data-testid='toggle-sidebar']")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.sidebar_visible == false))
assert snapshot.sidebar_visible == false
assert :ok = Automation.press(session, "Meta+B")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.sidebar_visible == true))
assert snapshot.sidebar_visible == true
assert :ok = Automation.press(session, "Meta+J")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.panel_visible == true))
assert snapshot.panel_visible == true
assert :ok = Automation.press(session, "Meta+J")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.panel_visible == false))
assert snapshot.panel_visible == false
assert :ok = Automation.press(session, "Meta+,")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.editor_title == "Settings"))
assert snapshot.editor_title == "Settings"
assert :ok = Automation.click(session, "[data-testid='toggle-assistant']")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.assistant_visible == true))
assert snapshot.assistant_visible == true
assert snapshot.panel_visible == false
@@ -92,7 +92,7 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.drag(session, "[data-resize='sidebar']", 90)
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.sidebar_width >= 360 and &1.sidebar_width <= 380))
assert snapshot.sidebar_width >= 360
assert snapshot.sidebar_width <= 380
@@ -100,7 +100,7 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.reload(session)
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.sidebar_visible == true and &1.sidebar_width >= resized_width - 2))
assert snapshot.sidebar_visible == true
assert snapshot.sidebar_width >= resized_width - 2
assert snapshot.sidebar_width <= resized_width + 2
@@ -140,12 +140,12 @@ defmodule BDS.Desktop.AutomationTest do
assert :ok = Automation.native_menu_action(session, "toggle_sidebar")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.sidebar_visible == false))
assert snapshot.sidebar_visible == false
assert :ok = Automation.native_menu_action(session, "edit_preferences")
snapshot = Automation.snapshot(session)
snapshot = await(session, &(&1.editor_title == "Settings"))
assert snapshot.editor_title == "Settings"
end
@@ -175,6 +175,24 @@ defmodule BDS.Desktop.AutomationTest do
end
end
# Polls snapshots until the predicate holds (or times out), returning the
# last snapshot. UI transitions after a keypress/click/menu action are
# asynchronous, so a single immediate snapshot can race under CPU load.
defp await(session, fun, timeout \\ 5_000)
defp await(session, _fun, timeout) when timeout <= 0, do: Automation.snapshot(session)
defp await(session, fun, timeout) do
snapshot = Automation.snapshot(session)
if fun.(snapshot) do
snapshot
else
Process.sleep(50)
await(session, fun, timeout - 50)
end
end
defp wait_until(fun, timeout \\ 5_000)
defp wait_until(fun, timeout) when timeout <= 0, do: fun.()

View File

@@ -241,7 +241,8 @@ defmodule BDS.Desktop.ShellLiveTest do
edit_draft: %{"name" => "news", "color" => "#3b82f6", "post_template_slug" => ""},
selected: ["news", "updates"],
merge_target: "news",
templates: [%{slug: "post-template", title: "Post Template"}]
templates: [%{slug: "post-template", title: "Post Template"}],
colour_presets: BDS.Desktop.ShellLive.TagsEditor.colour_presets()
}
}
end
@@ -283,17 +284,44 @@ defmodule BDS.Desktop.ShellLiveTest do
@tag :phase3
test "phase 3 editors and shared surfaces render utility-owned layouts" do
post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns())
template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns())
chat_html = render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html = render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns())
tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
post_html =
render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
media_html =
render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
script_html =
render_component(
&BDS.Desktop.ShellLive.ScriptEditor.render/1,
phase3_script_editor_assigns()
)
template_html =
render_component(
&BDS.Desktop.ShellLive.TemplateEditor.render/1,
phase3_template_editor_assigns()
)
chat_html =
render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html =
render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
settings_html =
render_component(
&BDS.Desktop.ShellLive.SettingsEditor.render/1,
phase3_settings_editor_assigns()
)
tags_html =
render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
assert post_html =~ "post-editor ui-editor-shell flex h-full min-h-0 flex-col"
assert post_html =~ "editor-header ui-editor-header flex shrink-0 items-start justify-between gap-3"
assert post_html =~
"editor-header ui-editor-header flex shrink-0 items-start justify-between gap-3"
assert post_html =~ "editor-field ui-field-stack flex flex-col gap-1.5"
assert post_html =~ "editor-toolbar ui-toolbar flex items-center gap-3"
@@ -312,7 +340,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert menu_html =~ "menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4"
assert menu_html =~ "menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2"
assert settings_html =~ "settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden"
assert settings_html =~
"settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden"
assert settings_html =~ "settings-header flex shrink-0 items-center justify-between gap-3"
assert tags_html =~ "tags-view-shell flex h-full min-h-0 flex-col overflow-hidden"
@@ -324,12 +354,32 @@ defmodule BDS.Desktop.ShellLiveTest do
conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint)
{:ok, view, _shell_html} = live_isolated(conn, BDS.Desktop.ShellLive)
post_html = render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
script_html = render_component(&BDS.Desktop.ShellLive.ScriptEditor.render/1, phase3_script_editor_assigns())
template_html = render_component(&BDS.Desktop.ShellLive.TemplateEditor.render/1, phase3_template_editor_assigns())
settings_html = render_component(&BDS.Desktop.ShellLive.SettingsEditor.render/1, phase3_settings_editor_assigns())
tags_html = render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
post_html =
render_component(&BDS.Desktop.ShellLive.PostEditor.render/1, phase3_post_editor_assigns())
media_html =
render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
script_html =
render_component(
&BDS.Desktop.ShellLive.ScriptEditor.render/1,
phase3_script_editor_assigns()
)
template_html =
render_component(
&BDS.Desktop.ShellLive.TemplateEditor.render/1,
phase3_template_editor_assigns()
)
settings_html =
render_component(
&BDS.Desktop.ShellLive.SettingsEditor.render/1,
phase3_settings_editor_assigns()
)
tags_html =
render_component(&BDS.Desktop.ShellLive.TagsEditor.render/1, phase3_tags_editor_assigns())
panel_html =
render_component(&BDS.Desktop.ShellLive.PanelRenderer.render_panel_body/1, %{
@@ -354,7 +404,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert script_html =~ ~s(class="status-badge ui-badge)
assert script_html =~ ~s(class="ui-input")
assert template_html =~ ~s(class="secondary templates-save-button ui-button ui-button-secondary)
assert template_html =~
~s(class="secondary templates-save-button ui-button ui-button-secondary)
assert template_html =~ ~s(class="status-badge ui-badge)
assert template_html =~ ~s(class="ui-input")
@@ -382,27 +434,44 @@ defmodule BDS.Desktop.ShellLiveTest do
conn = Plug.Conn.put_private(build_conn(), :phoenix_endpoint, BDS.Desktop.Endpoint)
{:ok, _view, shell_html} = live_isolated(conn, BDS.Desktop.ShellLive)
media_html = render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
chat_html = render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html = render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
media_html =
render_component(&BDS.Desktop.ShellLive.MediaEditor.render/1, phase3_media_editor_assigns())
chat_html =
render_component(&BDS.Desktop.ShellLive.ChatEditor.render/1, phase3_chat_editor_assigns())
menu_html =
render_component(&BDS.Desktop.ShellLive.MenuEditor.render/1, phase3_menu_editor_assigns())
assert shell_html =~ ~s(class="assistant-sidebar-context flex shrink-0 flex-col gap-2")
assert shell_html =~ ~s(class="assistant-sidebar-prompt min-h-[8rem] w-full resize-y")
assert shell_html =~ ~s(class="assistant-sidebar-start-button ui-button ui-button-primary")
assert shell_html =~ ~s(class="assistant-sidebar-welcome min-h-0 flex-1 overflow-auto")
assert media_html =~ "class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert media_html =~
"class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert media_html =~ "class=\"media-preview flex min-h-[16rem] items-center justify-center\""
assert media_html =~ ~s(class="media-details min-w-0")
assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col")
assert chat_html =~ ~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert chat_html =~ ~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
assert chat_html =~
~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert chat_html =~
~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
assert chat_html =~ ~s(class="chat-input-wrapper flex items-end gap-2")
assert menu_html =~ ~s(class="menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4")
assert menu_html =~ ~s(class="menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2")
assert menu_html =~ ~s(class="menu-editor-empty flex min-h-0 flex-1 items-center justify-center")
assert menu_html =~
~s(class="menu-editor-view ui-editor-shell flex h-full min-h-0 flex-col p-4")
assert menu_html =~
~s(class="menu-editor-toolbar ui-toolbar flex flex-wrap items-center gap-2")
assert menu_html =~
~s(class="menu-editor-empty flex min-h-0 flex-1 items-center justify-center")
end
alias BDS.Persistence
@@ -1071,7 +1140,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(data-testid="status-bar")
assert html =~ ~s(data-testid="status-task-button")
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="activity-button")
assert html =~ ~s(data-view="posts")
assert html =~ ~s(data-view="media")
@@ -1103,7 +1175,10 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click()
assert html =~ ~s(data-region="panel")
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~ ~s(data-testid="panel-close")
html =
@@ -1111,7 +1186,8 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='panel-close']")
|> render_click()
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html =
view
@@ -2034,13 +2110,17 @@ defmodule BDS.Desktop.ShellLiveTest do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "b", meta: true})
assert html =~ ~s(class="sidebar-shell flex min-w-0 shrink-0 overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "j", meta: true})
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html = render_keydown(view, "shortcut", %{key: "2", meta: true})
assert html =~ ~s(data-view="media")
@@ -2275,14 +2355,17 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Add published posts"
html = render_click(view, "select_panel_tab", %{"tab" => "output"})
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
html =
view
|> element("[data-testid='status-task-button']")
|> render_click()
refute html =~ ~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
refute html =~
~s(class="panel-shell flex min-h-0 shrink-0 flex-col overflow-hidden is-hidden")
assert Regex.match?(
~r/<button class="panel-tab [^"]*ui-tab[^"]*active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">/,
@@ -3384,13 +3467,22 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published"
})
assert published_script_html =~ ~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_script_html =~
~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_script_html =~ ~s(data-testid="script-editor")
assert published_script_html =~ ~s(data-testid="script-status-badge")
assert published_script_html =~ ~s(class="status-badge ui-badge status-published")
assert published_script_html =~ ~s(class="secondary scripts-save-button ui-button ui-button-secondary")
assert published_script_html =~ ~s(class="secondary scripts-run-button ui-button ui-button-secondary")
assert published_script_html =~ ~s(class="secondary scripts-check-button ui-button ui-button-secondary")
assert published_script_html =~
~s(class="secondary scripts-save-button ui-button ui-button-secondary")
assert published_script_html =~
~s(class="secondary scripts-run-button ui-button ui-button-secondary")
assert published_script_html =~
~s(class="secondary scripts-check-button ui-button ui-button-secondary")
assert published_script_html =~ "published"
assert published_script_html =~ "published script"
@@ -3405,12 +3497,19 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "published"
})
assert published_template_html =~ ~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_template_html =~
~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert published_template_html =~ ~s(data-testid="template-editor")
assert published_template_html =~ ~s(data-testid="template-status-badge")
assert published_template_html =~ ~s(class="status-badge ui-badge status-published")
assert published_template_html =~ ~s(class="secondary templates-save-button ui-button ui-button-secondary")
assert published_template_html =~ ~s(class="secondary templates-validate-button ui-button ui-button-secondary")
assert published_template_html =~
~s(class="secondary templates-save-button ui-button ui-button-secondary")
assert published_template_html =~
~s(class="secondary templates-validate-button ui-button ui-button-secondary")
assert published_template_html =~ "published"
assert published_template_html =~ "published template"
@@ -3599,7 +3698,8 @@ defmodule BDS.Desktop.ShellLiveTest do
})
assert html =~
"class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
"class=\"editor-content media-editor grid min-h-0 flex-1 gap-4 overflow-auto p-4 xl:grid-cols-[minmax(320px,1fr)_minmax(0,1.2fr)]\""
assert html =~ ~s(class="quick-actions-wrapper relative")
refute html =~ ~s(class="media-editor-form")
@@ -3624,7 +3724,10 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click()
assert html =~ ~s(class="translation-modal-backdrop")
assert html =~ ~s(class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden")
assert html =~
~s(class="translation-modal flex max-h-[80vh] w-full max-w-2xl flex-col overflow-hidden")
assert html =~ ~s(name="media_translation[title]")
assert html =~ ~s(name="media_translation[alt]")
assert html =~ ~s(name="media_translation[caption]")
@@ -3712,7 +3815,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => "Project settings"
})
assert settings_html =~ ~s(class="settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden")
assert settings_html =~
~s(class="settings-view-shell ui-editor-shell flex h-full min-h-0 flex-col overflow-hidden")
assert settings_html =~ ~s(class="setting-section ui-section-card")
refute settings_html =~ "Desktop workbench content routed through the Elixir shell."
@@ -3748,7 +3853,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => script.slug
})
assert script_html =~ ~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert script_html =~
~s(class="scripts-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert script_html =~ "scripts-monaco"
assert script_html =~ ~s(data-monaco-language="lua")
assert script_html =~ ~s(data-monaco-word-wrap="on")
@@ -3763,7 +3870,9 @@ defmodule BDS.Desktop.ShellLiveTest do
"subtitle" => template.slug
})
assert template_html =~ ~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert template_html =~
~s(class="templates-view-shell ui-editor-shell flex h-full min-h-0 flex-col")
assert template_html =~ "templates-monaco"
assert template_html =~ ~s(data-monaco-language="liquid")
assert template_html =~ ~s(data-monaco-word-wrap="on")
@@ -3779,7 +3888,10 @@ defmodule BDS.Desktop.ShellLiveTest do
})
assert chat_html =~ ~s(class="chat-panel ui-editor-shell flex h-full min-h-0 flex-col")
assert chat_html =~ ~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
assert chat_html =~
~s(class="chat-input-container ui-field-stack flex shrink-0 flex-col gap-3")
refute chat_html =~ "Desktop workbench content routed through the Elixir shell."
end
@@ -3799,7 +3911,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-testid="chat-model-selector-button")
assert html =~ ~s(class="chat-panel-title-main")
assert html =~ ~s(class="chat-model-selector-wrap relative shrink-0")
assert html =~ ~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
assert html =~
~s(class="chat-model-selector-button chat-model-selector-inline ui-button ui-button-secondary inline-flex items-center gap-2")
refute html =~ ~s(class="chat-panel-header-actions")
css = desktop_css_source()
@@ -3847,7 +3962,9 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='chat-model-selector-button']")
|> render_click()
assert selector_html =~ ~s(class="chat-model-selector-menu ui-dropdown-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col")
assert selector_html =~
~s(class="chat-model-selector-menu ui-dropdown-menu absolute right-0 top-full z-10 mt-2 flex min-w-56 flex-col")
assert selector_html =~ ~s(data-testid="chat-model-selector-option")
assert selector_html =~ "llama-current"
@@ -4699,7 +4816,9 @@ defmodule BDS.Desktop.ShellLiveTest do
{user_index, _length} = :binary.match(html, "Newest question")
assert assistant_index < user_index
assert html =~ ~r/<textarea[^>]*class="chat-input chat-surface-input ui-textarea"[^>]*disabled/
assert html =~
~r/<textarea[^>]*class="chat-input chat-surface-input ui-textarea"[^>]*disabled/
send_and_await(view, {
:chat_tool_call,
@@ -4919,6 +5038,74 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ ~s(<pre><code>)
end
test "git sidebar wires branch, changes, history, and action buttons to BDS.Git", %{
temp_dir: temp_dir
} do
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
tracked = Path.join(posts_dir, "tracked.md")
File.write!(tracked, "Old content\n")
init_git_repo!(temp_dir, "seed commit")
File.write!(tracked, "New content\n")
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
assert html =~ ~s(class="git-sidebar")
assert html =~ ~s(data-testid="git-branch")
assert html =~ "master"
assert html =~ ~s(data-testid="git-status-file")
assert html =~ "posts/tracked.md"
assert html =~ ~s(data-testid="git-commit-form")
assert html =~ ~s(data-testid="git-action-fetch")
assert html =~ ~s(data-testid="git-action-pull")
assert html =~ ~s(data-testid="git-action-push")
assert html =~ ~s(data-testid="git-action-prune-lfs")
assert html =~ ~s(data-testid="git-history-entry")
assert html =~ "seed commit"
end
test "git sidebar commit stages working tree and reloads to a clean state", %{
project: project,
temp_dir: temp_dir
} do
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
File.write!(Path.join(posts_dir, "seed.md"), "Seed\n")
init_git_repo!(temp_dir, "seed commit")
File.write!(Path.join(posts_dir, "added.md"), "Brand new\n")
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
html = render_submit(view, "git_commit", %{"git" => %{"message" => "Add new post"}})
refute html =~ "posts/added.md"
assert {:ok, %{commits: commits}} = BDS.Git.history(project.id, "master")
assert Enum.any?(commits, &(&1.subject == "Add new post"))
end
test "git sidebar shows the initialize control when the project is not a repo" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
view
|> element("[data-testid='activity-button'][data-view='git']")
|> render_click()
assert html =~ ~s(class="git-sidebar")
assert html =~ ~s(data-testid="git-initialize")
end
test "settings sidebar categories render the full old-app section model and target the requested section" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)

View File

@@ -256,6 +256,53 @@ defmodule BDS.DesktopTest do
assert_receive :window_quit_requested
end
test "the app owns final termination instead of delegating to Desktop.Window/System.halt" do
# Desktop.Window.quit/0 routes through System.halt/1, which runs the wx C++
# static destructors on exit and crashes on macOS. The app-owned shutdown
# must terminate the OS process directly with SIGKILL instead.
assert BDS.Desktop.Shutdown.quit_module() == BDS.Desktop.Shutdown
end
test "hard quit terminates the BEAM OS process with SIGKILL rather than libc exit" do
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
test_pid = self()
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
send(test_pid, {:os_killed, to_string(os_pid)})
:ok
end)
on_exit(fn -> restore_env(:desktop_os_kill_fun, previous_kill) end)
assert :ok = BDS.Desktop.Shutdown.quit()
assert_receive {:os_killed, beam_pid}
assert beam_pid == to_string(:os.getpid())
end
test "hard quit kills heart before itself so the app is not relaunched" do
previous_kill = Application.get_env(:bds, :desktop_os_kill_fun)
test_pid = self()
{:ok, fake_heart} =
Agent.start(fn -> :ok end, name: :heart)
Application.put_env(:bds, :desktop_os_kill_fun, fn os_pid ->
send(test_pid, {:os_killed, to_string(os_pid)})
:ok
end)
on_exit(fn ->
restore_env(:desktop_os_kill_fun, previous_kill)
if Process.alive?(fake_heart), do: Agent.stop(fake_heart)
end)
assert :ok = BDS.Desktop.Shutdown.quit()
# heart has no linked port here, so only the BEAM itself is killed, and the
# call must not crash while inspecting the heart process.
assert_receive {:os_killed, beam_pid}
assert beam_pid == to_string(:os.getpid())
end
test "desktop root html is a LiveView shell and references the generated asset entrypoints" do
conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))

View File

@@ -0,0 +1,39 @@
defmodule BDS.Embeddings.Backends.NeuralTest do
use ExUnit.Case, async: false
alias BDS.Embeddings.Backends.Neural
setup do
previous = Application.get_env(:bds, :embeddings)
on_exit(fn ->
if previous == nil do
Application.delete_env(:bds, :embeddings)
else
Application.put_env(:bds, :embeddings, previous)
end
end)
:ok
end
test "reports the configured spec model id and dimensions without loading the model" do
Application.put_env(:bds, :embeddings,
backend: Neural,
model_id: "Xenova/multilingual-e5-small",
model_repo: "intfloat/multilingual-e5-small",
dimensions: 384
)
assert %{model_id: "Xenova/multilingual-e5-small", dimensions: 384} = Neural.model_info()
end
test "implements the embeddings backend behaviour" do
behaviours =
Neural.module_info(:attributes)
|> Keyword.get_values(:behaviour)
|> List.flatten()
assert BDS.Embeddings.Backend in behaviours
end
end

View File

@@ -15,6 +15,62 @@ defmodule BDS.EmbeddingsTest do
end
end
defmodule BatchRecordingBackend do
@behaviour BDS.Embeddings.Backend
@recorder :embeddings_batch_recorder
@impl true
def model_info do
%{model_id: "batch/multilingual-e5-small", dimensions: 384}
end
@impl true
def embed(text, opts) do
BDS.Embeddings.Backends.InApp.embed(text, opts)
end
@impl true
def embed_many(texts, opts) do
Agent.update(@recorder, fn sizes -> [length(texts) | sizes] end)
BDS.Embeddings.Backends.InApp.embed_many(texts, opts)
end
end
defmodule CountingBackend do
@behaviour BDS.Embeddings.Backend
@counter :embeddings_force_counter
@impl true
def model_info, do: %{model_id: "counting/multilingual-e5-small", dimensions: 384}
@impl true
def embed(text, opts) do
Agent.update(@counter, &(&1 + 1))
BDS.Embeddings.Backends.InApp.embed(text, opts)
end
@impl true
def embed_many(texts, opts) do
Agent.update(@counter, &(&1 + length(texts)))
BDS.Embeddings.Backends.InApp.embed_many(texts, opts)
end
end
defmodule FailingBackend do
@behaviour BDS.Embeddings.Backend
@impl true
def model_info, do: %{model_id: "failing/multilingual-e5-small", dimensions: 384}
@impl true
def embed(_text, _opts), do: {:error, :model_unavailable}
@impl true
def embed_many(_texts, _opts), do: {:error, :model_unavailable}
end
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
@@ -297,30 +353,104 @@ defmodule BDS.EmbeddingsTest do
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Persistence is debounced (5s); force it to disk to assert the files.
:ok = BDS.Embeddings.Index.flush(project.id)
index_path = BDS.Embeddings.index_path(project.id)
assert File.exists?(index_path)
assert File.exists?(index_path <> ".meta.json")
refute String.starts_with?(index_path, BDS.Projects.project_data_dir(project))
cache_root = Application.fetch_env!(:bds, :project_cache_root) |> Path.expand()
assert index_path == Path.join([cache_root, "projects", project.id, "embeddings.usearch"])
snapshot = index_path |> File.read!() |> Jason.decode!()
assert snapshot["project_id"] == project.id
assert snapshot["model_id"] == "fake/multilingual-e5-small"
assert snapshot["dimensions"] == 384
assert snapshot["entries"][alpha.id]["label"] != nil
assert snapshot["entries"][alpha.id]["content_hash"] != nil
# The sidecar carries the dimension and the label→post_id mapping.
meta = (index_path <> ".meta.json") |> File.read!() |> Jason.decode!()
assert meta["dim"] == 384
post_ids = Enum.map(meta["labels"], fn [_label, post_id] -> post_id end)
assert alpha.id in post_ids
assert beta.id in post_ids
assert Enum.any?(snapshot["entries"][alpha.id]["neighbors"], fn neighbor ->
neighbor["post_id"] == beta.id
end)
# The HNSW index answers nearest-neighbour queries.
assert {:ok, [neighbor]} = BDS.Embeddings.find_similar(alpha.id, 1)
assert neighbor.post_id == beta.id
end
test "embedding index uses the app-internal persisted file name", %{project: project} do
assert BDS.Embeddings.index_path(project.id) =~ "/embeddings.usearch"
end
test "stored embedding vectors are packed Float32 BLOBs, not JSON text", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Blob",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, post} = BDS.Posts.publish_post(post.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
assert is_binary(key.vector)
# 384 dimensions * 4 bytes per little-endian Float32 (VectorCacheInDb).
assert byte_size(key.vector) == 384 * 4
refute String.starts_with?(key.vector, "[")
decoded = for <<value::float-32-little <- key.vector>>, do: value
assert length(decoded) == 384
# The packed vector still drives similarity queries.
assert {:ok, scores} = BDS.Embeddings.compute_similarities(post.id, [post.id])
assert is_map(scores)
end
test "rebuilding embeds posts in batches instead of one at a time", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
for index <- 1..5 do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Batch #{index}",
content: "space rocket orbit mission galaxy #{index}",
language: "en"
})
assert {:ok, _post} = BDS.Posts.publish_post(post.id)
end
# Simulate the post-migration state where the vector cache is empty, so the
# rebuild has to (re)embed every post.
BDS.Repo.delete_all(BDS.Embeddings.Key)
{:ok, _recorder} = Agent.start_link(fn -> [] end, name: :embeddings_batch_recorder)
Application.put_env(:bds, :embeddings,
backend: BatchRecordingBackend,
model_id: "batch/multilingual-e5-small",
dimensions: 384,
batch_size: 3
)
assert {:ok, rebuilt} = BDS.Embeddings.reindex_all(project.id)
assert length(rebuilt) == 5
batch_sizes = Agent.get(:embeddings_batch_recorder, & &1)
# 5 pending posts at batch_size 3 → one batch of 3 and one of 2, never
# one-at-a-time.
assert Enum.sort(batch_sizes, :desc) == [3, 2]
assert Enum.max(batch_sizes) > 1
end
test "reindex_all rebuilds stored embeddings for the whole project", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
@@ -351,43 +481,147 @@ defmodule BDS.EmbeddingsTest do
refreshed_key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
assert refreshed_key.content_hash == stale_key.content_hash
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(BDS.Embeddings.index_path(project.id))
end
test "sync_post refreshes snapshot drift when the embedding hash is already current", %{
test "similarity queries keep working when sync_post finds the embedding already current", %{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, alpha} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, beta} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "rocket launch orbit mission station",
language: "en"
})
assert {:ok, alpha} = BDS.Posts.publish_post(alpha.id)
assert {:ok, _beta} = BDS.Posts.publish_post(beta.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Re-syncing with an unchanged content hash is a no-op for the index...
assert :ok = BDS.Embeddings.sync_post(alpha.id)
# ...and nearest-neighbour queries still resolve through the HNSW index.
assert {:ok, [neighbor]} = BDS.Embeddings.find_similar(alpha.id, 1)
assert neighbor.post_id == beta.id
end
test "find_similar rebuilds the HNSW index on demand when none is loaded", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
assert {:ok, alpha} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Alpha",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, beta} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Beta",
content: "rocket launch orbit mission station",
language: "en"
})
assert {:ok, _alpha} = BDS.Posts.publish_post(alpha.id)
assert {:ok, _beta} = BDS.Posts.publish_post(beta.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
# Drop the in-memory index and remove the persisted files, then query: it
# must self-heal by rebuilding from the DB vectors.
:ok = BDS.Embeddings.Index.forget(project.id)
File.rm_rf!(BDS.Projects.project_cache_dir(project.id))
assert {:ok, similar} = BDS.Embeddings.find_similar(alpha.id, 1)
assert [%{post_id: post_id}] = similar
assert post_id == beta.id
end
test "explicit rebuild re-embeds every post even when content is unchanged", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
{:ok, _agent} = Agent.start_link(fn -> 0 end, name: :embeddings_force_counter)
Application.put_env(:bds, :embeddings,
backend: CountingBackend,
model_id: "counting/multilingual-e5-small",
dimensions: 384,
batch_size: 16
)
for index <- 1..3 do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Force #{index}",
content: "space rocket orbit mission galaxy #{index}",
language: "en"
})
assert {:ok, _post} = BDS.Posts.publish_post(post.id)
end
# Ignore embeds triggered while creating/publishing.
Agent.update(:embeddings_force_counter, fn _count -> 0 end)
# index_unindexed honours the content_hash skip: nothing to re-embed.
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
assert Agent.get(:embeddings_force_counter, & &1) == 0
# An explicit rebuild re-embeds all three regardless (ReindexAll).
assert {:ok, rebuilt} = BDS.Embeddings.reindex_all(project.id)
assert length(rebuilt) == 3
assert Agent.get(:embeddings_force_counter, & &1) == 3
end
test "embedding operations degrade gracefully when the model is unavailable", %{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
Application.put_env(:bds, :embeddings,
backend: FailingBackend,
model_id: "failing/multilingual-e5-small",
dimensions: 384
)
# Saving a post must not crash even though embedding fails; it is just left
# unindexed.
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
title: "Snapshot Repair",
title: "Offline",
content: "space rocket orbit mission galaxy",
language: "en"
})
assert {:ok, post} = BDS.Posts.publish_post(post.id)
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) == nil
key = BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
index_path = BDS.Embeddings.index_path(project.id)
# Explicit (re)index operations surface a clean error instead of crashing.
assert {:error, :model_unavailable} = BDS.Embeddings.reindex_all(project.id)
assert {:error, :model_unavailable} = BDS.Embeddings.index_unindexed(project.id)
snapshot = index_path |> File.read!() |> Jason.decode!()
drifted_snapshot =
put_in(snapshot, ["entries", post.id, "content_hash"], "stale-snapshot-hash")
File.write!(index_path, Jason.encode!(drifted_snapshot))
refute Enum.any?(BDS.Embeddings.diff_reports(project.id), &(&1.entity_id == post.id))
assert :ok = BDS.Embeddings.sync_post(post.id)
repaired_snapshot = index_path |> File.read!() |> Jason.decode!()
assert get_in(repaired_snapshot, ["entries", post.id, "content_hash"]) == key.content_hash
refute Enum.any?(BDS.Embeddings.diff_reports(project.id), &(&1.entity_id == post.id))
# Queries stay safe.
assert {:ok, []} = BDS.Embeddings.find_similar(post.id, 5)
end
end

View File

@@ -0,0 +1,91 @@
defmodule BDS.Generation.PagefindTest do
use ExUnit.Case, async: true
alias BDS.Generation.Pagefind
defp plan(language, blog_languages), do: %{language: language, blog_languages: blog_languages}
defp index_for(outputs, relative_path) do
{_path, content} = Enum.find(outputs, fn {path, _} -> path == relative_path end)
Jason.decode!(content)
end
defp content_for(outputs, relative_path) do
{_path, content} = Enum.find(outputs, fn {path, _} -> path == relative_path end)
content
end
test "indexes only pages marked with data-pagefind-body and scopes text to that region" do
post_html = """
<html><head><title>My First Post</title></head>
<body>
<nav>Home Archive Contact ignored-nav-term</nav>
<article data-pagefind-body><h1>My First Post</h1><p>Searchable elixir content.</p></article>
<footer>footer-term</footer>
</body></html>
"""
list_html = "<html><head><title>Home</title></head><body><ul>list page</ul></body></html>"
outputs =
Pagefind.build_outputs(plan("en", ["en"]), [
{"posts/first.html", post_html},
{"index.html", list_html}
])
index = index_for(outputs, "pagefind/index.json")
assert index["language"] == "en"
# Only the marked post page is indexed; the unmarked list page is excluded.
urls = Enum.map(index["pages"], & &1["url"])
assert "/posts/first.html" in urls
refute "/index.html" in urls
page = Enum.find(index["pages"], &(&1["url"] == "/posts/first.html"))
assert page["title"] == "My First Post"
# Body text is scoped to data-pagefind-body — nav/footer text is excluded.
assert page["text"] =~ "Searchable elixir content"
refute page["text"] =~ "ignored-nav-term"
refute page["text"] =~ "footer-term"
end
test "builds a separate index per language scoped by route prefix" do
en_post =
"<html><head><title>EN</title></head><body><article data-pagefind-body>english body</article></body></html>"
de_post =
"<html><head><title>DE</title></head><body><article data-pagefind-body>deutscher text</article></body></html>"
outputs =
Pagefind.build_outputs(plan("en", ["en", "de"]), [
{"posts/first.html", en_post},
{"de/posts/first.html", de_post}
])
en_index = index_for(outputs, "pagefind/index.json")
de_index = index_for(outputs, "de/pagefind/index.json")
assert en_index["language"] == "en"
assert Enum.map(en_index["pages"], & &1["url"]) == ["/posts/first.html"]
assert de_index["language"] == "de"
assert Enum.map(de_index["pages"], & &1["url"]) == ["/de/posts/first.html"]
end
test "emits a functional PagefindUI script and stylesheet per language" do
outputs =
Pagefind.build_outputs(plan("en", ["en"]), [
{"posts/first.html",
"<html><head><title>T</title></head><body><article data-pagefind-body>b</article></body></html>"}
])
js = content_for(outputs, "pagefind/pagefind-ui.js")
css = content_for(outputs, "pagefind/pagefind-ui.css")
# Real UI defines the PagefindUI global the search runtime instantiates.
assert js =~ "PagefindUI"
refute js =~ "window.bDSPagefind"
# Stylesheet carries real UI selectors, not a one-liner stub.
assert css =~ ".pagefind-ui__results"
end
end

View File

@@ -252,7 +252,7 @@ defmodule BDS.GenerationTest do
title: "Post View",
kind: :post,
content:
"<article class=\"post-template\"><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>"
"<article class=\"post-template\" data-pagefind-body><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>"
})
assert {:ok, published_post_template} = BDS.Templates.publish_template(post_template.id)

View File

@@ -61,8 +61,8 @@ defmodule BDS.GitTest do
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts ->
{"main\n", 0}
"git", ["log", "--format=%H%x09%s", "main"], _opts ->
{"a1\tLocal commit\nb2\tShared commit\n", 0}
"git", ["log", "--date=short", "--format=%H%x09%an%x09%ad%x09%s", "main"], _opts ->
{"a1\tAda\t2026-01-01\tLocal commit\nb2\tBabbage\t2026-01-02\tShared commit\n", 0}
"git", ["log", "--format=%H", "origin/main"], _opts ->
{"b2\nc3\n", 0}
@@ -83,7 +83,11 @@ defmodule BDS.GitTest do
assert diff.unstaged_diff == "unstaged diff"
assert {:ok, history} = Git.history(project.id, "main", runner: runner)
assert Enum.find(history.commits, &(&1.hash == "a1")).sync_status.kind == :local_only
a1 = Enum.find(history.commits, &(&1.hash == "a1"))
assert a1.sync_status.kind == :local_only
assert a1.author == "Ada"
assert a1.date == "2026-01-01"
assert a1.subject == "Local commit"
assert Enum.find(history.commits, &(&1.hash == "b2")).sync_status.kind == :both
assert Enum.find(history.commits, &(&1.hash == "c3")).sync_status.kind == :remote_only

View File

@@ -395,6 +395,8 @@ defmodule BDS.MaintenanceTest do
assert {:ok, _indexed} = BDS.Embeddings.index_unindexed(project.id)
index_path = BDS.Embeddings.index_path(project.id)
# Index persistence is debounced (5s); force it to assert the file.
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(index_path)
Repo.delete_all(from key in BDS.Embeddings.Key, where: key.project_id == ^project.id)
@@ -416,6 +418,8 @@ defmodule BDS.MaintenanceTest do
assert post.id in rebuilt_post_ids
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(index_path)
end

View File

@@ -236,6 +236,9 @@ defmodule BDS.MetadataTest do
assert metadata.semantic_similarity_enabled == true
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil
# Index persistence is debounced (5s); force it to assert the file.
:ok = BDS.Embeddings.Index.flush(project.id)
assert File.exists?(BDS.Embeddings.index_path(project.id))
end

View File

@@ -706,6 +706,44 @@ defmodule BDS.PreviewTest do
assert :ok = BDS.Preview.stop_preview(project.id)
end
test "stop_preview drains an inflight request before completing", %{project: project} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
main_language: "en",
blog_languages: ["en"]
})
assert {:ok, server} = BDS.Preview.start_preview(project.id)
# Open a raw connection and hold it open without sending the request line,
# so the server has a request task blocked in recv (an inflight request).
{:ok, socket} =
:gen_tcp.connect(to_charlist(server.host), server.port, [
:binary,
packet: :raw,
active: false
])
# Give the acceptor time to accept the connection and register the task.
Process.sleep(100)
test_pid = self()
spawn(fn ->
send(test_pid, {:stopped, BDS.Preview.stop_preview(project.id)})
end)
# Shutdown must not complete while the request is still inflight.
refute_receive {:stopped, _}, 300
# Completing the request lets the server drain and finish shutting down.
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\nhost: localhost\r\n\r\n")
assert_receive {:stopped, :ok}, 2_000
:gen_tcp.close(socket)
end
test "preview query params can override the rendered theme for generated and draft pages", %{
project: project
} do

View File

@@ -126,6 +126,35 @@ defmodule BDS.ScriptsTest do
refute File.exists?(Path.join(temp_dir, published.file_path))
end
test "publish_script rejects invalid Lua syntax", %{project: project} do
assert {:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Bad Script",
kind: :utility,
content: "function main( missing end"
})
assert {:error, {:invalid_script, _reason}} = BDS.Scripts.publish_script(script.id)
reloaded = Repo.get!(Script, script.id)
assert reloaded.status == :draft
assert reloaded.content == "function main( missing end"
end
test "publish_script allows valid Lua syntax", %{project: project} do
assert {:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Good Script",
kind: :utility,
content: "function main() return 42 end"
})
assert {:ok, published} = BDS.Scripts.publish_script(script.id)
assert published.status == :published
end
test "rebuild_scripts_from_files recreates published scripts from disk", %{
project: project,
temp_dir: temp_dir

View File

@@ -229,6 +229,13 @@ defmodule BDS.TagsTest do
] = Jason.decode!(File.read!(tags_path))
end
test "colour_presets contains exactly 17 unique valid hex colours" do
presets = BDS.Desktop.ShellLive.TagsEditor.colour_presets()
assert length(presets) == 17
assert length(Enum.uniq(presets)) == 17
assert Enum.all?(presets, &Regex.match?(~r/^#[0-9a-fA-F]{6}$/, &1))
end
defp errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->

View File

@@ -29,7 +29,7 @@ defmodule BDS.TemplatesTest do
assert template.status == :draft
assert template.enabled == true
assert template.version == 1
assert template.file_path == ""
assert template.file_path == "templates/article-view.liquid"
assert template.content == "<article>{{ content }}</article>"
assert {:ok, duplicate} =
@@ -43,6 +43,28 @@ defmodule BDS.TemplatesTest do
assert duplicate.slug == "article-view-2"
end
test "create_template writes template file to disk", %{project: project, temp_dir: temp_dir} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "My Layout",
kind: :post,
content: "<main>{{ content }}</main>"
})
assert template.file_path == "templates/my-layout.liquid"
full_path = Path.join(temp_dir, template.file_path)
assert File.exists?(full_path)
contents = File.read!(full_path)
assert contents =~ "---\nid: #{template.id}\n"
assert contents =~ "slug: my-layout\n"
assert contents =~ "title: My Layout\n"
assert contents =~ "kind: post\n"
assert contents =~ "\n---\n<main>{{ content }}</main>\n"
end
test "publish_template writes a liquid file with frontmatter and clears draft content", %{
project: project,
temp_dir: temp_dir
@@ -264,6 +286,35 @@ defmodule BDS.TemplatesTest do
assert reloaded_tag.post_template_slug == "feature-view"
end
test "publish_template rejects invalid Liquid syntax", %{project: project} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Bad Template",
kind: :post,
content: "{% for item in items %}unclosed"
})
assert {:error, {:invalid_liquid, _reason}} = BDS.Templates.publish_template(template.id)
reloaded = Repo.get!(BDS.Templates.Template, template.id)
assert reloaded.status == :draft
assert reloaded.content == "{% for item in items %}unclosed"
end
test "publish_template allows valid Liquid syntax", %{project: project} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
title: "Good Template",
kind: :post,
content: "{% for item in items %}{{ item }}{% endfor %}"
})
assert {:ok, published} = BDS.Templates.publish_template(template.id)
assert published.status == :published
end
test "rebuild_templates_from_files recreates published templates from disk", %{
project: project,
temp_dir: temp_dir