diff --git a/lib/towerops/accounts.ex b/lib/towerops/accounts.ex index 31e25395..70c3e2aa 100644 --- a/lib/towerops/accounts.ex +++ b/lib/towerops/accounts.ex @@ -33,6 +33,7 @@ defmodule Towerops.Accounts do nil """ + @spec get_user_by_email(String.t()) :: User.t() | nil def get_user_by_email(email) when is_binary(email) do Repo.get_by(User, email: email) end @@ -49,6 +50,7 @@ defmodule Towerops.Accounts do nil """ + @spec get_user_by_email_and_password(String.t(), String.t()) :: User.t() | nil def get_user_by_email_and_password(email, password) when is_binary(email) and is_binary(password) do user = Repo.get_by(User, email: email) if User.valid_password?(user, password), do: user @@ -68,6 +70,7 @@ defmodule Towerops.Accounts do nil """ + @spec get_user(String.t()) :: User.t() | nil def get_user(id) when is_binary(id) do Repo.get(User, id) end @@ -86,6 +89,7 @@ defmodule Towerops.Accounts do ** (Ecto.NoResultsError) """ + @spec get_user!(String.t()) :: User.t() def get_user!(id), do: Repo.get!(User, id) ## User registration @@ -99,6 +103,7 @@ defmodule Towerops.Accounts do %Ecto.Changeset{data: %User{}} """ + @spec change_user_registration(User.t(), map()) :: Ecto.Changeset.t() def change_user_registration(%User{} = user, attrs \\ %{}) do User.registration_changeset(user, attrs, hash_password: false, validate_unique: false) end @@ -115,6 +120,7 @@ defmodule Towerops.Accounts do {:error, %Ecto.Changeset{}} """ + @spec register_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def register_user(attrs) do user_changeset = %User{} @@ -147,6 +153,7 @@ defmodule Towerops.Accounts do """ @dialyzer {:nowarn_function, register_user_with_organization: 1} + @spec register_user_with_organization(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def register_user_with_organization(attrs) do user_changeset = %User{} diff --git a/lib/towerops/admin.ex b/lib/towerops/admin.ex index 70eca437..42b6d5aa 100644 --- a/lib/towerops/admin.ex +++ b/lib/towerops/admin.ex @@ -29,6 +29,7 @@ defmodule Towerops.Admin do iex> list_all_users(limit: 50, offset: 100) [%User{}, ...] """ + @spec list_all_users(Keyword.t()) :: [User.t()] def list_all_users(opts \\ []) do limit = Keyword.get(opts, :limit, 100) offset = Keyword.get(opts, :offset, 0) @@ -49,6 +50,7 @@ defmodule Towerops.Admin do iex> count_users() 42 """ + @spec count_users() :: integer() def count_users do Repo.aggregate(User, :count) end @@ -64,6 +66,7 @@ defmodule Towerops.Admin do iex> delete_user(invalid_id, superuser_id, "127.0.0.1") ** (Ecto.NoResultsError) """ + @spec delete_user(String.t(), String.t(), String.t()) :: {:ok, User.t()} | {:error, any()} def delete_user(user_id, superuser_id, ip_address) do user = Repo.get!(User, user_id) diff --git a/lib/towerops/agents.ex b/lib/towerops/agents.ex index 59c2bb44..6e4c3470 100644 --- a/lib/towerops/agents.ex +++ b/lib/towerops/agents.ex @@ -30,6 +30,7 @@ defmodule Towerops.Agents do {:error, %Ecto.Changeset{}} """ + @spec create_agent_token(String.t(), String.t()) :: {:ok, AgentToken.t(), String.t()} | {:error, Ecto.Changeset.t()} def create_agent_token(organization_id, name) do {token, changeset} = AgentToken.build_token(organization_id, name) @@ -54,6 +55,7 @@ defmodule Towerops.Agents do {:ok, %AgentToken{is_cloud_poller: true}, "abc123..."} """ + @spec create_cloud_poller(String.t()) :: {:ok, AgentToken.t(), String.t()} | {:error, Ecto.Changeset.t()} def create_cloud_poller(name) do {token, changeset} = AgentToken.build_token(nil, name, is_cloud_poller: true) @@ -75,6 +77,7 @@ defmodule Towerops.Agents do [%AgentToken{}, ...] """ + @spec list_organization_agent_tokens(String.t()) :: [AgentToken.t()] def list_organization_agent_tokens(organization_id) do AgentToken |> where([t], t.organization_id == ^organization_id and t.is_cloud_poller == false) @@ -93,6 +96,7 @@ defmodule Towerops.Agents do [%AgentToken{is_cloud_poller: true}, ...] """ + @spec list_cloud_pollers() :: [AgentToken.t()] def list_cloud_pollers do AgentToken |> where([t], t.is_cloud_poller == true) diff --git a/lib/towerops/alerts.ex b/lib/towerops/alerts.ex index b8026230..5d77540a 100644 --- a/lib/towerops/alerts.ex +++ b/lib/towerops/alerts.ex @@ -11,6 +11,7 @@ defmodule Towerops.Alerts do @doc """ Creates an alert for device status change. """ + @spec create_alert(map()) :: {:ok, Alert.t()} | {:error, Ecto.Changeset.t()} def create_alert(attrs) do %Alert{} |> Alert.changeset(attrs) @@ -20,6 +21,7 @@ defmodule Towerops.Alerts do @doc """ Returns the list of alerts for an device. """ + @spec list_devices_alerts(String.t(), integer()) :: [Alert.t()] def list_devices_alerts(device_id, limit \\ 100) do Repo.all( from(a in Alert, @@ -35,6 +37,7 @@ defmodule Towerops.Alerts do Returns the list of active (unresolved) alerts for an organization. Only returns equipment_down alerts, as equipment_up alerts are informational. """ + @spec list_organization_active_alerts(String.t()) :: [Alert.t()] def list_organization_active_alerts(organization_id) do Repo.all( from(a in Alert, @@ -52,6 +55,7 @@ defmodule Towerops.Alerts do @doc """ Returns the count of active (unresolved) alerts for an organization. """ + @spec count_active_alerts(String.t()) :: integer() def count_active_alerts(organization_id) do Repo.aggregate( from(a in Alert, @@ -74,6 +78,7 @@ defmodule Towerops.Alerts do - status: Filter by status ("active", "acknowledged", "resolved") - limit: Max number of alerts to return """ + @spec list_organization_alerts(String.t(), integer() | map()) :: [Alert.t()] def list_organization_alerts(organization_id, limit) when is_integer(limit) do list_organization_alerts(organization_id, %{"limit" => limit}) end diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex index f2691755..a016d942 100644 --- a/lib/towerops/devices.ex +++ b/lib/towerops/devices.ex @@ -21,6 +21,7 @@ defmodule Towerops.Devices do Returns the list of devices for a site. Ordered by custom display_order (if set), then alphabetically by name. """ + @spec list_site_devices(String.t()) :: [DeviceSchema.t()] def list_site_devices(site_id) do Repo.all( from(e in DeviceSchema, @@ -38,6 +39,7 @@ defmodule Towerops.Devices do - site_id: Filter by specific site - status: Filter by status ("up", "down", "unknown") """ + @spec list_organization_devices(String.t(), map()) :: [DeviceSchema.t()] def list_organization_devices(organization_id, filters \\ %{}) do query = from(e in DeviceSchema, @@ -67,6 +69,7 @@ defmodule Towerops.Devices do @doc """ Returns all devices for multiple organizations (for GDPR data access). """ + @spec list_devices_for_organizations([String.t()]) :: [DeviceSchema.t()] def list_devices_for_organizations(organization_ids) when is_list(organization_ids) do Repo.all( from(e in DeviceSchema, @@ -82,6 +85,7 @@ defmodule Towerops.Devices do @doc """ Returns the count of devices for an organization. """ + @spec count_organization_devices(String.t()) :: integer() def count_organization_devices(organization_id) do Repo.aggregate( from(e in DeviceSchema, @@ -94,6 +98,7 @@ defmodule Towerops.Devices do @doc """ Returns the count of devices for a site. """ + @spec count_site_devices(String.t()) :: integer() def count_site_devices(site_id) do Repo.aggregate( from(e in DeviceSchema, where: e.site_id == ^site_id), diff --git a/lib/towerops/monitoring.ex b/lib/towerops/monitoring.ex index fc61d312..7aaaf4ae 100644 --- a/lib/towerops/monitoring.ex +++ b/lib/towerops/monitoring.ex @@ -12,6 +12,7 @@ defmodule Towerops.Monitoring do @doc """ Creates a monitoring check. """ + @spec create_check(map()) :: {:ok, Check.t()} | {:error, Ecto.Changeset.t()} def create_check(attrs) do %Check{} |> Check.changeset(attrs) @@ -21,6 +22,7 @@ defmodule Towerops.Monitoring do @doc """ Returns the list of checks for an device. """ + @spec list_devices_checks(String.t(), integer()) :: [Check.t()] def list_devices_checks(device_id, limit \\ 100) do Repo.all(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: ^limit)) end @@ -28,6 +30,7 @@ defmodule Towerops.Monitoring do @doc """ Returns the latest check for an device. """ + @spec get_latest_check(String.t()) :: Check.t() | nil def get_latest_check(device_id) do Repo.one(from(c in Check, where: c.device_id == ^device_id, order_by: [desc: c.checked_at], limit: 1)) end @@ -35,6 +38,7 @@ defmodule Towerops.Monitoring do @doc """ Deletes old monitoring checks older than the given date. """ + @spec delete_old_checks(DateTime.t()) :: {integer(), nil | [term()]} def delete_old_checks(older_than_date) do Repo.delete_all(from(c in Check, where: c.checked_at < ^older_than_date)) end @@ -44,6 +48,7 @@ defmodule Towerops.Monitoring do Uses TimescaleDB continuous aggregates for performance when available, falls back to raw query calculation otherwise. """ + @spec get_hourly_stats(String.t(), DateTime.t(), DateTime.t()) :: {:ok, Postgrex.Result.t()} | {:error, Exception.t()} def get_hourly_stats(device_id, start_time, end_time) do # Try continuous aggregate first (much faster for large datasets) query = """ diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index ba50b11e..b6a98189 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -21,6 +21,7 @@ defmodule Towerops.Organizations do @doc """ Returns the list of organizations for a user. """ + @spec list_user_organizations(String.t()) :: [Organization.t()] def list_user_organizations(user_id) do Repo.all( from(o in Organization, @@ -36,6 +37,7 @@ defmodule Towerops.Organizations do @doc """ Checks if a user has access to an organization. """ + @spec user_has_access?(String.t(), String.t()) :: boolean() def user_has_access?(user_id, organization_id) do Repo.exists?( from m in Membership, @@ -46,11 +48,13 @@ defmodule Towerops.Organizations do @doc """ Gets a single organization by ID. """ + @spec get_organization!(String.t()) :: Organization.t() def get_organization!(id), do: Repo.get!(Organization, id) @doc """ Gets a single organization by slug. """ + @spec get_organization_by_slug!(String.t()) :: Organization.t() def get_organization_by_slug!(slug) do Repo.get_by!(Organization, slug: slug) end @@ -63,6 +67,7 @@ defmodule Towerops.Organizations do """ @dialyzer {:nowarn_function, create_organization: 2} @dialyzer {:nowarn_function, create_organization: 3} + @spec create_organization(map(), String.t(), Keyword.t()) :: {:ok, Organization.t()} | {:error, Ecto.Changeset.t()} def create_organization(attrs, user_id, opts \\ []) do bypass_limits = Keyword.get(opts, :bypass_limits, false) subscription_plan = Map.get(attrs, :subscription_plan) || Map.get(attrs, "subscription_plan") || "free" diff --git a/lib/towerops/sites.ex b/lib/towerops/sites.ex index c06f753a..96f3a123 100644 --- a/lib/towerops/sites.ex +++ b/lib/towerops/sites.ex @@ -15,6 +15,7 @@ defmodule Towerops.Sites do Returns the list of sites for an organization. Ordered by custom display_order (if set), then alphabetically by name. """ + @spec list_organization_sites(String.t()) :: [Site.t()] def list_organization_sites(organization_id) do Repo.all( from(s in Site, @@ -28,6 +29,7 @@ defmodule Towerops.Sites do @doc """ Returns the count of sites for an organization. """ + @spec count_organization_sites(String.t()) :: integer() def count_organization_sites(organization_id) do Repo.aggregate( from(s in Site, where: s.organization_id == ^organization_id), @@ -39,6 +41,7 @@ defmodule Towerops.Sites do Returns the list of root sites (sites without a parent) for an organization. Ordered by custom display_order (if set), then alphabetically by name. """ + @spec list_root_sites(String.t()) :: [Site.t()] def list_root_sites(organization_id) do Repo.all( from(s in Site, @@ -53,6 +56,7 @@ defmodule Towerops.Sites do Returns the list of child sites for a parent site. Ordered by custom display_order (if set), then alphabetically by name. """ + @spec list_child_sites(String.t()) :: [Site.t()] def list_child_sites(parent_site_id) do Repo.all( from(s in Site, @@ -65,6 +69,7 @@ defmodule Towerops.Sites do @doc """ Gets a single site. """ + @spec get_site!(String.t()) :: Site.t() def get_site!(id) do Site |> Repo.get!(id) @@ -74,6 +79,7 @@ defmodule Towerops.Sites do @doc """ Gets a single site. Returns nil if not found. """ + @spec get_site(String.t()) :: Site.t() | nil def get_site(id) do case Repo.get(Site, id) do nil -> nil @@ -84,6 +90,7 @@ defmodule Towerops.Sites do @doc """ Gets a single site belonging to an organization. """ + @spec get_organization_site!(String.t(), String.t()) :: Site.t() def get_organization_site!(organization_id, site_id) do Repo.one!( from(s in Site, @@ -97,6 +104,7 @@ defmodule Towerops.Sites do @doc """ Creates a site. """ + @spec create_site(map()) :: {:ok, Site.t()} | {:error, Ecto.Changeset.t()} def create_site(attrs) do %Site{} |> Site.changeset(attrs) diff --git a/lib/towerops/snmp.ex b/lib/towerops/snmp.ex index e555fb03..b2434bfa 100644 --- a/lib/towerops/snmp.ex +++ b/lib/towerops/snmp.ex @@ -38,6 +38,7 @@ defmodule Towerops.Snmp do iex> test_connection("192.168.1.99", "wrong", "2c") {:error, :timeout} """ + @spec test_connection(String.t(), String.t(), String.t(), integer()) :: {:ok, String.t()} | {:error, term()} def test_connection(ip, community, version, port \\ 161) do Client.test_connection( ip: ip, @@ -62,6 +63,7 @@ defmodule Towerops.Snmp do iex> discover_device(device) {:ok, %Device{}} """ + @spec discover_device(DeviceSchema.t()) :: {:ok, Device.t()} | {:error, term()} def discover_device(%DeviceSchema{} = device) do Discovery.discover_device(device) end @@ -76,6 +78,7 @@ defmodule Towerops.Snmp do iex> discover_all_for_org(org_id) {:ok, %{success: 10, failed: 2, errors: [:timeout, :no_response]}} """ + @spec discover_all_for_org(String.t()) :: {:ok, map()} def discover_all_for_org(org_id) do Discovery.discover_all(org_id) end @@ -93,6 +96,7 @@ defmodule Towerops.Snmp do iex> get_device(nonexistent_id) nil """ + @spec get_device(String.t()) :: Device.t() | nil def get_device(device_id) do Repo.get_by(Device, device_id: device_id) end diff --git a/mix.exs b/mix.exs index 70abad84..152f1987 100644 --- a/mix.exs +++ b/mix.exs @@ -98,7 +98,7 @@ defmodule Towerops.MixProject do plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, # Faster incremental analysis plt_add_deps: :apps_direct, - plt_add_apps: [:mix, :ex_unit], + plt_add_apps: [:mix, :ex_unit, :ecto], flags: [:unmatched_returns, :error_handling, :underspecs, :unknown], ignore_warnings: ".dialyzer_ignore.exs" ]