aprs.me/CLAUDE.md
Graham McIntire 4804bd16be
Add distributed Erlang clustering for single APRS-IS connection
Implement leader election to ensure only one APRS-IS connection across
multiple Kubernetes replicas. This prevents duplicate packet processing
and respects APRS-IS usage policies.

Key changes:
- Add leader election using :global registry
- Create connection manager for dynamic APRS-IS management
- Implement packet distribution from leader to all nodes
- Add Kubernetes headless service for node discovery
- Configure DNS-based clustering with libcluster
- Update deployment to support 3 replicas with clustering

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-25 10:30:29 -05:00

7.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Prerequisites

Before setting up the project, ensure you have the following installed:

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 archive.install hex mix_gleam --force - Install mix_gleam archive (required first step)
  • mix setup - Complete project setup (deps.get + ecto.setup + gleam compilation)
  • 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)

  • mix assets.deploy - Build and minify frontend assets (Tailwind CSS + ESBuild)
  • Phoenix uses ESBuild directly without Node.js package managers

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)
  • Gleam for additional type-safe modules (requires mix_gleam archive)

Test-Driven Development

MANDATORY: Follow strict test-driven development (TDD) practices:

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

  1. Run mix format - ALWAYS do this first
  2. Run mix compile --warnings-as-errors - ensure no warnings
  3. Run mix test - ensure all tests pass
  4. Only then commit and push your changes

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.

Kubernetes Commands

The app is deployed in a k3s cluster with the following structure:

  • App name: aprs
  • Namespace: aprs
  • K8s manifests location: ~/dev/infra/clusters/aprs/

Common kubectl commands for debugging:

# 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 <pod-name> -n aprs

# Describe pod for events and details
kubectl describe pod <pod-name> -n aprs

# Restart the deployment
kubectl rollout restart deployment/aprs -n aprs

# Check deployment status
kubectl rollout status deployment/aprs -n aprs

# Execute commands in the pod
kubectl exec -it deployment/aprs -n aprs -- /app/bin/aprsme remote

# Check cluster membership
kubectl exec -it <pod-name> -n aprs -- /app/bin/aprsme eval "Node.list()"

# Check leader status
kubectl exec -it <pod-name> -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. 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
  2. Connection Management:

    • Aprsme.Cluster.ConnectionManager starts/stops APRS-IS based on leadership
    • Uses DynamicSupervisor to manage connection lifecycle
    • Prevents duplicate connections and packet processing
  3. Kubernetes Configuration:

    • Headless service (aprs-headless) for node discovery
    • DNS-based clustering via libcluster
    • 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