This is a web application written using the Phoenix web framework. ## Mobile API The application provides a real-time WebSocket API for mobile clients (iOS/Android): - **Endpoint**: `wss://aprs.me/mobile/websocket` - **Channel**: `mobile:packets` - **Documentation**: See `docs/mobile-api.md` for complete API reference - **Implementation**: `lib/aprsme_web/channels/mobile_channel.ex` **Key Features:** - Real-time streaming of APRS packets - Geographic bounds filtering (only receive packets in viewport) - Dynamic bounds updates (follow user as they pan/zoom) - Efficient - leverages existing StreamingPacketsPubSub infrastructure ## 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 - **Always run mix format after changing any Elixir files** ## Browser / UI Verification - **Always use Tidewave's `browser_eval` tool** for browser interactions and UI verification - **Never use Puppeteer** (`mcp__puppeteer__*` tools) — Tidewave's browser is shared with the user - Use `browser.snapshot()` to inspect the DOM, `browser.click()` and `browser.fill()` to interact ## Git Commit Guidelines - **Never** add "Co-Authored-By: Claude" or similar AI co-authorship tags to commits - Commits should appear as authored solely by the user (Graham) - Keep commit messages clear and concise, describing what changed and why