logging fix and some other codex fixes

This commit is contained in:
Graham McIntire 2026-03-10 15:04:28 -05:00
parent 3e70f0ba48
commit e026930f25
No known key found for this signature in database
11 changed files with 74 additions and 63 deletions

1
.gitignore vendored
View file

@ -92,6 +92,7 @@ profiles.json
.runtime/
.claude/
.beads/
.agents/
.logs/
.direnv/

View file

@ -1906,10 +1906,22 @@ defmodule Towerops.Snmp do
end)
|> tap(fn {_s, errors, error_samples} ->
if errors > 0 do
Logger.warning("#{errors} ARP upsert failures for device #{device_id}",
samples_text =
case Enum.reverse(error_samples) do
[] ->
""
samples ->
"\nSample errors:\n" <>
Enum.map_join(samples, "\n", fn sample ->
" - IP: #{sample.ip}, MAC: #{sample.mac}, errors: #{inspect(sample.errors)}"
end)
end
Logger.warning(
"#{errors} ARP upsert failures for device #{device_id}#{samples_text}",
device_id: device_id,
error_count: errors,
sample_errors: Enum.reverse(error_samples)
error_count: errors
)
end
end)

View file

@ -398,12 +398,6 @@ defmodule ToweropsWeb.AgentLive.Index do
{:noreply, maybe_update_agent(socket, agent_token_id, organization_id)}
end
# Handled by StatusTitleComponent in layout - ignore to prevent crashes
@impl true
def handle_info({:alert_changed, _org_id}, socket) do
{:noreply, socket}
end
@impl true
def handle_info({:agents_stale, _stale_agents}, socket) do
{:noreply, reload_agent_data(socket)}

View file

@ -186,11 +186,6 @@ defmodule ToweropsWeb.AlertLive.Index do
{:noreply, load_alerts(socket, socket.assigns.current_scope.organization.id)}
end
@impl true
def handle_info({:alert_changed, _org_id}, socket) do
{:noreply, load_alerts(socket, socket.assigns.current_scope.organization.id)}
end
defp load_alerts(socket, organization_id) do
all_alerts = Alerts.list_organization_alerts(organization_id, %{"limit" => 500})

View file

@ -1,37 +1,9 @@
defmodule ToweropsWeb.Live.Components.StatusTitleComponent do
@moduledoc """
LiveComponent that subscribes to organization alert changes and updates the page title emoji.
LiveComponent that renders the browser-title status hook target.
"""
use ToweropsWeb, :live_component
alias ToweropsWeb.Helpers.StatusHelpers
def mount(socket) do
{:ok, socket}
end
def update(%{organization_id: org_id}, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{org_id}:alerts")
end
{:ok, assign(socket, :organization_id, org_id)}
end
def handle_info({:alert_changed, _org_id}, socket) do
# Get the organization from the parent socket
org_id = socket.assigns.organization_id
organization = Towerops.Organizations.get_organization!(org_id)
if organization do
emoji = StatusHelpers.status_emoji(organization)
# Push event to JavaScript hook to update the browser title
{:noreply, push_event(socket, "update_status_emoji", %{emoji: emoji})}
else
{:noreply, socket}
end
end
def render(assigns) do
~H"""
<div id="status-title-watcher" phx-hook="StatusTitle" phx-target={@myself} style="display: none;">

View file

@ -96,11 +96,6 @@ defmodule ToweropsWeb.DashboardLive do
{:noreply, load_dashboard_data(socket, socket.assigns.current_scope.organization.id)}
end
@impl true
def handle_info({:alert_changed, _org_id}, socket) do
{:noreply, load_dashboard_data(socket, socket.assigns.current_scope.organization.id)}
end
defp load_dashboard_data(socket, organization_id) do
organization = socket.assigns.current_scope.organization
summary = Dashboard.get_dashboard_summary(organization_id)

View file

@ -276,12 +276,6 @@ defmodule ToweropsWeb.DeviceLive.Index do
{:noreply, reload_device_stream(socket)}
end
# Handled by StatusTitleComponent in layout - ignore to prevent crashes
@impl true
def handle_info({:alert_changed, _org_id}, socket) do
{:noreply, socket}
end
defp tick_interval_ms do
if Application.get_env(:towerops, :env) == :test, do: :infinity, else: to_timeout(second: 15)
end

View file

@ -244,12 +244,6 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply, assign(socket, :show_check_form, false)}
end
# Handled by StatusTitleComponent in layout - ignore to prevent crashes
@impl true
def handle_info({:alert_changed, _org_id}, socket) do
{:noreply, socket}
end
# Private functions
defp maybe_subscribe_and_schedule_refresh(socket, device_id) do

View file

@ -369,6 +369,7 @@ defmodule ToweropsWeb.Router do
{ToweropsWeb.UserAuth, :require_authenticated_user},
{ToweropsWeb.UserAuth, :require_totp_enrollment},
{ToweropsWeb.UserAuth, :load_current_organization},
{ToweropsWeb.UserAuth, :subscribe_to_status_updates},
{ToweropsWeb.UserAuth, :handle_policy_consent}
] do
scope "/orgs/:org_slug", ToweropsWeb do
@ -389,6 +390,7 @@ defmodule ToweropsWeb.Router do
{ToweropsWeb.UserAuth, :require_authenticated_user},
{ToweropsWeb.UserAuth, :require_totp_enrollment},
{ToweropsWeb.UserAuth, :load_default_organization},
{ToweropsWeb.UserAuth, :subscribe_to_status_updates},
{ToweropsWeb.UserAuth, :handle_policy_consent}
] do
scope "/", ToweropsWeb do

View file

@ -19,6 +19,7 @@ defmodule ToweropsWeb.UserAuth do
alias Phoenix.LiveView
alias Towerops.Accounts
alias Towerops.Accounts.Scope
alias ToweropsWeb.Helpers.StatusHelpers
# Make the remember me cookie valid for 14 days. This should match
# the session validity setting in UserToken.
@ -545,9 +546,10 @@ defmodule ToweropsWeb.UserAuth do
def on_mount(:subscribe_to_status_updates, _params, _session, socket) do
organization = socket.assigns[:current_scope] && socket.assigns.current_scope.organization
if organization && LiveView.connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{organization.id}:alerts")
end
socket =
socket
|> maybe_subscribe_to_status_updates(organization)
|> attach_status_title_hook(organization)
{:cont, socket}
end
@ -980,4 +982,25 @@ defmodule ToweropsWeb.UserAuth do
defp determine_login_method(_conn, %{"remember_me" => _}), do: "password"
defp determine_login_method(_conn, _params), do: "password"
defp maybe_subscribe_to_status_updates(socket, organization) do
if organization && LiveView.connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{organization.id}:alerts")
end
socket
end
defp attach_status_title_hook(socket, nil), do: socket
defp attach_status_title_hook(socket, organization) do
LiveView.attach_hook(socket, :status_title_updates, :handle_info, fn
{:alert_changed, org_id}, socket when org_id == organization.id ->
emoji = StatusHelpers.status_emoji(organization)
{:halt, LiveView.push_event(socket, "update_status_emoji", %{emoji: emoji})}
_message, socket ->
{:cont, socket}
end)
end
end

View file

@ -144,6 +144,35 @@ defmodule ToweropsWeb.SiteLiveTest do
live(conn, ~p"/sites/#{other_site.id}")
end
end
test "handles alert_changed broadcasts without crashing and pushes title update", %{
conn: conn,
organization: organization
} do
{:ok, site} =
Sites.create_site(%{name: "Alert Site", organization_id: organization.id})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Tower Router",
ip_address: "10.0.0.1",
site_id: site.id,
organization_id: organization.id
})
{:ok, view, _html} = live(conn, ~p"/sites/#{site.id}")
{:ok, _alert} =
Towerops.Alerts.create_alert(%{
device_id: device.id,
alert_type: "device_down",
triggered_at: DateTime.utc_now(),
message: "Device down"
})
assert_push_event(view, "update_status_emoji", %{emoji: "🔴"})
assert render(view) =~ "Alert Site"
end
end
describe "Show - Metrics Bar" do