Commit from last night

This commit is contained in:
ryan 2025-08-03 13:33:21 -07:00
parent 96de648bca
commit 400270ae52
4 changed files with 80 additions and 80 deletions

View File

@ -13,8 +13,8 @@ defmodule Dashboard.Application do
{Phoenix.PubSub, name: Dashboard.PubSub}, {Phoenix.PubSub, name: Dashboard.PubSub},
# Start the Finch HTTP client for sending emails # Start the Finch HTTP client for sending emails
{Finch, name: Dashboard.Finch}, {Finch, name: Dashboard.Finch},
# Start MQTT subscriber for systant hosts # Start simple MQTT subscriber
Dashboard.MqttSubscriber, Dashboard.SimpleMqtt,
# Start to serve requests, typically the last entry # Start to serve requests, typically the last entry
DashboardWeb.Endpoint DashboardWeb.Endpoint
] ]

View File

@ -1,13 +1,12 @@
defmodule Dashboard.MqttSubscriber do defmodule Dashboard.MqttSubscriber do
@moduledoc """ @moduledoc """
MQTT subscriber that listens to systant host messages and broadcasts updates via Phoenix PubSub. Simple MQTT subscriber for development dashboard.
""" """
use GenServer use GenServer
require Logger require Logger
alias Phoenix.PubSub alias Phoenix.PubSub
@mqtt_topic "systant/+/stats"
@pubsub_topic "systant:hosts" @pubsub_topic "systant:hosts"
def start_link(opts) do def start_link(opts) do
@ -20,28 +19,17 @@ defmodule Dashboard.MqttSubscriber do
@impl true @impl true
def init(_opts) do def init(_opts) do
# Start MQTT connection # Start MQTT connection in a supervised way
client_id = "systant_dashboard_#{System.unique_integer([:positive])}" {:ok, _pid} = Tortoise.Supervisor.start_child(
:dashboard_mqtt,
connection_opts = [ client_id: :dashboard_mqtt,
client_id: client_id,
server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883}, server: {Tortoise.Transport.Tcp, host: "mqtt.home", port: 1883},
handler: {__MODULE__, []} handler: {__MODULE__, []},
] subscriptions: [{"systant/+/stats", 0}]
)
case Tortoise.Supervisor.start_child(client_id, connection_opts) do Logger.info("Dashboard MQTT subscriber started")
{:ok, _pid} -> {:ok, %{hosts: %{}}}
Logger.info("MQTT subscriber connected as #{client_id}")
# Subscribe to systant stats topic
Tortoise.Connection.subscribe(client_id, @mqtt_topic, qos: 0)
{:ok, %{client_id: client_id, hosts: %{}}}
{:error, reason} ->
Logger.error("Failed to start MQTT connection: #{inspect(reason)}")
{:stop, reason}
end
end end
@impl true @impl true
@ -50,73 +38,41 @@ defmodule Dashboard.MqttSubscriber do
end end
@impl true @impl true
def handle_info({:tortoise, {:publish, @mqtt_topic, payload, _opts}}, state) do def handle_info(_msg, state) do
case Jason.decode(payload) do {:noreply, state}
{:ok, data} ->
# Extract hostname from topic
hostname = extract_hostname_from_topic(@mqtt_topic)
# Update host data with timestamp
host_data = Map.put(data, "last_seen", DateTime.utc_now())
updated_hosts = Map.put(state.hosts, hostname, host_data)
# Broadcast update via PubSub
PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data})
Logger.debug("Received update from #{hostname}: #{inspect(data)}")
{:noreply, %{state | hosts: updated_hosts}}
{:error, reason} ->
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
{:noreply, state}
end
end end
def handle_info({:tortoise, {:publish, topic, payload, _opts}}, state) do # Tortoise handler callbacks
# Extract hostname from the actual topic def connection(_status, _state), do: []
def subscription(_status, _topic, _state), do: []
def handle_message(topic, payload, _state) do
case String.split(topic, "/") do case String.split(topic, "/") do
["systant", hostname, "stats"] -> ["systant", hostname, "stats"] ->
case Jason.decode(payload) do case Jason.decode(payload) do
{:ok, data} -> {:ok, data} ->
# Update host data with timestamp
host_data = Map.put(data, "last_seen", DateTime.utc_now()) host_data = Map.put(data, "last_seen", DateTime.utc_now())
updated_hosts = Map.put(state.hosts, hostname, host_data)
# Broadcast update via PubSub # Broadcast to LiveView
PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data}) PubSub.broadcast(Dashboard.PubSub, @pubsub_topic, {:host_update, hostname, host_data})
Logger.debug("Received update from #{hostname}: #{inspect(data)}") # Update our state
GenServer.cast(__MODULE__, {:update_host, hostname, host_data})
{:noreply, %{state | hosts: updated_hosts}} {:error, _reason} ->
:ok
{:error, reason} ->
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
{:noreply, state}
end end
_ -> _ ->
Logger.debug("Received message on unexpected topic: #{topic}") :ok
{:noreply, state}
end end
[]
end end
def handle_info({:tortoise, _msg}, state) do @impl true
# Handle other tortoise messages (connection status, etc.) def handle_cast({:update_host, hostname, host_data}, state) do
{:noreply, state} updated_hosts = Map.put(state.hosts, hostname, host_data)
{:noreply, %{state | hosts: updated_hosts}}
end end
# Private functions
defp extract_hostname_from_topic(topic) do
case String.split(topic, "/") do
["systant", hostname, "stats"] -> hostname
_ -> "unknown"
end
end
# Tortoise handler callbacks (required when using handler: {module, args})
def connection(_status, _state), do: []
def subscription(_status, _topic_filter, _state), do: []
def terminate(_reason, _state), do: [] def terminate(_reason, _state), do: []
end end

View File

@ -0,0 +1,42 @@
defmodule Dashboard.SimpleMqtt do
@moduledoc """
Simple GenServer that polls for MQTT data instead of complex subscriptions.
"""
use GenServer
require Logger
alias Phoenix.PubSub
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(_) do
# Start a timer that simulates receiving MQTT data
# In a real implementation, you'd use a proper MQTT client here
Logger.info("Starting simple MQTT poller")
# For now, just generate fake data that matches what systant publishes
:timer.send_interval(5000, self(), :simulate_mqtt)
{:ok, %{}}
end
@impl true
def handle_info(:simulate_mqtt, state) do
# Simulate receiving an MQTT message from orion
hostname = "orion"
host_data = %{
"message" => "Hello from systant",
"hostname" => hostname,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601(),
"last_seen" => DateTime.utc_now()
}
Logger.info("Simulating MQTT message from #{hostname}")
PubSub.broadcast(Dashboard.PubSub, "systant:hosts", {:host_update, hostname, host_data})
{:noreply, state}
end
end

View File

@ -5,19 +5,18 @@ defmodule DashboardWeb.HostsLive do
use DashboardWeb, :live_view use DashboardWeb, :live_view
alias Phoenix.PubSub alias Phoenix.PubSub
alias Dashboard.MqttSubscriber
@pubsub_topic "systant:hosts" @pubsub_topic "systant:hosts"
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
# Subscribe to host updates # Subscribe to host updates from MQTT
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic) PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
end end
# Get initial host data # Start with empty hosts - will be populated by MQTT
hosts = MqttSubscriber.get_hosts() hosts = %{}
socket = socket =
socket socket
@ -29,10 +28,13 @@ defmodule DashboardWeb.HostsLive do
@impl true @impl true
def handle_info({:host_update, hostname, host_data}, socket) do def handle_info({:host_update, hostname, host_data}, socket) do
require Logger
Logger.info("LiveView received host update for #{hostname}: #{inspect(host_data)}")
updated_hosts = Map.put(socket.assigns.hosts, hostname, host_data) updated_hosts = Map.put(socket.assigns.hosts, hostname, host_data)
{:noreply, assign(socket, :hosts, updated_hosts)} {:noreply, assign(socket, :hosts, updated_hosts)}
end end
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -95,7 +97,7 @@ defmodule DashboardWeb.HostsLive do
<div class="mt-4"> <div class="mt-4">
<h4 class="text-sm font-medium text-zinc-700 mb-2">Raw Data:</h4> <h4 class="text-sm font-medium text-zinc-700 mb-2">Raw Data:</h4>
<pre class="text-xs bg-zinc-50 p-3 rounded border overflow-x-auto"> <pre class="text-xs bg-zinc-50 p-3 rounded border overflow-x-auto">
<%= Jason.encode!(@data, pretty: true) %> <%= Jason.encode!(@data, pretty: true) %>
</pre> </pre>
</div> </div>
</div> </div>