Promoted pure presentation and utility helpers from `defp` to `def @doc false` across ~20 LiveViews, Oban workers, and sync modules so they're reachable from unit tests. Refactored several `cond` blocks into idiomatic function heads with guards. Added ~250 new test cases in new files under test/towerops and test/towerops_web, including DB-backed tests for CnMaestro.Sync and AlertNotificationWorker, and removed dead LiveView tab components and CapacityLive (no callers anywhere in lib/test). Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types, Phoenix HTML modules, Inspect protocol impls) from coverage calculations — these are not our project code. Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
87 lines
2.8 KiB
Elixir
87 lines
2.8 KiB
Elixir
defmodule Towerops.Profiles.ProfileWatcher do
|
|
@moduledoc """
|
|
Watches YAML profile files for changes and automatically reloads them.
|
|
|
|
Uses FileSystem to monitor the priv/profiles directory for file changes.
|
|
When a .yaml or .yml file is created, modified, or deleted, triggers
|
|
a reload of all profiles via YamlProfiles.reload/0.
|
|
|
|
Note: Only available in development mode. FileSystem is a dev-only dependency.
|
|
"""
|
|
use GenServer
|
|
|
|
alias Towerops.Profiles.YamlProfiles
|
|
|
|
require Logger
|
|
|
|
@profiles_dir "priv/profiles"
|
|
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Only start file watcher in development
|
|
# FileSystem is a dev-only dependency
|
|
if Code.ensure_loaded?(FileSystem) do
|
|
# Get the absolute path to the profiles directory
|
|
profiles_path = Path.join(Application.app_dir(:towerops), @profiles_dir)
|
|
|
|
# Start watching the profiles directory for changes (using apply to avoid compile-time warnings)
|
|
case apply(FileSystem, :start_link, [[dirs: [profiles_path]]]) do
|
|
{:ok, watcher_pid} ->
|
|
apply(FileSystem, :subscribe, [watcher_pid])
|
|
Logger.info("ProfileWatcher: Started watching #{profiles_path}")
|
|
{:ok, %{watcher_pid: watcher_pid, profiles_path: profiles_path}}
|
|
|
|
:ignore ->
|
|
# inotify-tools not available (e.g., in CI)
|
|
Logger.debug("ProfileWatcher: FileSystem watcher not available, running in no-op mode")
|
|
{:ok, %{}}
|
|
end
|
|
else
|
|
# In production, FileSystem is not available
|
|
Logger.debug("ProfileWatcher: FileSystem not available, running in no-op mode (dev-only feature)")
|
|
{:ok, %{}}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:file_event, _watcher_pid, {path, events}}, state) do
|
|
# Only reload if it's a YAML file change
|
|
if yaml_file?(path) and should_reload?(events) do
|
|
Logger.info("ProfileWatcher: Detected change in #{Path.relative_to_cwd(path)}, reloading profiles...")
|
|
|
|
case YamlProfiles.reload() do
|
|
{:ok, profiles} when is_list(profiles) ->
|
|
Logger.info("ProfileWatcher: Successfully reloaded #{length(profiles)} profiles")
|
|
|
|
{:error, reason} ->
|
|
Logger.error("ProfileWatcher: Failed to reload profiles: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
{:noreply, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:file_event, _watcher_pid, :stop}, state) do
|
|
Logger.warning("ProfileWatcher: File watcher stopped")
|
|
{:noreply, state}
|
|
end
|
|
|
|
@doc false
|
|
def yaml_file?(path) do
|
|
String.ends_with?(path, [".yaml", ".yml"])
|
|
end
|
|
|
|
@doc false
|
|
def should_reload?(events) when is_list(events) do
|
|
Enum.any?(events, fn event ->
|
|
event in [:created, :modified, :removed, :renamed]
|
|
end)
|
|
end
|
|
|
|
def should_reload?(_), do: false
|
|
end
|