diff --git a/lib/towerops_web/live/device_live/show.html.heex b/lib/towerops_web/live/device_live/show.html.heex
index a81e6411..867c33a6 100644
--- a/lib/towerops_web/live/device_live/show.html.heex
+++ b/lib/towerops_web/live/device_live/show.html.heex
@@ -211,36 +211,6 @@
-
- <%= if @latency_chart_data do %>
-
- <.link
- navigate={
- ~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/latency"
- }
- 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"
- >
-
-
- Ping Latency
-
- <.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
-
-
-
-
- <% end %>
<%= if @traffic_chart_data do %>
@@ -272,6 +242,36 @@
<% end %>
+
+ <%= if @latency_chart_data do %>
+
+ <.link
+ navigate={
+ ~p"/orgs/#{@current_organization.slug}/devices/#{@device.id}/graph/latency"
+ }
+ 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"
+ >
+
+
+ Ping Latency
+
+ <.icon name="hero-arrow-right" class="h-4 w-4 text-zinc-400" />
+
+
+
+
+ <% end %>
diff --git a/test/towerops_web/live/graph_live/show_test.exs b/test/towerops_web/live/graph_live/show_test.exs
new file mode 100644
index 00000000..43dfa099
--- /dev/null
+++ b/test/towerops_web/live/graph_live/show_test.exs
@@ -0,0 +1,343 @@
+defmodule ToweropsWeb.GraphLive.ShowTest do
+ use ToweropsWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+
+ alias Towerops.Monitoring
+ alias Towerops.Organizations
+ alias Towerops.Sites
+ alias Towerops.Snmp
+
+ setup :register_and_log_in_user
+
+ setup %{user: user} do
+ {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
+
+ {:ok, site} =
+ Sites.create_site(%{
+ name: "Test Site",
+ organization_id: organization.id
+ })
+
+ {:ok, device} =
+ Towerops.Devices.create_device(%{
+ name: "Test Router",
+ ip_address: "192.168.1.1",
+ site_id: site.id,
+ snmp_enabled: true
+ })
+
+ # Create SNMP device for sensor tests
+ {:ok, snmp_device} =
+ Snmp.create_snmp_device(%{
+ device_id: device.id,
+ sys_name: "test-router",
+ sys_descr: "Test Router",
+ sys_object_id: "1.3.6.1.4.1.9"
+ })
+
+ %{organization: organization, site: site, device: device, snmp_device: snmp_device}
+ end
+
+ describe "Latency graph" do
+ test "renders latency graph page with correct assigns", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ # Create monitoring checks
+ Monitoring.create_check(%{
+ device_id: device.id,
+ status: :success,
+ response_time_ms: 10.5,
+ checked_at: DateTime.utc_now()
+ })
+
+ {:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ # Verify page renders
+ assert html =~ "Ping Latency"
+ assert html =~ device.name
+
+ # Verify correct assigns are set
+ assert view.assigns.current_organization.id == org.id
+ assert view.assigns.device_id == device.id
+ assert view.assigns.sensor_type == "latency"
+ assert view.assigns.range == "24h"
+ end
+
+ test "uses current_organization in back link", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ {:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ # Verify back link uses organization slug (this was the bug)
+ assert html =~ ~s[/orgs/#{org.slug}/devices/#{device.id}]
+ end
+
+ test "displays empty state when no latency data", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ {:ok, _view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ assert html =~ "No sensor data available"
+ end
+ end
+
+ describe "Processor graph" do
+ test "renders processor graph page", %{
+ conn: conn,
+ device: device,
+ organization: org,
+ snmp_device: snmp_device
+ } do
+ # Create a CPU sensor
+ {:ok, sensor} =
+ Snmp.create_sensor(%{
+ snmp_device_id: snmp_device.id,
+ sensor_type: "cpu_load",
+ sensor_index: 1,
+ sensor_descr: "CPU 1",
+ sensor_unit: "%"
+ })
+
+ # Create sensor reading
+ Snmp.create_sensor_reading(%{
+ sensor_id: sensor.id,
+ value: 45.2,
+ checked_at: DateTime.utc_now()
+ })
+
+ {:ok, view, html} =
+ live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/processors")
+
+ assert html =~ "Processor Usage"
+ assert html =~ device.name
+
+ # Verify correct assigns
+ assert view.assigns.current_organization.id == org.id
+ assert view.assigns.sensor_type == "processors"
+ assert view.assigns.unit == "%"
+ end
+ end
+
+ describe "Memory graph" do
+ test "renders memory graph page", %{
+ conn: conn,
+ device: device,
+ organization: org,
+ snmp_device: snmp_device
+ } do
+ # Create a memory sensor
+ {:ok, sensor} =
+ Snmp.create_sensor(%{
+ snmp_device_id: snmp_device.id,
+ sensor_type: "memory_usage",
+ sensor_index: 1,
+ sensor_descr: "Memory",
+ sensor_unit: "%"
+ })
+
+ # Create sensor reading
+ Snmp.create_sensor_reading(%{
+ sensor_id: sensor.id,
+ value: 75.5,
+ checked_at: DateTime.utc_now()
+ })
+
+ {:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/memory")
+
+ assert html =~ "Memory Usage"
+ assert html =~ device.name
+
+ # Verify correct assigns
+ assert view.assigns.current_organization.id == org.id
+ assert view.assigns.sensor_type == "memory"
+ assert view.assigns.unit == "%"
+ end
+ end
+
+ describe "Traffic graph" do
+ test "renders overall traffic graph page", %{
+ conn: conn,
+ device: device,
+ organization: org,
+ snmp_device: snmp_device
+ } do
+ # Create an interface
+ {:ok, interface} =
+ Snmp.create_interface(%{
+ snmp_device_id: snmp_device.id,
+ if_index: 1,
+ if_descr: "eth0",
+ if_name: "eth0",
+ if_oper_status: "up",
+ if_speed: 1_000_000_000
+ })
+
+ # Create interface stats
+ Snmp.create_interface_stat(%{
+ interface_id: interface.id,
+ if_in_octets: 1000,
+ if_out_octets: 2000,
+ checked_at: DateTime.utc_now()
+ })
+
+ {:ok, view, html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic")
+
+ assert html =~ "Overall Traffic"
+ assert html =~ device.name
+
+ # Verify correct assigns
+ assert view.assigns.current_organization.id == org.id
+ assert view.assigns.sensor_type == "traffic"
+ assert view.assigns.unit == "bps"
+ assert view.assigns.show_zero_line == true
+ end
+
+ test "renders specific interface traffic graph", %{
+ conn: conn,
+ device: device,
+ organization: org,
+ snmp_device: snmp_device
+ } do
+ # Create an interface
+ {:ok, interface} =
+ Snmp.create_interface(%{
+ snmp_device_id: snmp_device.id,
+ if_index: 1,
+ if_descr: "eth0",
+ if_name: "eth0",
+ if_oper_status: "up",
+ if_speed: 1_000_000_000
+ })
+
+ # Create interface stats
+ Snmp.create_interface_stat(%{
+ interface_id: interface.id,
+ if_in_octets: 1000,
+ if_out_octets: 2000,
+ checked_at: DateTime.utc_now()
+ })
+
+ {:ok, view, html} =
+ live(
+ conn,
+ ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
+ )
+
+ assert html =~ "Overall Traffic"
+ assert html =~ device.name
+
+ # Verify correct assigns including interface_id
+ assert view.assigns.current_organization.id == org.id
+ assert view.assigns.interface_id == interface.id
+ end
+ end
+
+ describe "Time range selection" do
+ test "defaults to 24h range", %{conn: conn, device: device, organization: org} do
+ {:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ assert view.assigns.range == "24h"
+ end
+
+ test "accepts custom range parameter", %{conn: conn, device: device, organization: org} do
+ {:ok, view, _html} =
+ live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency?range=1h")
+
+ assert view.assigns.range == "1h"
+ end
+
+ test "changes range when user clicks range button", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ {:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ # Initially 24h
+ assert view.assigns.range == "24h"
+
+ # Change to 1h
+ view
+ |> element("button", "1 Hour")
+ |> render_click()
+
+ assert view.assigns.range == "1h"
+ end
+
+ test "preserves interface_id when changing range", %{
+ conn: conn,
+ device: device,
+ organization: org,
+ snmp_device: snmp_device
+ } do
+ # Create an interface
+ {:ok, interface} =
+ Snmp.create_interface(%{
+ snmp_device_id: snmp_device.id,
+ if_index: 1,
+ if_descr: "eth0",
+ if_name: "eth0",
+ if_oper_status: "up",
+ if_speed: 1_000_000_000
+ })
+
+ {:ok, view, _html} =
+ live(
+ conn,
+ ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/traffic?interface_id=#{interface.id}"
+ )
+
+ # Change range
+ view
+ |> element("button", "1 Hour")
+ |> render_click()
+
+ # Interface ID should be preserved
+ assert view.assigns.interface_id == interface.id
+ assert view.assigns.range == "1h"
+ end
+ end
+
+ describe "Assign validation" do
+ test "ensures current_organization assign exists, not organization", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ {:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ # current_organization should exist
+ assert Map.has_key?(view.assigns, :current_organization)
+ assert view.assigns.current_organization.id == org.id
+
+ # organization should NOT exist (this was the bug)
+ refute Map.has_key?(view.assigns, :organization)
+ end
+
+ test "has all required assigns for rendering", %{
+ conn: conn,
+ device: device,
+ organization: org
+ } do
+ {:ok, view, _html} = live(conn, ~p"/orgs/#{org.slug}/devices/#{device.id}/graph/latency")
+
+ # All required assigns should be present
+ assert Map.has_key?(view.assigns, :current_organization)
+ assert Map.has_key?(view.assigns, :device)
+ assert Map.has_key?(view.assigns, :device_id)
+ assert Map.has_key?(view.assigns, :sensor_type)
+ assert Map.has_key?(view.assigns, :range)
+ assert Map.has_key?(view.assigns, :chart_title)
+ assert Map.has_key?(view.assigns, :unit)
+ assert Map.has_key?(view.assigns, :auto_scale)
+ assert Map.has_key?(view.assigns, :show_zero_line)
+ end
+ end
+end