diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml index 5824fe8..7305142 100644 --- a/.github/workflows/dependency-updates.yml +++ b/.github/workflows/dependency-updates.yml @@ -26,7 +26,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - tags: aprs:latest + tags: aprsme:latest load: true no-cache: true pull: true @@ -36,7 +36,7 @@ jobs: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: - image-ref: aprs:latest + image-ref: aprsme:latest format: "sarif" output: "trivy-results.sarif" severity: "CRITICAL,HIGH" @@ -59,7 +59,7 @@ jobs: echo "### OS Package Vulnerabilities" >> dep-update-report.md echo "```" >> dep-update-report.md - trivy image --severity HIGH,CRITICAL --no-progress aprs:latest >> dep-update-report.md + trivy image --severity HIGH,CRITICAL --no-progress aprsme:latest >> dep-update-report.md echo "```" >> dep-update-report.md - name: Upload vulnerability summary diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 8937a4b..f6df75d 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -45,7 +45,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - tags: aprs:${{ github.sha }} + tags: aprsme:${{ github.sha }} load: true cache-from: type=gha cache-to: type=gha,mode=max @@ -53,7 +53,7 @@ jobs: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: - image-ref: aprs:${{ github.sha }} + image-ref: aprsme:${{ github.sha }} format: "sarif" output: "trivy-results.sarif" severity: "CRITICAL,HIGH" @@ -70,7 +70,7 @@ jobs: - name: Run Trivy for outdated OS packages uses: aquasecurity/trivy-action@master with: - image-ref: aprs:${{ github.sha }} + image-ref: aprsme:${{ github.sha }} format: "table" output: "os-packages.txt" severity: "HIGH,CRITICAL" @@ -79,7 +79,7 @@ jobs: - name: Run Trivy for outdated application dependencies uses: aquasecurity/trivy-action@master with: - image-ref: aprs:${{ github.sha }} + image-ref: aprsme:${{ github.sha }} format: "table" output: "app-packages.txt" severity: "HIGH,CRITICAL" @@ -99,7 +99,7 @@ jobs: uses: docker/scout-action@v1 with: command: quickview,cves - image: aprs:${{ github.sha }} + image: aprsme:${{ github.sha }} output-format: sarif only-severities: critical,high sarif-file: scout-results.sarif @@ -116,7 +116,7 @@ jobs: if: always() run: | echo "# Docker Image Security Report" > security-summary.md - echo "## Image: aprs:${{ github.sha }}" >> security-summary.md + echo "## Image: aprsme:${{ github.sha }}" >> security-summary.md echo "## Date: $(date)" >> security-summary.md echo "### OS Package Vulnerabilities" >> security-summary.md diff --git a/.gitignore b/.gitignore index 53bad17..7e3a710 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ erl_crash.dump *.ez # Ignore package tarball (built via "mix hex.build"). -aprs-*.tar +aprsme-*.tar # Ignore assets that are produced by build tools. /priv/static/assets/ diff --git a/Dockerfile.distroless b/Dockerfile.distroless index d0d9adb..7512670 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -121,7 +121,7 @@ WORKDIR /app # Copy the Elixir release from builder stage # The distroless nonroot user has UID 65532, so we use that -COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprs ./ +COPY --from=builder --chown=65532:65532 /app/_build/prod/rel/aprsme ./ # Create required directories with proper permissions USER 0 diff --git a/README.md b/README.md index 21299ac..c05bf38 100644 --- a/README.md +++ b/README.md @@ -83,19 +83,19 @@ The workflow is defined in `.github/workflows/deploy-k3s.yaml`. It will: * **Database Migrations:** The Kubernetes deployment for the application includes an init container that automatically runs database migrations (`/app/bin/migrate`) before the main application container starts. * **Health Check Endpoint:** The application deployment (`k8s/app-deployment.yaml`) is configured with readiness and liveness probes that expect a health check endpoint at `/health` (returning HTTP 200). You may need to add this to your Phoenix application's router and controller: - * In `lib/aprs_web/router.ex`: + * In `lib/aprsme_web/router.ex`: ```elixir - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through :browser # ... other routes get "/health", PageController, :health end ``` - * In `lib/aprs_web/controllers/page_controller.ex` (or another suitable controller): + * In `lib/aprsme_web/controllers/page_controller.ex` (or another suitable controller): ```elixir def health(conn, _params) do # You can add more sophisticated checks here if needed - json(conn, %{status: "ok", version: Application.spec(:aprs, :vsn)}) + json(conn, %{status: "ok", version: Application.spec(:aprsme, :vsn)}) end ``` * **Image Name:** The application deployment manifest (`k8s/app-deployment.yaml`) uses a placeholder image name `your-ghcr-username/aprs-app:latest`. The CI/CD pipeline automatically replaces this with the correct image name from GHCR (e.g., `ghcr.io/your-github-owner/your-repo-name:`). diff --git a/config/config.exs b/config/config.exs index 754bad7..56b89b8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -14,36 +14,36 @@ import Config # # For production it's recommended to configure a different adapter # at the `config/runtime.exs`. -config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Local +config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Local # Configures the endpoint -config :aprs, AprsWeb.Endpoint, +config :aprsme, AprsmeWeb.Endpoint, url: [host: "localhost"], render_errors: [ - formats: [html: AprsWeb.ErrorHTML, json: AprsWeb.ErrorJSON], + formats: [html: AprsmeWeb.ErrorHTML, json: AprsmeWeb.ErrorJSON], layout: false ], - pubsub_server: Aprs.PubSub, + pubsub_server: Aprsme.PubSub, live_view: [signing_salt: "ees098qG"] # Configure Oban for background jobs -config :aprs, Oban, - repo: Aprs.Repo, +config :aprsme, Oban, + repo: Aprsme.Repo, plugins: [ {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 7}, {Oban.Plugins.Cron, crontab: [ - {"0 0 * * *", Aprs.Workers.PacketCleanupWorker}, - {"0 3 * * 1", Aprs.DeviceIdentification.Worker} + {"0 0 * * *", Aprsme.Workers.PacketCleanupWorker}, + {"0 3 * * 1", Aprsme.DeviceIdentification.Worker} ]} ], queues: [default: 10, maintenance: 2] -config :aprs, - ecto_repos: [Aprs.Repo] +config :aprsme, + ecto_repos: [Aprsme.Repo] -config :aprs, - ecto_repos: [Aprs.Repo], +config :aprsme, + ecto_repos: [Aprsme.Repo], aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"), aprs_is_port: 10_152, aprs_is_default_filter: System.get_env("APRS_FILTER"), diff --git a/config/dev.exs b/config/dev.exs index c181b1a..632bcca 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,7 +1,7 @@ import Config # Configure your database -config :aprs, Aprs.Repo, +config :aprsme, Aprsme.Repo, # For development, we disable any cache and enable # debugging and code reloading. # @@ -11,14 +11,14 @@ config :aprs, Aprs.Repo, username: "postgres", password: "postgres", hostname: "localhost", - database: "aprs_dev", + database: "aprsme_dev", stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10, log: false, - types: Aprs.PostgresTypes + types: Aprsme.PostgresTypes -config :aprs, AprsWeb.Endpoint, +config :aprsme, AprsmeWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. http: [ip: {127, 0, 0, 1}, port: 4000], @@ -37,13 +37,13 @@ config :aprs, AprsWeb.Endpoint, ] # Watch static and templates for browser reloading. -config :aprs, AprsWeb.Endpoint, +config :aprsme, AprsmeWeb.Endpoint, live_reload: [ patterns: [ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/aprs_web/(live|views)/.*(ex)$", - ~r"lib/aprs_web/templates/.*(eex)$", + ~r"lib/aprsme_web/(live|views)/.*(ex)$", + ~r"lib/aprsme_web/templates/.*(eex)$", ~r"assets/vendor/.*(js|css)$" ] ] @@ -53,7 +53,7 @@ config :aprs, AprsWeb.Endpoint, # mix phx.gen.cert # # Run `mix help phx.gen.cert` for more information. -config :aprs, dev_routes: true +config :aprsme, dev_routes: true # Do not include metadata nor timestamps in development logs # diff --git a/config/prod.exs b/config/prod.exs index 120a831..ffc7404 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -9,7 +9,7 @@ import Config # manifest is generated by the `mix phx.digest` task, # which you should run after static files are built and # before starting your production server. -config :aprs, AprsWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" +config :aprsme, AprsmeWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" # Runtime production configuration, including reading @@ -26,4 +26,4 @@ config :esbuild, config :logger, level: :info # Configures Swoosh API Client -config :swoosh, :api_client, Aprs.Finch +config :swoosh, :api_client, Aprsme.Finch diff --git a/config/runtime.exs b/config/runtime.exs index ca16b19..61b9f9d 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -12,13 +12,13 @@ import Config # If you use `mix release`, you need to explicitly enable the server # by passing the PHX_SERVER=true when you start it: # -# PHX_SERVER=true bin/aprs start +# PHX_SERVER=true bin/aprsme start # # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # script that automatically sets the env var above. # Always start the server in production/docker environments if System.get_env("PHX_SERVER") || config_env() == :prod do - config :aprs, AprsWeb.Endpoint, server: true + config :aprsme, AprsmeWeb.Endpoint, server: true end if config_env() == :prod do @@ -46,14 +46,14 @@ if config_env() == :prod do host = System.get_env("PHX_HOST") || "example.com" port = String.to_integer(System.get_env("PORT") || "4000") - config :aprs, Aprs.Repo, + config :aprsme, Aprsme.Repo, # ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), socket_options: maybe_ipv6, - types: Aprs.PostgresTypes + types: Aprsme.PostgresTypes - config :aprs, AprsWeb.Endpoint, + config :aprsme, AprsmeWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ # Bind on all IPv4 interfaces for better container compatibility @@ -66,10 +66,10 @@ if config_env() == :prod do secret_key_base: secret_key_base, server: true - # config :aprs, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + # config :aprsme, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - config :aprs, - ecto_repos: [Aprs.Repo], + config :aprsme, + ecto_repos: [Aprsme.Repo], aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"), aprs_is_port: 14_580, aprs_is_default_filter: System.get_env("APRS_FILTER"), @@ -94,7 +94,7 @@ if config_env() == :prod do # To get SSL working, you will need to add the `https` key # to your endpoint configuration: # - # config :aprs, AprsWeb.Endpoint, + # config :aprsme, AprsmeWeb.Endpoint, # https: [ # ..., # port: 443, @@ -116,7 +116,7 @@ if config_env() == :prod do # We also recommend setting `force_ssl` in your endpoint, ensuring # no data is ever sent via http, always redirecting to https: # - # config :aprs, AprsWeb.Endpoint, + # config :aprsme, AprsmeWeb.Endpoint, # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. @@ -127,7 +127,7 @@ if config_env() == :prod do # Also, you may need to configure the Swoosh API client of your choice if you # are not using SMTP. Here is an example of the configuration: # - # config :aprs, Aprs.Mailer, + # config :aprsme, Aprsme.Mailer, # adapter: Swoosh.Adapters.Mailgun, # api_key: System.get_env("MAILGUN_API_KEY"), # domain: System.get_env("MAILGUN_DOMAIN") diff --git a/config/test.exs b/config/test.exs index 1ff9eda..a7c202c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,40 +1,40 @@ import Config # In test we don't send emails. -config :aprs, Aprs.Mailer, adapter: Swoosh.Adapters.Test +config :aprsme, Aprsme.Mailer, adapter: Swoosh.Adapters.Test # Configure your database # # The MIX_TEST_PARTITION environment variable can be used # to provide built-in test partitioning in CI environment. # Run `mix help test` for more information. -config :aprs, Aprs.Repo, +config :aprsme, Aprsme.Repo, username: "postgres", password: "postgres", hostname: "localhost", - database: "aprs_test#{System.get_env("MIX_TEST_PARTITION")}", + database: "aprsme_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: System.schedulers_online() * 2, - types: Aprs.PostgresTypes + types: Aprsme.PostgresTypes # We don't run a server during test. If one is required, # you can enable the server option below. -config :aprs, AprsWeb.Endpoint, +config :aprsme, AprsmeWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], secret_key_base: "IV9+ENaw9i8xjReRk4sULRvRgsmFVTGQwQGGrf4G+Q/SFMeHBCNWRlPXQ2YvT36R", server: false # Disable Oban during tests to prevent background job execution -config :aprs, Oban, testing: :inline +config :aprsme, Oban, testing: :inline # Disable initialize replay delay in test environment -config :aprs, :initialize_replay_delay, 0 +config :aprsme, :initialize_replay_delay, 0 # Configure the packets module to use the mock in tests -config :aprs, :packets_module, Aprs.PacketsMock +config :aprsme, :packets_module, Aprsme.PacketsMock # Disable APRS-IS external connections in test environment -config :aprs, +config :aprsme, aprs_is_server: "mock.aprs.test", aprs_is_port: 14_580, aprs_is_default_filter: "r/33/-96/100", @@ -43,7 +43,7 @@ config :aprs, disable_aprs_connection: true # Disable automatic migrations during tests -config :aprs, auto_migrate: false +config :aprsme, auto_migrate: false # Only in tests, remove the complexity from the password hashing algorithm config :bcrypt_elixir, :log_rounds, 1 diff --git a/k8s/app-deployment.yaml b/k8s/app-deployment.yaml index 1982478..48f7c97 100644 --- a/k8s/app-deployment.yaml +++ b/k8s/app-deployment.yaml @@ -49,5 +49,5 @@ spec: initialDelaySeconds: 30 periodSeconds: 15 # Note: For the health endpoint, you might need to create a simple one in your Phoenix router -# e.g., scope "/", AprsWeb do; get "/health", PageController, :health; end +# e.g., scope "/", AprsmeWeb do; get "/health", PageController, :health; end # and a `def health(conn, _params), do: json(conn, %{status: "ok"})` in PageController. diff --git a/lib/aprs.ex b/lib/aprs.ex deleted file mode 100644 index 3470bde..0000000 --- a/lib/aprs.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Aprs do - @moduledoc """ - Aprs keeps the contexts that define your domain - and business logic. - - Contexts are also responsible for managing your data, regardless - if it comes from the database, an external API or others. - """ -end diff --git a/lib/aprs/mailer.ex b/lib/aprs/mailer.ex deleted file mode 100644 index 31ac5f0..0000000 --- a/lib/aprs/mailer.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Aprs.Mailer do - @moduledoc false - use Swoosh.Mailer, otp_app: :aprs -end diff --git a/lib/aprs/presence.ex b/lib/aprs/presence.ex deleted file mode 100644 index 680071d..0000000 --- a/lib/aprs/presence.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Aprs.Presence do - @moduledoc false - use Phoenix.Presence, otp_app: :aprs, pubsub_server: Aprs.PubSub -end diff --git a/lib/aprs_web.ex b/lib/aprs_web.ex index 1c6ec9c..5288bd7 100644 --- a/lib/aprs_web.ex +++ b/lib/aprs_web.ex @@ -1,20 +1,20 @@ -defmodule AprsWeb do +defmodule AprsmeWeb do @moduledoc """ The entrypoint for defining your web interface, such as controllers, components, channels, and so on. This can be used in your application as: - use AprsWeb, :controller - use AprsWeb, :html + use AprsmeWeb, :controller + use AprsmeWeb, :html - The definitions below will be executed for every controller, + The definitions below will be executed for every view, component, etc, so keep them short and clean, focused on imports, uses and aliases. Do NOT define functions inside the quoted expressions - below. Instead, define additional modules and import - those modules here. + below. Instead, define additional functions and do the + imports in your modules. """ def static_paths, do: ~w(assets fonts images aprs-symbols favicon.ico robots.txt) @@ -23,9 +23,10 @@ defmodule AprsWeb do quote do use Phoenix.Router, helpers: false - # Import common connection and controller functions to use in pipelines import Phoenix.Controller import Phoenix.LiveView.Router + + # Import common connection and controller functions to use in pipelines import Plug.Conn end end @@ -33,17 +34,18 @@ defmodule AprsWeb do def channel do quote do use Phoenix.Channel + + import AprsmeWeb.Gettext end end def controller do quote do use Phoenix.Controller, - namespace: AprsWeb, formats: [:html, :json], - layouts: [html: AprsWeb.Layouts] + layouts: [html: AprsmeWeb.Layouts] - import AprsWeb.Gettext + import AprsmeWeb.Gettext import Plug.Conn unquote(verified_routes()) @@ -53,7 +55,7 @@ defmodule AprsWeb do def live_view do quote do use Phoenix.LiveView, - layout: {AprsWeb.Layouts, :app} + layout: {AprsmeWeb.Layouts, :app} unquote(html_helpers()) end @@ -82,11 +84,11 @@ defmodule AprsWeb do defp html_helpers do quote do + import AprsmeWeb.CoreComponents + import AprsmeWeb.Gettext # HTML escaping functionality - # Core UI components and translation - import AprsWeb.CoreComponents - import AprsWeb.Gettext import Phoenix.HTML + # Core UI components and translation # Shortcut for generating JS commands alias Phoenix.LiveView.JS @@ -99,9 +101,9 @@ defmodule AprsWeb do def verified_routes do quote do use Phoenix.VerifiedRoutes, - endpoint: AprsWeb.Endpoint, - router: AprsWeb.Router, - statics: AprsWeb.static_paths() + endpoint: AprsmeWeb.Endpoint, + router: AprsmeWeb.Router, + statics: AprsmeWeb.static_paths() end end diff --git a/lib/aprsme.ex b/lib/aprsme.ex new file mode 100644 index 0000000..3d09540 --- /dev/null +++ b/lib/aprsme.ex @@ -0,0 +1,5 @@ +defmodule Aprsme do + @moduledoc """ + The main application module for Aprsme. + """ +end diff --git a/lib/aprs/accounts.ex b/lib/aprsme/accounts.ex similarity index 98% rename from lib/aprs/accounts.ex rename to lib/aprsme/accounts.ex index a7a8523..dd34fe6 100644 --- a/lib/aprs/accounts.ex +++ b/lib/aprsme/accounts.ex @@ -1,14 +1,14 @@ -defmodule Aprs.Accounts do +defmodule Aprsme.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false - alias Aprs.Accounts.User - alias Aprs.Accounts.UserNotifier - alias Aprs.Accounts.UserToken - alias Aprs.Repo + alias Aprsme.Accounts.User + alias Aprsme.Accounts.UserNotifier + alias Aprsme.Accounts.UserToken + alias Aprsme.Repo ## Database getters diff --git a/lib/aprs/accounts/user.ex b/lib/aprsme/accounts/user.ex similarity index 96% rename from lib/aprs/accounts/user.ex rename to lib/aprsme/accounts/user.ex index d81e560..a311716 100644 --- a/lib/aprs/accounts/user.ex +++ b/lib/aprsme/accounts/user.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Accounts.User do +defmodule Aprsme.Accounts.User do @moduledoc false use Ecto.Schema @@ -89,7 +89,7 @@ defmodule Aprs.Accounts.User do defp do_validate_unique_email(changeset, true) do changeset - |> unsafe_validate_unique(:email, Aprs.Repo) + |> unsafe_validate_unique(:email, Aprsme.Repo) |> unique_constraint(:email) end @@ -143,7 +143,7 @@ defmodule Aprs.Accounts.User do If there is no user or the user doesn't have a password, we call `Bcrypt.no_user_verify/0` to avoid timing attacks. """ - def valid_password?(%Aprs.Accounts.User{hashed_password: hashed_password}, password) + def valid_password?(%Aprsme.Accounts.User{hashed_password: hashed_password}, password) when is_binary(hashed_password) and byte_size(password) > 0 do Bcrypt.verify_pass(password, hashed_password) end diff --git a/lib/aprs/accounts/user_notifier.ex b/lib/aprsme/accounts/user_notifier.ex similarity index 96% rename from lib/aprs/accounts/user_notifier.ex rename to lib/aprsme/accounts/user_notifier.ex index 6116c9d..c838eb3 100644 --- a/lib/aprs/accounts/user_notifier.ex +++ b/lib/aprsme/accounts/user_notifier.ex @@ -1,8 +1,8 @@ -defmodule Aprs.Accounts.UserNotifier do +defmodule Aprsme.Accounts.UserNotifier do @moduledoc false import Swoosh.Email - alias Aprs.Mailer + alias Aprsme.Mailer # Delivers the email using the application mailer. defp deliver(recipient, subject, body) do diff --git a/lib/aprs/accounts/user_token.ex b/lib/aprsme/accounts/user_token.ex similarity index 98% rename from lib/aprs/accounts/user_token.ex rename to lib/aprsme/accounts/user_token.ex index 42f22f1..e374d7b 100644 --- a/lib/aprs/accounts/user_token.ex +++ b/lib/aprsme/accounts/user_token.ex @@ -1,10 +1,10 @@ -defmodule Aprs.Accounts.UserToken do +defmodule Aprsme.Accounts.UserToken do @moduledoc false use Ecto.Schema import Ecto.Query - alias Aprs.Accounts.UserToken + alias Aprsme.Accounts.UserToken @hash_algorithm :sha256 @rand_size 32 @@ -20,7 +20,7 @@ defmodule Aprs.Accounts.UserToken do field(:token, :binary) field(:context, :string) field(:sent_to, :string) - belongs_to(:user, Aprs.Accounts.User) + belongs_to(:user, Aprsme.Accounts.User) timestamps(updated_at: false) end diff --git a/lib/aprs/application.ex b/lib/aprsme/application.ex similarity index 62% rename from lib/aprs/application.ex rename to lib/aprsme/application.ex index b854051..8baa709 100644 --- a/lib/aprs/application.ex +++ b/lib/aprsme/application.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Application do +defmodule Aprsme.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false @@ -16,35 +16,40 @@ defmodule Aprs.Application do children = [ # Start the Telemetry supervisor - AprsWeb.Telemetry, + AprsmeWeb.Telemetry, # Start the Ecto repository - Aprs.Repo, + Aprsme.Repo, # Start the PubSub system - {Phoenix.PubSub, name: Aprs.PubSub}, + {Phoenix.PubSub, name: Aprsme.PubSub}, # Start Finch - {Finch, name: Aprs.Finch}, + {Finch, name: Aprsme.Finch}, # Start the Endpoint (http/https) - AprsWeb.Endpoint, - # Start a worker by calling: Aprs.Worker.start_link(arg) - # {Aprs.Worker, arg} + AprsmeWeb.Endpoint, + # Start a worker by calling: Aprsme.Worker.start_link(arg) + # {Aprsme.Worker, arg} {Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()}, - {Cluster.Supervisor, [topologies, [name: Aprs.ClusterSupervisor]]}, + {Cluster.Supervisor, [topologies, [name: Aprsme.ClusterSupervisor]]}, # Start Oban for background jobs - {Oban, :aprs |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)}, - Aprs.Presence, - Aprs.AprsIsConnection, - Aprs.PostgresNotifier + {Oban, :aprsme |> Application.get_env(Oban, []) |> Keyword.put(:queues, default: 10, maintenance: 2)}, + Aprsme.Presence, + Aprsme.AprsIsConnection, + Aprsme.PostgresNotifier ] - children = maybe_add_is_supervisor(children, Application.get_env(:aprs, :env)) + children = maybe_add_is_supervisor(children, Application.get_env(:aprsme, :env)) # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options - opts = [strategy: :one_for_one, name: Aprs.Supervisor] + opts = [strategy: :one_for_one, name: Aprsme.Supervisor] {:ok, sup} = Supervisor.start_link(children, opts) # Now that the Repo is started, run the refresh in a background task - Task.start(fn -> Aprs.DeviceIdentification.maybe_refresh_devices() end) + # Skip in test environment to avoid DBConnection.OwnershipError + env = Application.get_env(:aprsme, :env) + + if env != :test do + Task.start(fn -> Aprsme.DeviceIdentification.maybe_refresh_devices() end) + end {:ok, sup} end @@ -53,12 +58,12 @@ defmodule Aprs.Application do # whenever the application is updated. @impl true def config_change(changed, _new, removed) do - AprsWeb.Endpoint.config_change(changed, removed) + AprsmeWeb.Endpoint.config_change(changed, removed) :ok end defp migrate do - auto_migrate = Application.get_env(:aprs, :auto_migrate, true) + auto_migrate = Application.get_env(:aprsme, :auto_migrate, true) do_migrate(auto_migrate) rescue error -> @@ -70,7 +75,7 @@ defmodule Aprs.Application do end defp maybe_add_is_supervisor(children, env) when env in [:prod, :dev] do - children ++ [Aprs.Is.IsSupervisor] + children ++ [Aprsme.Is.IsSupervisor] end defp maybe_add_is_supervisor(children, _env), do: children @@ -79,7 +84,7 @@ defmodule Aprs.Application do require Logger Logger.info("Running database migrations...") - Aprs.Release.migrate() + Aprsme.Release.migrate() Logger.info("Database migrations completed") end diff --git a/lib/aprs/aprs_is_connection.ex b/lib/aprsme/aprs_is_connection.ex similarity index 78% rename from lib/aprs/aprs_is_connection.ex rename to lib/aprsme/aprs_is_connection.ex index 6ecf3ce..e732181 100644 --- a/lib/aprs/aprs_is_connection.ex +++ b/lib/aprsme/aprs_is_connection.ex @@ -1,4 +1,4 @@ -defmodule Aprs.AprsIsConnection do +defmodule Aprsme.AprsIsConnection do @moduledoc """ Maintains a supervised TCP connection to APRS-IS, with reconnection logic and exponential backoff. Broadcasts each received line via Phoenix.PubSub and emits @@ -43,13 +43,13 @@ defmodule Aprs.AprsIsConnection do case connect_aprs_is() do {:ok, socket} -> Logger.info("Connected to APRS-IS") - :telemetry.execute([:aprs, :is, :connected], %{}, %{}) + :telemetry.execute([:aprsme, :is, :connected], %{}, %{}) {:noreply, %{state | socket: socket, backoff: @reconnect_initial}} {:error, reason} -> Logger.error("APRS-IS connection failed: #{inspect(reason)}. Retrying in #{state.backoff} ms.") - :telemetry.execute([:aprs, :is, :connect_error], %{}, %{reason: reason}) + :telemetry.execute([:aprsme, :is, :connect_error], %{}, %{reason: reason}) schedule_connect(state.backoff) {:noreply, %{state | socket: nil, backoff: min(state.backoff * 2, @reconnect_max)}} end @@ -58,15 +58,15 @@ defmodule Aprs.AprsIsConnection do @impl true def handle_info({:tcp, _socket, data}, state) do # Each line received from APRS-IS - Phoenix.PubSub.broadcast(Aprs.PubSub, @pubsub_topic, {:aprs_is_line, data}) - :telemetry.execute([:aprs, :is, :packet], %{size: byte_size(data)}, %{data: data}) + Phoenix.PubSub.broadcast(Aprsme.PubSub, @pubsub_topic, {:aprsme_is_line, data}) + :telemetry.execute([:aprsme, :is, :packet], %{size: byte_size(data)}, %{data: data}) {:noreply, state} end @impl true def handle_info({:tcp_closed, _socket}, state) do Logger.warning("APRS-IS connection closed. Reconnecting...") - :telemetry.execute([:aprs, :is, :disconnected], %{}, %{}) + :telemetry.execute([:aprsme, :is, :disconnected], %{}, %{}) schedule_connect(@reconnect_initial) {:noreply, %{state | socket: nil, backoff: @reconnect_initial}} end @@ -74,7 +74,7 @@ defmodule Aprs.AprsIsConnection do @impl true def handle_info({:tcp_error, _socket, reason}, state) do Logger.error("APRS-IS TCP error: #{inspect(reason)}. Reconnecting...") - :telemetry.execute([:aprs, :is, :tcp_error], %{}, %{reason: reason}) + :telemetry.execute([:aprsme, :is, :tcp_error], %{}, %{reason: reason}) schedule_connect(@reconnect_initial) {:noreply, %{state | socket: nil, backoff: @reconnect_initial}} end @@ -102,11 +102,11 @@ defmodule Aprs.AprsIsConnection do end defp connect_aprs_is do - host = Application.get_env(:aprs, :aprs_is_host, ~c"rotate.aprs2.net") - port = Application.get_env(:aprs, :aprs_is_port, 14_580) - callsign = Application.get_env(:aprs, :aprs_is_callsign, "N0CALL") - passcode = Application.get_env(:aprs, :aprs_is_passcode, "00000") - filter = Application.get_env(:aprs, :aprs_is_filter, "") + host = Application.get_env(:aprsme, :aprsme_is_host, ~c"rotate.aprs2.net") + port = Application.get_env(:aprsme, :aprsme_is_port, 14_580) + callsign = Application.get_env(:aprsme, :aprsme_is_callsign, "N0CALL") + passcode = Application.get_env(:aprsme, :aprsme_is_passcode, "00000") + filter = Application.get_env(:aprsme, :aprsme_is_filter, "") opts = [:binary, active: true, packet: :line, keepalive: true] diff --git a/lib/aprs/archiver.ex b/lib/aprsme/archiver.ex similarity index 86% rename from lib/aprs/archiver.ex rename to lib/aprsme/archiver.ex index 6dedd17..59f655e 100644 --- a/lib/aprs/archiver.ex +++ b/lib/aprsme/archiver.ex @@ -1,13 +1,13 @@ -defmodule Aprs.Archiver do +defmodule Aprsme.Archiver do @moduledoc false use GenServer - alias AprsWeb.Endpoint + alias AprsmeWeb.Endpoint require Jason require Logger - # alias Aprs.{Packet, Repo} + # alias Aprsme.{Packet, Repo} @topic "call" # API diff --git a/lib/aprs/bad_packet.ex b/lib/aprsme/bad_packet.ex similarity index 93% rename from lib/aprs/bad_packet.ex rename to lib/aprsme/bad_packet.ex index ba13b2c..1e57020 100644 --- a/lib/aprs/bad_packet.ex +++ b/lib/aprsme/bad_packet.ex @@ -1,4 +1,4 @@ -defmodule Aprs.BadPacket do +defmodule Aprsme.BadPacket do @moduledoc false use Ecto.Schema @@ -17,7 +17,7 @@ defmodule Aprs.BadPacket do @type t :: %__MODULE__{} @doc false - @spec changeset(Aprs.BadPacket.t(), map()) :: Ecto.Changeset.t() + @spec changeset(Aprsme.BadPacket.t(), map()) :: Ecto.Changeset.t() def changeset(bad_packet, attrs) do bad_packet |> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at]) diff --git a/lib/aprs/convert.ex b/lib/aprsme/convert.ex similarity index 92% rename from lib/aprs/convert.ex rename to lib/aprsme/convert.ex index 61fcb87..80381c0 100644 --- a/lib/aprs/convert.ex +++ b/lib/aprsme/convert.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Convert do +defmodule Aprsme.Convert do @moduledoc false @spec wind(number(), :ultimeter, :mph) :: float() diff --git a/lib/aprs/data_extended.ex b/lib/aprsme/data_extended.ex similarity index 79% rename from lib/aprs/data_extended.ex rename to lib/aprsme/data_extended.ex index 01ad076..5c5acef 100644 --- a/lib/aprs/data_extended.ex +++ b/lib/aprsme/data_extended.ex @@ -1,13 +1,13 @@ -defmodule Aprs.DataExtended do +defmodule Aprsme.DataExtended do @moduledoc false use Ecto.Schema import Ecto.Changeset - alias Aprs.DataExtended + alias Aprsme.DataExtended embedded_schema do - field :aprs_messaging, :boolean, default: false + field :aprsme_messaging, :boolean, default: false field :comment, :string field :data_type, :string field :latitude, :decimal @@ -19,11 +19,11 @@ defmodule Aprs.DataExtended do @type t :: %__MODULE__{} @doc false - @spec changeset(Aprs.DataExtended.t(), map()) :: Ecto.Changeset.t() + @spec changeset(Aprsme.DataExtended.t(), map()) :: Ecto.Changeset.t() def changeset(%DataExtended{} = data_extended, attrs) do data_extended |> cast(attrs, [ - :aprs_messaging, + :aprsme_messaging, :comment, :data_type, :latitude, @@ -32,7 +32,7 @@ defmodule Aprs.DataExtended do :symbol_table_id ]) |> validate_required([ - :aprs_messaging, + :aprsme_messaging, :comment, :data_type, :symbol_code, diff --git a/lib/aprs/device_identification.ex b/lib/aprsme/device_identification.ex similarity index 87% rename from lib/aprs/device_identification.ex rename to lib/aprsme/device_identification.ex index 79cca14..3486d8d 100644 --- a/lib/aprs/device_identification.ex +++ b/lib/aprsme/device_identification.ex @@ -1,12 +1,12 @@ -defmodule Aprs.DeviceIdentification do +defmodule Aprsme.DeviceIdentification do @moduledoc """ Handles APRS device identification based on the APRS device identification database. """ import Ecto.Query - alias Aprs.Devices - alias Aprs.Repo + alias Aprsme.Devices + alias Aprsme.Repo @device_patterns [ {~r/^ \x00\x00$/, "Original MIC-E"}, @@ -41,13 +41,13 @@ defmodule Aprs.DeviceIdentification do ## Examples - iex> Aprs.DeviceIdentification.identify_device(">" <> <<0>> <> "^") + iex> Aprsme.DeviceIdentification.identify_device(">" <> <<0>> <> "^") "Kenwood TH-D74" - iex> Aprs.DeviceIdentification.identify_device("`_#") + iex> Aprsme.DeviceIdentification.identify_device("`_#") "Yaesu VX-8G" - iex> Aprs.DeviceIdentification.identify_device("not-a-match") + iex> Aprsme.DeviceIdentification.identify_device("not-a-match") "Unknown" """ @spec identify_device(String.t()) :: String.t() @@ -64,10 +64,10 @@ defmodule Aprs.DeviceIdentification do ## Examples - iex> "Kenwood" in Aprs.DeviceIdentification.known_manufacturers() + iex> "Kenwood" in Aprsme.DeviceIdentification.known_manufacturers() true - iex> Enum.member?(Aprs.DeviceIdentification.known_manufacturers(), "AP510") + iex> Enum.member?(Aprsme.DeviceIdentification.known_manufacturers(), "AP510") true """ @spec known_manufacturers() :: [String.t()] @@ -93,10 +93,10 @@ defmodule Aprs.DeviceIdentification do ## Examples - iex> Aprs.DeviceIdentification.known_models("Kenwood") + iex> Aprsme.DeviceIdentification.known_models("Kenwood") ["TH-D74", "TH-D74A", "DM-710", "DM-700"] - iex> Aprs.DeviceIdentification.known_models("Unknown") + iex> Aprsme.DeviceIdentification.known_models("Unknown") [] """ @spec known_models(String.t()) :: [String.t()] @@ -131,7 +131,7 @@ defmodule Aprs.DeviceIdentification do def fetch_and_upsert_devices do req = Finch.build(:get, @url) - case Finch.request(req, Aprs.Finch) do + case Finch.request(req, Aprsme.Finch) do {:ok, %Finch.Response{status: 200, body: body}} -> {:ok, json} = Jason.decode(body) upsert_devices(json) @@ -170,7 +170,7 @@ defmodule Aprs.DeviceIdentification do # Helper to enqueue the job def enqueue_refresh_job do - Oban.insert!(Aprs.DeviceIdentification.Worker.new(%{})) + Oban.insert!(Aprsme.DeviceIdentification.Worker.new(%{})) end @doc """ @@ -179,6 +179,8 @@ defmodule Aprs.DeviceIdentification do Note: This function requires the devices table to be seeded and a running Repo context. """ + def lookup_device_by_identifier(nil), do: nil + def lookup_device_by_identifier(identifier) when is_binary(identifier) do # Fetch all device patterns from DB devices = Repo.all(Devices) @@ -218,13 +220,13 @@ defmodule Aprs.DeviceIdentification do end end -defmodule Aprs.DeviceIdentification.Worker do +defmodule Aprsme.DeviceIdentification.Worker do @moduledoc false use Oban.Worker, queue: :default, max_attempts: 1 @impl true def perform(_job) do - Aprs.DeviceIdentification.maybe_refresh_devices() + Aprsme.DeviceIdentification.maybe_refresh_devices() :ok end end diff --git a/lib/aprsme/device_parser.ex b/lib/aprsme/device_parser.ex new file mode 100644 index 0000000..45e0546 --- /dev/null +++ b/lib/aprsme/device_parser.ex @@ -0,0 +1,32 @@ +defmodule Aprsme.DeviceParser do + @moduledoc """ + Extracts device identifiers from APRS packet data. + """ + + @doc """ + Extracts a device identifier from packet data. + Returns the device identifier string or nil if not found. + """ + @spec extract_device_identifier(map()) :: String.t() | nil + def extract_device_identifier(packet_data) do + # Try to extract from various possible locations + packet_data + |> Map.get(:device_identifier) + |> case do + nil -> extract_from_data_extended(packet_data) + identifier -> identifier + end + end + + defp extract_from_data_extended(packet_data) do + data_extended = Map.get(packet_data, :data_extended) || %{} + + # Try to extract from symbol information + symbol_table_id = Map.get(data_extended, :symbol_table_id) || Map.get(data_extended, "symbol_table_id") + symbol_code = Map.get(data_extended, :symbol_code) || Map.get(data_extended, "symbol_code") + + if symbol_table_id && symbol_code do + "#{symbol_table_id}#{symbol_code}" + end + end +end diff --git a/lib/aprs/devices.ex b/lib/aprsme/devices.ex similarity index 95% rename from lib/aprs/devices.ex rename to lib/aprsme/devices.ex index d903460..9e5d7fe 100644 --- a/lib/aprs/devices.ex +++ b/lib/aprsme/devices.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Devices do +defmodule Aprsme.Devices do @moduledoc false use Ecto.Schema diff --git a/test/support/devices_seeder.exs b/lib/aprsme/devices_seeder.ex similarity index 91% rename from test/support/devices_seeder.exs rename to lib/aprsme/devices_seeder.ex index dde43ba..fc05a39 100644 --- a/test/support/devices_seeder.exs +++ b/lib/aprsme/devices_seeder.ex @@ -1,9 +1,9 @@ -# test/support/devices_seeder.exs -alias Aprs.Devices -alias Aprs.Repo - -defmodule DevicesSeeder do +defmodule Aprsme.DevicesSeeder do @moduledoc false + + alias Aprsme.Devices + alias Aprsme.Repo + def seed_from_json(path \\ "test/support/test_devices.json") do {:ok, body} = File.read(path) {:ok, json} = Jason.decode(body) diff --git a/lib/aprs/encoding_utils.ex b/lib/aprsme/encoding_utils.ex similarity index 90% rename from lib/aprs/encoding_utils.ex rename to lib/aprsme/encoding_utils.ex index ca57922..524f7cb 100644 --- a/lib/aprs/encoding_utils.ex +++ b/lib/aprsme/encoding_utils.ex @@ -1,4 +1,4 @@ -defmodule Aprs.EncodingUtils do +defmodule Aprsme.EncodingUtils do @moduledoc """ Utilities for handling encoding issues in APRS packet data. @@ -7,7 +7,7 @@ defmodule Aprs.EncodingUtils do to web clients. """ - alias Parser.Types.MicE + alias Aprs.Types.MicE @doc """ Sanitizes a binary to ensure it can be safely JSON encoded. @@ -18,10 +18,10 @@ defmodule Aprs.EncodingUtils do ## Examples - iex> Aprs.EncodingUtils.sanitize_string("Hello World") + iex> Aprsme.EncodingUtils.sanitize_string("Hello World") "Hello World" - iex> Aprs.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>) + iex> Aprsme.EncodingUtils.sanitize_string(<<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>) "HelloƓWorld" """ @spec sanitize_string(binary() | nil | any()) :: binary() | nil | any() @@ -49,7 +49,7 @@ defmodule Aprs.EncodingUtils do Sanitizes all string fields in an APRS packet to ensure safe JSON encoding. """ @spec sanitize_packet(struct() | map()) :: struct() | map() - def sanitize_packet(%Aprs.Packet{} = packet) do + def sanitize_packet(%Aprsme.Packet{} = packet) do %{ packet | information_field: sanitize_string(packet.information_field), @@ -97,7 +97,7 @@ defmodule Aprs.EncodingUtils do ## Examples - iex> Aprs.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>) + iex> Aprsme.EncodingUtils.to_hex(<<72, 101, 108, 108, 111>>) "48656C6C6F" """ @spec to_hex(binary()) :: String.t() @@ -113,10 +113,10 @@ defmodule Aprs.EncodingUtils do ## Examples - iex> Aprs.EncodingUtils.encoding_info("Hello") + iex> Aprsme.EncodingUtils.encoding_info("Hello") %{valid_utf8: true, byte_count: 5, char_count: 5} - iex> Aprs.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>) + iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>) %{valid_utf8: false, byte_count: 5, invalid_at: 2} """ @spec encoding_info(binary()) :: map() diff --git a/lib/aprs/is/is.ex b/lib/aprsme/is/is.ex similarity index 85% rename from lib/aprs/is/is.ex rename to lib/aprsme/is/is.ex index 28aa510..6b9790e 100644 --- a/lib/aprs/is/is.ex +++ b/lib/aprsme/is/is.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Is do +defmodule Aprsme.Is do @moduledoc false use GenServer @@ -29,8 +29,8 @@ defmodule Aprs.Is do @impl true def init(_opts) do - env = Application.get_env(:aprs, :env) - disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false) + env = Application.get_env(:aprsme, :env) + disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false) do_init(env, disable_connection) end @@ -53,11 +53,11 @@ defmodule Aprs.Is do Process.sleep(2000) # Get startup parameters - server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") - port = Application.get_env(:aprs, :aprs_is_port, 14_580) - default_filter = Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100") - aprs_user_id = Application.get_env(:aprs, :aprs_is_login_id, "W5ISP") - aprs_passcode = Application.get_env(:aprs, :aprs_is_password, "-1") + server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net") + port = Application.get_env(:aprsme, :aprsme_is_port, 14_580) + default_filter = Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100") + aprs_user_id = Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP") + aprs_passcode = Application.get_env(:aprsme, :aprsme_is_password, "-1") # Record connection start time connected_at = DateTime.utc_now() @@ -95,7 +95,7 @@ defmodule Aprs.Is do else _ -> Logger.error("Unable to establish connection or log in to APRS-IS") - {:stop, :aprs_connection_failed} + {:stop, :aprsme_connection_failed} end end @@ -110,8 +110,8 @@ defmodule Aprs.Is do case Process.whereis(__MODULE__) do nil -> # GenServer is not running (disconnected) - server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") - port = Application.get_env(:aprs, :aprs_is_port, 14_580) + server = Application.get_env(:aprsme, :aprsme_is_server, nil) + port = Application.get_env(:aprsme, :aprsme_is_port, 14_580) %{ connected: false, @@ -119,10 +119,10 @@ defmodule Aprs.Is do port: port, connected_at: nil, uptime_seconds: 0, - login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"), - filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"), + login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"), + filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"), packet_stats: default_packet_stats(), - stored_packet_count: Aprs.Packets.get_total_packet_count() + stored_packet_count: Aprsme.Packets.get_total_packet_count() } _pid -> @@ -131,8 +131,8 @@ defmodule Aprs.Is do catch :exit, _ -> # GenServer exists but not responding - server = Application.get_env(:aprs, :aprs_is_server, ~c"rotate.aprs2.net") - port = Application.get_env(:aprs, :aprs_is_port, 14_580) + server = Application.get_env(:aprsme, :aprsme_is_server, ~c"rotate.aprs2.net") + port = Application.get_env(:aprsme, :aprsme_is_port, 14_580) %{ connected: false, @@ -140,10 +140,10 @@ defmodule Aprs.Is do port: port, connected_at: nil, uptime_seconds: 0, - login_id: Application.get_env(:aprs, :aprs_is_login_id, "W5ISP"), - filter: Application.get_env(:aprs, :aprs_is_default_filter, "r/33/-96/100"), + login_id: Application.get_env(:aprsme, :aprsme_is_login_id, "W5ISP"), + filter: Application.get_env(:aprsme, :aprsme_is_default_filter, "r/33/-96/100"), packet_stats: default_packet_stats(), - stored_packet_count: Aprs.Packets.get_total_packet_count() + stored_packet_count: Aprsme.Packets.get_total_packet_count() } end end @@ -167,8 +167,8 @@ defmodule Aprs.Is do {:ok, :ssl.sslsocket()} | {:error, any()} defp connect_to_aprs_is(server, port) do # Additional safeguard: prevent connections in test environment - env = Application.get_env(:aprs, :env) - disable_connection = Application.get_env(:aprs, :disable_aprs_connection, false) + env = Application.get_env(:aprsme, :env) + disable_connection = Application.get_env(:aprsme, :disable_aprs_connection, false) do_connect_to_aprs_is(server, port, env, disable_connection) end @@ -186,7 +186,8 @@ defmodule Aprs.Is do defp do_connect_to_aprs_is(server, port, _env, false) do Logger.debug("Connecting to: #{server}:#{port}") opts = [:binary, active: true] - :gen_tcp.connect(String.to_charlist(server), port, opts) + server_string = if is_list(server), do: List.to_string(server), else: server + :gen_tcp.connect(String.to_charlist(server_string), port, opts) end @spec send_login_string(:ssl.sslsocket(), String.t(), String.t(), String.t()) :: @@ -202,7 +203,7 @@ defmodule Aprs.Is do @spec create_timer(non_neg_integer()) :: reference() defp create_timer(timeout) do - Process.send_after(self(), :aprs_no_message_timeout, timeout) + Process.send_after(self(), :aprsme_no_message_timeout, timeout) end @spec create_keepalive_timer(non_neg_integer()) :: reference() @@ -212,7 +213,7 @@ defmodule Aprs.Is do @impl true def handle_call({:send_message, message}, _from, state) do - next_ack_number = :ets.update_counter(:aprs, :message_number, 1) + next_ack_number = :ets.update_counter(:aprsme, :message_number, 1) # Append ack number message = message <> "{" <> to_string(next_ack_number) <> "\r" @@ -231,16 +232,16 @@ defmodule Aprs.Is do login_id: state.login_params.user_id, filter: state.login_params.filter, packet_stats: state.packet_stats, - stored_packet_count: Aprs.Packets.get_total_packet_count() + stored_packet_count: Aprsme.Packets.get_total_packet_count() } {:reply, status, state} end @impl true - def handle_info(:aprs_no_message_timeout, state) do + def handle_info(:aprsme_no_message_timeout, state) do Logger.error("Socket timeout detected. Killing genserver.") - {:stop, :aprs_timeout, state} + {:stop, :aprsme_timeout, state} end def handle_info(:send_keepalive, state) do @@ -283,7 +284,7 @@ defmodule Aprs.Is do end) # Start a new timer - timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout) + timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout) state = state @@ -318,7 +319,7 @@ defmodule Aprs.Is do end) # Start a new timer - timer = Process.send_after(self(), :aprs_no_message_timeout, @aprs_timeout) + timer = Process.send_after(self(), :aprsme_no_message_timeout, @aprs_timeout) state = state @@ -408,7 +409,7 @@ defmodule Aprs.Is do def dispatch(""), do: nil def dispatch(message) do - case Parser.parse(message) do + case Aprs.parse(message) do {:ok, parsed_message} -> # Store the packet in the database for future replay # Use Task to avoid slowing down the main process @@ -424,13 +425,13 @@ defmodule Aprs.Is do attrs = struct_to_map(packet_data) # Extract additional data from the parsed packet including raw packet - attrs = Aprs.Packet.extract_additional_data(attrs, message) + attrs = Aprsme.Packet.extract_additional_data(attrs, message) # Normalize data_type to string if it's an atom attrs = normalize_data_type(attrs) # Store in database through the Packets context - case Aprs.Packets.store_packet(attrs) do + case Aprsme.Packets.store_packet(attrs) do {:ok, _packet} -> # Packet stored successfully :ok @@ -455,19 +456,19 @@ defmodule Aprs.Is do end) # Broadcast to live clients - AprsWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message) + AprsmeWeb.Endpoint.broadcast("aprs_messages", "packet", parsed_message) {:error, :invalid_packet} -> Logger.debug("PARSE ERROR: invalid packet") - Aprs.Packets.store_bad_packet(message, %{ + Aprsme.Packets.store_bad_packet(message, %{ message: "Invalid packet format", type: "ParseError" }) {:error, error} -> Logger.debug("PARSE ERROR: " <> error) - Aprs.Packets.store_bad_packet(message, %{message: error, type: "ParseError"}) + Aprsme.Packets.store_bad_packet(message, %{message: error, type: "ParseError"}) end end diff --git a/lib/aprs/is/is_supervisor.ex b/lib/aprsme/is/is_supervisor.ex similarity index 86% rename from lib/aprs/is/is_supervisor.ex rename to lib/aprsme/is/is_supervisor.ex index 681a40d..e4407a7 100644 --- a/lib/aprs/is/is_supervisor.ex +++ b/lib/aprsme/is/is_supervisor.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Is.IsSupervisor do +defmodule Aprsme.Is.IsSupervisor do @moduledoc false use Supervisor @@ -9,7 +9,7 @@ defmodule Aprs.Is.IsSupervisor do @impl true def init(:ok) do children = [ - {Aprs.Is, []} + {Aprsme.Is, []} ] # Supervisor will restart children max 3 times in 5 seconds diff --git a/lib/aprsme/mailer.ex b/lib/aprsme/mailer.ex new file mode 100644 index 0000000..273a214 --- /dev/null +++ b/lib/aprsme/mailer.ex @@ -0,0 +1,4 @@ +defmodule Aprsme.Mailer do + @moduledoc false + use Swoosh.Mailer, otp_app: :aprsme +end diff --git a/lib/aprs/packet.ex b/lib/aprsme/packet.ex similarity index 97% rename from lib/aprs/packet.ex rename to lib/aprsme/packet.ex index d68f76a..4d18a5f 100644 --- a/lib/aprs/packet.ex +++ b/lib/aprsme/packet.ex @@ -1,11 +1,11 @@ -defmodule Aprs.Packet do +defmodule Aprsme.Packet do @moduledoc false - use Aprs.Schema + use Aprsme.Schema import Ecto.Changeset - alias Aprs.DataExtended - alias Parser.Types.MicE + alias Aprs.Types.MicE + alias Aprsme.DataExtended schema "packets" do field(:base_callsign, :string) @@ -30,7 +30,7 @@ defmodule Aprs.Packet do # Additional packet data field(:comment, :string) field(:timestamp, :string) - field(:aprs_messaging, :boolean, default: false) + field(:aprsme_messaging, :boolean, default: false) # Weather data field(:temperature, :float) @@ -65,7 +65,7 @@ defmodule Aprs.Packet do @type t :: %__MODULE__{} @doc false - @spec changeset(Aprs.Packet.t(), map()) :: Ecto.Changeset.t() + @spec changeset(Aprsme.Packet.t(), map()) :: Ecto.Changeset.t() def changeset(packet, attrs) do # Convert atom data_type to string attrs = normalize_data_type(attrs) @@ -90,7 +90,7 @@ defmodule Aprs.Packet do :symbol_table_id, :comment, :timestamp, - :aprs_messaging, + :aprsme_messaging, :temperature, :humidity, :wind_speed, @@ -277,8 +277,8 @@ defmodule Aprs.Packet do |> maybe_put(:comment, data_extended[:comment] || data_extended["comment"]) |> maybe_put(:timestamp, data_extended[:timestamp] || data_extended["timestamp"]) |> maybe_put( - :aprs_messaging, - data_extended[:aprs_messaging?] || data_extended["aprs_messaging?"] + :aprsme_messaging, + data_extended[:aprsme_messaging?] || data_extended["aprs_messaging?"] ) end @@ -519,14 +519,14 @@ defmodule Aprs.Packet do @doc """ Get latitude from a packet's location geometry. """ - @spec lat(Aprs.Packet.t()) :: number() | nil + @spec lat(Aprsme.Packet.t()) :: number() | nil def lat(%__MODULE__{location: %Geo.Point{coordinates: {_lon, lat}}}), do: lat def lat(_), do: nil @doc """ Get longitude from a packet's location geometry. """ - @spec lon(Aprs.Packet.t()) :: number() | nil + @spec lon(Aprsme.Packet.t()) :: number() | nil def lon(%__MODULE__{location: %Geo.Point{coordinates: {lon, _lat}}}), do: lon def lon(_), do: nil diff --git a/lib/aprs/packet_replay.ex b/lib/aprsme/packet_replay.ex similarity index 98% rename from lib/aprs/packet_replay.ex rename to lib/aprsme/packet_replay.ex index 9c55c56..0d371c0 100644 --- a/lib/aprs/packet_replay.ex +++ b/lib/aprsme/packet_replay.ex @@ -1,4 +1,4 @@ -defmodule Aprs.PacketReplay do +defmodule Aprsme.PacketReplay do @moduledoc """ Handles replaying of historical APRS packets alongside live packets. @@ -11,8 +11,8 @@ defmodule Aprs.PacketReplay do use GenServer - alias Aprs.Packets - alias AprsWeb.Endpoint + alias Aprsme.Packets + alias AprsmeWeb.Endpoint require Logger @@ -393,7 +393,7 @@ defmodule Aprs.PacketReplay do @spec via_tuple(String.t()) :: {:via, Registry, {atom(), String.t()}} defp via_tuple(user_id) do - {:via, Registry, {Aprs.ReplayRegistry, "replay:#{user_id}"}} + {:via, Registry, {Aprsme.ReplayRegistry, "replay:#{user_id}"}} end @spec sanitize_packet_for_transport(any()) :: map() diff --git a/lib/aprs/packet_replay_behaviour.ex b/lib/aprsme/packet_replay_behaviour.ex similarity index 76% rename from lib/aprs/packet_replay_behaviour.ex rename to lib/aprsme/packet_replay_behaviour.ex index 6cdf341..8b3373d 100644 --- a/lib/aprs/packet_replay_behaviour.ex +++ b/lib/aprsme/packet_replay_behaviour.ex @@ -1,10 +1,10 @@ -defmodule Aprs.PacketReplayBehaviour do +defmodule Aprsme.PacketReplayBehaviour do @moduledoc """ Behavior definition for the PacketReplay module. This allows us to mock the PacketReplay module in tests. """ - @callback start_replay(pid(), list(Aprs.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()} + @callback start_replay(pid(), list(Aprsme.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()} @callback stop_replay(reference()) :: :ok @callback pause_replay(reference()) :: :ok | {:error, :not_found} @callback resume_replay(reference()) :: :ok | {:error, :not_found} diff --git a/lib/aprs/packets.ex b/lib/aprsme/packets.ex similarity index 97% rename from lib/aprs/packets.ex rename to lib/aprsme/packets.ex index 15f90e8..a97d6bb 100644 --- a/lib/aprs/packets.ex +++ b/lib/aprsme/packets.ex @@ -1,16 +1,16 @@ -defmodule Aprs.Packets do +defmodule Aprsme.Packets do @moduledoc """ The Packets context. """ - @behaviour Aprs.PacketsBehaviour + @behaviour Aprsme.PacketsBehaviour import Ecto.Query, warn: false - alias Aprs.BadPacket - alias Aprs.Packet - alias Aprs.Repo - alias Parser.Types.MicE + alias Aprs.Types.MicE + alias Aprsme.BadPacket + alias Aprsme.Packet + alias Aprsme.Repo @doc """ Stores a packet in the database. @@ -34,8 +34,8 @@ defmodule Aprs.Packets do end) |> normalize_ssid() |> then(fn attrs -> - device_identifier = Parser.DeviceParser.extract_device_identifier(packet_data) - matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier) + device_identifier = Aprsme.DeviceParser.extract_device_identifier(packet_data) + matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier) canonical_identifier = if matched_device, do: matched_device.identifier, else: device_identifier Map.put(attrs, :device_identifier, canonical_identifier) end) @@ -460,7 +460,7 @@ defmodule Aprs.Packets do """ @impl true def clean_old_packets do - retention_days = Application.get_env(:aprs, :packet_retention_days, 365) + retention_days = Application.get_env(:aprsme, :packet_retention_days, 365) cutoff_time = DateTime.add(DateTime.utc_now(), -retention_days * 86_400, :second) {deleted_count, _} = Repo.delete_all(from(p in Packet, where: p.received_at < ^cutoff_time)) @@ -533,7 +533,7 @@ defmodule Aprs.Packets do defp sanitize_packet_strings(list) when is_list(list), do: Enum.map(list, &sanitize_packet_strings/1) defp sanitize_packet_strings(binary) when is_binary(binary) do - s = Aprs.EncodingUtils.sanitize_string(binary) + s = Aprsme.EncodingUtils.sanitize_string(binary) if is_binary(s), do: s, else: "" end diff --git a/lib/aprs/packets_behaviour.ex b/lib/aprsme/packets_behaviour.ex similarity index 93% rename from lib/aprs/packets_behaviour.ex rename to lib/aprsme/packets_behaviour.ex index 4f342f6..9d21bed 100644 --- a/lib/aprs/packets_behaviour.ex +++ b/lib/aprsme/packets_behaviour.ex @@ -1,4 +1,4 @@ -defmodule Aprs.PacketsBehaviour do +defmodule Aprsme.PacketsBehaviour do @moduledoc """ Behaviour for Packets module to allow mocking in tests """ diff --git a/lib/aprs/passcode.ex b/lib/aprsme/passcode.ex similarity index 95% rename from lib/aprs/passcode.ex rename to lib/aprsme/passcode.ex index b72a775..f6241bb 100644 --- a/lib/aprs/passcode.ex +++ b/lib/aprsme/passcode.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Passcode do +defmodule Aprsme.Passcode do @moduledoc """ Module for generating APRS passcodes from callsigns. The passcode is a hash of the callsign used for authentication with APRS-IS servers. @@ -14,7 +14,7 @@ defmodule Aprs.Passcode do - The generated passcode as an integer ## Examples - iex> Aprs.Passcode.generate("W5ISP") + iex> Aprsme.Passcode.generate("W5ISP") 15748 """ @spec generate(String.t()) :: non_neg_integer() diff --git a/lib/aprs/postgres_notifier.ex b/lib/aprsme/postgres_notifier.ex similarity index 72% rename from lib/aprs/postgres_notifier.ex rename to lib/aprsme/postgres_notifier.ex index b65d0fc..cadaed2 100644 --- a/lib/aprs/postgres_notifier.ex +++ b/lib/aprsme/postgres_notifier.ex @@ -1,4 +1,4 @@ -defmodule Aprs.PostgresNotifier do +defmodule Aprsme.PostgresNotifier do @moduledoc """ Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts them via Phoenix.PubSub for reactive, event-driven updates. @@ -6,9 +6,9 @@ defmodule Aprs.PostgresNotifier do use GenServer @event_channel "aprs_events" - @event_topic "postgres:aprs_events" + @event_topic "postgres:aprsme_events" @packet_channel "aprs_packets" - @packet_topic "postgres:aprs_packets" + @packet_topic "postgres:aprsme_packets" def start_link(_opts) do GenServer.start_link(__MODULE__, nil, name: __MODULE__) @@ -16,7 +16,7 @@ defmodule Aprs.PostgresNotifier do @impl true def init(_) do - {:ok, conn} = Postgrex.Notifications.start_link(Aprs.Repo.config()) + {:ok, conn} = Postgrex.Notifications.start_link(Aprsme.Repo.config()) {:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel) {:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel) {:ok, %{conn: conn}} @@ -24,14 +24,14 @@ defmodule Aprs.PostgresNotifier do @impl true def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do - Phoenix.PubSub.broadcast(Aprs.PubSub, @event_topic, {:postgres_notify, payload}) + Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload}) {:noreply, state} end def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do case Jason.decode(payload) do {:ok, packet} -> - Phoenix.PubSub.broadcast(Aprs.PubSub, @packet_topic, {:postgres_packet, packet}) + Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet}) _ -> :noop diff --git a/lib/aprs/postgres_types.ex b/lib/aprsme/postgres_types.ex similarity index 81% rename from lib/aprs/postgres_types.ex rename to lib/aprsme/postgres_types.ex index ac31325..05a8555 100644 --- a/lib/aprs/postgres_types.ex +++ b/lib/aprsme/postgres_types.ex @@ -1,5 +1,5 @@ Postgrex.Types.define( - Aprs.PostgresTypes, + Aprsme.PostgresTypes, [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), json: Jason ) diff --git a/lib/aprsme/presence.ex b/lib/aprsme/presence.ex new file mode 100644 index 0000000..1663079 --- /dev/null +++ b/lib/aprsme/presence.ex @@ -0,0 +1,4 @@ +defmodule Aprsme.Presence do + @moduledoc false + use Phoenix.Presence, otp_app: :aprsme, pubsub_server: Aprsme.PubSub +end diff --git a/lib/aprs/release.ex b/lib/aprsme/release.ex similarity index 81% rename from lib/aprs/release.ex rename to lib/aprsme/release.ex index 8ec18f7..5dba83d 100644 --- a/lib/aprs/release.ex +++ b/lib/aprsme/release.ex @@ -1,9 +1,9 @@ -defmodule Aprs.Release do +defmodule Aprsme.Release do @moduledoc """ Used for executing DB release tasks when run in production without Mix installed. """ - @app :aprs + @app :aprsme def migrate do require Logger @@ -12,7 +12,7 @@ defmodule Aprs.Release do Logger.info("Deployment timestamp: #{deployed_at}") # Run migrations - {:ok, _, _} = Ecto.Migrator.with_repo(Aprs.Repo, &Ecto.Migrator.run(&1, :up, all: true)) + {:ok, _, _} = Ecto.Migrator.with_repo(Aprsme.Repo, &Ecto.Migrator.run(&1, :up, all: true)) end def rollback(repo, version) do @@ -33,7 +33,7 @@ defmodule Aprs.Release do deployed_at = read_deployment_timestamp() # Add to application config - Application.put_env(:aprs, :deployed_at, deployed_at) + Application.put_env(:aprsme, :deployed_at, deployed_at) deployed_at end @@ -42,7 +42,7 @@ defmodule Aprs.Release do Get the deployment timestamp. """ def deployed_at do - Application.get_env(:aprs, :deployed_at) || DateTime.utc_now() + Application.get_env(:aprsme, :deployed_at) || DateTime.utc_now() end defp read_deployment_timestamp do diff --git a/lib/aprs/repo.ex b/lib/aprsme/repo.ex similarity index 54% rename from lib/aprs/repo.ex rename to lib/aprsme/repo.ex index 1fda4f0..bd892f4 100644 --- a/lib/aprs/repo.ex +++ b/lib/aprsme/repo.ex @@ -1,5 +1,5 @@ -defmodule Aprs.Repo do +defmodule Aprsme.Repo do use Ecto.Repo, - otp_app: :aprs, + otp_app: :aprsme, adapter: Ecto.Adapters.Postgres end diff --git a/lib/aprs/schema.ex b/lib/aprsme/schema.ex similarity index 87% rename from lib/aprs/schema.ex rename to lib/aprsme/schema.ex index 9f26e92..0fec416 100644 --- a/lib/aprs/schema.ex +++ b/lib/aprsme/schema.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Schema do +defmodule Aprsme.Schema do @moduledoc false defmacro __using__(_) do quote do diff --git a/lib/aprs/workers/packet_cleanup_worker.ex b/lib/aprsme/workers/packet_cleanup_worker.ex similarity index 92% rename from lib/aprs/workers/packet_cleanup_worker.ex rename to lib/aprsme/workers/packet_cleanup_worker.ex index 23f1bfd..7a90896 100644 --- a/lib/aprs/workers/packet_cleanup_worker.ex +++ b/lib/aprsme/workers/packet_cleanup_worker.ex @@ -1,4 +1,4 @@ -defmodule Aprs.Workers.PacketCleanupWorker do +defmodule Aprsme.Workers.PacketCleanupWorker do @moduledoc """ Oban worker for cleaning up old APRS packet data. @@ -18,8 +18,8 @@ defmodule Aprs.Workers.PacketCleanupWorker do # Import modules needed for database operations - alias Aprs.Packet - alias Aprs.Repo + alias Aprsme.Packet + alias Aprsme.Repo require Logger @@ -52,7 +52,7 @@ defmodule Aprs.Workers.PacketCleanupWorker do deleted_count = cleanup_old_packets() # Log results - retention_days = Application.get_env(:aprs, :packet_retention_days, 365) + retention_days = Application.get_env(:aprsme, :packet_retention_days, 365) Logger.info("APRS packet cleanup complete: removed #{deleted_count} packets older than #{retention_days} days") @@ -92,6 +92,6 @@ defmodule Aprs.Workers.PacketCleanupWorker do end defp packets_module do - Application.get_env(:aprs, :packets_module, Aprs.Packets) + Application.get_env(:aprsme, :packets_module, Aprsme.Packets) end end diff --git a/lib/aprs_web/components/core_components.ex b/lib/aprsme_web/components/core_components.ex similarity index 98% rename from lib/aprs_web/components/core_components.ex rename to lib/aprsme_web/components/core_components.ex index 3f19e97..f4e6c77 100644 --- a/lib/aprs_web/components/core_components.ex +++ b/lib/aprsme_web/components/core_components.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.CoreComponents do +defmodule AprsmeWeb.CoreComponents do @moduledoc """ Provides core UI components. @@ -10,7 +10,7 @@ defmodule AprsWeb.CoreComponents do [heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project. """ use Phoenix.Component - use Gettext, backend: AprsWeb.Gettext + use Gettext, backend: AprsmeWeb.Gettext alias Phoenix.HTML.Form alias Phoenix.LiveView.JS @@ -620,9 +620,9 @@ defmodule AprsWeb.CoreComponents do # should be written to the errors.po file. The :count option is # set by Ecto and indicates we should also apply plural rules. if count = opts[:count] do - Gettext.dngettext(AprsWeb.Gettext, "errors", msg, msg, count, opts) + Gettext.dngettext(AprsmeWeb.Gettext, "errors", msg, msg, count, opts) else - Gettext.dgettext(AprsWeb.Gettext, "errors", msg, opts) + Gettext.dgettext(AprsmeWeb.Gettext, "errors", msg, opts) end end diff --git a/lib/aprs_web/components/layouts.ex b/lib/aprsme_web/components/layouts.ex similarity index 68% rename from lib/aprs_web/components/layouts.ex rename to lib/aprsme_web/components/layouts.ex index 0c059e2..0ba89ab 100644 --- a/lib/aprs_web/components/layouts.ex +++ b/lib/aprsme_web/components/layouts.ex @@ -1,6 +1,6 @@ -defmodule AprsWeb.Layouts do +defmodule AprsmeWeb.Layouts do @moduledoc false - use AprsWeb, :html + use AprsmeWeb, :html embed_templates "layouts/*" diff --git a/lib/aprs_web/components/layouts/app.html.heex b/lib/aprsme_web/components/layouts/app.html.heex similarity index 100% rename from lib/aprs_web/components/layouts/app.html.heex rename to lib/aprsme_web/components/layouts/app.html.heex diff --git a/lib/aprs_web/components/layouts/map.html.heex b/lib/aprsme_web/components/layouts/map.html.heex similarity index 100% rename from lib/aprs_web/components/layouts/map.html.heex rename to lib/aprsme_web/components/layouts/map.html.heex diff --git a/lib/aprs_web/components/layouts/root.html.heex b/lib/aprsme_web/components/layouts/root.html.heex similarity index 100% rename from lib/aprs_web/components/layouts/root.html.heex rename to lib/aprsme_web/components/layouts/root.html.heex diff --git a/lib/aprs_web/controllers/api/v1/callsign_controller.ex b/lib/aprsme_web/controllers/api/v1/callsign_controller.ex similarity index 93% rename from lib/aprs_web/controllers/api/v1/callsign_controller.ex rename to lib/aprsme_web/controllers/api/v1/callsign_controller.ex index 1d16b3e..109babe 100644 --- a/lib/aprs_web/controllers/api/v1/callsign_controller.ex +++ b/lib/aprsme_web/controllers/api/v1/callsign_controller.ex @@ -1,13 +1,13 @@ -defmodule AprsWeb.Api.V1.CallsignController do +defmodule AprsmeWeb.Api.V1.CallsignController do @moduledoc """ API v1 controller for callsign-related endpoints. """ - use AprsWeb, :controller + use AprsmeWeb, :controller - alias Aprs.Packets - alias AprsWeb.Api.V1.CallsignJSON + alias Aprsme.Packets + alias AprsmeWeb.Api.V1.CallsignJSON - action_fallback AprsWeb.Api.V1.FallbackController + action_fallback AprsmeWeb.Api.V1.FallbackController @doc """ Get the most recent packet for a given callsign. diff --git a/lib/aprs_web/controllers/api/v1/fallback_controller.ex b/lib/aprsme_web/controllers/api/v1/fallback_controller.ex similarity index 92% rename from lib/aprs_web/controllers/api/v1/fallback_controller.ex rename to lib/aprsme_web/controllers/api/v1/fallback_controller.ex index 08cbc6b..193f6f6 100644 --- a/lib/aprs_web/controllers/api/v1/fallback_controller.ex +++ b/lib/aprsme_web/controllers/api/v1/fallback_controller.ex @@ -1,18 +1,18 @@ -defmodule AprsWeb.Api.V1.FallbackController do +defmodule AprsmeWeb.Api.V1.FallbackController do @moduledoc """ Translates controller action results into valid `Plug.Conn` responses. See `Phoenix.Controller.action_fallback/1` for more details. """ - use AprsWeb, :controller + use AprsmeWeb, :controller - alias AprsWeb.Api.V1.ErrorJSON + alias AprsmeWeb.Api.V1.ErrorJSON # This clause handles errors returned by Ecto's insert/update/delete. def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn |> put_status(:unprocessable_entity) - |> put_view(json: AprsWeb.Api.V1.ChangesetJSON) + |> put_view(json: AprsmeWeb.Api.V1.ChangesetJSON) |> render(:error, changeset: changeset) end diff --git a/lib/aprs_web/controllers/api/v1/json/callsign_json.ex b/lib/aprsme_web/controllers/api/v1/json/callsign_json.ex similarity index 97% rename from lib/aprs_web/controllers/api/v1/json/callsign_json.ex rename to lib/aprsme_web/controllers/api/v1/json/callsign_json.ex index e6afba7..279e8a0 100644 --- a/lib/aprs_web/controllers/api/v1/json/callsign_json.ex +++ b/lib/aprsme_web/controllers/api/v1/json/callsign_json.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.Api.V1.CallsignJSON do +defmodule AprsmeWeb.Api.V1.CallsignJSON do @moduledoc """ Renders callsign and packet data for API v1. """ - alias Aprs.Packet + alias Aprsme.Packet def render("show.json", %{packet: packet}) do %{data: packet_json(packet)} @@ -38,7 +38,7 @@ defmodule AprsWeb.Api.V1.CallsignJSON do symbol: symbol_json(packet), comment: packet.comment, timestamp: packet.timestamp, - aprs_messaging: packet.aprs_messaging, + aprs_messaging: packet.aprsme_messaging, weather: weather_json(packet), equipment: equipment_json(packet), message: message_json(packet), diff --git a/lib/aprs_web/controllers/api/v1/json/changeset_json.ex b/lib/aprsme_web/controllers/api/v1/json/changeset_json.ex similarity index 94% rename from lib/aprs_web/controllers/api/v1/json/changeset_json.ex rename to lib/aprsme_web/controllers/api/v1/json/changeset_json.ex index 09ad47d..42cb7ac 100644 --- a/lib/aprs_web/controllers/api/v1/json/changeset_json.ex +++ b/lib/aprsme_web/controllers/api/v1/json/changeset_json.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.Api.V1.ChangesetJSON do +defmodule AprsmeWeb.Api.V1.ChangesetJSON do @moduledoc """ Renders changeset errors for API v1. """ diff --git a/lib/aprs_web/controllers/api/v1/json/error_json.ex b/lib/aprsme_web/controllers/api/v1/json/error_json.ex similarity index 97% rename from lib/aprs_web/controllers/api/v1/json/error_json.ex rename to lib/aprsme_web/controllers/api/v1/json/error_json.ex index 194406d..9ebfd34 100644 --- a/lib/aprs_web/controllers/api/v1/json/error_json.ex +++ b/lib/aprsme_web/controllers/api/v1/json/error_json.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.Api.V1.ErrorJSON do +defmodule AprsmeWeb.Api.V1.ErrorJSON do @moduledoc """ Renders error responses for API v1. """ diff --git a/lib/aprs_web/controllers/error_html.ex b/lib/aprsme_web/controllers/error_html.ex similarity index 90% rename from lib/aprs_web/controllers/error_html.ex rename to lib/aprsme_web/controllers/error_html.ex index 30d4323..179c185 100644 --- a/lib/aprs_web/controllers/error_html.ex +++ b/lib/aprsme_web/controllers/error_html.ex @@ -1,6 +1,6 @@ -defmodule AprsWeb.ErrorHTML do +defmodule AprsmeWeb.ErrorHTML do @moduledoc false - use AprsWeb, :html + use AprsmeWeb, :html # If you want to customize your error pages, # uncomment the embed_templates/1 call below diff --git a/lib/aprs_web/controllers/error_json.ex b/lib/aprsme_web/controllers/error_json.ex similarity index 93% rename from lib/aprs_web/controllers/error_json.ex rename to lib/aprsme_web/controllers/error_json.ex index fce5114..0effb6c 100644 --- a/lib/aprs_web/controllers/error_json.ex +++ b/lib/aprsme_web/controllers/error_json.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.ErrorJSON do +defmodule AprsmeWeb.ErrorJSON do @moduledoc false # If you want to customize a particular status code, # you may add your own clauses, such as: diff --git a/lib/aprs_web/controllers/page_controller.ex b/lib/aprsme_web/controllers/page_controller.ex similarity index 85% rename from lib/aprs_web/controllers/page_controller.ex rename to lib/aprsme_web/controllers/page_controller.ex index ae79aa6..74f79e4 100644 --- a/lib/aprs_web/controllers/page_controller.ex +++ b/lib/aprsme_web/controllers/page_controller.ex @@ -1,6 +1,6 @@ -defmodule AprsWeb.PageController do +defmodule AprsmeWeb.PageController do @moduledoc false - use AprsWeb, :controller + use AprsmeWeb, :controller def home(conn, _params) do render(conn, :home) @@ -14,14 +14,14 @@ defmodule AprsWeb.PageController do # Check database connection db_healthy = try do - Aprs.Repo.query!("SELECT 1") + Aprsme.Repo.query!("SELECT 1") true rescue _ -> false end # Get application version - version = :aprs |> Application.spec(:vsn) |> List.to_string() + version = :aprsme |> Application.spec(:vsn) |> List.to_string() if db_healthy do json(conn, %{ @@ -45,7 +45,7 @@ defmodule AprsWeb.PageController do def ready(conn, _params) do # Simple readiness check without database dependency # This is faster for startup health checks - version = :aprs |> Application.spec(:vsn) |> List.to_string() + version = :aprsme |> Application.spec(:vsn) |> List.to_string() json(conn, %{ status: "ready", @@ -56,10 +56,10 @@ defmodule AprsWeb.PageController do def status_json(conn, _params) do # Get APRS-IS connection status - aprs_status = Aprs.Is.get_status() + aprs_status = Aprsme.Is.get_status() # Get application version - version = :aprs |> Application.spec(:vsn) |> List.to_string() + version = :aprsme |> Application.spec(:vsn) |> List.to_string() # Calculate uptime in a human-readable format uptime_display = format_uptime(aprs_status.uptime_seconds) diff --git a/lib/aprs_web/controllers/page_html.ex b/lib/aprsme_web/controllers/page_html.ex similarity index 50% rename from lib/aprs_web/controllers/page_html.ex rename to lib/aprsme_web/controllers/page_html.ex index 78a01af..1f31ba3 100644 --- a/lib/aprs_web/controllers/page_html.ex +++ b/lib/aprsme_web/controllers/page_html.ex @@ -1,6 +1,6 @@ -defmodule AprsWeb.PageHTML do +defmodule AprsmeWeb.PageHTML do @moduledoc false - use AprsWeb, :html + use AprsmeWeb, :html embed_templates "page_html/*" end diff --git a/lib/aprs_web/controllers/page_html/packets.html.heex b/lib/aprsme_web/controllers/page_html/packets.html.heex similarity index 100% rename from lib/aprs_web/controllers/page_html/packets.html.heex rename to lib/aprsme_web/controllers/page_html/packets.html.heex diff --git a/lib/aprs_web/controllers/user_session_controller.ex b/lib/aprsme_web/controllers/user_session_controller.ex similarity index 90% rename from lib/aprs_web/controllers/user_session_controller.ex rename to lib/aprsme_web/controllers/user_session_controller.ex index 31161de..5765da0 100644 --- a/lib/aprs_web/controllers/user_session_controller.ex +++ b/lib/aprsme_web/controllers/user_session_controller.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.UserSessionController do +defmodule AprsmeWeb.UserSessionController do @moduledoc false - use AprsWeb, :controller + use AprsmeWeb, :controller - alias Aprs.Accounts - alias AprsWeb.UserAuth + alias Aprsme.Accounts + alias AprsmeWeb.UserAuth def create(conn, %{"_action" => "registered"} = params) do create(conn, params, "Account created successfully!") diff --git a/lib/aprs_web/endpoint.ex b/lib/aprsme_web/endpoint.ex similarity index 89% rename from lib/aprs_web/endpoint.ex rename to lib/aprsme_web/endpoint.ex index 4d85315..0d033e9 100644 --- a/lib/aprs_web/endpoint.ex +++ b/lib/aprsme_web/endpoint.ex @@ -1,7 +1,7 @@ -defmodule AprsWeb.Endpoint do +defmodule AprsmeWeb.Endpoint do @moduledoc false - use Phoenix.Endpoint, otp_app: :aprs + use Phoenix.Endpoint, otp_app: :aprsme # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. @@ -21,7 +21,7 @@ defmodule AprsWeb.Endpoint do # when deploying your static files in production. plug Plug.Static, at: "/", - from: :aprs, + from: :aprsme, gzip: false, only: ~w(assets fonts images aprs-symbols favicon.ico robots.txt) @@ -31,7 +31,7 @@ defmodule AprsWeb.Endpoint do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader plug Phoenix.CodeReloader - plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprs + plug Phoenix.Ecto.CheckRepoStatus, otp_app: :aprsme end plug Phoenix.LiveDashboard.RequestLogger, @@ -49,5 +49,5 @@ defmodule AprsWeb.Endpoint do plug Plug.MethodOverride plug Plug.Head plug Plug.Session, @session_options - plug AprsWeb.Router + plug AprsmeWeb.Router end diff --git a/lib/aprs_web/gettext.ex b/lib/aprsme_web/gettext.ex similarity index 85% rename from lib/aprs_web/gettext.ex rename to lib/aprsme_web/gettext.ex index 8ffbf14..43ede16 100644 --- a/lib/aprs_web/gettext.ex +++ b/lib/aprsme_web/gettext.ex @@ -1,11 +1,11 @@ -defmodule AprsWeb.Gettext do +defmodule AprsmeWeb.Gettext do @moduledoc """ A module providing Internationalization with a gettext-based API. By using [Gettext](https://hexdocs.pm/gettext), your module gains a set of macros for translations, for example: - import AprsWeb.Gettext + import AprsmeWeb.Gettext # Simple translation gettext("Here is the string to translate") @@ -20,5 +20,5 @@ defmodule AprsWeb.Gettext do See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. """ - use Gettext.Backend, otp_app: :aprs + use Gettext.Backend, otp_app: :aprsme end diff --git a/lib/aprs_web/live/about_live.ex b/lib/aprsme_web/live/about_live.ex similarity index 69% rename from lib/aprs_web/live/about_live.ex rename to lib/aprsme_web/live/about_live.ex index 304b611..a3af7f9 100644 --- a/lib/aprs_web/live/about_live.ex +++ b/lib/aprsme_web/live/about_live.ex @@ -1,6 +1,6 @@ -defmodule AprsWeb.AboutLive do +defmodule AprsmeWeb.AboutLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view @impl true def mount(_params, _session, socket) do diff --git a/lib/aprs_web/live/about_live.html.heex b/lib/aprsme_web/live/about_live.html.heex similarity index 100% rename from lib/aprs_web/live/about_live.html.heex rename to lib/aprsme_web/live/about_live.html.heex diff --git a/lib/aprs_web/live/api_docs_live.ex b/lib/aprsme_web/live/api_docs_live.ex similarity index 99% rename from lib/aprs_web/live/api_docs_live.ex rename to lib/aprsme_web/live/api_docs_live.ex index 261315d..ce75a1b 100644 --- a/lib/aprs_web/live/api_docs_live.ex +++ b/lib/aprsme_web/live/api_docs_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.ApiDocsLive do +defmodule AprsmeWeb.ApiDocsLive do @moduledoc """ LiveView for API documentation page. """ - use AprsWeb, :live_view + use AprsmeWeb, :live_view @impl true def mount(_params, _session, socket) do @@ -60,7 +60,7 @@ defmodule AprsWeb.ApiDocsLive do end defp make_http_request(callsign) do - alias Aprs.Packets + alias Aprsme.Packets # Validate callsign format (same as controller) callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/ @@ -112,7 +112,7 @@ defmodule AprsWeb.ApiDocsLive do "symbol" => format_symbol(packet), "comment" => packet.comment, "timestamp" => packet.timestamp, - "aprs_messaging" => packet.aprs_messaging, + "aprsme_messaging" => packet.aprsme_messaging, "weather" => format_weather(packet), "equipment" => format_equipment(packet), "message" => format_message(packet), @@ -373,7 +373,7 @@ defmodule AprsWeb.ApiDocsLive do }, "comment": "Mobile Station", "timestamp": null, - "aprs_messaging": false, + "aprsme_messaging": false, "weather": null, "equipment": { "manufacturer": "Kenwood", diff --git a/lib/aprs_web/live/bad_packets_live/index.ex b/lib/aprsme_web/live/bad_packets_live/index.ex similarity index 90% rename from lib/aprs_web/live/bad_packets_live/index.ex rename to lib/aprsme_web/live/bad_packets_live/index.ex index 9da2ee9..9198d42 100644 --- a/lib/aprs_web/live/bad_packets_live/index.ex +++ b/lib/aprsme_web/live/bad_packets_live/index.ex @@ -1,17 +1,17 @@ -defmodule AprsWeb.BadPacketsLive.Index do +defmodule AprsmeWeb.BadPacketsLive.Index do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view import Ecto.Query - alias Aprs.BadPacket - alias Aprs.Repo + alias Aprsme.BadPacket + alias Aprsme.Repo @impl true def mount(_params, _session, socket) do if connected?(socket) do # Subscribe to Postgres notifications for bad packets - Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_events") + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_events") # Load initial bad packets bad_packets = fetch_bad_packets() # Extra safeguard to ensure we never show more than 100 diff --git a/lib/aprs_web/live/bad_packets_live/index.html.heex b/lib/aprsme_web/live/bad_packets_live/index.html.heex similarity index 100% rename from lib/aprs_web/live/bad_packets_live/index.html.heex rename to lib/aprsme_web/live/bad_packets_live/index.html.heex diff --git a/lib/aprs_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex similarity index 95% rename from lib/aprs_web/live/info_live/show.ex rename to lib/aprsme_web/live/info_live/show.ex index c992526..5c86cb8 100644 --- a/lib/aprs_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.InfoLive.Show do +defmodule AprsmeWeb.InfoLive.Show do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Packets - alias AprsWeb.MapLive.PacketUtils + alias Aprsme.Packets + alias AprsmeWeb.MapLive.PacketUtils @neighbor_radius_km 10 @neighbor_limit 10 diff --git a/lib/aprs_web/live/info_live/show.html.heex b/lib/aprsme_web/live/info_live/show.html.heex similarity index 92% rename from lib/aprs_web/live/info_live/show.html.heex rename to lib/aprsme_web/live/info_live/show.html.heex index 972bed6..db49697 100644 --- a/lib/aprs_web/live/info_live/show.html.heex +++ b/lib/aprsme_web/live/info_live/show.html.heex @@ -1,4 +1,4 @@ -<% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %> +<% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet || %{}) %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_code = symbol_code || ">" %> <% symbol_table_num = @@ -13,14 +13,14 @@ |> String.to_charlist() |> List.first() |> (fn c -> if is_integer(c), do: c, else: 63 end).() %> -<% symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %> -<% symbol_index = symbol_code_ord - 33 %> +<% _symbol_img = "/aprs-symbols/aprs-symbols-24-#{symbol_table_num}@2x.png" %> +<% _symbol_index = symbol_code_ord - 33 %>

APRS station {@callsign} <%= if @packet do %> - <% {symbol_table_id, symbol_code} = AprsWeb.MapLive.PacketUtils.get_symbol_info(@packet) %> + <% {symbol_table_id, symbol_code} = AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@packet) %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_code = symbol_code || ">" %> <% symbol_table_num = @@ -57,7 +57,7 @@ Location: {@packet.lat}, {@packet.lon}

- Last position: {AprsWeb.MapLive.PacketUtils.get_timestamp( + Last position: {AprsmeWeb.MapLive.PacketUtils.get_timestamp( @packet )}
@@ -92,7 +92,7 @@ <%= if neighbor.packet do %> <% {symbol_table_id, symbol_code} = - AprsWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %> + AprsmeWeb.MapLive.PacketUtils.get_symbol_info(neighbor.packet) %> <% symbol_table = if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %> <% symbol_code = symbol_code || ">" %> diff --git a/lib/aprs_web/live/map_live/callsign_view.ex b/lib/aprsme_web/live/map_live/callsign_view.ex similarity index 99% rename from lib/aprs_web/live/map_live/callsign_view.ex rename to lib/aprsme_web/live/map_live/callsign_view.ex index 5025cf1..adddb0b 100644 --- a/lib/aprs_web/live/map_live/callsign_view.ex +++ b/lib/aprsme_web/live/map_live/callsign_view.ex @@ -1,11 +1,11 @@ -defmodule AprsWeb.MapLive.CallsignView do +defmodule AprsmeWeb.MapLive.CallsignView do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Packets - alias AprsWeb.Endpoint - alias AprsWeb.MapLive.MapHelpers - alias AprsWeb.MapLive.PacketUtils + alias Aprsme.Packets + alias AprsmeWeb.Endpoint + alias AprsmeWeb.MapLive.MapHelpers + alias AprsmeWeb.MapLive.PacketUtils @default_center %{lat: 39.0, lng: -98.0} @default_zoom 4 diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex similarity index 98% rename from lib/aprs_web/live/map_live/index.ex rename to lib/aprsme_web/live/map_live/index.ex index 9776309..9bc10fb 100644 --- a/lib/aprs_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -1,34 +1,34 @@ -defmodule AprsWeb.MapLive.Index do +defmodule AprsmeWeb.MapLive.Index do @moduledoc """ LiveView for displaying real-time APRS packets on a map """ - use AprsWeb, :live_view + use AprsmeWeb, :live_view - import AprsWeb.TimeHelpers, only: [time_ago_in_words: 1] + import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1] - alias AprsWeb.Endpoint - alias AprsWeb.MapLive.MapHelpers - alias AprsWeb.MapLive.PacketUtils + alias AprsmeWeb.Endpoint + alias AprsmeWeb.MapLive.MapHelpers + alias AprsmeWeb.MapLive.PacketUtils alias Phoenix.LiveView.Socket @default_center %{lat: 39.8283, lng: -98.5795} @default_zoom 5 - @finch_name Aprs.Finch - @initialize_replay_delay Application.compile_env(:aprs, :initialize_replay_delay, 500) + @finch_name Aprsme.Finch + @initialize_replay_delay Application.compile_env(:aprsme, :initialize_replay_delay, 500) @impl true def mount(_params, _session, socket) do if connected?(socket) do # Subscribe to packet updates - Phoenix.PubSub.subscribe(Aprs.PubSub, "packets") - Phoenix.PubSub.subscribe(Aprs.PubSub, "bad_packets") + Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets") + Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets") # Schedule periodic cleanup of old packets Process.send_after(self(), :cleanup_old_packets, 60_000) end # Get deployment timestamp from config (set during application startup) - deployed_at = Aprs.Release.deployed_at() + deployed_at = Aprsme.Release.deployed_at() one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second) @@ -38,7 +38,7 @@ defmodule AprsWeb.MapLive.Index do if connected?(socket) do Endpoint.subscribe("aprs_messages") - Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets") + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") maybe_start_geolocation(socket) end @@ -99,7 +99,7 @@ defmodule AprsWeb.MapLive.Index do defp maybe_start_geolocation(socket) do if geolocation_enabled?() do ip_for_geolocation = - if Application.get_env(:aprs, AprsWeb.Endpoint)[:code_reloader] do + if Application.get_env(:aprsme, AprsmeWeb.Endpoint)[:code_reloader] do # For testing geolocation in dev environment, use a public IP address. # This will be geolocated to Mountain View, CA. "8.8.8.8" @@ -116,7 +116,7 @@ defmodule AprsWeb.MapLive.Index do end defp geolocation_enabled? do - Application.get_env(:aprs, :disable_aprs_connection, false) != true + Application.get_env(:aprsme, :disable_aprs_connection, false) != true end defp extract_ip(socket) do @@ -1175,7 +1175,7 @@ defmodule AprsWeb.MapLive.Index do } # Call the database through the Packets context - packets_module = Application.get_env(:aprs, :packets_module, Aprs.Packets) + packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets) packets = packets_module.get_packets_for_replay(packets_params) # Sort packets by received_at timestamp to ensure chronological replay diff --git a/lib/aprs_web/live/map_live/map_helpers.ex b/lib/aprsme_web/live/map_live/map_helpers.ex similarity index 98% rename from lib/aprs_web/live/map_live/map_helpers.ex rename to lib/aprsme_web/live/map_live/map_helpers.ex index b94bb6e..bfb7b94 100644 --- a/lib/aprs_web/live/map_live/map_helpers.ex +++ b/lib/aprsme_web/live/map_live/map_helpers.ex @@ -1,7 +1,7 @@ -defmodule AprsWeb.MapLive.MapHelpers do +defmodule AprsmeWeb.MapLive.MapHelpers do @moduledoc false - alias Parser.Types.MicE + alias Aprs.Types.MicE @spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil} def get_coordinates(%{data_extended: %MicE{} = mic_e}) do diff --git a/lib/aprs_web/live/map_live/packet_utils.ex b/lib/aprsme_web/live/map_live/packet_utils.ex similarity index 98% rename from lib/aprs_web/live/map_live/packet_utils.ex rename to lib/aprsme_web/live/map_live/packet_utils.ex index 7da4290..511e740 100644 --- a/lib/aprs_web/live/map_live/packet_utils.ex +++ b/lib/aprsme_web/live/map_live/packet_utils.ex @@ -1,10 +1,10 @@ -defmodule AprsWeb.MapLive.PacketUtils do +defmodule AprsmeWeb.MapLive.PacketUtils do @moduledoc """ Shared utilities for extracting and processing packet data in map views. """ - alias AprsWeb.MapLive.MapHelpers - alias AprsWeb.TimeHelpers + alias AprsmeWeb.MapLive.MapHelpers + alias AprsmeWeb.TimeHelpers @doc """ Safely extracts a value from a packet or data_extended map with fallback support. diff --git a/lib/aprs_web/live/packets_live/callsign_view.ex b/lib/aprsme_web/live/packets_live/callsign_view.ex similarity index 94% rename from lib/aprs_web/live/packets_live/callsign_view.ex rename to lib/aprsme_web/live/packets_live/callsign_view.ex index 6fc408a..dfabb07 100644 --- a/lib/aprs_web/live/packets_live/callsign_view.ex +++ b/lib/aprsme_web/live/packets_live/callsign_view.ex @@ -1,14 +1,14 @@ -defmodule AprsWeb.PacketsLive.CallsignView do +defmodule AprsmeWeb.PacketsLive.CallsignView do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view import Ecto.Query - alias Aprs.EncodingUtils - alias Aprs.Packet - alias Aprs.Repo - alias AprsWeb.Endpoint - alias Parser.DeviceParser + alias Aprsme.DeviceParser + alias Aprsme.EncodingUtils + alias Aprsme.Packet + alias Aprsme.Repo + alias AprsmeWeb.Endpoint @impl true def mount(%{"callsign" => callsign}, _session, socket) do @@ -66,7 +66,7 @@ defmodule AprsWeb.PacketsLive.CallsignView do canonical_identifier = if is_binary(device_identifier) do - matched_device = Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier) + matched_device = Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier) if matched_device, do: matched_device.identifier, else: device_identifier else device_identifier @@ -208,7 +208,10 @@ defmodule AprsWeb.PacketsLive.CallsignView do defp enrich_packet_with_device_info(packet) do device_identifier = Map.get(packet, :device_identifier) || Map.get(packet, "device_identifier") - device = if is_binary(device_identifier), do: Aprs.DeviceIdentification.lookup_device_by_identifier(device_identifier) + + device = + if is_binary(device_identifier), do: Aprsme.DeviceIdentification.lookup_device_by_identifier(device_identifier) + model = if device, do: device.model vendor = if device, do: device.vendor diff --git a/lib/aprs_web/live/packets_live/callsign_view.html.heex b/lib/aprsme_web/live/packets_live/callsign_view.html.heex similarity index 100% rename from lib/aprs_web/live/packets_live/callsign_view.html.heex rename to lib/aprsme_web/live/packets_live/callsign_view.html.heex diff --git a/lib/aprs_web/live/packets_live/index.ex b/lib/aprsme_web/live/packets_live/index.ex similarity index 74% rename from lib/aprs_web/live/packets_live/index.ex rename to lib/aprsme_web/live/packets_live/index.ex index 05c88f0..d69938f 100644 --- a/lib/aprs_web/live/packets_live/index.ex +++ b/lib/aprsme_web/live/packets_live/index.ex @@ -1,15 +1,15 @@ -defmodule AprsWeb.PacketsLive.Index do +defmodule AprsmeWeb.PacketsLive.Index do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.EncodingUtils - alias AprsWeb.Endpoint + alias Aprsme.EncodingUtils + alias AprsmeWeb.Endpoint @impl true def mount(_params, _session, socket) do if connected?(socket) do Endpoint.subscribe("aprs_messages") - Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets") + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") end {:ok, assign(socket, :packets, [])} @@ -26,7 +26,7 @@ defmodule AprsWeb.PacketsLive.Index do @impl true def handle_info({:postgres_packet, payload}, socket) do - sanitized_payload = Aprs.EncodingUtils.sanitize_packet(payload) + sanitized_payload = Aprsme.EncodingUtils.sanitize_packet(payload) packets = Enum.take([sanitized_payload | socket.assigns.packets], 100) socket = assign(socket, :packets, packets) {:noreply, socket} diff --git a/lib/aprs_web/live/packets_live/index.html.heex b/lib/aprsme_web/live/packets_live/index.html.heex similarity index 96% rename from lib/aprs_web/live/packets_live/index.html.heex rename to lib/aprsme_web/live/packets_live/index.html.heex index 5a45468..dae0a44 100644 --- a/lib/aprs_web/live/packets_live/index.html.heex +++ b/lib/aprsme_web/live/packets_live/index.html.heex @@ -51,8 +51,8 @@ Map.get(packet, :lat) || if( Map.has_key?(packet, :location) and not is_nil(packet.location) and - function_exported?(Aprs.Packet, :lat, 1), - do: Aprs.Packet.lat(packet), + function_exported?(Aprsme.Packet, :lat, 1), + do: Aprsme.Packet.lat(packet), else: nil ) || case Map.get(packet, :data_extended) do @@ -86,8 +86,8 @@ Map.get(packet, :lon) || if( Map.has_key?(packet, :location) and not is_nil(packet.location) and - function_exported?(Aprs.Packet, :lon, 1), - do: Aprs.Packet.lon(packet), + function_exported?(Aprsme.Packet, :lon, 1), + do: Aprsme.Packet.lon(packet), else: nil ) || case Map.get(packet, :data_extended) do diff --git a/lib/aprs_web/live/status_live/index.ex b/lib/aprsme_web/live/status_live/index.ex similarity index 99% rename from lib/aprs_web/live/status_live/index.ex rename to lib/aprsme_web/live/status_live/index.ex index f87b526..6458ae8 100644 --- a/lib/aprs_web/live/status_live/index.ex +++ b/lib/aprsme_web/live/status_live/index.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.StatusLive.Index do +defmodule AprsmeWeb.StatusLive.Index do @moduledoc """ LiveView for displaying real-time APRS-IS connection status """ - use AprsWeb, :live_view + use AprsmeWeb, :live_view # 30 seconds @refresh_interval 1_000 @@ -327,11 +327,11 @@ defmodule AprsWeb.StatusLive.Index do # Private functions defp get_aprs_status do - Aprs.Is.get_status() + Aprsme.Is.get_status() end defp get_app_version do - :aprs |> Application.spec(:vsn) |> List.to_string() + :aprsme |> Application.spec(:vsn) |> List.to_string() end defp refresh_status(socket) do diff --git a/lib/aprs_web/live/user_confirmation_instructions_live.ex b/lib/aprsme_web/live/user_confirmation_instructions_live.ex similarity index 91% rename from lib/aprs_web/live/user_confirmation_instructions_live.ex rename to lib/aprsme_web/live/user_confirmation_instructions_live.ex index 9fdd6b8..7311c55 100644 --- a/lib/aprs_web/live/user_confirmation_instructions_live.ex +++ b/lib/aprsme_web/live/user_confirmation_instructions_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.UserConfirmationInstructionsLive do +defmodule AprsmeWeb.UserConfirmationInstructionsLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts + alias Aprsme.Accounts def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/user_confirmation_live.ex b/lib/aprsme_web/live/user_confirmation_live.ex similarity index 95% rename from lib/aprs_web/live/user_confirmation_live.ex rename to lib/aprsme_web/live/user_confirmation_live.ex index 3b8f7a6..7727fb3 100644 --- a/lib/aprs_web/live/user_confirmation_live.ex +++ b/lib/aprsme_web/live/user_confirmation_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.UserConfirmationLive do +defmodule AprsmeWeb.UserConfirmationLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts + alias Aprsme.Accounts def render(%{live_action: :edit} = assigns) do ~H""" diff --git a/lib/aprs_web/live/user_forgot_password_live.ex b/lib/aprsme_web/live/user_forgot_password_live.ex similarity index 93% rename from lib/aprs_web/live/user_forgot_password_live.ex rename to lib/aprsme_web/live/user_forgot_password_live.ex index 7705ee7..5831e35 100644 --- a/lib/aprs_web/live/user_forgot_password_live.ex +++ b/lib/aprsme_web/live/user_forgot_password_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.UserForgotPasswordLive do +defmodule AprsmeWeb.UserForgotPasswordLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts + alias Aprsme.Accounts def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/user_login_live.ex b/lib/aprsme_web/live/user_login_live.ex similarity index 92% rename from lib/aprs_web/live/user_login_live.ex rename to lib/aprsme_web/live/user_login_live.ex index 64d0efa..b7f2d31 100644 --- a/lib/aprs_web/live/user_login_live.ex +++ b/lib/aprsme_web/live/user_login_live.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.UserLoginLive do +defmodule AprsmeWeb.UserLoginLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts - alias Aprs.Accounts.User + alias Aprsme.Accounts + alias Aprsme.Accounts.User def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/user_registration_live.ex b/lib/aprsme_web/live/user_registration_live.ex similarity index 94% rename from lib/aprs_web/live/user_registration_live.ex rename to lib/aprsme_web/live/user_registration_live.ex index 3729a39..04da7d0 100644 --- a/lib/aprs_web/live/user_registration_live.ex +++ b/lib/aprsme_web/live/user_registration_live.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.UserRegistrationLive do +defmodule AprsmeWeb.UserRegistrationLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts - alias Aprs.Accounts.User + alias Aprsme.Accounts + alias Aprsme.Accounts.User def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/user_reset_password_live.ex b/lib/aprsme_web/live/user_reset_password_live.ex similarity index 96% rename from lib/aprs_web/live/user_reset_password_live.ex rename to lib/aprsme_web/live/user_reset_password_live.ex index 87bc649..10dbb20 100644 --- a/lib/aprs_web/live/user_reset_password_live.ex +++ b/lib/aprsme_web/live/user_reset_password_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.UserResetPasswordLive do +defmodule AprsmeWeb.UserResetPasswordLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts + alias Aprsme.Accounts def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/user_settings_live.ex b/lib/aprsme_web/live/user_settings_live.ex similarity index 98% rename from lib/aprs_web/live/user_settings_live.ex rename to lib/aprsme_web/live/user_settings_live.ex index f6b6039..75590a9 100644 --- a/lib/aprs_web/live/user_settings_live.ex +++ b/lib/aprsme_web/live/user_settings_live.ex @@ -1,8 +1,8 @@ -defmodule AprsWeb.UserSettingsLive do +defmodule AprsmeWeb.UserSettingsLive do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Accounts + alias Aprsme.Accounts def render(assigns) do ~H""" diff --git a/lib/aprs_web/live/weather_live/callsign_view.ex b/lib/aprsme_web/live/weather_live/callsign_view.ex similarity index 83% rename from lib/aprs_web/live/weather_live/callsign_view.ex rename to lib/aprsme_web/live/weather_live/callsign_view.ex index 4aee465..525a394 100644 --- a/lib/aprs_web/live/weather_live/callsign_view.ex +++ b/lib/aprsme_web/live/weather_live/callsign_view.ex @@ -1,9 +1,9 @@ -defmodule AprsWeb.WeatherLive.CallsignView do +defmodule AprsmeWeb.WeatherLive.CallsignView do @moduledoc false - use AprsWeb, :live_view + use AprsmeWeb, :live_view - alias Aprs.Packets - alias AprsWeb.MapLive.PacketUtils + alias Aprsme.Packets + alias AprsmeWeb.MapLive.PacketUtils @impl true def mount(%{"callsign" => callsign}, _session, socket) do diff --git a/lib/aprs_web/live/weather_live/callsign_view.html.heex b/lib/aprsme_web/live/weather_live/callsign_view.html.heex similarity index 100% rename from lib/aprs_web/live/weather_live/callsign_view.html.heex rename to lib/aprsme_web/live/weather_live/callsign_view.html.heex diff --git a/lib/aprs_web/router.ex b/lib/aprsme_web/router.ex similarity index 77% rename from lib/aprs_web/router.ex rename to lib/aprsme_web/router.ex index a3a43a5..6fabf49 100644 --- a/lib/aprs_web/router.ex +++ b/lib/aprsme_web/router.ex @@ -1,14 +1,14 @@ -defmodule AprsWeb.Router do - use AprsWeb, :router +defmodule AprsmeWeb.Router do + use AprsmeWeb, :router - import AprsWeb.UserAuth + import AprsmeWeb.UserAuth import Phoenix.LiveDashboard.Router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash - plug :put_root_layout, {AprsWeb.Layouts, :root} + plug :put_root_layout, {AprsmeWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers plug :fetch_current_user @@ -18,17 +18,17 @@ defmodule AprsWeb.Router do plug :accepts, ["json"] end - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through :api get "/health", PageController, :health get "/ready", PageController, :ready get "/status.json", PageController, :status_json end - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through :browser - live_dashboard "/dashboard", metrics: AprsWeb.Telemetry + live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry live_session :regular_pages do live "/status", StatusLive.Index, :index @@ -41,21 +41,21 @@ defmodule AprsWeb.Router do live "/info/:callsign", InfoLive.Show, :show end - live_session :map_pages, layout: {AprsWeb.Layouts, :map} do + live_session :map_pages, layout: {AprsmeWeb.Layouts, :map} do live "/", MapLive.Index, :index live "/:callsign", MapLive.CallsignView, :index end end # API v1 routes - scope "/api/v1", AprsWeb.Api.V1, as: :api_v1 do + scope "/api/v1", AprsmeWeb.Api.V1, as: :api_v1 do pipe_through :api get "/callsign/:callsign", CallsignController, :show end # Enable LiveDashboard and Swoosh mailbox preview in development - if Application.compile_env(:aprs, :dev_routes) do + if Application.compile_env(:aprsme, :dev_routes) do # If you want to use the LiveDashboard in production, you should put # it behind authentication and allow only admins to access it. # If your application does not have an admins-only section yet, @@ -65,18 +65,18 @@ defmodule AprsWeb.Router do scope "/dev" do pipe_through :browser - # live_dashboard "/dashboard", metrics: AprsWeb.Telemetry + # live_dashboard "/dashboard", metrics: AprsmeWeb.Telemetry forward "/mailbox", Plug.Swoosh.MailboxPreview end end ## Authentication routes - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] live_session :redirect_if_user_is_authenticated, - on_mount: [{AprsWeb.UserAuth, :redirect_if_user_is_authenticated}] do + on_mount: [{AprsmeWeb.UserAuth, :redirect_if_user_is_authenticated}] do live "/users/register", UserRegistrationLive, :new live "/users/log_in", UserLoginLive, :new live "/users/reset_password", UserForgotPasswordLive, :new @@ -86,23 +86,23 @@ defmodule AprsWeb.Router do post "/users/log_in", UserSessionController, :create end - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through [:browser, :require_authenticated_user] live_session :require_authenticated_user, - on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do + on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do live "/users/settings", UserSettingsLive, :edit live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email end end - scope "/", AprsWeb do + scope "/", AprsmeWeb do pipe_through [:browser] delete "/users/log_out", UserSessionController, :delete live_session :current_user, - on_mount: [{AprsWeb.UserAuth, :mount_current_user}] do + on_mount: [{AprsmeWeb.UserAuth, :mount_current_user}] do live "/users/confirm/:token", UserConfirmationLive, :edit live "/users/confirm", UserConfirmationInstructionsLive, :new end diff --git a/lib/aprs_web/telemetry.ex b/lib/aprsme_web/telemetry.ex similarity index 89% rename from lib/aprs_web/telemetry.ex rename to lib/aprsme_web/telemetry.ex index 4c3ff7b..2f21028 100644 --- a/lib/aprs_web/telemetry.ex +++ b/lib/aprsme_web/telemetry.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.Telemetry do +defmodule AprsmeWeb.Telemetry do @moduledoc false use Supervisor @@ -54,23 +54,23 @@ defmodule AprsWeb.Telemetry do ), # Database Metrics - summary("aprs.repo.query.total_time", + summary("aprsme.repo.query.total_time", unit: {:native, :millisecond}, description: "The sum of the other measurements" ), - summary("aprs.repo.query.decode_time", + summary("aprsme.repo.query.decode_time", unit: {:native, :millisecond}, description: "The time spent decoding the data received from the database" ), - summary("aprs.repo.query.query_time", + summary("aprsme.repo.query.query_time", unit: {:native, :millisecond}, description: "The time spent executing the query" ), - summary("aprs.repo.query.queue_time", + summary("aprsme.repo.query.queue_time", unit: {:native, :millisecond}, description: "The time spent waiting for a database connection" ), - summary("aprs.repo.query.idle_time", + summary("aprsme.repo.query.idle_time", unit: {:native, :millisecond}, description: "The time the connection spent waiting before being checked out for the query" ), @@ -87,7 +87,7 @@ defmodule AprsWeb.Telemetry do [ # A module, function and arguments to be invoked periodically. # This function must call :telemetry.execute/3 and a metric must be added above. - # {AprsWeb, :count_users, []} + # {AprsmeWeb, :count_users, []} ] end end diff --git a/lib/aprs_web/time_helpers.ex b/lib/aprsme_web/time_helpers.ex similarity index 98% rename from lib/aprs_web/time_helpers.ex rename to lib/aprsme_web/time_helpers.ex index d4d7dd8..7da3993 100644 --- a/lib/aprs_web/time_helpers.ex +++ b/lib/aprsme_web/time_helpers.ex @@ -1,4 +1,4 @@ -defmodule AprsWeb.TimeHelpers do +defmodule AprsmeWeb.TimeHelpers do @moduledoc """ Shared helpers for formatting time and dates in the web layer. """ diff --git a/lib/aprs_web/user_auth.ex b/lib/aprsme_web/user_auth.ex similarity index 94% rename from lib/aprs_web/user_auth.ex rename to lib/aprsme_web/user_auth.ex index 7fef8c3..3ced575 100644 --- a/lib/aprs_web/user_auth.ex +++ b/lib/aprsme_web/user_auth.ex @@ -1,11 +1,11 @@ -defmodule AprsWeb.UserAuth do +defmodule AprsmeWeb.UserAuth do @moduledoc false - use AprsWeb, :verified_routes + use AprsmeWeb, :verified_routes import Phoenix.Controller import Plug.Conn - alias Aprs.Accounts + alias Aprsme.Accounts # Make the remember me cookie valid for 60 days. # If you want bump or reduce this value, also change @@ -74,7 +74,7 @@ defmodule AprsWeb.UserAuth do user_token && Accounts.delete_user_session_token(user_token) if live_socket_id = get_session(conn, :live_socket_id) do - AprsWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) + AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) end conn @@ -131,16 +131,16 @@ defmodule AprsWeb.UserAuth do Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate the current_user: - defmodule AprsWeb.PageLive do - use AprsWeb, :live_view + defmodule AprsmeWeb.PageLive do + use AprsmeWeb, :live_view - on_mount {AprsWeb.UserAuth, :mount_current_user} + on_mount {AprsmeWeb.UserAuth, :mount_current_user} ... end Or use the `live_session` of your router to invoke the on_mount callback: - live_session :authenticated, on_mount: [{AprsWeb.UserAuth, :ensure_authenticated}] do + live_session :authenticated, on_mount: [{AprsmeWeb.UserAuth, :ensure_authenticated}] do live "/profile", ProfileLive, :index end """ diff --git a/lib/parser.ex b/lib/parser.ex deleted file mode 100644 index 5a8bc59..0000000 --- a/lib/parser.ex +++ /dev/null @@ -1,970 +0,0 @@ -defmodule Parser do - @moduledoc """ - Main parsing library - """ - alias Aprs.Convert - alias Parser.Item - alias Parser.MicE - alias Parser.Object - alias Parser.PHG - alias Parser.Status - alias Parser.Telemetry - alias Parser.Weather - - # Simple APRS position parsing to replace parse_aprs_position - defp parse_aprs_position(lat, lon) do - # Regex for latitude: 2 deg, 2+ min, 1 dir (N/S) - # Regex for longitude: 3 deg, 2+ min, 1 dir (E/W) - lat_re = ~r/^(\d{2})(\d{2}\.\d+)([NS])$/ - lon_re = ~r/^(\d{3})(\d{2}\.\d+)([EW])$/ - - with [_, lat_deg, lat_min, lat_dir] <- Regex.run(lat_re, lat), - [_, lon_deg, lon_min, lon_dir] <- Regex.run(lon_re, lon) do - lat_val = - Decimal.add(Decimal.new(lat_deg), Decimal.div(Decimal.new(lat_min), Decimal.new("60"))) - - lon_val = - Decimal.add(Decimal.new(lon_deg), Decimal.div(Decimal.new(lon_min), Decimal.new("60"))) - - lat = if lat_dir == "S", do: Decimal.negate(lat_val), else: lat_val - lon = if lon_dir == "W", do: Decimal.negate(lon_val), else: lon_val - %{latitude: lat, longitude: lon} - else - _ -> %{latitude: nil, longitude: nil} - end - end - - @type packet :: %{ - id: String.t(), - sender: String.t(), - path: String.t(), - destination: String.t(), - information_field: String.t(), - data_type: atom(), - base_callsign: String.t(), - ssid: String.t(), - data_extended: map() | nil, - received_at: DateTime.t() - } - - @type parse_result :: {:ok, packet()} | {:error, atom() | String.t()} - - @type position_ambiguity :: 0..4 - - @spec parse(String.t()) :: parse_result() - def parse(message) when is_binary(message) do - do_parse(message) - rescue - _ -> - {:error, :invalid_packet} - end - - def parse(_), do: {:error, :invalid_packet} - - @spec do_parse(String.t()) :: parse_result() - defp do_parse(message) do - with {:ok, [sender, path, data]} <- split_packet(message), - {:ok, callsign_parts} <- parse_callsign(sender), - {:ok, data_type} <- parse_datatype_safe(data), - {:ok, [destination, path]} <- split_path(path), - :ok <- validate_path(path) do - data_trimmed = String.trim(data) - data_without_type = String.slice(data_trimmed, 1..-1//1) - data_extended = parse_data(data_type, destination, data_without_type) - - base_callsign = List.first(callsign_parts) - - ssid = - case List.last(callsign_parts) do - nil -> - nil - - s when is_binary(s) -> - s - - i when is_integer(i) -> - to_string(i) - - _ -> - nil - end - - {:ok, - %{ - id: 16 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower), - sender: sender, - path: path, - destination: destination, - information_field: data_trimmed, - data_type: data_type, - base_callsign: base_callsign, - ssid: ssid, - data_extended: data_extended, - received_at: DateTime.truncate(DateTime.utc_now(), :microsecond) - }} - end - rescue - _ -> - {:error, :invalid_packet} - end - - # Validate path for too many components - def validate_path(path) when is_binary(path) and path != "" do - if length(String.split(path, ",")) > 8 do - {:error, "Too many path components"} - else - :ok - end - end - - def validate_path(_), do: :ok - - # Safely split packet into components - @spec split_packet(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} - def split_packet(message) do - split_packet_parts(String.split(message, [">", ":"], parts: 3)) - end - - @spec split_packet_parts([String.t()]) :: {:ok, [String.t()]} | {:error, String.t()} - defp split_packet_parts([sender, path, data]) when byte_size(sender) > 0 and byte_size(path) > 0 do - {:ok, [sender, path, data]} - end - - @spec split_packet_parts(list()) :: {:error, String.t()} - defp split_packet_parts(_), do: {:error, "Invalid packet format"} - - # Safely split path into destination and digipeater path - @spec split_path(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} - def split_path(path) when is_binary(path) do - split = String.split(path, ",", parts: 2) - split_path_parts(split) - end - - defp split_path_parts([destination, digi_path]), do: {:ok, [destination, digi_path]} - defp split_path_parts([destination]), do: {:ok, [destination, ""]} - defp split_path_parts(_), do: {:error, "Invalid path format"} - - # Safe version of parse_datatype that returns {:ok, type} or {:error, reason} - @spec parse_datatype_safe(String.t()) :: {:ok, atom()} | {:error, String.t()} - def parse_datatype_safe(""), do: {:error, "Empty data"} - def parse_datatype_safe(data), do: {:ok, parse_datatype(data)} - - @spec parse_callsign(String.t()) :: {:ok, [String.t()]} | {:error, String.t()} - def parse_callsign(callsign) do - case Parser.AX25.parse_callsign(callsign) do - {:ok, {base, ssid}} -> {:ok, [base, ssid]} - {:error, reason} -> {:error, reason} - end - end - - # One of the nutty exceptions in the APRS protocol has to do with this - # data type indicator. It's usually the first character of the message. - # However, in some rare cases, the ! indicator can be anywhere in the - # first 40 characters of the message. I'm not going to deal with that - # weird case right now. It seems like its for a specific type of old - # TNC hardware that probably doesn't even exist anymore. - @spec parse_datatype(String.t()) :: atom() - def parse_datatype(<<":", _::binary>>), do: :message - def parse_datatype(<<">", _::binary>>), do: :status - def parse_datatype("!" <> _), do: :position - def parse_datatype("/" <> _), do: :timestamped_position - def parse_datatype("=" <> _), do: :position_with_message - def parse_datatype("@" <> _), do: :timestamped_position_with_message - def parse_datatype(";" <> _), do: :object - def parse_datatype("`" <> _), do: :mic_e_old - def parse_datatype("'" <> _), do: :mic_e_old - def parse_datatype("_" <> _), do: :weather - def parse_datatype("T" <> _), do: :telemetry - def parse_datatype("$" <> _), do: :raw_gps_ultimeter - def parse_datatype("<" <> _), do: :station_capabilities - def parse_datatype("?" <> _), do: :query - def parse_datatype("{" <> _), do: :user_defined - def parse_datatype("}" <> _), do: :third_party_traffic - def parse_datatype("%" <> _), do: :item - def parse_datatype(")" <> _), do: :item - def parse_datatype("*" <> _), do: :peet_logging - def parse_datatype("," <> _), do: :invalid_test_data - def parse_datatype("#DFS" <> _), do: :df_report - def parse_datatype("#PHG" <> _), do: :phg_data - def parse_datatype("#" <> _), do: :phg_data - def parse_datatype(_), do: :unknown_datatype - - @spec parse_data(atom(), String.t(), String.t()) :: map() | nil - def parse_data(:mic_e, destination, data), do: MicE.parse(data, destination) - def parse_data(:mic_e_old, destination, data), do: MicE.parse(data, destination) - def parse_data(:object, _destination, data), do: Object.parse(data) - def parse_data(:item, _destination, data), do: Item.parse(data) - def parse_data(:weather, _destination, data), do: Weather.parse(data) - def parse_data(:telemetry, _destination, data), do: Telemetry.parse(data) - def parse_data(:status, _destination, data), do: Status.parse(data) - def parse_data(:phg_data, _destination, data), do: PHG.parse(data) - - def parse_data(:peet_logging, _destination, data), do: Parser.SpecialDataHelpers.parse_peet_logging(data) - - def parse_data(:invalid_test_data, _destination, data), do: Parser.SpecialDataHelpers.parse_invalid_test_data(data) - - def parse_data(:raw_gps_ultimeter, _destination, data) do - case Parser.NMEAHelpers.parse_nmea_sentence(data) do - {:error, error} -> - %{ - data_type: :raw_gps_ultimeter, - error: error, - nmea_type: nil, - raw_data: data, - latitude: nil, - longitude: nil - } - end - end - - def parse_data(:df_report, _destination, data) do - if String.starts_with?(data, "DFS") and byte_size(data) >= 7 do - <<"DFS", s, h, g, d, rest::binary>> = data - - %{ - df_strength: Parser.PHGHelpers.parse_df_strength(s), - height: Parser.PHGHelpers.parse_phg_height(h), - gain: Parser.PHGHelpers.parse_phg_gain(g), - directivity: Parser.PHGHelpers.parse_phg_directivity(d), - comment: rest, - data_type: :df_report - } - else - %{ - df_data: data, - data_type: :df_report - } - end - end - - def parse_data(:user_defined, _destination, data), do: parse_user_defined(data) - def parse_data(:third_party_traffic, _destination, data), do: parse_third_party_traffic(data) - - def parse_data(:message, _destination, data) do - case Regex.run(~r/^:([^:]+):(.+?)(\{(\d+)\})?$/s, data) do - [_, addressee, message_text, _full_ack, message_number] -> - %{ - data_type: :message, - addressee: String.trim(addressee), - message_text: String.trim(message_text), - message_number: message_number - } - - [_, addressee, message_text] -> - %{ - data_type: :message, - addressee: String.trim(addressee), - message_text: String.trim(message_text) - } - - _ -> - nil - end - end - - def parse_data(:position, destination, <<"!", rest::binary>>) do - parse_data(:position, destination, rest) - end - - def parse_data(:position, _destination, <<"/", _::binary>> = data) do - result = parse_position_without_timestamp(data) - if result.data_type == :malformed_position, do: result, else: %{result | data_type: :position} - end - - def parse_data(:position, _destination, data) do - result = parse_position_without_timestamp(data) - if result.data_type == :malformed_position, do: result, else: %{result | data_type: :position} - end - - def parse_data(:position_with_message, _destination, data) do - result = parse_position_with_message_without_timestamp(data) - %{result | data_type: :position} - end - - def parse_data(:timestamped_position, _destination, data) do - parse_position_with_timestamp(false, data, :timestamped_position) - end - - def parse_data(:timestamped_position_with_message, _destination, data) do - case data do - <> -> - weather_start = String.starts_with?(rest, "_") - - if weather_start do - result = - Parser.parse_position_with_datetime_and_weather( - true, - time, - latitude, - sym_table_id, - longitude, - symbol_code, - rest - ) - - add_has_location(result) - else - result = parse_position_with_timestamp(true, data, :timestamped_position_with_message) - add_has_location(result) - end - - _ -> - result = parse_position_with_timestamp(true, data, :timestamped_position_with_message) - add_has_location(result) - end - end - - def parse_data(:station_capabilities, _destination, data), do: parse_station_capabilities(data) - def parse_data(:query, _destination, data), do: parse_query(data) - - # Catch-all for unknown or unsupported types - def parse_data(_type, _destination, _data), do: nil - - defp add_has_location(result) do - Map.put( - result, - :has_location, - (is_number(result[:latitude]) or is_struct(result[:latitude], Decimal)) and - (is_number(result[:longitude]) or is_struct(result[:longitude], Decimal)) - ) - end - - @spec parse_position_with_datetime_and_weather( - boolean(), - binary(), - binary(), - binary(), - binary(), - binary(), - binary() - ) :: map() - def parse_position_with_datetime_and_weather( - aprs_messaging?, - time, - latitude, - sym_table_id, - longitude, - symbol_code, - weather_report - ) do - pos = parse_aprs_position(latitude, longitude) - weather_data = Parser.Weather.parse_weather_data(weather_report) - - %{ - latitude: pos.latitude, - longitude: pos.longitude, - timestamp: time, - symbol_table_id: sym_table_id, - symbol_code: symbol_code, - weather: weather_data, - data_type: :position_with_datetime_and_weather, - aprs_messaging?: aprs_messaging? - } - end - - @spec decode_compressed_position(binary()) :: map() - def decode_compressed_position( - <<"/", latitude::binary-size(4), longitude::binary-size(4), symbol_code::binary-size(1), _cs::binary-size(2), - _compression_type::binary-size(2), _rest::binary>> - ) do - lat = Parser.CompressedPositionHelpers.convert_to_base91(latitude) - lon = Parser.CompressedPositionHelpers.convert_to_base91(longitude) - - %{ - latitude: lat, - longitude: lon, - symbol_code: symbol_code - } - end - - @spec convert_to_base91(binary()) :: integer() - def convert_to_base91(<>) do - [v1, v2, v3, v4] = to_charlist(value) - (v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4 - end - - # Helper to extract course and speed from comment field (e.g., "/123/045" or "123/045") - @spec extract_course_and_speed(String.t()) :: {integer() | nil, float() | nil} - defp extract_course_and_speed(comment) do - # Match "/123/045" or "123/045" at the start of the comment - case Regex.run(~r"/?(\d{3})/(\d{3})", comment) do - [_, course, speed] -> {String.to_integer(course), String.to_integer(speed) * 1.0} - _ -> {nil, nil} - end - end - - # Patch parse_position_without_timestamp to include course/speed - @spec parse_position_without_timestamp(String.t()) :: map() - def parse_position_without_timestamp(position_data) do - case position_data do - <> -> - parse_position_uncompressed(latitude, sym_table_id, longitude, symbol_code, comment) - - <> -> - parse_position_short_uncompressed(latitude, sym_table_id, longitude) - - <<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1), - cs::binary-size(2), compression_type::binary-size(1), comment::binary>> -> - parse_position_compressed( - latitude_compressed, - longitude_compressed, - symbol_code, - cs, - compression_type, - comment - ) - - _ -> - parse_position_malformed(position_data) - end - end - - defp parse_position_uncompressed(latitude, sym_table_id, longitude, symbol_code, comment) do - %{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude) - ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude) - dao_data = parse_dao_extension(comment) - {course, speed} = extract_course_and_speed(comment) - - has_position = - (is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal)) - - base_map = %{ - latitude: lat, - longitude: lon, - timestamp: nil, - symbol_table_id: sym_table_id, - symbol_code: symbol_code, - comment: comment, - data_type: :position, - aprs_messaging?: false, - compressed?: false, - position_ambiguity: ambiguity, - dao: dao_data, - course: course, - speed: speed, - has_position: has_position - } - - if sym_table_id == "/" and symbol_code == "_" do - weather_map = Parser.Weather.parse_weather_data(comment) - Map.merge(base_map, weather_map) - else - base_map - end - end - - defp parse_position_short_uncompressed(latitude, sym_table_id, longitude) do - %{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude) - ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude) - - has_position = - (is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal)) - - %{ - latitude: lat, - longitude: lon, - timestamp: nil, - symbol_table_id: sym_table_id, - symbol_code: "_", - data_type: :position, - aprs_messaging?: false, - compressed?: false, - position_ambiguity: ambiguity, - dao: nil, - course: nil, - speed: nil, - has_position: has_position - } - end - - defp parse_position_compressed(latitude_compressed, longitude_compressed, symbol_code, cs, compression_type, comment) do - converted_lat = Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed) - converted_lon = Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed) - compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs) - ambiguity = Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type) - - has_position = - (is_number(converted_lat) or is_struct(converted_lat, Decimal)) and - (is_number(converted_lon) or is_struct(converted_lon, Decimal)) - - base_data = %{ - latitude: converted_lat, - longitude: converted_lon, - symbol_table_id: "/", - symbol_code: symbol_code, - comment: comment, - position_format: :compressed, - compression_type: compression_type, - data_type: :position, - compressed?: true, - position_ambiguity: ambiguity, - dao: nil, - has_position: has_position - } - - Map.merge(base_data, compressed_cs) - rescue - _e -> - %{ - latitude: nil, - longitude: nil, - symbol_table_id: "/", - symbol_code: symbol_code, - comment: comment, - position_format: :compressed, - compression_type: compression_type, - data_type: :position, - compressed?: true, - position_ambiguity: Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type), - dao: nil, - course: nil, - speed: nil, - has_position: false - } - end - - defp parse_position_malformed(position_data) do - %{ - latitude: nil, - longitude: nil, - timestamp: nil, - symbol_table_id: nil, - symbol_code: nil, - data_type: :malformed_position, - aprs_messaging?: false, - compressed?: false, - comment: String.trim(position_data), - dao: nil, - course: nil, - speed: nil, - has_position: false - } - end - - # Patch parse_position_with_message_without_timestamp to propagate course/speed - @spec parse_position_with_message_without_timestamp(String.t()) :: map() - def parse_position_with_message_without_timestamp(position_data) do - result = parse_position_without_timestamp(position_data) - Map.put(result, :aprs_messaging?, true) - end - - # Patch parse_position_with_timestamp to extract course/speed from comment - @spec parse_position_with_timestamp(boolean(), binary(), atom()) :: map() - def parse_position_with_timestamp( - aprs_messaging?, - <>, - _data_type - ) do - case Parser.UtilityHelpers.validate_position_data(latitude, longitude) do - {:ok, {lat, lon}} -> - position = parse_aprs_position(latitude, longitude) - {course, speed} = extract_course_and_speed(comment) - - %{ - latitude: lat, - longitude: lon, - position: position, - time: Parser.UtilityHelpers.validate_timestamp(time), - symbol_table_id: sym_table_id, - symbol_code: symbol_code, - comment: comment, - data_type: :position, - aprs_messaging?: aprs_messaging?, - compressed?: false, - course: course, - speed: speed - } - - _ -> - # Fallback: try to extract lat/lon using regex if binary pattern match fails - regex = - ~r/^(?