# TowerOps Coding Conventions Project-specific patterns and conventions for the TowerOps Phoenix LiveView application. ## File Organization ### LiveView Modules LiveView pages live under `lib/towerops_web/live/` organized by domain: ``` lib/towerops_web/live/ schedule_live/ index.ex # List page module index.html.heex # List page template form.ex # Create/edit form module form.html.heex # Form template show.ex # Detail page module show.html.heex # Detail page template ``` - **`.ex`** files contain the LiveView module (mount, handle_params, handle_event, etc.) - **`.html.heex`** files contain the corresponding HEEx template - Module naming: `ToweropsWeb.ScheduleLive.Index`, `ToweropsWeb.ScheduleLive.Form`, etc. ### Test Files Tests mirror the `lib/` structure under `test/`: ``` test/ towerops/ # Context/business logic tests towerops_web/ # Web layer tests snmpkit/ # SNMP toolkit tests integration/ # Integration tests (SNMP, discovery) support/ # Test helpers and fixtures fixtures/ # Factory/fixture modules mix/ # Mix task tests test_helper.exs ``` ## Navigation & Page Structure ### `active_page` Assign Every LiveView sets an `active_page` assign in `mount/3` to highlight the correct nav item: ```elixir def mount(_params, _session, socket) do {:ok, assign(socket, :active_page, "schedules")} end ``` Valid values: `"dashboard"`, `"devices"`, `"sites"`, `"network-map"`, `"alerts"`, `"maintenance"`, `"schedules"`, `"insights"`, `"trace"`, `"activity"`, `"settings"`, `"help"` The layout component (`Layouts.authenticated`) checks `@active_page` to apply the `active` class to nav links. ### Breadcrumbs Use the `ToweropsWeb.Components.Breadcrumbs` component (`.breadcrumb`) for hierarchical navigation: ```heex <.breadcrumb items={[ %{label: "Dashboard", navigate: ~p"/dashboard"}, %{label: "Devices", navigate: ~p"/devices"}, %{label: "My Device"} ]} /> ``` - Each item is a map with `:label` (required) and `:navigate` (optional) - Omit `:navigate` on the last item (current page) - The first item automatically gets a home icon ## Forms ### Standard Form Pattern Forms use `phx-change="validate"` for real-time validation and `phx-submit="save"` for submission: ```heex <.simple_form for={@form} phx-change="validate" phx-submit="save"> <.input field={@form[:name]} label={t("Name")} /> <:actions> <.button>{t("Save")} ``` The LiveView handles both events: ```elixir def handle_event("validate", %{"schedule" => params}, socket) do changeset = Schedule.changeset(socket.assigns.schedule, params) {:noreply, assign(socket, :form, to_form(changeset, action: :validate))} end def handle_event("save", %{"schedule" => params}, socket) do # ... create or update logic end ``` ## Flash Messages Use `put_flash/3` with `:info` for success and `:error` for failures. Always wrap messages in `t()`: ```elixir socket |> put_flash(:info, t("Schedule created successfully.")) |> push_navigate(to: ~p"/schedules") ``` ```elixir socket |> put_flash(:error, t("Could not save schedule.")) ``` ## Translations (i18n) All user-facing strings must use the `t()` macro (or domain-specific variants). Defined in `ToweropsWeb.GettextHelpers`: | Macro | Domain | Example | |-------|--------|---------| | `t/1` | Common UI | `t("Save")`, `t("Cancel")` | | `t_auth/1` | Authentication | `t_auth("Log in")` | | `t_equipment/1` | Equipment | `t_equipment("Add Device")` | | `t_admin/1` | Admin | `t_admin("Impersonate User")` | | `t_email/1` | Emails | `t_email("Welcome!")` | Interpolation: `t("Welcome, %{name}!", name: user.name)` Error messages use the standard `gettext/1` function. ## `current_scope` Assign The `@current_scope` assign is available in all authenticated views. It's a struct containing: - **`.user`** — the current `User` struct (email, id, `is_superuser`, etc.) - **`.organization`** — the current `Organization` (name, slug, settings) - **`.timezone`** — the user's timezone string (falls back to `"UTC"`) - **`.impersonating?`** — boolean, true when a superuser is impersonating another user Access in templates: ```heex <%= @current_scope.user.email %> <%= @current_scope.organization.name %> ``` Access in LiveView: ```elixir organization = socket.assigns.current_scope.organization user = socket.assigns.current_scope.user ``` The layout displays an impersonation banner when `@current_scope.impersonating?` is true. ## CSS & Styling ### Tailwind CSS All styling uses Tailwind utility classes. The project uses **DaisyUI** as a component library on top of Tailwind. ### Dark Mode Dark mode is supported via the `dark:` prefix: ```heex
``` Always provide dark mode variants for backgrounds, text colors, and borders. ### Icons Icons use Heroicons via the `.icon` component with `hero-*` names: ```heex <.icon name="hero-plus" /> <.icon name="hero-pencil-square" class="size-5" /> <.icon name="hero-check" class="size-4 text-green-300" /> <.icon name="hero-home-mini" class="h-4 w-4" /> ``` Use the mini variant (`hero-*-mini`) for small inline icons and the default (outline) for larger UI elements. ## LiveView Conventions ### Module Boilerplate ```elixir defmodule ToweropsWeb.ScheduleLive.Index do @moduledoc false use ToweropsWeb, :live_view alias Towerops.OnCall @impl true def mount(_params, _session, socket) do {:ok, assign(socket, :active_page, "schedules")} end @impl true def handle_params(params, _url, socket) do # ... end end ``` - Always use `@impl true` for callback annotations - Set `active_page` in `mount/3` - Use `handle_params/3` for loading data based on URL params / live_action ### Scoped Routes Routes are scoped under the organization slug: ``` /orgs/:org_slug/devices /orgs/:org_slug/schedules/:id ``` Use `~p` sigil for path generation with `@current_scope`: ```elixir ~p"/orgs/#{@current_scope.organization.slug}/devices" ```