# 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