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 SpatialPubSub 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