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},
# Start the Finch HTTP client for sending emails
{Finch, name: Dashboard.Finch},
# Start MQTT subscriber for systant hosts
Dashboard.MqttSubscriber,
# Start simple MQTT subscriber
Dashboard.SimpleMqtt,
# Start to serve requests, typically the last entry
DashboardWeb.Endpoint
]

View File

@ -1,13 +1,12 @@
defmodule Dashboard.MqttSubscriber do
@moduledoc """
MQTT subscriber that listens to systant host messages and broadcasts updates via Phoenix PubSub.
Simple MQTT subscriber for development dashboard.
"""
use GenServer
require Logger
alias Phoenix.PubSub
@mqtt_topic "systant/+/stats"
@pubsub_topic "systant:hosts"
def start_link(opts) do
@ -20,28 +19,17 @@ defmodule Dashboard.MqttSubscriber do
@impl true
def init(_opts) do
# Start MQTT connection
client_id = "systant_dashboard_#{System.unique_integer([:positive])}"
connection_opts = [
client_id: client_id,
# Start MQTT connection in a supervised way
{:ok, _pid} = Tortoise.Supervisor.start_child(
:dashboard_mqtt,
client_id: :dashboard_mqtt,
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
{:ok, _pid} ->
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
Logger.info("Dashboard MQTT subscriber started")
{:ok, %{hosts: %{}}}
end
@impl true
@ -50,73 +38,41 @@ defmodule Dashboard.MqttSubscriber do
end
@impl true
def handle_info({:tortoise, {:publish, @mqtt_topic, payload, _opts}}, state) do
case Jason.decode(payload) do
{: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)}")
def handle_info(_msg, state) do
{:noreply, state}
end
end
def handle_info({:tortoise, {:publish, topic, payload, _opts}}, state) do
# Extract hostname from the actual topic
# Tortoise handler callbacks
def connection(_status, _state), do: []
def subscription(_status, _topic, _state), do: []
def handle_message(topic, payload, _state) do
case String.split(topic, "/") do
["systant", hostname, "stats"] ->
case Jason.decode(payload) do
{:ok, data} ->
# 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
# Broadcast to LiveView
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} ->
Logger.warning("Failed to decode JSON payload: #{inspect(reason)}")
{:noreply, state}
{:error, _reason} ->
:ok
end
_ ->
Logger.debug("Received message on unexpected topic: #{topic}")
{:noreply, state}
:ok
end
[]
end
def handle_info({:tortoise, _msg}, state) do
# Handle other tortoise messages (connection status, etc.)
{:noreply, state}
@impl true
def handle_cast({:update_host, hostname, host_data}, state) do
updated_hosts = Map.put(state.hosts, hostname, host_data)
{:noreply, %{state | hosts: updated_hosts}}
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: []
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
alias Phoenix.PubSub
alias Dashboard.MqttSubscriber
@pubsub_topic "systant:hosts"
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
# Subscribe to host updates
# Subscribe to host updates from MQTT
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
end
# Get initial host data
hosts = MqttSubscriber.get_hosts()
# Start with empty hosts - will be populated by MQTT
hosts = %{}
socket =
socket
@ -29,10 +28,13 @@ defmodule DashboardWeb.HostsLive do
@impl true
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)
{:noreply, assign(socket, :hosts, updated_hosts)}
end
@impl true
def render(assigns) do
~H"""