diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml
index 9ae42ca..03510e9 100644
--- a/.github/workflows/build-macos.yml
+++ b/.github/workflows/build-macos.yml
@@ -6,11 +6,13 @@ name: Build macOS
# Runs the full ExUnit suite on macOS (arm64) and produces the signed DMG
# artifact that release.yml uploads to the GitHub Release.
#
-# macos-26 = Apple Silicon (M-series), latest Xcode preinstalled (26.6 as of
-# this writing — priv/macos/src/*.swift needs its toolchain for Swift 6 language mode
-# / strict concurrency checking). GitHub bills macOS runners 10x vs Linux, so
-# this workflow deliberately does not run on every PR — only on tags or
-# on-demand dispatch.
+# xcode-27 = Apple Silicon (M-series) macOS 27 image with Xcode 27 (Swift 6.4)
+# as the default toolchain — still a public-preview label, so move back to
+# the plain `macos-27` label once GitHub promotes it to GA. priv/macos/src/*.swift
+# is built in Swift 6 language mode (strict concurrency checking); the 6.4
+# compiler still accepts nothing newer than `-swift-version 6`. GitHub bills
+# macOS runners 10x vs Linux, so this workflow deliberately does not run on
+# every PR — only on tags or on-demand dispatch.
on:
workflow_call:
@@ -26,7 +28,7 @@ concurrency:
jobs:
build:
name: Test and package (macOS arm64)
- runs-on: macos-26
+ runs-on: xcode-27
timeout-minutes: 45
env:
@@ -36,6 +38,11 @@ jobs:
- name: Check out WebUI
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Show Swift toolchain
+ run: |
+ xcodebuild -version
+ swift --version
+
- name: Install SwiftLint
run: brew install swiftlint
@@ -48,7 +55,7 @@ jobs:
run: swiftlint lint --strict
- name: Install Erlang and Elixir
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
+ uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index 1cdf5f6..a49c867 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -63,7 +63,7 @@ jobs:
--configuration Release --runtime win-x64 -warnaserror
- name: Install Erlang and Elixir
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
+ uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6c408b6..6bd1e79 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,7 +46,7 @@ jobs:
publish:
name: Publish GitHub Release
needs: [macos, windows]
- runs-on: ubuntu-24.04
+ runs-on: ubuntu-26.04
timeout-minutes: 10
if: >-
startsWith(github.ref, 'refs/tags/') &&
diff --git a/.github/workflows/web-build-test-analyze.yml b/.github/workflows/web-build-test-analyze.yml
index 595d62e..96c25a9 100644
--- a/.github/workflows/web-build-test-analyze.yml
+++ b/.github/workflows/web-build-test-analyze.yml
@@ -1,7 +1,7 @@
name: Build WebUI, Test and Analyze
# Runs on every PR and every push to main. Kept intentionally on the cheapest
-# GitHub-hosted image (ubuntu-24.04, 1x billing) — no macOS/Windows packaging
+# GitHub-hosted image (ubuntu-26.04, 1x billing) — no macOS/Windows packaging
# happens here. Platform artifacts are produced by .github/workflows/release.yml.
on:
@@ -21,7 +21,7 @@ concurrency:
jobs:
quality:
name: Quality and tests
- runs-on: ubuntu-24.04
+ runs-on: ubuntu-26.04
timeout-minutes: 20
env:
@@ -40,7 +40,7 @@ jobs:
version: latest
- name: Install Erlang and Elixir
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1
+ uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
diff --git a/.tool-versions b/.tool-versions
index 4c96bef..5617120 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
-erlang 29.0.5
-elixir 1.20.3-otp-29
+erlang 29.1.1
+elixir 1.20.4-otp-29
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1bbfa1c..4e02ac4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,7 +28,7 @@ Requires Elixir 1.20+ on OTP 29 (see `.tool-versions` for the exact toolchain CI
ELIXIR_TORRENT_PATH=../ElixirTorrent mix deps.get
```
-The native launchers are optional for most work and only build on their own platform: the Swift shell (`priv/macos/src/`) needs Xcode with the Swift 6 toolchain, and the C# WinUI 3 shell (`priv/windows/Launcher/`) needs the .NET 10 SDK on Windows 11 x64.
+The native launchers are optional for most work and only build on their own platform: the Swift shell (`priv/macos/src/`) needs Xcode 27 with the Swift 6.4 toolchain, and the C# WinUI 3 shell (`priv/windows/Launcher/`) needs the .NET 10 SDK on Windows 11 x64.
## Before opening a pull request
diff --git a/README.md b/README.md
index d9a5da3..12b4762 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://github.com/daniboybye/ElixirTorrentWebUI/releases/latest) [](https://github.com/daniboybye/ElixirTorrentWebUI) [](https://github.com/daniboybye/ElixirTorrentWebUI/actions/workflows/web-build-test-analyze.yml) [](https://codecov.io/gh/daniboybye/ElixirTorrentWebUI) [](https://github.com/daniboybye/ElixirTorrentWebUI/commits/main) [](https://github.com/daniboybye/ElixirTorrent) [](https://github.com/daniboybye/ElixirTorrentWebUI/releases/latest) [](LICENSE)
-[](https://elixir-lang.org) [](https://www.erlang.org) [](https://www.swift.org) [](https://dotnet.microsoft.com)
+[](https://elixir-lang.org) [](https://www.erlang.org) [](https://www.swift.org) [](https://dotnet.microsoft.com)
Phoenix (LiveView) Web UI and desktop shell for the
[`elixir_torrent`](https://hex.pm/packages/elixir_torrent) BitTorrent engine.
@@ -83,7 +83,7 @@ mix mac.dmg
```
Output lands in `dist/` (e.g. `ElixirTorrent Web.app`,
-`ElixirTorrent Web-0.4.0-macos-arm64.dmg` on Apple Silicon, or `…-macos-x64.dmg`
+`ElixirTorrent Web-0.4.1-macos-arm64.dmg` on Apple Silicon, or `…-macos-x64.dmg`
on Intel).
The **Liquid Glass** icon (`Assets.car`, macOS 26+) requires **Xcode 26+**
diff --git a/config/config.exs b/config/config.exs
index 5c4113c..6322fa8 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -37,7 +37,7 @@ config :elixir_torrent_web_ui, ElixirTorrentWebUIWeb.Endpoint,
# Configure esbuild (the version is required)
config :esbuild,
- version: "0.28.1",
+ version: "0.28.2",
elixir_torrent_web_ui: [
args:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
diff --git a/config/dev.exs b/config/dev.exs
index 92e6480..c13c37f 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -65,6 +65,9 @@ config :elixir_torrent_web_ui, ElixirTorrentWebUIWeb.Endpoint,
# Enable dev routes for dashboard and mailbox
config :elixir_torrent_web_ui, dev_routes: true
+# Show Debug-only tools (e.g. "Delete log files") in Settings.
+config :elixir_torrent_web_ui, debug_tools: true
+
# Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n"
diff --git a/config/prod.exs b/config/prod.exs
index e5b8f9f..da8d861 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -25,6 +25,7 @@ end
# level can still be raised back to `:info` over RPC without a rebuild.
if System.get_env("ELIXIR_TORRENT_DEBUG_BUILD") == "1" do
config :logger, level: :debug
+ config :elixir_torrent_web_ui, :debug_tools, true
else
config :logger,
level: :info,
diff --git a/lib/elixir_torrent_web_ui/log_files.ex b/lib/elixir_torrent_web_ui/log_files.ex
new file mode 100644
index 0000000..c0d34e1
--- /dev/null
+++ b/lib/elixir_torrent_web_ui/log_files.ex
@@ -0,0 +1,53 @@
+defmodule ElixirTorrentWebUI.LogFiles do
+ @moduledoc false
+
+ # Logs written while ElixirTorrent runs live directly in the data directory:
+ # `server.log` (release stdout/stderr, including Engine output), `debug.log`
+ # and the rotated `*.log.N` siblings.
+ #
+ # The desktop launcher keeps `server.log` open for the life of the server, so
+ # deleting it would leave the release writing into an unlinked file. Active
+ # logs are therefore truncated in place and only rotated copies are removed.
+
+ @spec enabled?() :: boolean()
+ def enabled?, do: Application.get_env(:elixir_torrent_web_ui, :debug_tools, false)
+
+ @spec delete_all(Path.t()) :: {:ok, non_neg_integer()} | {:error, [{Path.t(), term()}]}
+ def delete_all(root \\ ElixirTorrentWebUI.DataDir.root()) do
+ results =
+ Enum.map(active_logs(root), &truncate/1) ++ Enum.map(rotated_logs(root), &remove/1)
+
+ case for({:error, _} = error <- results, do: error) do
+ [] -> {:ok, Enum.count(results, &(&1 == :ok))}
+ errors -> {:error, Enum.map(errors, fn {:error, failure} -> failure end)}
+ end
+ end
+
+ @spec active_logs(Path.t()) :: [Path.t()]
+ defp active_logs(root), do: wildcard_files(Path.join(root, "*.log"))
+
+ @spec rotated_logs(Path.t()) :: [Path.t()]
+ defp rotated_logs(root), do: wildcard_files(Path.join(root, "*.log.[0-9]*"))
+
+ @spec wildcard_files(Path.t()) :: [Path.t()]
+ defp wildcard_files(pattern), do: Enum.filter(Path.wildcard(pattern), ®ular?/1)
+
+ @spec regular?(Path.t()) :: boolean()
+ defp regular?(path), do: match?({:ok, %File.Stat{type: :regular}}, File.stat(path))
+
+ @spec truncate(Path.t()) :: :ok | {:error, {Path.t(), term()}}
+ defp truncate(path) do
+ case File.write(path, "") do
+ :ok -> :ok
+ {:error, reason} -> {:error, {path, reason}}
+ end
+ end
+
+ @spec remove(Path.t()) :: :ok | {:error, {Path.t(), term()}}
+ defp remove(path) do
+ case File.rm(path) do
+ :ok -> :ok
+ {:error, reason} -> {:error, {path, reason}}
+ end
+ end
+end
diff --git a/lib/elixir_torrent_web_ui_web/live/torrents_live.ex b/lib/elixir_torrent_web_ui_web/live/torrents_live.ex
index 7cd8dfc..e670e9d 100644
--- a/lib/elixir_torrent_web_ui_web/live/torrents_live.ex
+++ b/lib/elixir_torrent_web_ui_web/live/torrents_live.ex
@@ -6,6 +6,7 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
Engine,
IssueReport,
Locale,
+ LogFiles,
MagnetIngest,
StatsStore,
TorrentIngest,
@@ -32,6 +33,7 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
|> assign(:settings_locale, locale)
|> assign(:settings_download_folder, download_folder)
|> assign(:download_folder, download_folder)
+ |> assign(:debug_tools?, LogFiles.enabled?())
|> assign(:default_handler, DefaultHandler.status())
|> assign(:default_prompt_dismissed, false)
|> assign(:report_open, false)
@@ -339,6 +341,20 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
|> assign_torrents(Engine.list_torrents(socket.assigns.expanded))}
end
+ @impl Phoenix.LiveView
+ def handle_event("delete_log_files", _params, %{assigns: %{debug_tools?: true}} = socket) do
+ case LogFiles.delete_all() do
+ {:ok, _count} ->
+ {:noreply, put_flash(socket, :info, gettext("Log files deleted"))}
+
+ {:error, failures} ->
+ Logger.warning("TorrentsLive: delete_log_files failed reasons=#{inspect(failures)}")
+ {:noreply, put_flash(socket, :error, gettext("Could not delete log files"))}
+ end
+ end
+
+ def handle_event("delete_log_files", _params, socket), do: {:noreply, socket}
+
@impl Phoenix.LiveView
def handle_event("request_default_handler", _params, socket) do
case DefaultHandler.request_default() do
@@ -689,6 +705,7 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
open={@settings_open}
locale={@settings_locale}
download_folder={@settings_download_folder}
+ debug_tools={@debug_tools?}
default_handler={@default_handler}
languages={ElixirTorrentWebUI.Languages.list()}
/>
@@ -1286,6 +1303,7 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
attr :download_folder, :string, required: true
attr :default_handler, :map, required: true
attr :languages, :list, required: true
+ attr :debug_tools, :boolean, default: false
@doc false
def settings_dialog(assigns) do
@@ -1419,6 +1437,26 @@ defmodule ElixirTorrentWebUIWeb.TorrentsLive do
+
+
{gettext("Debug")}
+
+ {gettext("Remove the log files written while ElixirTorrent runs.")}
+
+
+
+