towerops/CLAUDE.md

11 KiB

CLAUDE.md

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

CRITICAL: Read AGENTS.md First

Before starting any work in this repository, you MUST read AGENTS.md in the project root.

  • AGENTS.md contains comprehensive Phoenix/LiveView/Elixir guidelines that are mandatory for this project
  • These guidelines take precedence over general development practices when there's a conflict
  • Always read AGENTS.md at the start of a new conversation or when resuming work
  • Follow the project-specific patterns, conventions, and constraints documented there

Project Overview

Towerops is a Phoenix 1.8 web application built with Elixir, using Ecto for database operations (PostgreSQL), LiveView for real-time interactions, and Tailwind CSS v4 for styling.

Data Model Relationships

┌─────────────┐
│    User     │
│ (Accounts)  │
└──────┬──────┘
       │ owns/member of (many-to-many)
       │
       ↓
┌──────────────────────────────────────────────────────┐
│                   Organization                        │
│                  (Organizations)                      │
└────┬────────────┬─────────────┬──────────────────────┘
     │            │             │
     │ has many   │ has many    │ has many
     │            │             │
     ↓            ↓             ↓
┌─────────┐  ┌──────────┐  ┌────────────────┐
│  Site   │  │Equipment │  │  AgentToken    │
│ (Sites) │  │(Equipment│  │   (Agents)     │
└────┬────┘  └────┬─────┘  └────────┬───────┘
     │            │                  │
     │ has many   │ has one          │ assigned via
     │            │                  │
     ↓            ↓                  ↓
┌──────────┐ ┌──────────────┐  ┌──────────────────┐
│Equipment │ │  SNMPDevice  │  │ AgentAssignment  │◀──┐
│(Equipment│ │(Snmp.Devices)│  │    (Agents)      │   │
└────┬─────┘ └──────────────┘  └──────────────────┘   │
     │                                  │               │
     │ has many                         └───────────────┘
     ├────────────────┬─────────────────┘
     │                │               assigns equipment
     ↓                ↓                  to agent
┌──────────────┐ ┌─────────┐
│MonitoringCheck│ │ Alert   │
│ (Monitoring) │ │ (Alerts)│
└──────────────┘ └─────────┘

Key Relationships:
- User can own/belong to multiple Organizations
- Organization has default_agent_token_id (optional)
- Equipment belongs to both Site and Organization (denormalized)
- Equipment can be assigned to one AgentToken via AgentAssignment
- Equipment has one SNMPDevice with Sensors and Interfaces
- Equipment has many MonitoringChecks (polling results)
- Equipment has many Alerts (equipment_down, equipment_up)
- AgentToken authenticates remote agents for local SNMP polling

Note to AI assistants: When you make changes to the data model (migrations, schema additions, new contexts), update this diagram to reflect the current state.

Essential Commands

Setup and Development

  • mix setup - Install dependencies, create/migrate database, setup and build assets
  • mix phx.server - Start the Phoenix server (accessible at http://localhost:4000)
  • iex -S mix phx.server - Start server with interactive Elixir shell

Testing and Quality

  • mix test - Run all tests (automatically creates and migrates test database)
  • mix test test/path/to/specific_test.exs - Run a specific test file
  • mix test --failed - Re-run only previously failed tests
  • mix test --cover - Run tests with coverage report (generates HTML report in cover/ directory)
  • mix precommit - Run before committing: compiles with warnings as errors, unlocks unused deps, formats code, and runs tests

Coverage Target: 90% minimum (currently configured threshold)

  • Coverage reports show line-by-line coverage in cover/ directory
  • View detailed HTML report: open cover/modules_*.html
  • Current status: 60.40% overall (as of Jan 12, 2026, 6:35 PM)
  • Recent improvements (Session 1):
    • Towerops.Snmp: 10.87% → 97.83% (+87%)
    • Towerops.Snmp.Profiles.NetSnmp: 56.79% → 92.59% (+35.8%)
    • Towerops.Monitoring.Supervisor: 21.21% → 63.64% (+42%)
    • Towerops.Snmp.Profiles.Cisco: 20.22% → 57.30% (+37%)
    • ToweropsWeb.EquipmentLive.Show: 35.51% → 46.38% (+10.87%)

Coverage Notes:

  • Protobuf-generated modules (Towerops.Agent.*) show 0% but don't need tests
  • Focus areas for improvement (in priority order):
    • Towerops.Monitoring context (33.33%)
    • SNMP profiles: NetSnmp (56.79%), Base (69.52%), Mikrotik (77.97%)
    • LiveView modules: EquipmentLive.Show (35.51%), EquipmentLive.Form (46.73%)
    • Accounts modules: UserCredentialController (33.93%), Accounts context (61.82%)
    • 0% coverage modules (Admin, WebAuthn, etc.) - may be intentionally untested

Database

  • mix ecto.create - Create the database
  • mix ecto.migrate - Run pending migrations
  • mix ecto.reset - Drop, create, and migrate database
  • mix ecto.gen.migration migration_name_using_underscores - Generate a new migration file

Assets

  • mix assets.build - Build CSS and JS assets (Tailwind + esbuild)
  • mix assets.deploy - Build minified assets for production

Architecture

Application Structure

The application follows standard Phoenix conventions with clear separation between business logic (lib/towerops/) and web interface (lib/towerops_web/):

  • Towerops.Application - OTP application that supervises:

    • Telemetry for metrics
    • Repo (Ecto) for database
    • DNSCluster for service discovery
    • PubSub for pub/sub messaging
    • Endpoint (web server)
  • ToweropsWeb - Main web module that provides use macros for:

    • :router - Route definitions
    • :controller - Traditional request/response controllers
    • :live_view - LiveView modules
    • :live_component - LiveView component modules
    • :html - Phoenix.Component modules

Key Configuration Details

  • Binary IDs: Generators use binary (UUID) primary keys by default (binary_id: true)
  • Timestamps: Use :utc_datetime for all timestamps
  • Web server: Uses Bandit adapter (not Cowboy)
  • HTTP client: Uses :req library (Req module) - this is the only approved HTTP client
  • Ecto repos: [Towerops.Repo]

Web Layer Patterns

All LiveViews, LiveComponents, and HTML modules automatically get these imports/aliases via html_helpers/0:

  • ToweropsWeb.CoreComponents - Core UI components (<.button>, <.input>, <.form>, etc.)
  • ToweropsWeb.Layouts - Layout components (aliased, no need to re-alias)
  • Phoenix.LiveView.JS - Client-side JS commands
  • Gettext for translations
  • Verified routes with ~p sigil

Asset Pipeline

  • Tailwind CSS v4: Uses new @import "tailwindcss" syntax in assets/css/app.css, no tailwind.config.js needed
  • esbuild: Bundles JavaScript from assets/js/app.js
  • All vendor scripts/styles must be imported into app.js/app.css - cannot reference external src/href in layouts
  • No inline <script> tags in templates - use colocated LiveView hooks instead

Development Environment

  • Dev routes enabled for:
    • LiveDashboard at /dev/dashboard
    • Swoosh mailbox preview at /dev/mailbox
  • Phoenix LiveReload watches for file changes
  • Code reloader enabled via listeners: [Phoenix.CodeReloader]

Project-Specific Constraints

Key constraints from AGENTS.md (see that file for complete details):

  • Use mix precommit before committing changes
  • Use :req (Req) for all HTTP requests - never use :httpoison, :tesla, or :httpc
  • Never use daisyUI - write custom Tailwind components for world-class design
  • LiveView templates must start with <Layouts.app flash={@flash}>
  • Use <.icon name="hero-x-mark"> component for icons, never Heroicons modules
  • Use LiveView streams for collections to avoid memory issues
  • Forms must use to_form/2 in LiveView and <.form for={@form}> in templates
  • Never access changesets directly in templates - always use the form assign
  • Tailwind v4 uses new import syntax - never use @apply in CSS
  • when adding/updating hex modules, only use https://hex.pm
  • assets are rebuilt on save and don't need to be built with mix assets.build
  • be sure to always run mix format after you modify an elixir file
  • when you run mix format, don't specify a file, let it format everything

Testing Patterns

SNMP Mocking with Mox

The application uses Mox for SNMP client mocking in tests. Key patterns:

SNMP Adapter Mock Format:

  • snmp_adapter().get/3 returns {:ok, value} or {:error, reason}
  • snmp_adapter().walk/3 returns {:ok, [%{oid: "...", value: ...}]} (list of maps, NOT a map)
  • Values from get/3 are already extracted (no type wrapper needed in mocks)

Mock Expectations:

# Mock get_multiple - it calls get/3 once per OID
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
  case oid do
    "1.3.6.1.2.1.1.1.0" -> {:ok, "Cisco IOS Software"}  # sysDescr
    "1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 9]}  # sysObjectID
    "1.3.6.1.2.1.1.3.0" -> {:ok, 12_345}  # sysUpTime (integer)
    "1.3.6.1.2.1.1.4.0" -> {:ok, "admin@example.com"}  # sysContact
    "1.3.6.1.2.1.1.5.0" -> {:ok, "test-device"}  # sysName
    "1.3.6.1.2.1.1.6.0" -> {:ok, "Test Location"}  # sysLocation
  end
end)

# Mock walk - returns list of OID/value maps
expect(SnmpMock, :walk, fn _target, _oid, _opts ->
  {:ok, [
    %{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}
  ]}
end)

# Empty walk result (no data found)
expect(SnmpMock, :walk, fn _, _, _ ->
  {:ok, []}  # NOT {:ok, %{}}
end)

Common Pitfalls:

  • Returning {:ok, %{}} from walk - should be {:ok, []}
  • Returning type wrappers like {:integer, 123} - values are already extracted
  • Not matching OIDs in get expectations - use pattern matching on OID string
  • Expecting wrong number of calls - get_multiple/2 calls get/3 once per OID

TimescaleDB Tests:

  • Tests that query continuous aggregates should be tagged with @tag :skip
  • Reason: Test database doesn't have TimescaleDB continuous aggregates configured
  • Example: get_hourly_stats/3, get_daily_stats/3, get_uptime_percentage/1

Test Organization

  • Use DataCase for tests that need database access
  • Use ConnCase for controller/LiveView tests
  • Use async: true for tests that can run in parallel (most unit tests)
  • Use async: false for tests with shared state (supervisor tests, integration tests)
  • never open the test coverage html files