This is a web application written using the Phoenix web framework. ## Elixir guidelines - Elixir lists **do not support index based access via the access syntax** **Never do this (invalid)**: i = 0 mylist = ["blue", "green"] mylist[i] Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: i = 0 mylist = ["blue", "green"] Enum.at(mylist, i) - Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals. **Never do this (invalid)**: <%= if condition do %> ... <% else if other_condition %> ... <% end %> Instead **always** do this: <%= cond do %> <% condition -> %> ... <% condition2 -> %> ... <% true -> %> ... <% end %> - Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: # INVALID: we are rebinding inside the `if` and the result never gets assigned if connected?(socket) do socket = assign(socket, :val, val) end # VALID: we rebind the result of the `if` to a new variable socket = if connected?(socket) do assign(socket, :val, val) end - Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}` - **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors - **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets - Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) - Don't use `String.to_atom/1` on user input (memory leak risk) - Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards - Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)` - Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option ## Mix guidelines - Read the docs and options before using tasks (by using `mix help task_name`) - To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed` - `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason ## Phoenix guidelines - Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes. - You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie: scope "/admin", AppWeb.Admin do pipe_through :browser live "/users", UserLive, :index end the UserLive route would point to the `AppWeb.Admin.UserLive` module - `Phoenix.View` no longer is needed or included with Phoenix, don't use it ## Ecto Guidelines - **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email` - Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` - `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` - `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed - You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields - Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct ## Phoenix HTML guidelines - Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` - **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated - When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` - **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`) - For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) - HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `
` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:

      
        let obj = {key: "val"}
      

  Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax

- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:

      Text

  and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)

  and **never** do this, since it's invalid (note the missing `[` and `]`):

       ...
      => Raises compile syntax error on invalid HEEx attr syntax

- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.

  **Always** do this:

      
{@my_assign} <%= if @some_block_condition do %> {@another_assign} <% end %>
and **Never** do this – the program will terminate with a syntax error: <%!-- THIS IS INVALID NEVER EVER DO THIS --%>
{if @invalid_block_construct do} {end}
## Phoenix LiveView guidelines - **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews - **Avoid LiveComponent's** unless you have a strong, specific need for them - LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive` - Remember anytime you use `phx-hook="MyHook"` and that js hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute - **Never** write embedded ` tags within templates** ### UI/UX & design guidelines - **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles - Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions) - Ensure **clean typography, spacing, and layout balance** for a refined, premium look - Focus on **delightful details** like hover effects, loading states, and smooth page transitions ## Authentication - **Always** handle authentication flow at the router level with proper redirects - **Always** be mindful of where to place routes. `phx.gen.auth` creates multiple router plugs and `live_session` scopes: - A `live_session :current_user` scope - For routes that need the current user but don't require authentication - A `live_session :require_authenticated_user` scope - For routes that require authentication - In both cases, a `@current_scope` is assigned to the Plug connection and LiveView socket - **Always let the user know in which router scopes, `live_session`, and pipeline you are placing the route, AND SAY WHY** - `phx.gen.auth` assigns the `current_scope` assign - it **does not assign the `current_user` assign**. - To derive/access `current_user`, **always use the `current_scope.user` assign**, never use **`@current_user`** in templates or LiveViews - **Never** duplicate `live_session` names. A `live_session :current_user` can only be defined __once__ in the router, so all routes for the `live_session :current_user` must be grouped in a single block - Anytime you hit `current_scope` errors or the logged in session isn't displaying the right content, **always double check the router and ensure you are using the correct `live_session` described below** ### Routes that require authentication LiveViews that require login should **always be placed inside the __existing__ `live_session :require_authenticated_user` block**: scope "/", AppWeb do pipe_through [:browser, :require_authenticated_user] live_session :require_authenticated_user, on_mount: [{AppWeb.UserAuth, :ensure_authenticated}] do # phx.gen.auth generated routes live "/users/settings", UserSettingsLive, :edit live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email # our own routes that require logged in user live "/", MyLiveThatRequiresAuth, :index end end ### Routes that work with or without authentication LiveViews that can work with or without authentication, **always use the __existing__ `:current_user` scope**, ie: scope "/", MyAppWeb do pipe_through [:browser] live_session :current_user, on_mount: [{MyAppWeb.UserAuth, :mount_current_scope}] do # our own routes that work with or without authentication live "/", PublicLive end end ## Prerequisites Before setting up the project, ensure you have the following installed: - Elixir 1.17+ - Erlang/OTP - PostgreSQL with PostGIS extension ## Project Overview This is an Elixir Phoenix LiveView application that serves as a real-time APRS (Automatic Packet Reporting System) tracker and visualizer. It connects to the APRS-IS network to receive live amateur radio packets and displays them on an interactive map interface. ## Development Commands ### Setup - `mix setup` - Complete project setup (deps.get + ecto.setup) - `mix deps.get` - Install dependencies - `mix ecto.setup` - Create database, run migrations, and seed data - `mix ecto.reset` - Drop and recreate database - `mix phx.server` - Start Phoenix server (http://localhost:4000) - `iex -S mix phx.server` - Start server in interactive Elixir shell ### Testing - `mix test` - Run full test suite - `mix test --stale` - Run only tests affected by code changes - `mix test.watch` - Continuous testing with file watching - `mix test --cover` - Generate test coverage reports ### Code Quality - `mix format` - Format code according to .formatter.exs - `mix credo` - Static code analysis and style checking - `mix dialyzer` - Static type analysis (must run and fix errors/warnings) - `mix sobelow` - Security vulnerability scanning - **CRITICAL**: ALWAYS run `mix format` BEFORE committing - never commit unformatted code - **MANDATORY**: Run `mix compile --warnings-as-errors` and ensure it passes before considering any task complete ### Assets (No Node.js Required) - `mix assets.deploy` - Build and minify frontend assets (Tailwind CSS + ESBuild) - Phoenix uses standalone ESBuild and Tailwind binaries - no npm/yarn needed - JavaScript bundling handled by ESBuild - CSS compilation handled by Tailwind CLI ## Architecture ### Core Components - **Aprsme.AprsIsConnection** - TCP connection to APRS-IS network with reconnection logic - **Aprsme.PacketConsumer** - Processes incoming APRS packets using GenStage pipeline - **Aprsme.Packet** - Database schema for APRS packets with PostGIS geographic data - **AprsmeWeb.MapLive.Index** - Main real-time map interface using Phoenix LiveView - **Aprsme.Workers.PacketCleanupWorker** - Oban background job for data cleanup ### Data Flow 1. APRS-IS connection receives packets via TCP 2. PacketConsumer processes packets through GenStage pipeline 3. Packets stored in PostgreSQL with PostGIS geographic indexing 4. LiveView broadcasts real-time updates to connected clients via PubSub 5. Background workers handle cleanup and maintenance tasks ### Key Dependencies - Phoenix LiveView for real-time UI without JavaScript - PostGIS for geographic data storage and spatial queries - Oban for background job processing - GenStage for packet processing pipelines - Tailwind CSS + ESBuild for frontend assets (no Node.js) ## Test-Driven Development **MANDATORY**: Follow strict test-driven development (TDD) practices: **CRITICAL**: When a new feature or bug is introduced, ALWAYS write a test for it FIRST, then write the code to satisfy the test. This is non-negotiable. 1. **Red Phase**: Write failing tests first before implementing any functionality 2. **Green Phase**: Write minimal code to make tests pass 3. **Refactor Phase**: Improve code while keeping tests green ### TDD Workflow - **ALWAYS** write tests before implementing new features or fixing bugs - When addressing a bug, first write a test that reproduces the bug (it should fail) - When adding a feature, first write tests that define the expected behavior - Start with the simplest failing test case - Write only enough code to make the test pass - Refactor with confidence knowing tests will catch regressions - Run `mix test` frequently during development - Use `mix test.watch` for continuous feedback ### Testing Patterns Tests use comprehensive mocking to prevent external connections: - APRS-IS connections are mocked in test environment - Database uses sandbox mode for isolation - External API calls mocked with Mox library - Write unit tests for business logic, integration tests for workflows - Test edge cases and error conditions thoroughly - Maintain high test coverage with `mix test --cover` ## Code Style Guidelines - **CRITICAL**: Never write production code without tests first - Use LiveView for UI interactions, minimize JavaScript - Prefer pattern matching over if/case statements - Follow idiomatic Elixir conventions - **CRITICAL**: ALWAYS run `mix format` BEFORE committing - this is non-negotiable - Address any compiler warnings - Run `mix dialyzer` and fix all errors/warnings - **MANDATORY**: Run `mix compile --warnings-as-errors` and ensure it passes before considering any task complete - Use function composition over nested conditionals - Write descriptive test names that explain behavior ### Pre-Commit Checklist **CRITICAL**: NEVER commit or push code with syntax errors or compilation failures. Always validate before committing. **MANDATORY PRE-COMMIT STEPS** - Must be executed in this exact order before EVERY git commit: 1. **`mix format`** - ALWAYS run this FIRST before any git operations - This formats all Elixir code according to project standards - **NEVER skip this step** - unformatted code should never be committed 2. **`mix compile --warnings-as-errors`** - ensure no warnings or compilation errors - Must pass with zero warnings and zero errors 3. **`mix test`** - ensure all tests pass (at minimum, ensure no syntax/compilation errors) - At minimum verify no compilation failures even if some tests fail 4. **MANDATORY**: If any step fails, fix the issues before proceeding 5. Only after ALL checks pass should you commit and push your changes **NEVER PUSH BROKEN CODE**: Syntax errors, compilation failures, or basic test failures should be fixed immediately before any git operations. Pushing broken code breaks CI/CD pipelines and wastes deployment resources. **REMEMBER**: Always run `mix format` before every commit - this is non-negotiable and must become automatic habit. ## Git Commit Messages When creating git commits: - Write clear, concise commit messages following conventional commit format (e.g., `feat:`, `fix:`, `refactor:`, `docs:`) - Focus on the "why" rather than the "what" in commit messages - **DO NOT** add "Generated with Claude Code" or similar attribution to commit messages - **DO NOT** add Co-Authored-By lines for Claude - Keep commit messages professional and focused solely on the code changes ## Important Documentation Updates - **MANDATORY**: Whenever you implement improvements or changes to the system: 1. Update `/CHANGELOG.md` with: - Add new entries under `[Unreleased]` section - Use categories: Added, Changed, Fixed, Removed - Be specific and user-focused in descriptions 2. Update `/docs/improvement-todos.md` with: - Mark completed items as done with the implementation date - Add any new improvements discovered during implementation - Update priority levels based on new insights - Document any technical decisions or trade-offs made - This ensures continuity across sessions and helps track progress on system improvements ## Web Testing - **MANDATORY**: When viewing any website or web application, always use Puppeteer to take screenshots and interact with the page - Use `mcp__puppeteer__puppeteer_navigate`, `mcp__puppeteer__puppeteer_screenshot`, and other Puppeteer tools - This ensures accurate visual feedback and proper testing of the user interface ## Deployment The application supports Kubernetes deployment with manifests in `k8s/` directory and GitHub Actions CI/CD pipeline. Database migrations run automatically via init containers. ### Infrastructure The application runs on a highly available infrastructure: - **Kubernetes**: k3s cluster deployed across 3 VMs - **Virtualization**: Proxmox VE hosting the VMs - **Hardware**: 3 Intel N100 nodes, each with: - 32GB RAM - 1TB SSD storage - Low power consumption (~15W per node) - **Distribution**: VMs spread across physical nodes for hardware redundancy ### Kubernetes Commands The app is deployed in a k3s cluster with the following structure: - **App name**: `aprs` - **Namespace**: `aprs` - **Deployment**: StatefulSet with 2-3 replicas - **Manifests**: Located in `~/dev/infra/clusters/aprs/` Common kubectl commands for debugging: ```bash # Check pod status kubectl get pods -n aprs # Get logs from the app kubectl logs -f deployment/aprs -n aprs # Get logs from a specific pod kubectl logs -n aprs # Describe pod for events and details kubectl describe pod -n aprs # Restart the statefulset kubectl rollout restart statefulset/aprs -n aprs # Check statefulset status kubectl rollout status statefulset/aprs -n aprs # Execute commands in the pod (StatefulSet) kubectl exec -it aprs-0 -n aprs -- /app/bin/aprsme remote # Check cluster membership kubectl exec -it -n aprs -- /app/bin/aprsme eval "Node.list()" # Check leader status kubectl exec -it -n aprs -- /app/bin/aprsme eval "Aprsme.Cluster.LeaderElection.is_leader?()" ``` ### Clustering Architecture The application uses distributed Erlang clustering to ensure only one APRS-IS connection across multiple replicas: 1. **StatefulSet Deployment**: - Uses Kubernetes StatefulSet for stable pod names (aprs-0, aprs-1, etc.) - Headless service provides DNS entries for each pod - Stable network identities enable Erlang distribution 2. **Leader Election**: Uses `:global` registry for distributed leader election - Only the elected leader maintains the APRS-IS connection - Automatic failover when leader goes down - Leader election managed by `Aprsme.Cluster.LeaderElection` 3. **Connection Management**: - `Aprsme.Cluster.ConnectionManager` starts/stops APRS-IS based on leadership - Uses `DynamicSupervisor` to manage connection lifecycle - Prevents duplicate connections and packet processing 4. **Cluster Configuration**: - Uses `libcluster` with Kubernetes.DNS strategy - Automatic node discovery via headless service - Erlang cookie configured via RELEASE_COOKIE environment variable - Environment variables: - `CLUSTER_ENABLED=true` - Enables clustering - `RELEASE_NODE` - Erlang node name - `RELEASE_COOKIE` - Erlang distribution cookie 4. **Deployment**: - Default replicas: 3 (configurable in `aprs-deployment.yaml`) - Only leader processes APRS packets - All nodes serve web traffic ## 1Password Integration When working with passwords and secrets: - **ALWAYS** use 1Password account ID: `YOOATCZZSVGH7AD6VABUVPORLI` - Store all passwords, API keys, and secrets in 1Password - Use the `op` CLI tool for programmatic access - Never hardcode passwords in configuration files