towerops/lib/towerops_web/live/equipment_live/show.html.heex
Graham McIntire 806b293ead
Fix impersonation to show correct user data and banner
Two issues were preventing impersonation from working correctly:

1. Templates were not passing current_scope to Layouts.authenticated,
   so the impersonation banner never displayed.

2. fetch_current_scope_for_user was looking up the user from the
   session token, which always pointed to the superuser. This caused
   the superuser to see their own organizations and equipment instead
   of the impersonated user's data.

Changes:
- Pass current_scope={@current_scope} to all Layouts.authenticated calls
- Store target_user_id in session during impersonation
- Fetch target user directly from database using target_user_id
- Update both fetch_current_scope_for_user (for controllers) and
  mount_current_scope (for LiveViews) to properly handle impersonation
- Clean up target_user_id from session when impersonation ends

Now when a superuser impersonates a user, they correctly see:
- The impersonation banner at the top with exit link
- The target user's organizations and equipment
- All data scoped to the impersonated user
2026-01-06 13:22:13 -06:00

528 lines
25 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="equipment"
>
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment.name}</h1>
<p class="text-sm text-zinc-600 dark:text-zinc-400 font-mono">{@equipment.ip_address}</p>
</div>
<div class="flex gap-2">
<.button navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"
}>
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
</.button>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-zinc-200 dark:border-zinc-700">
<nav class="-mb-px flex space-x-8">
<.link
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=overview"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "overview" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
end
]}
>
Overview
</.link>
<%= if @snmp_device do %>
<.link
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=ports"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "ports" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
end
]}
>
Ports
</.link>
<% end %>
<.link
patch={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}?tab=logs"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "logs" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
end
]}
>
Logs
</.link>
</nav>
</div>
</div>
<!-- Tab Content -->
<div class="mt-6">
<%= case @active_tab do %>
<% "overview" -> %>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<!-- Left Column: Device Information & Traffic -->
<div class="space-y-6">
<!-- Device Information -->
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Device Information
</h3>
</div>
<div class="p-4">
<dl class="space-y-3">
<%= if @snmp_device do %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">System Name</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@snmp_device.sys_name || @equipment.name}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Resolved IP</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
{@equipment.ip_address}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Hardware</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@snmp_device.model || "N/A"}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Operating System</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{if @snmp_device.manufacturer && @snmp_device.firmware_version do
"#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}"
else
@snmp_device.sys_descr || "N/A"
end}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Object ID</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
{@snmp_device.sys_object_id || "N/A"}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Contact</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@snmp_device.sys_contact || "N/A"}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Location</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@snmp_device.sys_location || "N/A"}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Uptime</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{format_uptime(@snmp_device.sys_uptime)}
</dd>
</div>
<% else %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Name</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@equipment.name}
</dd>
</div>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">IP Address</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100 font-mono">
{@equipment.ip_address}
</dd>
</div>
<% end %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Device Added</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{format_device_age(@equipment.inserted_at)}
</dd>
</div>
<div class="flex justify-between py-2">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">Last Discovered</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{if @equipment.last_discovery_at do
format_device_age(@equipment.last_discovery_at)
else
"Never"
end}
</dd>
</div>
</dl>
</div>
</div>
<!-- Overall Traffic Chart -->
<%= if @traffic_chart_data do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/traffic"
}
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Overall Traffic
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
</div>
</.link>
<div class="p-4">
<div
id="traffic-chart"
phx-hook="SensorChart"
data-chart={@traffic_chart_data}
data-unit="bps"
data-auto-scale="true"
data-show-zero-line="true"
style="height: 300px;"
>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
</div>
<!-- Right Column: Graphs and Metrics -->
<div class="space-y-6">
<!-- CPU Usage Chart -->
<%= if @cpu_chart_data do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/processors"
}
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Processors
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
</div>
</.link>
<div class="p-4">
<div
id="cpu-chart"
phx-hook="SensorChart"
data-chart={@cpu_chart_data}
style="height: 300px;"
>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
<!-- Memory Usage Chart -->
<%= if @memory_chart_data do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/memory"
}
class="block px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-750 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Memory Usage
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
</div>
</.link>
<div class="p-4">
<div
id="memory-chart"
phx-hook="SensorChart"
data-chart={@memory_chart_data}
style="height: 300px;"
>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
<!-- Storage Usage -->
<%= if @storage_sensors && length(@storage_sensors) > 0 do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Storage Usage
</h3>
</div>
<div class="p-4">
<dl class="space-y-3">
<%= for sensor <- @storage_sensors do %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/storage"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
N/A
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Temperature Sensors -->
<%= if @temperature_sensors && length(@temperature_sensors) > 0 do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Temperature
</h3>
</div>
<div class="p-4">
<dl class="space-y-3">
<%= for sensor <- @temperature_sensors do %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/temperature"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
N/A
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Voltage Sensors -->
<%= if @voltage_sensors && length(@voltage_sensors) > 0 do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Voltage</h3>
</div>
<div class="p-4">
<dl class="space-y-3">
<%= for sensor <- @voltage_sensors do %>
<div class="flex justify-between py-2 border-b border-zinc-100 dark:border-zinc-800">
<dt class="text-sm text-zinc-600 dark:text-zinc-400">
<.link
navigate={
~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/graph/voltage"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
N/A
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
</div>
</div>
<% "ports" -> %>
<%= if @snmp_interfaces && length(@snmp_interfaces) > 0 do %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
Network Interfaces
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
<thead class="bg-zinc-50 dark:bg-zinc-900">
<tr class="text-xs text-zinc-600 dark:text-zinc-400">
<th class="px-4 py-3 text-left font-medium">#</th>
<th class="px-4 py-3 text-left font-medium">Name</th>
<th class="px-4 py-3 text-left font-medium">Status</th>
<th class="px-4 py-3 text-left font-medium">Speed</th>
<th class="px-4 py-3 text-left font-medium">MAC</th>
<th class="px-4 py-3 text-left font-medium">Type</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
<%= for interface <- @snmp_interfaces do %>
<tr class="text-sm hover:bg-zinc-50 dark:hover:bg-zinc-900">
<td class="px-4 py-3 text-zinc-900 dark:text-zinc-100">
{interface.if_index}
</td>
<td class="px-4 py-3">
<div class="font-medium text-zinc-900 dark:text-zinc-100">
{interface.if_name || interface.if_descr}
</div>
<%= if interface.if_alias do %>
<div class="text-xs text-zinc-500 dark:text-zinc-400">
{interface.if_alias}
</div>
<% end %>
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
interface.if_oper_status == "up" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
interface.if_oper_status == "down" &&
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
interface.if_oper_status not in ["up", "down"] &&
"bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
]}>
{String.upcase(interface.if_oper_status || "unknown")}
</span>
</td>
<td class="px-4 py-3 text-zinc-900 dark:text-zinc-100">
{format_speed(interface.if_speed)}
</td>
<td class="px-4 py-3 font-mono text-xs text-zinc-600 dark:text-zinc-400">
{interface.if_phys_address || "-"}
</td>
<td class="px-4 py-3 text-xs text-zinc-600 dark:text-zinc-400">
{interface.if_type || "-"}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-12">
<p class="text-zinc-500 dark:text-zinc-400">No interfaces discovered</p>
</div>
<% end %>
<% "logs" -> %>
<div class="bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700">
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Event Logs</h3>
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1">
Showing the most recent 100 events
</p>
</div>
<%= if Enum.any?(@events) do %>
<div class="divide-y divide-zinc-200 dark:divide-zinc-700">
<%= for event <- @events do %>
<div class="px-4 py-3 hover:bg-zinc-50 dark:hover:bg-zinc-750">
<div class="flex items-start gap-3">
<!-- Severity Icon -->
<div class={[
"flex-shrink-0 mt-0.5",
event.severity == "info" && "text-blue-500",
event.severity == "warning" && "text-yellow-500",
event.severity == "critical" && "text-red-500"
]}>
<%= if event.severity == "info" do %>
<.icon name="hero-information-circle" class="w-5 h-5" />
<% end %>
<%= if event.severity == "warning" do %>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<% end %>
<%= if event.severity == "critical" do %>
<.icon name="hero-x-circle" class="w-5 h-5" />
<% end %>
</div>
<!-- Event Details -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
{event.message}
</p>
<div class="mt-1 flex items-center gap-3 text-xs text-zinc-500 dark:text-zinc-400">
<span>{format_event_type(event.event_type)}</span>
<span>•</span>
<span class="capitalize">{event.severity}</span>
</div>
</div>
<time class="flex-shrink-0 text-xs text-zinc-500 dark:text-zinc-400">
{time_ago(event.occurred_at)}
</time>
</div>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="p-8 text-center">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-zinc-400" />
<h3 class="mt-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
No events yet
</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Events will appear here as interface states change, speeds change, or other monitored changes occur.
</p>
</div>
<% end %>
</div>
<% _ -> %>
<div class="text-center py-12">
<p class="text-zinc-500 dark:text-zinc-400">Tab not found</p>
</div>
<% end %>
</div>
</Layouts.authenticated>