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 @@ [![GitHub release](https://img.shields.io/github/v/release/daniboybye/ElixirTorrentWebUI?label=release&logo=github&color=181717)](https://github.com/daniboybye/ElixirTorrentWebUI/releases/latest) [![GitHub](https://img.shields.io/badge/source-ElixirTorrentWebUI-181717?logo=github)](https://github.com/daniboybye/ElixirTorrentWebUI) [![CI](https://img.shields.io/github/actions/workflow/status/daniboybye/ElixirTorrentWebUI/web-build-test-analyze.yml?branch=main&label=CI&logo=github)](https://github.com/daniboybye/ElixirTorrentWebUI/actions/workflows/web-build-test-analyze.yml) [![codecov](https://codecov.io/gh/daniboybye/ElixirTorrentWebUI/branch/main/graph/badge.svg)](https://codecov.io/gh/daniboybye/ElixirTorrentWebUI) [![Last commit](https://img.shields.io/github/last-commit/daniboybye/ElixirTorrentWebUI/main)](https://github.com/daniboybye/ElixirTorrentWebUI/commits/main) [![Engine](https://img.shields.io/badge/Engine-ElixirTorrent-181717?logo=github)](https://github.com/daniboybye/ElixirTorrent) [![Platforms](https://img.shields.io/badge/platforms-macOS%20%7C%20Windows-lightgrey)](https://github.com/daniboybye/ElixirTorrentWebUI/releases/latest) [![License](https://img.shields.io/github/license/daniboybye/ElixirTorrentWebUI?label=license&color=blue)](LICENSE) -[![Elixir](https://img.shields.io/badge/elixir-%7E%3E%201.20-4B275F?logo=elixir)](https://elixir-lang.org) [![OTP](https://img.shields.io/badge/OTP-29-A90533?logo=erlang)](https://www.erlang.org) [![Swift](https://img.shields.io/badge/swift-6.3-F05138?logo=swift&logoColor=white)](https://www.swift.org) [![.NET](https://img.shields.io/badge/10-512BD4?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com) +[![Elixir](https://img.shields.io/badge/elixir-%7E%3E%201.20-4B275F?logo=elixir)](https://elixir-lang.org) [![OTP](https://img.shields.io/badge/OTP-29-A90533?logo=erlang)](https://www.erlang.org) [![Swift](https://img.shields.io/badge/swift-6.4-F05138?logo=swift&logoColor=white)](https://www.swift.org) [![.NET](https://img.shields.io/badge/10-512BD4?logo=dotnet&logoColor=white)](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.")} +

+ +
+