This is a web application written using the Phoenix web framework. ## Project guidelines - Use `mix precommit` alias when you are done with all changes and fix any pending issues - Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps - Always provide translations of all text that is visible in the UI - Never put Ecto queries directly in LiveViews. Instead always put them in the appropriate context module ### Module design and complexity **IMPORTANT**: When working with modules that are becoming too large or complex: - **Monitor module size and complexity**: If a module exceeds ~500-600 lines or contains deeply nested logic with high cyclomatic complexity, consider refactoring - **Break out logical concerns**: Extract related functionality into separate, focused modules that handle a single responsibility - **Use helper modules**: For complex domains, consider creating dedicated modules for specific sub-concerns: - Positioning logic (e.g., `Tasks.Positioning`) - Dependency management (e.g., `Tasks.Dependencies`) - Validation logic (e.g., `Tasks.Validation`) - Query builders (e.g., `Tasks.Queries`) - **Extract helper functions**: When a function becomes complex (cyclomatic complexity > 9), extract complex conditional logic into smaller, well-named helper functions - **Maintain clear module boundaries**: Each module should have a clear, single purpose with a well-defined public API - **Document module organization**: When splitting modules, update documentation to explain the new structure and how modules relate to each other This approach improves: - Code maintainability and readability - Test isolation and coverage - Collaboration between developers - Ability to reason about individual components - Credo compliance and code quality metrics ### Quality guidelines **ALWAYS** follow these quality guidelines: - **IMPORTANT**: When you complete a task that has new functions, write unit tests for the new function - **IMPORTANT**: When you complete a task that updates code, make sure all existing unit tests pass and write new tests if needed - Each time you write or update a unit test run them with `mix test` and ensure they pass - **IMPORTANT**: When you complete a task run `mix test --cover` and ensure coverage is above the threshold - **IMPORTANT**: When you complete a task run `mix credo --strict` to check for code quality issues and fix them - **IMPORTANT**: Run `mix dialyzer` for static type analysis — use `.dialyzer_ignore.exs` only for dependency PLT gaps, vendored code, and external language artifacts (Gleam, NIFs). Never suppress warnings from project source code; fix those at the root cause. If legacy warnings are too numerous for a single PR, add them to the ignore file temporarily with a TODO comment and scheduled cleanup date. - **IMPORTANT**: Run `mix precommit` before committing: compiles with warnings as errors, formats, runs tests ### Security guidelines **ALWAYS** follow these security guidelines: - **IMPORTANT**: When you add or update a dependency run `mix deps.audit` and `mix hex.audit` to check for security issues - **IMPORTANT**: When you add or update a dependency run `mix hex.outdated` to check for outdated dependencies ### Phoenix v1.8 guidelines - **Always** begin your LiveView templates with `` which wraps all inner content - The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again - Anytime you run into errors with no `current_scope` assign: - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `` - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed - Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module - Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar - **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors - If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your custom classes must fully style the input ### JS and CSS guidelines - **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces. - Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`: @import "tailwindcss" source(none); @source "../css"; @source "../js"; @source "../../lib/towerops_web"; - **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new` - **Never** use `@apply` when writing raw css - **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design - Out of the box **only the app.js and app.css bundles are supported** - You cannot reference an external vendor'd script `src` or link `href` in the layouts - You must import the vendor deps into app.js and app.css to use them - **Never write inline ` - colocated hooks are automatically integrated into the app.js bundle - colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber` #### External phx-hook External JS hooks (`
`) must be placed in `assets/js/` and passed to the LiveSocket constructor: const MyHook = { mounted() { ... } } let liveSocket = new LiveSocket("/live", Socket, { hooks: { MyHook } }); #### Memory leak prevention in JS hooks Always remove event listeners in `destroyed()`: ```typescript const MyHook = { handleClick: null as ((e: Event) => void) | null, mounted(this: any) { this.handleClick = (e: Event) => { /* logic */ } this.el.addEventListener("click", this.handleClick) }, destroyed(this: any) { if (this.handleClick) { this.el.removeEventListener("click", this.handleClick) this.handleClick = null } } } ``` - Clean up third-party instances (Chart.js, Cytoscape, etc.) in `destroyed()` - Store event handler references — never pass anonymous functions to `addEventListener` - Set hook properties to `null` in `destroyed()` - Phoenix.PubSub auto-cleans when LiveView terminates — no manual cleanup needed #### Pushing events between client and server Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle. **Always** return or rebind the socket on `push_event/3` when pushing events: # re-bind socket so we maintain event state to be pushed socket = push_event(socket, "my_event", %{...}) # or return the modified socket directly: def handle_event("some_event", _, socket) do {:noreply, push_event(socket, "my_event", %{...})} end Pushed events can then be picked up in a JS hook with `this.handleEvent`: mounted() { this.handleEvent("my_event", data => console.log("from server:", data)); } Clients can also push an event to the server and receive a reply with `this.pushEvent`: mounted() { this.el.addEventListener("click", e => { this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply)); }) } Where the server handled it via: def handle_event("my_event", %{"one" => 1}, socket) do {:reply, %{two: 2}, socket} end ### LiveView tests - `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions - Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions - Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests - **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc - **Never** test against raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")` - Instead of relying on testing text content, which can change, favor testing for the presence of key elements - Focus on testing outcomes rather than implementation details - Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be - When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie: html = render(view) document = LazyHTML.from_fragment(html) matches = LazyHTML.filter(document, "your-complex-selector") IO.inspect(matches, label: "Matches") ### Form handling #### Creating a form from params If you want to create a form based on `handle_event` params: def handle_event("submitted", params, socket) do {:noreply, assign(socket, form: to_form(params))} end When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys. You can also specify a name to nest the params: def handle_event("submitted", %{"user" => user_params}, socket) do {:noreply, assign(socket, form: to_form(user_params, as: :user))} end #### Creating a form from changesets When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: defmodule Towerops.Users.User do use Ecto.Schema ... end And then you create a changeset that you pass to `to_form`: %Towerops.Users.User{} |> Ecto.Changeset.change() |> to_form() Once the form is submitted, the params will be available under `%{"user" => user_params}`. In the template, the form assign can be passed to the `<.form>` function component: <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> <.input field={@form[:field]} type="text" /> Always give the form an explicit, unique DOM ID, like `id="todo-form"`. #### Accessing form data from LiveView events **Problem**: `form.params` is empty until `phx-change="validate"` fires. **Solution**: Access current values from changeset: ```elixir def handle_event("test_connection", _params, socket) do changeset = socket.assigns.form.source form_data = Ecto.Changeset.apply_changes(changeset) # Returns struct with all values # Convert to map if needed device_map = %{"ip_address" => form_data.ip_address, ...} end ``` This combines: DB values + default values + user changes. #### Avoiding form errors **Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**: <%!-- ALWAYS do this (valid) --%> <.form for={@form} id="my-form"> <.input field={@form[:field]} type="text" /> And **never** do this: <%!-- NEVER do this (invalid) --%> <.form for={@changeset} id="my-form"> <.input field={@changeset[:field]} type="text" /> - You are FORBIDDEN from accessing the changeset in the template as it will cause errors - **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset