6.9 KiB
6.9 KiB
Project Conventions
Rules extracted from commit history that are specific to this project and not already covered by CLAUDE.md usage rules.
Commit Messages
- Imperative present tense, single-line, under 60 characters
- Describe intent/behavior, not implementation details
- Reverts use
Revert "Original message"convention - NEVER ADD a "Co-Authored-By" reference in the message body
- If you're working on a GitHub issue, make sure to include the issue ID in the commit message body. Specifies if the commit closes the issue.
Architecture
- Context modules own all queries. LiveViews never query the database directly -- they call context functions.
- Schemas hold pure accessor/helper functions on the struct (e.g.,
RecordSet.count_by_status/1). No side effects in schemas. - LiveView standard structure:
mount/3sets@current_section,handle_params/3loads data and sets@page_title(via pattern-matched privatepage_title/2),handle_info/2receives LiveComponent messages. - LiveComponents communicate with parent via
send(self(), {__MODULE__, msg}). - Function components go in domain-specific modules:
CoreComponentsfor generic UI,RecordComponentsfor records,ScrobbleComponentsfor scrobbles,SearchComponentsfor search. - External API integrations follow a three-module pattern: Facade (public API), API (Req HTTP client), Config (NimbleOptions).
- Behaviours are separate modules containing only
@callbackdefinitions. Concrete implementations use@behaviourand@impl true. - Oban workers are thin wrappers that delegate to context modules.
perform/1should be minimal. - Shared utilities live at parent namespace level (
MusicLibrary.Batch, notMusicLibrary.Records.Batch). - Domain sub-modules group under their context. Modules strongly related to a context live as sub-modules (e.g.,
Chats.StreamProvider,Chats.RecordChat), distinct from shared utilities which live at the parent namespace level. - Shared LiveView logic goes in
LiveHelpers.*modules. Event handlers, param parsing, and rendering helpers used across 2+ LiveViews are extracted toLiveHelpers.*rather than duplicated.
Extraction / Refactoring
- Extract when duplicated 3+ times. Identical template markup in 3+ places becomes a function component.
- Delete thin wrapper modules with a single caller -- inline them instead.
- Parameterize the differences when extracting shared logic.
- Private helpers go at module bottom, public functions first.
Template / UI
- Gettext wraps ALL user-facing strings. Every commit that adds UI text must also update
.pot/.pofiles. - Dark mode always paired:
text-zinc-900 dark:text-zinc-100,bg-zinc-50 dark:bg-zinc-800. - Wishlisted items get dimmed styling:
opacity-60 hover:opacity-100 transition-opacity. - Icons inside buttons use the
iconclass instead of explicit size classes (h-5 w-5,size-3.5, etc.). Theiconclass is provided by Fluxon and auto-sizes icons based on the button'ssizeprop. - Artist name display uses the MusicBrainz
joinphrasefield — never join artist names with a literal", ". - Charts use CSS Grid and responsive HTML, not SVG.
Routes / Navigation
- Three routes per resource with show modals:
:show,:edit(at/show/edit),:add_*(at/show/add-*). - Modals close via
JS.patchback to the base route. - Search state in URL query params via
push_patch. - Filter empty params from URLs:
Enum.filter(fn {_, v} -> v not in ["", nil] end). - Conditional links based on
purchased_at: determines/collection/vs/wishlist/paths. /dev/namespace is for developer tooling only (LiveDashboard, Oban Web, ErrorTracker). User-facing admin routes belong in the main authenticatedlive_session.
Database
- SQLite JSON patterns:
json_each()andjson_extract()viafragmentfor JSON column queries. Expression-based indexes onjson_extractfor performance. - Materialized views via triggers (SQLite lacks native materialized views). Use explicit
up/downin migrations for non-reversible DDL. - Read-only schemas for materialized/view tables:
@primary_key false, no changeset functions, no timestamps. - Every
executeprovides both up and down SQL. Every index has a comment explaining which query it helps. - Config-driven constants. Pagination defaults and similar magic numbers live in
config/config.exs, read viaApplication.compile_env!/2into module attributes.
Error Handling
- Toast notifications:
put_toast/3(arity 3) in LiveViews,put_toast!/2(arity 2) in LiveComponents.:infofor success,:errorfor failures. - User-facing error reasons use
ErrorMessages.friendly_message/1— neverinspect(reason). Call sites keep their contextual prefix (e.g.gettext("Error refreshing cover")) and append": " <> ErrorMessages.friendly_message(reason)for the reason part.Logger.errorcalls keepinspectfor debugging. handle_asyncalways handles three cases:{:ok, {:ok, result}},{:ok, {:error, reason}}, and{:exit, reason}.- Data cascade on upstream changes: When artist metadata changes, regenerate dependent record embeddings.
- Non-actionable errors use
ErrorTracker.Ignorerbehaviour to filter (e.g., NoRouteError from bot scanners) rather than blocking paths in the endpoint. - Muted errors skip notifications.
ErrorTracker.ErrorNotifierchecks themutedflag before sending email notifications.
Testing
@tag :logged_outfor public endpoint tests.@tag :capture_logon tests with expected error log output.- Fixture modules use
System.unique_integer([:positive])for unique names and call through context functions (not rawRepo.insert). - Verify outcomes through context modules, not just UI assertions. Delete tests assert both
refute has_element?andassert_raise Ecto.NoResultsError. render_hook/3for testing JS hook interactions.
Tech Debt / Hygiene
- Clean up first, then enforce. Remove all violations before enabling a lint rule.
- Reverts are total. Remove every trace: source, CSS, npm deps, config.
- Never leak sensitive data in prod.
show_sensitive_data_on_connection_error: false. - Commits are small and single-purpose. One logical change per commit.
- Unused aliases are removed when their module is no longer referenced. Aliases stay alphabetically sorted.
- Markdown sanitization via MDEx (ammonia). Use
Markdown.to_html/1for user content. Annotate raw output with# sobelow_skip ["XSS.Raw"]and a comment explaining the sanitization. - Sobelow runs on CI and pre-commit in skip mode for security analysis.
- All modules require
@moduledoc. The CredoModuleDoccheck is enforced in strict mode.
JavaScript
- Factory function pattern for JS hooks when two hooks share logic.
- Data attributes (
data-*) for HTML-to-JS communication. Hooks readdatasetandpushEventto the server.