From bdcb49185a18811d73db3f3cfb91bee329d60653 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 15 May 2024 12:27:57 -0700 Subject: [PATCH] [Enhancement] Adds in-app diagnostics page (#243) * Added improved sidebar menuing * Added new view for getting diagnostic data * Changed default log level to debug * Disabled false-positive static analysis --- .sobelow-conf | 3 +- config/runtime.exs | 5 +- lib/pinchflat_web/components/layouts.ex | 99 +++++++++++++++++-- .../components/layouts/app.html.heex | 2 +- .../layouts/partials/sidebar.html.heex | 9 +- .../settings/setting_controller.ex | 16 +++ .../controllers/settings/setting_html.ex | 10 ++ .../settings/setting_html/app_info.html.heex | 26 +++++ lib/pinchflat_web/router.ex | 3 + .../controllers/setting_controller_test.exs | 31 ++++++ 10 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 lib/pinchflat_web/controllers/settings/setting_html/app_info.html.heex diff --git a/.sobelow-conf b/.sobelow-conf index 0c4088d9..bf1db34b 100644 --- a/.sobelow-conf +++ b/.sobelow-conf @@ -16,7 +16,8 @@ "Config.HTTPS", "Config.CSP", "XSS.ContentType", - "Traversal.SendFile" + "Traversal.SendFile", + "Traversal.SendDownload" ], ignore_files: [], version: false diff --git a/config/runtime.exs b/config/runtime.exs index 6cb062fb..8e47790a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -53,7 +53,7 @@ if config_env() == :prod do # For testing alternate journal modes (see issue #137) journal_mode = String.to_existing_atom(System.get_env("JOURNAL_MODE", "wal")) - config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "info")) + config :logger, level: String.to_existing_atom(System.get_env("LOG_LEVEL", "debug")) config :pinchflat, yt_dlp_executable: System.find_executable("yt-dlp"), @@ -64,7 +64,8 @@ if config_env() == :prod do tmpfile_directory: Path.join([System.tmp_dir!(), "pinchflat", "data"]), dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY"), expose_feed_endpoints: expose_feed_endpoints, - timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC" + timezone: System.get_env("TIMEZONE") || System.get_env("TZ") || "UTC", + log_path: log_path config :tzdata, :data_dir, System.get_env("TZ_DATA_DIR", "/etc/elixir_tzdata_data") diff --git a/lib/pinchflat_web/components/layouts.ex b/lib/pinchflat_web/components/layouts.ex index 0efadc1f..9d858981 100644 --- a/lib/pinchflat_web/components/layouts.ex +++ b/lib/pinchflat_web/components/layouts.ex @@ -4,28 +4,107 @@ defmodule PinchflatWeb.Layouts do embed_templates "layouts/*" embed_templates "layouts/partials/*" + @doc """ + Renders a sidebar menu item link + + ## Examples + + <.sidebar_link icon="hero-home" text="Home" href="/" /> + """ attr :icon, :string, required: true attr :text, :string, required: true attr :href, :any, required: true attr :target, :any, default: "_self" def sidebar_item(assigns) do - # I'm testing out grouping classes here. Tentative order: font, layout, color, animation, state-modifiers ~H""" -
  • - <.link - href={@href} - target={@target} +
  • + <.sidebar_link icon={@icon} text={@text} href={@href} target={@target} /> +
  • + """ + end + + @doc """ + Renders a sidebar menu item with a submenu + + ## Examples + + <.sidebar_submenu icon="hero-home" text="Home" current_path="/"> + <:submenu icon="hero-home" text="Home" href="/" /> + + """ + + attr :icon, :string, required: true + attr :text, :string, required: true + attr :current_path, :string, required: true + + slot :submenu do + attr :icon, :string + attr :text, :string + attr :href, :any + attr :target, :any + end + + def sidebar_submenu(assigns) do + initially_selected = Enum.any?(assigns[:submenu], &(&1[:href] == assigns[:current_path])) + assigns = Map.put(assigns, :initially_selected, initially_selected) + + ~H""" +
  • + - <.icon name={@icon} /> <%= @text %> - + + <.icon name={@icon} /> <%= @text %> + + + <.icon name="hero-chevron-up" x-bind:class="{ 'rotate-180': selected }" /> + + + +
  • """ end + + @doc """ + Renders a sidebar menu item link + + ## Examples + + <.sidebar_link icon="hero-home" text="Home" href="/" /> + """ + attr :icon, :string + attr :text, :string, required: true + attr :href, :any, required: true + attr :target, :any, default: "_self" + attr :class, :string, default: "" + + def sidebar_link(assigns) do + ~H""" + <.link + href={@href} + target={@target} + class={[ + "font-medium", + "group relative flex items-center gap-2.5 rounded-sm px-4 py-2 duration-300 ease-in-out", + "duration-300 ease-in-out", + "hover:bg-meta-4", + @class + ]} + > + <.icon :if={@icon} name={@icon} /> <%= @text %> + + """ + end end diff --git a/lib/pinchflat_web/components/layouts/app.html.heex b/lib/pinchflat_web/components/layouts/app.html.heex index ffb79a69..63762ff4 100644 --- a/lib/pinchflat_web/components/layouts/app.html.heex +++ b/lib/pinchflat_web/components/layouts/app.html.heex @@ -1,5 +1,5 @@
    - <.sidebar /> + <.sidebar conn={@conn} />
    <.header params={@conn.params} /> diff --git a/lib/pinchflat_web/components/layouts/partials/sidebar.html.heex b/lib/pinchflat_web/components/layouts/partials/sidebar.html.heex index d8b8de96..2d7ae0d2 100644 --- a/lib/pinchflat_web/components/layouts/partials/sidebar.html.heex +++ b/lib/pinchflat_web/components/layouts/partials/sidebar.html.heex @@ -26,7 +26,14 @@ <.sidebar_item icon="hero-home" text="Home" href={~p"/"} /> <.sidebar_item icon="hero-tv" text="Sources" href={~p"/sources"} /> <.sidebar_item icon="hero-adjustments-vertical" text="Media Profiles" href={~p"/media_profiles"} /> - <.sidebar_item icon="hero-cog-6-tooth" text="Settings" href={~p"/settings"} /> + <.sidebar_submenu + icon="hero-cog-6-tooth" + text="Config" + current_path={Phoenix.Controller.current_path(@conn)} + > + <:submenu text="Settings" href={~p"/settings"} /> + <:submenu text="App Info" href={~p"/app_info"} /> +
    diff --git a/lib/pinchflat_web/controllers/settings/setting_controller.ex b/lib/pinchflat_web/controllers/settings/setting_controller.ex index c87d51f4..5e009c1d 100644 --- a/lib/pinchflat_web/controllers/settings/setting_controller.ex +++ b/lib/pinchflat_web/controllers/settings/setting_controller.ex @@ -23,4 +23,20 @@ defmodule PinchflatWeb.Settings.SettingController do render(conn, "show.html", changeset: changeset) end end + + def app_info(conn, _params) do + render(conn, "app_info.html") + end + + def download_logs(conn, _params) do + log_path = Application.get_env(:pinchflat, :log_path) + + if log_path && File.exists?(log_path) do + send_download(conn, {:file, log_path}, filename: "pinchflat-logs-#{Date.utc_today()}.txt") + else + conn + |> put_flash(:error, "Log file couldn't be found") + |> redirect(to: ~p"/app_info") + end + end end diff --git a/lib/pinchflat_web/controllers/settings/setting_html.ex b/lib/pinchflat_web/controllers/settings/setting_html.ex index 392975d2..bb5e255c 100644 --- a/lib/pinchflat_web/controllers/settings/setting_html.ex +++ b/lib/pinchflat_web/controllers/settings/setting_html.ex @@ -18,4 +18,14 @@ defmodule PinchflatWeb.Settings.SettingHTML do ~s(Server endpoint for Apprise notifications when new media is found. See Apprise docs for more information) end + + def diagnostic_info_string do + """ + App Version: #{Application.spec(:pinchflat)[:vsn]} + yt-dlp Version: #{Settings.get!(:yt_dlp_version)} + Apprise Version: #{Settings.get!(:apprise_version)} + System Architecture: #{to_string(:erlang.system_info(:system_architecture))} + Timezone: #{Application.get_env(:pinchflat, :timezone)} + """ + end end diff --git a/lib/pinchflat_web/controllers/settings/setting_html/app_info.html.heex b/lib/pinchflat_web/controllers/settings/setting_html/app_info.html.heex new file mode 100644 index 00000000..d1628c3f --- /dev/null +++ b/lib/pinchflat_web/controllers/settings/setting_html/app_info.html.heex @@ -0,0 +1,26 @@ +
    +
    +

    + App Info +

    +
    +
    +
    +
    + <.button color="bg-primary" rounding="rounded-lg" x-data="{ copied: false }" x-on:click={~s" + copyWithCallbacks( + `#{diagnostic_info_string()}`, + () => copied = true, + () => copied = false + ) + "}> + Copy Diagnotic Info + <.icon name="hero-check" class="ml-2 h-4 w-4" /> + + <.link href={~p"/download_logs"}> + <.button color="bg-primary" rounding="rounded-lg" class="ml-4"> + Download Logs + + +
    +
    diff --git a/lib/pinchflat_web/router.ex b/lib/pinchflat_web/router.ex index e3dd14e4..1e1ee7d0 100644 --- a/lib/pinchflat_web/router.ex +++ b/lib/pinchflat_web/router.ex @@ -30,7 +30,10 @@ defmodule PinchflatWeb.Router do resources "/media_profiles", MediaProfiles.MediaProfileController resources "/search", Searches.SearchController, only: [:show], singleton: true + resources "/settings", Settings.SettingController, only: [:show, :update], singleton: true + get "/app_info", Settings.SettingController, :app_info + get "/download_logs", Settings.SettingController, :download_logs resources "/sources", Sources.SourceController do post "/force_download_pending", Sources.SourceController, :force_download_pending diff --git a/test/pinchflat_web/controllers/setting_controller_test.exs b/test/pinchflat_web/controllers/setting_controller_test.exs index 0063ebcb..24dea44b 100644 --- a/test/pinchflat_web/controllers/setting_controller_test.exs +++ b/test/pinchflat_web/controllers/setting_controller_test.exs @@ -1,6 +1,8 @@ defmodule PinchflatWeb.SettingControllerTest do use PinchflatWeb.ConnCase + alias Pinchflat.Utils.FilesystemUtils + describe "show settings" do test "renders the page", %{conn: conn} do conn = get(conn, ~p"/settings") @@ -20,4 +22,33 @@ defmodule PinchflatWeb.SettingControllerTest do assert html_response(conn, 200) =~ update_attrs[:apprise_server] end end + + describe "app_info" do + test "renders the page", %{conn: conn} do + conn = get(conn, ~p"/app_info") + + assert html_response(conn, 200) =~ "App Info" + end + end + + describe "download_logs" do + test "downloads logs", %{conn: conn} do + log_path = Path.join([System.tmp_dir!(), "pinchflat", "data", "pinchflat.log"]) + FilesystemUtils.write_p(log_path, "test log data") + Application.put_env(:pinchflat, :log_path, log_path) + + conn = get(conn, ~p"/download_logs") + + assert response(conn, 200) =~ "test log data" + + Application.put_env(:pinchflat, :log_path, nil) + end + + test "redirects when log file is not found", %{conn: conn} do + conn = get(conn, ~p"/download_logs") + + assert redirected_to(conn) == ~p"/app_info" + assert conn.assigns[:flash]["error"] == "Log file couldn't be found" + end + end end