defmodule AprsmeWeb.PageController do @moduledoc false use AprsmeWeb, :controller alias Aprsme.Cluster.LeaderElection def health(conn, _params) do version = :aprsme |> Application.spec(:vsn) |> List.to_string() respond_health(conn, version, Application.get_env(:aprsme, :health_status, :healthy)) end defp respond_health(conn, version, :draining) do health_error(conn, version, "draining", "Application is draining connections") end defp respond_health(conn, version, _status) do respond_shutdown_or_db(conn, version, Aprsme.ShutdownHandler.shutting_down?()) end defp respond_shutdown_or_db(conn, version, true) do health_error(conn, version, "shutting_down", "Application is shutting down") end defp respond_shutdown_or_db(conn, version, false) do respond_db_check(conn, version, db_healthy?()) end defp respond_db_check(conn, version, true) do json(conn, %{ status: "ok", version: version, database: "connected", timestamp: DateTime.utc_now() }) end defp respond_db_check(conn, version, false) do conn |> put_status(503) |> json(%{ status: "error", version: version, database: "disconnected", timestamp: DateTime.utc_now() }) end defp health_error(conn, version, status, message) do conn |> put_status(503) |> json(%{ status: status, version: version, message: message, timestamp: DateTime.utc_now() }) end defp db_healthy? do _ = Aprsme.Repo.query!("SELECT 1") true rescue _ -> false end @sitemap_paths ~w(/ /about /api /status /packets) def sitemap(conn, _params) do base_url = AprsmeWeb.Endpoint.url() urls = Enum.map(@sitemap_paths, fn path -> " #{base_url}#{path}" end) xml = """ #{Enum.join(urls, "\n")} """ conn |> put_resp_content_type("application/xml") |> send_resp(200, xml) end def api_catalog(conn, _params) do base_url = AprsmeWeb.Endpoint.url() ws_url = String.replace(base_url, ~r/^http/, "ws") body = %{ linkset: [ %{ anchor: "#{base_url}/api/v1", "service-doc": [%{href: "#{base_url}/api", type: "text/html"}], status: [%{href: "#{base_url}/status.json", type: "application/json"}] }, %{ anchor: "#{ws_url}/mobile/websocket", "service-doc": [ %{ href: "https://github.com/gmcintire/aprs.me/blob/main/docs/mobile-api.md", type: "text/markdown" } ], status: [%{href: "#{base_url}/status.json", type: "application/json"}] } ] } conn |> put_resp_content_type("application/linkset+json") |> send_resp(200, Jason.encode!(body)) end def ready(conn, _params) do # Simple readiness check without database dependency # This is faster for startup health checks version = :aprsme |> Application.spec(:vsn) |> List.to_string() json(conn, %{ status: "ready", version: version, timestamp: DateTime.utc_now() }) end def status_json(conn, _params) do # Get cluster-wide APRS-IS connection status aprs_status = LeaderElection.get_cluster_aprs_status() # Get application version version = :aprsme |> Application.spec(:vsn) |> List.to_string() # Calculate uptime in a human-readable format uptime_display = format_uptime(aprs_status.uptime_seconds) json(conn, %{ aprs_is: %{ connected: aprs_status.connected, server: aprs_status.server, port: aprs_status.port, connected_at: aprs_status.connected_at, uptime_seconds: aprs_status.uptime_seconds, uptime_display: uptime_display, login_id: aprs_status.login_id, filter: aprs_status.filter }, application: %{ version: version, timestamp: DateTime.utc_now() } }) end defp format_uptime(seconds) when seconds <= 0, do: "Not connected" defp format_uptime(seconds) do days = div(seconds, 86_400) hours = div(rem(seconds, 86_400), 3600) minutes = div(rem(seconds, 3600), 60) secs = rem(seconds, 60) format_uptime_parts(days, hours, minutes, secs) end defp format_uptime_parts(d, h, m, s) when d > 0, do: "#{d}d #{h}h #{m}m #{s}s" defp format_uptime_parts(_d, h, m, s) when h > 0, do: "#{h}h #{m}m #{s}s" defp format_uptime_parts(_d, _h, m, s) when m > 0, do: "#{m}m #{s}s" defp format_uptime_parts(_d, _h, _m, s), do: "#{s}s" end