Flick is a ranked voting web application built with Elixir and Phoenix LiveView. It lets users create ballots and capture ranked votes without requiring accounts. The app module is Flick and the web module is FlickWeb.
- Use
mix precommitalias when you are done with all changes and fix any pending issues - Add every new CI check to the
precommitalias inmix.exs, unless it needs a tool that isn't a Mix task.precommitmirrors CI's Mix checks, so a green local run predicts a green pull request - 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 - To check how the app behaves, use local data, such as
mix run priv/repo/seeds.exs, rather than the production database. When a task needs production data, ask the user for a copy of the database
- To use the result of an
if,case, orcond, bind the whole expression:socket = if connected?(socket), do: assign(socket, :val, val), else: socket. A variable rebound inside the block is lost when the block ends. - Put one module in each file.
- Read struct fields with dot syntax, such as
ballot.question_title, because structs do not implementAccess. - Use Elixir's
Date,Time,DateTime, andCalendarfor date and time work. Add a date or time library only when asked. - When one dependency misbehaves, clean only that one with
mix deps.clean <name>. A full--allclean is almost never needed.
The files under docs/agents/ hold guidance that only some tasks need. Read the matching file before you start the work it covers. When a task spans several areas, read each file that applies.
- Before you change a LiveView, a function component, a HEEx template, or a form, read
docs/agents/liveview.md. - Before you write or change a test, read
docs/agents/testing.md. - Before you change a schema, a changeset, a query, or a migration, read
docs/agents/ecto.md. - Before you change the router, a controller, or an HTML view module, read
docs/agents/phoenix.md. For aliveroute, also readdocs/agents/liveview.md. For a route under/admin, also readdocs/agents/observability.md, which says where LiveDashboard must stay. - Before you change CSS, JavaScript under
assets/, or the look of a page, readdocs/agents/frontend.md. - Before you change
render.yamlor anything about deploys, use the Render tools, reach production data, or touch Paraxial, readdocs/agents/hosting.md. - Before you change metrics, traces, alerts,
instrumentation/, orrel/env.sh.eex, readdocs/agents/observability.md. Also read it before you inspect a running node, or to check how a change affects latency or query counts.
Ballots move through three states, controlled by published_at and closed_at timestamps:
- draft —
published_atis nil; ballot can be freely edited - published —
published_atis set; ballot is read-only, voters can cast votes - closed —
closed_atis set; voting is stopped, results are final
Many operations gate on state. Always check state before allowing edits or votes. The context functions publish_ballot/2 and close_ballot/2 in Flick.RankedVoting handle transitions.
Every ballot has two identifiers:
url_slug— public identifier used in all voter-facing URLs (e.g./ballot/:url_slug)secret— UUID required for admin/edit access (e.g./ballot/:url_slug/:secret/edit)
Always require both url_slug and secret for any operation that modifies a ballot.
Votes have a weight field (float, default 1.0). Results calculations must account for weights — a vote with weight: 2.0 counts twice. Admins can edit vote weights inline on a published ballot.
There is no user account system. The /admin/* scope is protected by HTTP Basic Auth (credentials from BASIC_AUTH_ADMIN_USERNAME / BASIC_AUTH_ADMIN_PASSWORD env vars). Ballot-specific edit/admin access is not protected by Basic Auth; it is granted via the ballot's url_slug and per-ballot secret in routes such as /ballot/:url_slug/:secret/edit. Public ballot creation and voting are completely open.
The description field on ballots accepts user-provided Markdown. Always render it through Flick.Markdown.render_to_html/1, which runs MDEx and then sanitizes the output via HtmlSanitizeEx. Never render raw user content directly in templates.
Flick displays timestamps in the user's local timezone. The timezone string is captured from the browser via get_connect_params in LiveView and stored as a socket assign. Always pass time_zone through to components that display datetimes and use Flick.DateTimeFormatter for formatting. Default to "UTC" if not present.
Each domain module and LiveView has a @moduledoc that says what it does. All ballot and vote operations go through the context module Flick.RankedVoting, in lib/flick/ranked_voting.ex.