fix dialyzer things

This commit is contained in:
Graham McIntire 2026-01-03 12:56:40 -06:00
parent 5ae974d7aa
commit 0f9266027f
No known key found for this signature in database
13 changed files with 167 additions and 126 deletions

View file

@ -93,21 +93,24 @@ defmodule Towerops.Accounts do
{:error, %Ecto.Changeset{}}
"""
@dialyzer {:nowarn_function, register_user_with_organization: 1}
def register_user_with_organization(attrs) do
user_changeset = User.email_changeset(%User{}, attrs)
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, user_changeset)
|> Ecto.Multi.run(:organization, fn _repo, %{user: user} ->
org_name = attrs["organization_name"] || "Personal"
multi = Ecto.Multi.new()
multi = Ecto.Multi.insert(multi, :user, user_changeset)
Towerops.Organizations.create_organization(
%{name: org_name},
user.id
)
end)
|> Repo.transaction()
|> case do
multi =
Ecto.Multi.run(multi, :organization, fn _repo, %{user: user} ->
org_name = attrs["organization_name"] || "Personal"
Towerops.Organizations.create_organization(
%{name: org_name},
user.id
)
end)
case Repo.transaction(multi) do
{:ok, %{user: user}} -> {:ok, user}
{:error, :user, changeset, _} -> {:error, changeset}
{:error, :organization, changeset, _} -> {:error, changeset}

View file

@ -60,7 +60,7 @@ defmodule Towerops.Alerts do
where: s.organization_id == ^organization_id,
order_by: [desc: a.triggered_at],
limit: ^limit,
preload: [equipment: {e, site: s}, acknowledged_by: :acknowledged_by]
preload: [equipment: {e, site: s}, acknowledged_by: []]
)
)
end

View file

@ -52,13 +52,13 @@ defmodule Towerops.Monitoring.EquipmentMonitor do
@impl true
def handle_info(:check_equipment, state) do
perform_check(state.equipment_id)
_ = perform_check(state.equipment_id)
{:noreply, state}
end
@impl true
def handle_cast(:check_now, state) do
perform_check(state.equipment_id)
_ = perform_check(state.equipment_id)
{:noreply, state}
end
@ -88,19 +88,21 @@ defmodule Towerops.Monitoring.EquipmentMonitor do
new_status = if status == :success, do: :up, else: :down
old_status = equipment.status
Equipment.update_equipment_status(equipment, new_status)
_ = Equipment.update_equipment_status(equipment, new_status)
# Create alerts if status changed
if old_status != new_status do
handle_status_change(equipment_id, old_status, new_status)
end
_ =
if old_status != new_status do
handle_status_change(equipment_id, old_status, new_status)
end
# Broadcast status change via PubSub
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"equipment:#{equipment_id}",
{:equipment_status_changed, equipment_id, new_status, response_time}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"equipment:#{equipment_id}",
{:equipment_status_changed, equipment_id, new_status, response_time}
)
# Only schedule next check if monitoring is enabled
if equipment.monitoring_enabled do
@ -124,18 +126,20 @@ defmodule Towerops.Monitoring.EquipmentMonitor do
})
# Send email notification in background (not in test environment)
if !test_env?() do
Task.start(fn ->
Alerts.send_alert_notification(alert)
end)
end
_ =
if !test_env?() do
Task.start(fn ->
Alerts.send_alert_notification(alert)
end)
end
# Broadcast alert
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:new",
{:new_alert, equipment_id, :equipment_down}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:new",
{:new_alert, equipment_id, :equipment_down}
)
end
{_, :up} ->
@ -149,31 +153,30 @@ defmodule Towerops.Monitoring.EquipmentMonitor do
})
# Send email notification in background (not in test environment)
if !test_env?() do
Task.start(fn ->
Alerts.send_alert_notification(alert)
end)
end
_ =
if !test_env?() do
Task.start(fn ->
Alerts.send_alert_notification(alert)
end)
end
# Resolve any active equipment_down alerts
case Alerts.get_active_alert(equipment_id, :equipment_down) do
nil ->
:ok
_ =
case Alerts.get_active_alert(equipment_id, :equipment_down) do
nil ->
:ok
alert ->
Alerts.resolve_alert(alert)
end
alert ->
Alerts.resolve_alert(alert)
end
# Broadcast recovery
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:resolved",
{:alert_resolved, equipment_id, :equipment_down}
)
{_, :unknown} ->
# Status is unknown - no action needed
:ok
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:resolved",
{:alert_resolved, equipment_id, :equipment_down}
)
end
end

View file

@ -21,13 +21,14 @@ defmodule Towerops.Monitoring.Supervisor do
# Start all monitors after supervisor is initialized (but not in test mode)
# In test mode, the Repo uses Sandbox pool which we can detect
if !test_mode?() do
Task.start(fn ->
# Wait briefly for the supervisor tree to be fully initialized
Process.sleep(100)
start_all_monitors()
end)
end
_ =
if !test_mode?() do
Task.start(fn ->
# Wait briefly for the supervisor tree to be fully initialized
Process.sleep(100)
start_all_monitors()
end)
end
Supervisor.init(children, strategy: :one_for_one)
end

View file

@ -43,18 +43,21 @@ defmodule Towerops.Organizations do
@doc """
Creates an organization and adds the creator as owner.
"""
@dialyzer {:nowarn_function, create_organization: 2}
def create_organization(attrs, user_id) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:organization, Organization.changeset(%Organization{}, attrs))
|> Ecto.Multi.insert(:membership, fn %{organization: organization} ->
Membership.changeset(%Membership{}, %{
organization_id: organization.id,
user_id: user_id,
role: :owner
})
end)
|> Repo.transaction()
|> case do
multi = Ecto.Multi.new()
multi = Ecto.Multi.insert(multi, :organization, Organization.changeset(%Organization{}, attrs))
multi =
Ecto.Multi.insert(multi, :membership, fn %{organization: organization} ->
Membership.changeset(%Membership{}, %{
organization_id: organization.id,
user_id: user_id,
role: :owner
})
end)
case Repo.transaction(multi) do
{:ok, %{organization: organization}} -> {:ok, organization}
{:error, :organization, changeset, _} -> {:error, changeset}
{:error, :membership, changeset, _} -> {:error, changeset}
@ -201,23 +204,28 @@ defmodule Towerops.Organizations do
@doc """
Accepts an invitation and creates a membership.
"""
@dialyzer {:nowarn_function, accept_invitation: 2}
def accept_invitation(invitation, user_id) do
Ecto.Multi.new()
|> Ecto.Multi.update(:invitation, fn _ ->
Ecto.Changeset.change(invitation, %{
accepted_at: DateTime.truncate(DateTime.utc_now(), :second),
accepted_by_id: user_id
})
end)
|> Ecto.Multi.insert(:membership, fn _ ->
Membership.changeset(%Membership{}, %{
organization_id: invitation.organization_id,
user_id: user_id,
role: invitation.role
})
end)
|> Repo.transaction()
|> case do
multi = Ecto.Multi.new()
multi =
Ecto.Multi.update(multi, :invitation, fn _ ->
Ecto.Changeset.change(invitation, %{
accepted_at: DateTime.truncate(DateTime.utc_now(), :second),
accepted_by_id: user_id
})
end)
multi =
Ecto.Multi.insert(multi, :membership, fn _ ->
Membership.changeset(%Membership{}, %{
organization_id: invitation.organization_id,
user_id: user_id,
role: invitation.role
})
end)
case Repo.transaction(multi) do
{:ok, %{membership: membership}} -> {:ok, membership}
{:error, _failed_operation, changeset, _changes_so_far} -> {:error, changeset}
end

View file

@ -25,7 +25,15 @@ defmodule Towerops.Organizations.Invitation do
@doc false
def changeset(invitation, attrs) do
invitation
|> cast(attrs, [:email, :role, :organization_id, :invited_by_id])
|> cast(attrs, [
:email,
:role,
:organization_id,
:invited_by_id,
:token,
:expires_at,
:accepted_at
])
|> validate_required([:email, :role, :organization_id, :invited_by_id])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/)
|> generate_token()
@ -37,12 +45,22 @@ defmodule Towerops.Organizations.Invitation do
end
defp generate_token(changeset) do
token = 32 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
put_change(changeset, :token, token)
# Only generate token if not already provided
if get_field(changeset, :token) do
changeset
else
token = 32 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
put_change(changeset, :token, token)
end
end
defp set_expires_at(changeset) do
expires_at = DateTime.utc_now() |> DateTime.add(7, :day) |> DateTime.truncate(:second)
put_change(changeset, :expires_at, expires_at)
# Only set expires_at if not already provided
if get_field(changeset, :expires_at) do
changeset
else
expires_at = DateTime.utc_now() |> DateTime.add(7, :day) |> DateTime.truncate(:second)
put_change(changeset, :expires_at, expires_at)
end
end
end

View file

@ -6,7 +6,7 @@ defmodule Towerops.Release do
@app :towerops
def migrate do
load_app()
_ = load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
@ -14,7 +14,7 @@ defmodule Towerops.Release do
end
def rollback(repo, version) do
load_app()
_ = load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
@ -24,7 +24,7 @@ defmodule Towerops.Release do
defp load_app do
# Many platforms require SSL when connecting to the database
Application.ensure_all_started(:ssl)
_ = Application.ensure_all_started(:ssl)
Application.ensure_loaded(@app)
end
end

View file

@ -50,12 +50,13 @@ defmodule ToweropsWeb.UserSessionController do
# magic link request
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_login_instructions(
user,
&url(~p"/users/log-in/#{&1}")
)
end
_ =
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_login_instructions(
user,
&url(~p"/users/log-in/#{&1}")
)
end
info =
"If your email is in our system, you will receive instructions for logging in shortly."

View file

@ -19,11 +19,12 @@ defmodule ToweropsWeb.UserSettingsController do
case Accounts.change_user_email(user, user_params) do
%{valid?: true} = changeset ->
Accounts.deliver_user_update_email_instructions(
Ecto.Changeset.apply_action!(changeset, :insert),
user.email,
&url(~p"/users/settings/confirm-email/#{&1}")
)
_ =
Accounts.deliver_user_update_email_instructions(
Ecto.Changeset.apply_action!(changeset, :insert),
user.email,
&url(~p"/users/settings/confirm-email/#{&1}")
)
conn
|> put_flash(

View file

@ -9,10 +9,11 @@ defmodule ToweropsWeb.AlertLive.Index do
organization = socket.assigns.current_organization
# Subscribe to alert events
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:resolved")
end
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:resolved")
end
{:ok,
socket
@ -34,7 +35,7 @@ defmodule ToweropsWeb.AlertLive.Index do
@impl true
def handle_event("acknowledge", %{"id" => id}, socket) do
alert = Alerts.get_alert!(id)
user_id = socket.assigns.current_user.id
user_id = socket.assigns.current_scope.user.id
case Alerts.acknowledge_alert(alert, user_id) do
{:ok, _alert} ->

View file

@ -11,10 +11,11 @@ defmodule ToweropsWeb.DashboardLive do
organization = socket.assigns.current_organization
# Subscribe to real-time alert updates
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:resolved")
end
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:new")
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:resolved")
end
{:ok,
socket

View file

@ -16,9 +16,10 @@ defmodule ToweropsWeb.EquipmentLive.Show do
checks = Monitoring.list_equipment_checks(id, 10)
# Subscribe to status updates for this equipment
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}")
end
_ =
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}")
end
{:noreply,
socket

View file

@ -42,6 +42,11 @@ defmodule Towerops.Alerts.AlertNotifierTest do
site_id: site.id
})
# Consume confirmation emails from user creation
assert_email_sent(subject: "Confirmation instructions")
assert_email_sent(subject: "Confirmation instructions")
assert_email_sent(subject: "Confirmation instructions")
%{owner: owner, admin: admin, member: member, organization: organization, equipment: equipment}
end
@ -66,9 +71,8 @@ defmodule Towerops.Alerts.AlertNotifierTest do
assert length(results) == 2
# Verify emails were sent to owner and admin
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}")
assert_email_sent(to: owner.email)
assert_email_sent(to: admin.email)
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}", to: owner.email)
assert_email_sent(subject: "[#{organization.name}] Equipment Down: #{equipment.name}", to: admin.email)
end
test "sends equipment_up alert to owners and admins", %{
@ -91,9 +95,8 @@ defmodule Towerops.Alerts.AlertNotifierTest do
assert length(results) == 2
# Verify emails were sent
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}")
assert_email_sent(to: owner.email)
assert_email_sent(to: admin.email)
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}", to: owner.email)
assert_email_sent(subject: "[#{organization.name}] Equipment Recovered: #{equipment.name}", to: admin.email)
end
test "equipment_down alert includes correct information", %{
@ -112,7 +115,7 @@ defmodule Towerops.Alerts.AlertNotifierTest do
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
assert_email_sent(fn email ->
email.to == [{nil, owner.email}] &&
email.to == [{"", owner.email}] &&
email.subject =~ "Equipment Down" &&
email.text_body =~ organization.name &&
email.text_body =~ equipment.name &&
@ -138,7 +141,7 @@ defmodule Towerops.Alerts.AlertNotifierTest do
{:ok, _results} = AlertNotifier.deliver_alert_notification(alert)
assert_email_sent(fn email ->
email.to == [{nil, owner.email}] &&
email.to == [{"", owner.email}] &&
email.subject =~ "Equipment Recovered" &&
email.text_body =~ organization.name &&
email.text_body =~ equipment.name &&