# Development Progress Log

**Project:** Mila - Membership Management System  
**Repository:** https://git.local-it.org/local-it/mitgliederverwaltung  
**License:** AGPLv3  
**Status:** Early Development (⚠️ Not Production Ready)

---

## Table of Contents

1. [Project Overview](#project-overview)
2. [Setup and Foundation](#setup-and-foundation)
3. [Major Features Implementation](#major-features-implementation)
4. [Implementation Decisions](#implementation-decisions)
5. [Build and Deployment](#build-and-deployment)
6. [Testing Strategy](#testing-strategy)
7. [Common Issues and Solutions](#common-issues-and-solutions)
8. [Future Improvements](#future-improvements)
9. [Team Knowledge Base](#team-knowledge-base)

---

## Project Overview

### Vision
Simple, usable, self-hostable membership management for small to mid-sized clubs.

### Philosophy
*"Software should help people spend less time on administration and more time on their community."*

### Core Principles
- ✅ **Simple:** Focused on essential club needs
- ✅ **Usable:** Clean, accessible UI for everyday volunteers
- ✅ **Flexible:** Customizable data fields, role-based permissions
- ✅ **Open:** 100% free and open source, no vendor lock-in
- ✅ **Self-hostable:** Full control over data and deployment

### Target Users
- Small to mid-sized clubs
- Volunteer administrators (non-technical)
- Club members (self-service access)

---

## Setup and Foundation

### Initial Project Setup

For **current setup instructions**, see [`README.md`](../README.md#-quick-start-development).

**Historical context:**

#### 1. Phoenix Project Initialization (Sprint 0)

```bash
mix phx.new mv --no-ecto --no-mailer
```

**Reasoning:**
- `--no-ecto`: Using Ash Framework with AshPostgres instead
- `--no-mailer`: Added Swoosh later for better control

#### 2. Technology Choices

**For complete tech stack details, see [`CODE_GUIDELINES.md`](../CODE_GUIDELINES.md#project-context).**

**Key decisions:**
- **Elixir 1.18.3 + OTP 27**: Latest stable versions for performance
- **Ash Framework 3.0**: Declarative resource layer, reduces boilerplate
- **Phoenix LiveView 1.1.0-rc.3**: Real-time UI without JavaScript complexity
- **Tailwind CSS 4.0**: Utility-first styling with custom build
- **PostgreSQL 17**: Advanced features (full-text search, JSONB, citext)
- **Bandit**: Modern HTTP server, better than Cowboy for LiveView

#### 3. Version Management (asdf)

**Tool:** asdf 0.16.5 for consistent environments across team

**Versions pinned in `.tool-versions`:**
- Elixir 1.18.3-otp-27
- Erlang 27.3.4  
- Just 1.46.0

#### 4. Database Setup

**PostgreSQL Extensions:**
```sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";  -- UUID generation (via uuid_generate_v7 function)
CREATE EXTENSION IF NOT EXISTS "citext";      -- Case-insensitive text
CREATE EXTENSION IF NOT EXISTS "pg_trgm";     -- Trigram-based fuzzy search
```

**Migration Strategy:**
```bash
mix ash.codegen --name <migration_name>  # Generate from Ash resources
mix ash.migrate                           # Apply migrations
```

**Reasoning:** Ash generates migrations from resource definitions, ensuring schema matches code.

#### 5. Development Workflow (Just)

Chose **Just** over Makefile for:
- Better error messages
- Cleaner syntax
- Cross-platform compatibility

**Core commands:** See [README.md](../README.md#-development)

---

## Major Features Implementation

### Sprint History & Key Pull Requests

Based on closed PRs from https://git.local-it.org/local-it/mitgliederverwaltung/pulls?state=closed:

#### Phase 1: Foundation (Sprint 0-2)

**Sprint 0 - Vorarbeit**
- Initial project setup
- Technology stack decisions
- Repository structure

**Sprint 2 - 07.05 - 28.05**
- Basic Phoenix setup
- Initial database schema
- Development environment configuration

#### Phase 2: Core Features (Sprint 3-5)

**Sprint 3 - 28.05 - 09.07**
- Member CRUD operations
- Basic custom field system
- Initial UI with Tailwind CSS

**Sprint 4 - 09.07 - 30.07**
- CustomFieldValue types implementation
- Data validation
- Error handling improvements

**Sprint 5 - 31.07 - 11.09**

**PR #138:** *Customize login screen and members as landing page* (closes #68, #137)
- Custom login UI with DaisyUI
- Members page as default landing
- Improved navigation flow

**PR #139:** *Added PR and issue templates* (closes #129)
- GitHub/GitLab issue templates
- PR template for consistent reviews
- Contribution guidelines

**PR #147:** *Add seed data for members*
- Comprehensive seed data
- Test users and members
- CustomFieldValue type examples

#### Phase 3: Search & Navigation (Sprint 6)

**Sprint 6 - 11.09 - 02.10**

**PR #163:** *Implement full-text search for members* (closes #11) 🔍
- PostgreSQL full-text search with tsvector
- Weighted search fields (names: A, email/notes: B, contact: C)
- GIN index for performance
- Auto-updating trigger
- Migration: `20250912085235_AddSearchVectorToMembers.exs`

```elixir
# Search implementation highlights
attribute :search_vector, AshPostgres.Tsvector,
  writable?: false,
  public?: false,
  select_by_default?: false
```

**Key learnings:**
- Simple lexer used (no German stemming initially)
- Weighted fields improve relevance
- GIN index essential for performance

#### Phase 4: Sorting & User Management (Sprint 7)

**Sprint 7 - 02.10 - 23.10**

**PR #166:** *Sorting header for members list* (closes #152, #175)
- Sortable table headers component
- Multi-column sorting support
- Visual indicators for sort direction
- Accessibility improvements (ARIA labels)

**PR #172:** *Create logical link between users and members* (closes #164)
- Optional 1:1 relationship (0..1 ↔ 0..1)
- User `belongs_to` Member
- Member `has_one` User
- Foundation for email sync feature
- Migration: `20250926164519_member_relation.exs`

**PR #148:** *Fix error when deleting members*
- Cascade delete handling
- Proper foreign key constraints
- Error message improvements

**PR #173:** *Link to user data from profile button* (closes #170)
- Profile navigation improvements
- User-member relationship display
- Better UX for linked accounts

**PR #178:** *Polish README* (closes #158)
- Updated documentation
- Better onboarding instructions
- Screenshots and examples

#### Phase 5: Email Synchronization (Sprint 8)

**Sprint 8 - 23.10 - 13.11**

**PR #181:** *Sync email between user and member* (closes #167) ✉️
- Bidirectional email synchronization between User and Member
- User.email as source of truth on linking
- Custom Ash changes with conditional execution
- Complex validation logic to prevent conflicts
- Migration: `20251016130855_add_constraints_for_user_member_and_property.exs`

**See:** [`docs/email-sync.md`](email-sync.md) for complete sync rules and decision tree.

---

#### Phase 6: Search Enhancement & OIDC Improvements (Sprint 9)

**Sprint 9 - 01.11 - 13.11 (finalized)**

**PR #187:** *Implement fuzzy search* (closes #162) 🔍
- PostgreSQL `pg_trgm` extension for trigram-based fuzzy search
- 6 new GIN trigram indexes on members table:
  - first_name, last_name, email, city, street, notes
- Combined search strategy: Full-text (tsvector) + Trigram similarity
- Configurable similarity threshold (default 0.2)
- Migration: `20251001141005_add_trigram_to_members.exs`
- 443 lines of comprehensive tests

**Key learnings:**
- Trigram indexes significantly improve fuzzy matching
- Combined FTS + trigram provides best user experience
- word_similarity() better for partial word matching than similarity()
- Similarity threshold of 0.2 balances precision and recall

**Implementation highlights:**
```elixir
# New Ash action: :search with fuzzy matching
read :search do
  argument :query, :string, allow_nil?: true
  argument :similarity_threshold, :float, allow_nil?: true
  # Uses fragment() for pg_trgm operators: %, similarity(), word_similarity()
end

# Public function for LiveView usage
def fuzzy_search(query, opts) do
  Ash.Query.for_read(query, :search, %{query: query_string})
end
```

---

**PR #192:** *OIDC handling and linking* (closes #171) 🔐
- Secure OIDC account linking with password verification
- Security fix: Filter OIDC sign-in by `oidc_id` instead of email
- New custom error: `PasswordVerificationRequired`
- New validation: `OidcEmailCollision` for email conflict detection
- New LiveView: `LinkOidcAccountLive` for interactive linking
- Automatic linking for passwordless users (no password prompt)
- Password verification required for password-protected accounts
- Comprehensive security logging for audit trail
- Locale persistence via secure cookie (1 year TTL)
- Documentation: `docs/oidc-account-linking.md`

**Security improvements:**
- Prevents account takeover via OIDC email matching
- Password verification before linking OIDC to password accounts
- All linking attempts logged with appropriate severity
- CSRF protection on linking forms
- Secure cookie flags: `http_only`, `secure`, `same_site: "Lax"`

**Test coverage:**
- 5 new comprehensive test files (1,793 lines total):
  - `user_authentication_test.exs` (265 lines)
  - `oidc_e2e_flow_test.exs` (415 lines)
  - `oidc_email_update_test.exs` (271 lines)
  - `oidc_password_linking_test.exs` (496 lines)
  - `oidc_passwordless_linking_test.exs` (210 lines)
- Extended `oidc_integration_test.exs` (+136 lines)

**Key learnings:**
- Account linking requires careful security considerations
- Passwordless users should be auto-linked (better UX)
- Audit logging essential for security-critical operations
- Locale persistence improves user experience post-logout

---

**PR #193:** *Docs, Code Guidelines and Progress Log* 📚
- Complete project documentation suite (5,554 lines)
- New documentation files:
  - `CODE_GUIDELINES.md` (2,578 lines) - Comprehensive development guidelines
  - `docs/database-schema-readme.md` (392 lines) - Database documentation
  - `docs/database_schema.dbml` (329 lines) - DBML schema definition
  - `docs/development-progress-log.md` (1,227 lines) - This file
  - `docs/feature-roadmap.md` (743 lines) - Feature planning and roadmap
- Reduced redundancy in README.md (links to detailed docs)
- Cross-referenced documentation for easy navigation

---

**PR #201:** *Code documentation and refactoring* 🔧
- @moduledoc for ALL modules (51 modules documented)
- @doc for all public functions
- Enabled Credo `ModuleDoc` check (enforces documentation standards)
- Refactored complex functions:
  - `MemberLive.Index.handle_event/3` - Split sorting logic into smaller functions
  - `AuthController.handle_auth_failure/2` - Reduced cyclomatic complexity
- Documentation coverage: 100% for core modules

**Key learnings:**
- @moduledoc enforcement improves code maintainability
- Refactoring complex functions improves readability
- Documentation should explain "why" not just "what"
- Credo helps maintain consistent code quality

---

**PR #208:** *Show custom fields per default in member overview* 🔧
- added show_in_overview as attribute to custom fields
- show custom fields in member overview per default
- can be set to false in the settings for the specific custom field

## Implementation Decisions

### Architecture Patterns

#### 1. Ash Framework Over Traditional Phoenix

**Decision:** Use Ash Framework as the primary data layer instead of traditional Ecto contexts.

**Reasoning:**
- **Declarative resource definitions** reduce boilerplate
- **Built-in authorization** with policies
- **Type safety** with calculations and aggregates
- **Code generation** for migrations

**Trade-offs:**
- Steeper learning curve
- Less common in Phoenix community
- Newer ecosystem (fewer resources)
- More opinionated structure

**Outcome:** 
- ✅ Faster feature development
- ✅ Consistent API across resources
- ⚠️ Requires team training

#### 2. Domain-Driven Design

**Decision:** Organize by business domains (Accounts, Membership) rather than technical layers.

**Reasoning:**
- Clear separation of concerns
- Business logic separate from web layer
- Scalable for future domains (payments, communications)

**For detailed project structure, see [`CODE_GUIDELINES.md`](../CODE_GUIDELINES.md#11-project-structure).**

#### 3. Bidirectional Email Sync

**Problem:** Users and Members can exist independently, but when linked, emails must stay synchronized.

**Solution:** Custom Ash changes with conditional execution

**Why not simpler approaches?**
- ❌ Single email table: Too restrictive (members without users need emails)
- ❌ Always sync: Performance concerns, unnecessary for unlinked entities
- ❌ Manual sync: Error-prone, inconsistent
- ✅ Conditional sync with validations: Flexible, safe, performant

**Complete documentation:** See [`docs/email-sync.md`](email-sync.md) for decision tree and sync rules.

#### 4. CustomFieldValue System (EAV Pattern)

**Implementation:** Entity-Attribute-Value pattern with union types

```elixir
# CustomFieldValue Type defines schema
defmodule Mv.Membership.CustomField do
  attribute :name, :string           # "Membership Number"
  attribute :value_type, :atom       # :string, :integer, :boolean, :date, :email
  attribute :immutable, :boolean     # Can't change after creation
  attribute :required, :boolean      # All members must have this
  attribute :show_in_overview, :boolean # "If true, this custom field will be displayed in the member overview table"
end

# CustomFieldValue stores values
defmodule Mv.Membership.CustomFieldValue do
  attribute :value, :union,          # Polymorphic value storage
    constraints: [
      types: [
        string: [type: :string],
        integer: [type: :integer],
        boolean: [type: :boolean],
        date: [type: :date],
        email: [type: Mv.Membership.Email]
      ]
    ]
  belongs_to :member
  belongs_to :custom_field
end
```

**Reasoning:**
- Clubs need different custom fields
- No schema migrations for new fields
- Type safety with union types
- Centralized custom field management

**Constraints:**
- One custom field value per custom field per member (composite unique index)
- Properties deleted with member (CASCADE)
- CustomFieldValue types protected if in use (RESTRICT)

#### 5. Authentication Strategy

**Multi-Strategy Authentication:**

```elixir
authentication do
  strategies do
    # Password-based
    password :password do
      identity_field :email
      hash_provider AshAuthentication.BcryptProvider
    end
    
    # OIDC (Rauthy)
    oidc :rauthy do
      client_id Mv.Secrets
      base_url Mv.Secrets
      client_secret Mv.Secrets
    end
  end
end
```

**Reasoning:**
- Flexibility: Clubs choose authentication method
- Self-hosting: OIDC with Rauthy (open source)
- Fallback: Password auth for non-SSO users
- Security: bcrypt for password hashing

**Token Management:**
- Store all tokens (store_all_tokens? true)
- JWT-based sessions
- Token revocation support

#### 6. UI Framework Choice

**Tailwind CSS + DaisyUI**

**Reasoning:**
- **Tailwind:** Utility-first, no custom CSS
- **DaisyUI:** Pre-built components, consistent design
- **Heroicons:** Icon library, inline SVG
- **Phoenix LiveView 1.1.0-rc.3:** Server-rendered, minimal JavaScript

**Trade-offs:**
- Larger HTML (utility classes)
- Learning curve for utility-first CSS
- ✅ Faster development
- ✅ Consistent styling
- ✅ Mobile-responsive out of the box

#### 7. Search Implementation (Full-Text + Fuzzy)

**Two-Tiered Search Strategy:**

**A) Full-Text Search (tsvector + GIN Index)**

```sql
-- Auto-updating trigger
CREATE FUNCTION members_search_vector_trigger() RETURNS trigger AS $$
BEGIN
  NEW.search_vector :=
    setweight(to_tsvector('simple', coalesce(NEW.first_name, '')), 'A') ||
    setweight(to_tsvector('simple', coalesce(NEW.last_name, '')), 'A') ||
    setweight(to_tsvector('simple', coalesce(NEW.email, '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(NEW.notes, '')), 'B') ||
    setweight(to_tsvector('simple', coalesce(NEW.city, '')), 'C') ||
    -- ... more fields
  RETURN NEW;
END
$$ LANGUAGE plpgsql;
```

**B) Fuzzy Search (pg_trgm + Trigram GIN Indexes)**

Added November 2025 (PR #187):

```elixir
# Ash action combining FTS + trigram similarity
read :search do
  argument :query, :string
  argument :similarity_threshold, :float

  prepare fn query, _ctx ->
    # 1. Full-text search (tsvector)
    # 2. Trigram similarity (%, similarity(), word_similarity())
    # 3. Substring matching (contains, ilike)
  end
end
```

**6 Trigram Indexes:**
- first_name, last_name, email, city, street, notes
- GIN index with `gin_trgm_ops` operator class

**Reasoning:**
- Native PostgreSQL features (no external service)
- Combined approach handles typos + partial matches
- Fast with GIN indexes
- Simple lexer (no German stemming initially)
- Similarity threshold configurable (default 0.2)

**Why not Elasticsearch/Meilisearch?**
- Overkill for small to mid-sized clubs
- Additional infrastructure complexity
- PostgreSQL full-text + fuzzy sufficient for 10k+ members
- Better integration with existing stack

### Deviations from Initial Plans

#### 1. No Ecto Schemas

**Original Plan:** Traditional Phoenix with Ecto  
**Actual:** Ash Resources with AshPostgres

**Why:** Ash provides more features with less code (policies, admin, code generation)

#### 2. Bidirectional Email Sync

**Original Plan:** Single email, always linked  
**Actual:** Optional link with conditional sync

**Why:** Members can exist without user accounts (flexibility requirement)

#### 3. UUIDv7 for Members

**Original Plan:** Standard UUIDv4  
**Actual:** UUIDv7 for members, v4 for others

```elixir
# Member uses UUIDv7 (sortable by creation time)
uuid_v7_primary_key :id

# Users use standard UUID
uuid_primary_key :id
```

**Why:** Better database performance, chronological ordering

#### 4. No Default Create Action for Users

**Decision:** Intentionally exclude default `:create` action

```elixir
actions do
  # Explicitly NO default :create
  defaults [:read, :destroy]
  
  # Use specific create actions instead
  create :create_user
  create :register_with_password
  create :register_with_rauthy
end
```

**Why:** Bypass email sync if default create used (safety measure)

---

## Build and Deployment

### Development Workflow

**For current setup instructions, see [`README.md`](../README.md#-quick-start-development).**

**Key workflow decisions:**
- **Just** as task runner: Simplifies common tasks, better than raw mix commands
- **Docker Compose** for services: Consistent environments, easy local OIDC testing
- **Seed data included**: Realistic test data for development

#### Database Migrations

**Key migrations in chronological order (26 total):**
1. `20250421101957_initialize_extensions_1.exs` - PostgreSQL extensions (uuid-ossp, citext, pg_trgm)
2. `20250528163901_initial_migration.exs` - Core tables (members, custom_field_values, custom_fields - originally property_types/properties)
3. `20250617090641_member_fields.exs` - Member attributes expansion
4. `20250617132424_member_delete.exs` - Member deletion constraints
5. `20250620110849_add_accounts_domain_extensions.exs` - Accounts domain extensions
6. `20250620110850_add_accounts_domain.exs` - Users & tokens tables
7. `20250912085235_AddSearchVectorToMembers.exs` - Full-text search (tsvector + GIN index)
8. `20250926164519_member_relation.exs` - User-Member link (optional 1:1)
9. `20250926180341_add_unique_email_to_members.exs` - Unique email constraint on members
10. `20251001141005_add_trigram_to_members.exs` - Fuzzy search (pg_trgm + 6 GIN trigram indexes)
11. `20251016130855_add_constraints_for_user_member_and_property.exs` - Email sync constraints
12. `20251113163600_rename_properties_to_custom_fields_extensions_1.exs` - Rename properties extensions
13. `20251113163602_rename_properties_to_custom_fields.exs` - Rename property_types → custom_fields, properties → custom_field_values
14. `20251113180429_add_slug_to_custom_fields.exs` - Add slug to custom fields
15. `20251113183538_change_custom_field_delete_cascade.exs` - Change delete cascade behavior
16. `20251119160509_add_show_in_overview_to_custom_fields.exs` - Add show_in_overview flag
17. `20251127134451_add_settings_table.exs` - Create settings table (singleton)
18. `20251201115939_add_member_field_visibility_to_settings.exs` - Add member_field_visibility JSONB to settings
19. `20251202145404_remove_birth_date_from_members.exs` - Remove birth_date field
20. `20251204123714_add_custom_field_values_to_search_vector.exs` - Include custom field values in search vector
21. `20251211151449_add_membership_fees_tables.exs` - Create membership_fee_types and membership_fee_cycles tables
22. `20251211172549_remove_immutable_from_custom_fields.exs` - Remove immutable flag from custom fields
23. `20251211195058_add_membership_fee_settings.exs` - Add membership fee settings to settings table
24. `20251218113900_remove_paid_from_members.exs` - Remove paid boolean from members (replaced by cycle status)
25. `20260102155350_remove_phone_number_and_make_fields_optional.exs` - Remove phone_number, make first_name/last_name optional
26. `20260106161215_add_authorization_domain.exs` - Create roles table and add role_id to users

**Learning:** Ash's code generation from resources ensures schema always matches code.

#### Environment Variables & Secrets

**Key environment variables:**
- `SECRET_KEY_BASE` - Phoenix session encryption
- `TOKEN_SIGNING_SECRET` - JWT token signing
- `OIDC_CLIENT_SECRET` - Rauthy OAuth2 client secret
- `DATABASE_URL` - PostgreSQL connection (production only)

**Secret management approach:**
- Development: `.env` file (gitignored)
- Production: `config/runtime.exs` reads from environment
- Generation: `mix phx.gen.secret`

**For complete setup, see [`README.md`](../README.md#-configuration) and [`README.md - Testing SSO`](../README.md#-testing-sso-locally).**

### Testing

**Key testing decisions:**
- **Ecto Sandbox:** Isolated, concurrent tests
- **ExUnit:** Built-in testing framework (no external dependencies)
- **Test structure:** Mirrors application structure (accounts/, membership/, mv_web/)

**Important test patterns:**
- Email sync edge cases (see `test/accounts/email_sync_edge_cases_test.exs`)
- User-Member relationship tests (see `test/accounts/user_member_relationship_test.exs`)
- LiveView integration tests

**For testing guidelines, see [`CODE_GUIDELINES.md - Testing Standards`](../CODE_GUIDELINES.md#4-testing-standards).**

### Code Quality

**Tools in use:**
- **Credo** `~> 1.7`: Static code analysis  
- **Sobelow** `~> 0.14`: Security analysis
- **mix_audit** `~> 2.1`: Dependency vulnerability scanning
- **mix format**: Auto-formatting (2-space indentation, 120 char line length)

**CI/CD:** Drone CI runs linting, formatting checks, tests, and security scans on every push.

**Build Status:** [![Build Status](https://drone.dev.local-it.cloud/api/badges/local-it/mitgliederverwaltung/status.svg)](https://drone.dev.local-it.cloud/local-it/mitgliederverwaltung)

**For detailed guidelines, see [`CODE_GUIDELINES.md`](../CODE_GUIDELINES.md).**

### Docker Deployment

**Deployment strategy:**
- **Multi-stage build:** Builder stage (Debian + Elixir) → Runtime stage (Debian slim)
- **Assets:** Compiled during build with `mix assets.deploy`
- **Releases:** Mix release for production (smaller image, faster startup)
- **Migrations:** Run via `Mv.Release.migrate` module

**Key decisions:**
- **Bandit** instead of Cowboy: Better LiveView performance
- **Postgres 16** in production: Stable, well-tested
- **Separate dev/prod compose files:** Different needs (dev has Rauthy, Mailcrab)
- **Release module** (`Mv.Release`): Handles migrations and seeding in production

**For complete deployment instructions, see [`README.md - Production Deployment`](../README.md#-production-deployment).**

### Automated Dependency Updates

**Tool:** Renovate (via Drone CI)

**Configuration:** `renovate_backend_config.js`

**Key decisions:**
- **Schedule:** First week of each month (reduces PR noise)
- **Grouping:** Mix dependencies, asdf tools, postgres updates grouped
- **Disabled:** Elixir/Erlang auto-updates (manual version management via asdf)

**Why disabled for Elixir/Erlang?**
- OTP version coupling requires careful testing
- Version compatibility with dependencies
- Manual control preferred for core runtime

**For details, see [`CODE_GUIDELINES.md - Dependency Management`](../CODE_GUIDELINES.md#14-dependency-management).**

---

## Testing Strategy

### Test process environment

`test/test_helper.exs` clears Vereinfacht and OIDC-related environment variables at startup (same rationale as not hitting real APIs when `.env` is loaded). `Mv.Config` prefers ENV over database settings; without this, OIDC sign-in redirect tests would depend on the developer shell and become flaky. Tests that need specific OIDC env values set them in `setup` and restore with `on_exit`.

### Test Coverage Areas

#### 1. Unit Tests (Domain Logic)

**Example: Member Email Validation**
```elixir
defmodule Mv.Membership.MemberTest do
  use Mv.DataCase, async: true

  describe "email validation" do
    test "accepts valid email" do
      assert {:ok, member} = create_member(%{email: "valid@example.com"})
    end
    
    test "rejects invalid email" do
      assert {:error, _} = create_member(%{email: "invalid"})
    end
  end
end
```

#### 2. Integration Tests (Cross-Domain)

**Example: User-Member Relationship**
```elixir
defmodule Mv.Accounts.UserMemberRelationshipTest do
  use Mv.DataCase, async: true

  test "linking user to member syncs emails" do
    {:ok, user} = create_user(%{email: "user@example.com"})
    {:ok, member} = create_member(%{email: "member@example.com"})
    
    # Link user to member
    {:ok, updated_member} = link_user_to_member(user, member)
    
    # Member email should match user email
    assert updated_member.email == "user@example.com"
  end
end
```

#### 3. LiveView Tests

**Example: Member List Sorting**
```elixir
defmodule MvWeb.MemberLive.IndexTest do
  use MvWeb.ConnCase, async: true
  import Phoenix.LiveViewTest

  test "sorting members by last name", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/members")
    
    # Click sort header
    view
    |> element("th[phx-click='sort']")
    |> render_click()
    
    # Verify sorted order in view
    assert has_element?(view, "#member-1")
  end
end
```

#### 4. Component Tests

**Example: Search Bar**
```elixir
defmodule MvWeb.Components.SearchBarTest do
  use MvWeb.ConnCase, async: true
  import Phoenix.LiveViewTest

  test "renders search input" do
    assigns = %{search_query: "", id: "search"}
    
    html = render_component(&search_bar/1, assigns)
    
    assert html =~ "input"
    assert html =~ ~s(type="search")
  end
end
```

### Onboarding / Join (Issue #308, TDD)

**Subtask 1 – JoinRequest resource and public policies (done):**
- Resource: `Mv.Membership.JoinRequest` with attributes (status, email, first_name, last_name, form_data, schema_version, confirmation_token_hash, confirmation_token_expires_at, submitted_at, etc.), actions `submit` (create), `get_by_confirmation_token_hash` (read), `confirm` (update). Migration: `20260309141437_add_join_requests.exs`.
- Policies: Public actions allowed with `actor: nil` via `Mv.Authorization.Checks.ActorIsNil` (submit, get_by_confirmation_token_hash, confirm); default read remains Forbidden for unauthenticated.
- Domain: `Mv.Membership.submit_join_request/2`, `Mv.Membership.confirm_join_request/2` (token hashing via `JoinRequest.hash_confirmation_token/1`, lookup, expiry check, idempotency for :submitted/:approved/:rejected).
- Test file: `test/membership/join_request_test.exs` – all tests pass; policy test and expired-token test implemented.

**Subtask 2 – Submit and confirm flow (done):**
- **Unified email layout:** phoenix_swoosh with `MvWeb.EmailLayoutView` (layout) and `MvWeb.EmailsView` (body templates). All transactional emails (join confirmation, user confirmation, password reset) use the same layout. Config: `config :mv, :mail_from, {name, email}` (default `{"Mila", "noreply@example.com"}`); override in runtime.exs.
- **Join confirmation:** Domain wrapper `submit_join_request/2` generates token (or uses optional `:confirmation_token` in attrs for tests), creates JoinRequest via action `:submit`, then sends one email via `MvWeb.Emails.JoinConfirmationEmail`. Route `GET /confirm_join/:token` (JoinConfirmController) updates to `submitted`; idempotent; expired/invalid handled.
- **Senders migrated:** `SendNewUserConfirmationEmail`, `SendPasswordResetEmail` use layout + `Mv.Mailer.mail_from/0`.
- **Cleanup:** Mix task `mix join_requests.cleanup_expired` hard-deletes JoinRequests in `pending_confirmation` with expired `confirmation_token_expires_at` (authorize?: false). For cron/Oban.
- **Gettext:** New email strings in default domain; German translations in de/LC_MESSAGES/default.po; English msgstr filled for email-related strings.
- **PR review follow-ups (Join confirmation):** Join confirmation email uses `Mailer.deliver/2` with `Mailer.smtp_config/0` (same config as test mail). On delivery failure the domain returns `{:error, :email_delivery_failed}` (logged via `Logger.error`), and the JoinLive shows an error message (no success UI). Comment in `submit_join_request/2` clarifies that the raw token is hashed by `JoinRequest.Changes.SetConfirmationToken`. Cleanup task uses `Ash.bulk_destroy` and logs partial errors without halting. Layout uses assigns `app_name` and `locale` (from config/Gettext) instead of hardcoded "Mila" and `lang="de"`. Production `runtime.exs` sets `:mail_from` from ENV (`MAIL_FROM_NAME`, `MAIL_FROM_EMAIL`). Layout reference unified to `"layout.html"`; redundant `put_layout` removed from senders.
- Tests: `join_request_test.exs`, `join_request_submit_email_test.exs`, `join_confirm_controller_test.exs` – all pass.

**Subtask 3 – Admin: Join form settings (done):**
- **Setting resource** (`lib/membership/setting.ex`): 3 new attributes `join_form_enabled` (boolean, default false), `join_form_field_ids` ({:array, :string} – ordered list of member field names or custom field UUIDs), `join_form_field_required` (:map – field ID → boolean). Added to `:create` and `:update` accept lists. Validation rejects field IDs that are neither valid member field names nor UUID format. Migration: `20260310114701_add_join_form_settings_to_settings.exs`.
- **NormalizeJoinFormSettings** (`lib/membership/setting/changes/normalize_join_form_settings.ex`): Change applied on create/update whenever join form attrs are changing. Ensures email is always in `join_form_field_ids`, forces `join_form_field_required["email"] = true`, drops required flags for fields not in `join_form_field_ids`.
- **Domain** (`lib/membership/membership.ex`): `Mv.Membership.get_join_form_allowlist/0` – returns `[]` when join form is disabled, otherwise a list of `%{id, required, type}` maps (type = `:member_field` or `:custom_field` based on ID format).
- **GlobalSettingsLive** (`lib/mv_web/live/global_settings_live.ex`): New "Join Form" / "Beitrittsformular" section between Club Settings and Vereinfacht. Checkbox to enable/disable, table of selected fields (with Required checkbox per field – email always checked/disabled, other fields can be toggled), "Add field" dropdown with all unselected member fields and custom fields, "Save Join Form Settings" button. State is managed locally (not via AshPhoenix.Form); saved on explicit save click.
- **Translations**: 14 new German strings in `priv/gettext/de/LC_MESSAGES/default.po` (Beitrittsformular, Felder im Beitrittsformular, Feld hinzufügen, etc.).
- Tests: All 13 tests in `test/membership/setting_join_form_test.exs` pass; full test suite 1900 tests, 0 failures.

### Test Data Management

**Seed Data:**
- Admin user: `admin@localhost` / `testpassword` (configurable via `ADMIN_EMAIL` env var)
- Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner
- Linked accounts: Maria Weber, Thomas Klein
- CustomFieldValue types: String, Date, Boolean, Email

**Test Helpers:**
```elixir
# test/support/fixtures.ex
def member_fixture(attrs \\ %{}) do
  default_attrs = %{
    first_name: "Test",
    last_name: "User",
    email: "test#{System.unique_integer()}@example.com"
  }

  {:ok, member} =
    default_attrs
    |> Map.merge(attrs)
    |> Mv.Membership.create_member()

  member
end
```

**Testing best practices applied:**
- Async by default with Ecto Sandbox
- Descriptive test names explaining behavior
- Arrange-Act-Assert pattern
- One assertion per test
- Fixtures for test data setup

**For complete guidelines, see [`CODE_GUIDELINES.md - Testing Standards`](../CODE_GUIDELINES.md#4-testing-standards).**

---

## Common Issues and Solutions

### 1. Email Synchronization Conflicts

**Issue:** Creating user/member with email that exists in other table (unlinked).

**Error:** 
```
"Email already used by another (unlinked) member"
```

**Root Cause:** Custom validation prevents cross-table email conflicts for linked entities.

**Solution:** 
- Link existing entities first
- Or use different email
- Validation only applies to linked entities

**Documentation:** `docs/email-sync.md`

### 2. Ash Migration Conflicts

**Issue:** Migrations out of sync with resource definitions.

**Symptoms:**
- Migration fails
- Columns don't match resource attributes
- Foreign keys missing

**Solution:**
```bash
# Rollback conflicting migrations
mix ash_postgres.rollback -n 1

# Delete migration files
rm priv/repo/migrations/<timestamp>_*.exs
rm priv/resource_snapshots/repo/<resource>_*.json

# Regenerate
mix ash.codegen --name <migration_name>

# Or use Just helper
just regen-migrations <name>
```

### 3. OIDC Authentication Not Working

**Issue:** OIDC login fails with redirect error.

**Symptoms:**
- "Invalid redirect_uri"
- "Client not found"

**Checklist:**
1. ✅ Rauthy running: `docker compose ps`
2. ✅ Client created in Rauthy admin panel
3. ✅ Redirect URI matches exactly: `http://localhost:4000/auth/user/oidc/callback`
4. ✅ OIDC_CLIENT_SECRET in .env
5. ✅ App restarted after .env update

**Debug:**
```bash
# Check Rauthy logs
docker compose logs rauthy

# Check app logs for OIDC errors
mix phx.server
```

### 4. Full-Text Search Not Working

**Issue:** Search returns no results.

**Symptoms:**
- Empty search results
- tsvector not updated

**Solution:**
```sql
-- Check if trigger exists
SELECT tgname FROM pg_trigger WHERE tgrelid = 'members'::regclass;

-- Manually update search_vector (if trigger missing)
UPDATE members SET search_vector = 
  setweight(to_tsvector('simple', first_name), 'A') ||
  setweight(to_tsvector('simple', last_name), 'A');

-- Or recreate trigger
psql mv_dev < priv/repo/migrations/20250912085235_AddSearchVectorToMembers.exs
```

### 5. Docker Build Fails

**Issue:** Production Docker build fails.

**Common causes:**
- Mix dependencies compilation errors
- Asset compilation fails
- Missing environment variables

**Solution:**
```bash
# Clean build cache
docker builder prune

# Build with no cache
docker build --no-cache -t mila:latest .

# Check build logs for specific error
docker build -t mila:latest . 2>&1 | tee build.log
```

### 6. Test Failures After Migration

**Issue:** Tests fail after running new migration.

**Symptoms:**
- `column does not exist`
- `relation does not exist`

**Solution:**
```bash
# Reset test database
MIX_ENV=test mix ash.reset

# Or manually
MIX_ENV=test mix ecto.drop
MIX_ENV=test mix ash.setup

# Run tests again
mix test
```

### 7. Credo/Formatter Conflicts

**Issue:** CI fails with formatting/style issues.

**Solution:**
```bash
# Format all files
mix format

# Check what would change
mix format --check-formatted --dry-run

# Run Credo
mix credo --strict

# Auto-fix some issues
mix credo suggest --format=oneline
```

### 8. CustomFieldValue Value Type Mismatch

**Issue:** CustomFieldValue value doesn't match custom_field definition.

**Error:**
```
"Expected type :integer, got :string"
```

**Solution:**
Ensure custom field value matches custom_field.value_type:

```elixir
# CustomFieldValue Type: value_type = :integer
custom_field = get_custom_field("age")

# CustomFieldValue Value: must be integer union type
{:ok, custom_field_value} = create_custom_field_value(%{
  value: %{type: :integer, value: 25},  # Not "25" as string
  custom_field_id: custom_field.id
})
```

---

## Future Improvements

### Planned Features (Roadmap)

Based on open milestones: https://git.local-it.org/local-it/mitgliederverwaltung/pulls

#### High Priority

1. **Roles & Permissions** 🔐
   - Admin, Treasurer, Member roles
   - Resource-level permissions
   - Ash policies for authorization

2. **Payment Tracking** 💰
   - Payment history
   - Fee calculations
   - Due dates and reminders
   - Import from vereinfacht API

3. **Intuitive Navigation** 🧭
   - Breadcrumbs
   - Better menu structure
   - Search in navigation

#### Medium Priority

4. **Email Communication** 📧
   - Send emails to members
   - Email templates
   - Bulk email (with consent)

5. **Member Self-Service** 👤
   - Members update own data
   - Online application
   - Profile management

6. **Advanced Filtering** 🔍
   - Multi-field filters
   - Saved filter presets
   - Export filtered results

7. **Accessibility Improvements** ♿
   - WCAG 2.1 AA compliance
   - Screen reader optimization
   - Keyboard navigation
   - High contrast mode

#### Low Priority

8. **Document Management** 📄
   - Attach files to members
   - Document templates
   - Digital signatures

9. **Reporting & Analytics** 📊
   - Membership statistics
   - Payment reports
   - Custom reports

10. **Staging Environment** 🔧
    - Separate staging server
    - Automated deployments
    - Preview branches

### Technical Debt

1. **German Stemming for Search**
   - Current: Simple lexer
   - Needed: German language support in full-text search
   - Library: `ts_german` or Snowball

2. **Performance Optimization**
   - Add more indexes based on query patterns
   - Optimize N+1 queries (use Ash preloading)
   - Lazy loading for large datasets

3. **Error Handling Improvements**
   - Better user-facing error messages
   - Error tracking (Sentry integration?)
   - Graceful degradation

4. **Test Coverage**
   - Current: ~70% (estimated)
   - Goal: >85%
   - Focus: Email sync edge cases, validation logic

5. **Documentation**
   - User manual
   - Admin guide
   - API documentation (if needed)
   - Video tutorials

### Infrastructure Improvements

1. **Monitoring**
   - Application metrics (Prometheus?)
   - Error tracking
   - Performance monitoring

2. **Backup Strategy**
   - Automated database backups
   - Point-in-time recovery
   - Backup testing

3. **Scalability**
   - Database connection pooling
   - Caching strategy (ETS, Redis?)
   - CDN for assets

4. **Security Hardening**
   - Rate limiting
   - CSRF protection (already enabled)
   - Security headers
   - Regular security audits

---

## Team Knowledge Base

### Key Contacts & Resources

**Repository:** https://git.local-it.org/local-it/mitgliederverwaltung  
**CI/CD:** https://drone.dev.local-it.cloud/local-it/mitgliederverwaltung  
**Issues:** https://git.local-it.org/local-it/mitgliederverwaltung/-/issues  
**Pull Requests:** https://git.local-it.org/local-it/mitgliederverwaltung/pulls

### Development Conventions

#### Commit Messages

Follow conventional commits:
```
<type>: <subject>

<body>

<footer>
```

**Types:**
- `feat:` New feature
- `fix:` Bug fix
- `docs:` Documentation
- `style:` Formatting
- `refactor:` Code refactoring
- `test:` Tests
- `chore:` Maintenance

**Example:**
```
feat: add email synchronization for user-member links

Implement bidirectional email sync between users and members.
User.email is source of truth on initial link, then syncs both ways.

Closes #167
```

#### Branch Naming

```
feature/<issue-number>-<short-description>
bugfix/<issue-number>-<short-description>
chore/<description>
```

**Examples:**
- `feature/167-email-sync`
- `bugfix/152-sorting-header`
- `chore/update-dependencies`

#### Pull Request Template

```markdown
## Description
[Brief description of changes]

## Related Issue
Closes #[issue number]

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Testing
- [ ] Tests pass locally
- [ ] New tests added
- [ ] Manual testing completed

## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No new warnings
```

### Useful Commands Cheat Sheet

```bash
# Development
just run                    # Start everything
just test                   # Run tests
just lint                   # Check code quality
just audit                  # Security checks
just format                 # Format code

# Database
just reset-database         # Reset DB
just seed-database          # Load seeds
just regen-migrations NAME  # Regenerate migrations

# Ash Framework
mix ash.codegen --name NAME          # Generate migration
mix ash.setup                        # Setup database
mix ash.reset                        # Reset database
mix ash_postgres.rollback -n N       # Rollback N migrations

# Testing
mix test                             # All tests
mix test --cover                     # With coverage
mix test test/path/to/test.exs       # Specific file
mix test test/path/to/test.exs:42    # Specific line

# Code Quality
mix format                           # Format all
mix format --check-formatted         # Check format
mix credo                            # Linting
mix credo --strict                   # Strict mode
mix sobelow --config                 # Security scan
mix deps.audit                       # Dependency audit

# Docker
docker compose up -d                 # Start services
docker compose logs -f app           # Follow logs
docker compose down                  # Stop services
docker compose exec app bash         # Shell into container

# Production
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate"
```

### Debugging Tips

**1. IEx (Interactive Elixir):**
```bash
# Start with IEx
iex -S mix phx.server

# In IEx:
iex> import Ecto.Query
iex> alias Mv.Repo
iex> Repo.all(Mv.Membership.Member) |> IO.inspect()
```

**2. LiveView Debugging:**
```elixir
# In LiveView module
def handle_event("some_event", params, socket) do
  IO.inspect(params, label: "DEBUG PARAMS")
  IO.inspect(socket.assigns, label: "DEBUG ASSIGNS")
  # ...
end
```

**3. Query Debugging:**
```elixir
# Enable query logging
config :logger, level: :debug

# In code
import Ecto.Query
Mv.Repo.all(from m in Mv.Membership.Member, where: m.paid == true)
|> IO.inspect(label: "QUERY RESULT")
```

**4. Ash Debugging:**
```elixir
# Enable Ash debug logging
config :ash, :log_level, :debug

# Inspect changeset errors
case Mv.Membership.create_member(attrs) do
  {:error, error} -> IO.inspect(error, label: "ASH ERROR")
  result -> result
end
```

### Common Gotchas

1. **Ash Actions Must Be Defined**
   - Can't use Ecto directly on Ash resources
   - Always use Ash actions: `Ash.create`, `Ash.update`, etc.

2. **Email Sync Only for Linked Entities**
   - Unlinked users/members don't validate cross-table emails
   - Validation kicks in only when linking

3. **Migrations Must Be Run in Order**
   - Ash migrations depend on resource snapshots
   - Don't skip migrations

4. **LiveView Assigns Are Immutable**
   - Must return new socket: `{:noreply, assign(socket, key: value)}`
   - Can't mutate: `socket.assigns.key = value` ❌

5. **Test Database Must Be Reset After Schema Changes**
   - `MIX_ENV=test mix ash.reset` after migrations

6. **Docker Compose Networks**
   - Dev uses `network_mode: host` for Rauthy access
   - Prod should use proper Docker networks

7. **Secrets in runtime.exs, Not config.exs**
   - `config.exs` is compile-time
   - `runtime.exs` is runtime (for env vars)

### Learning Resources

**Ash Framework:**
- Official Docs: https://hexdocs.pm/ash/
- AshPostgres: https://hexdocs.pm/ash_postgres/
- AshAuthentication: https://hexdocs.pm/ash_authentication/

**Phoenix Framework:**
- Phoenix Guides: https://hexdocs.pm/phoenix/overview.html
- LiveView Docs: https://hexdocs.pm/phoenix_live_view/

**Tailwind CSS:**
- Tailwind Docs: https://tailwindcss.com/docs
- DaisyUI Components: https://daisyui.com/components/

**PostgreSQL:**
- Full-Text Search: https://www.postgresql.org/docs/current/textsearch.html
- Constraints: https://www.postgresql.org/docs/current/ddl-constraints.html

---

## Session: Bulk Email Copy Feature (2025-12-02)

### Feature Summary
Implemented bulk email copy functionality for selected members (#230). Users can select members and copy their email addresses to clipboard.

**Key Features:**
- Copy button appears only when visible members are selected
- Email format: `First Last <email>` with semicolon separator (email client compatible)
- Button shows count of visible selected members (respects search/filter)
- CopyToClipboard JavaScript hook with clipboard API + fallback for older browsers
- Bilingual UI (English/German)

### Key Decisions

1. **Email Format:** "First Last <email>" with semicolon - standard for all major email clients
2. **Visible Member Count:** Button shows only visible selected members, not total selected (better UX when filtering)
3. **Server→Client:** Used `push_event/3` - server formats data, client handles clipboard

### Files Changed
- `lib/mv_web/live/member_live/index.ex` - Event handler, helper function
- `lib/mv_web/live/member_live/index.html.heex` - Copy button
- `assets/js/app.js` - CopyToClipboard hook
- `test/mv_web/member_live/index_test.exs` - 9 new tests
- `priv/gettext/de/LC_MESSAGES/default.po` - German translations

---

## Session: User-Member Linking UI Enhancement (2025-01-13)

### Feature Summary
Implemented user-member linking functionality in User Edit/Create views with fuzzy search autocomplete, email conflict handling, and accessibility support.

**Key Features:**
- Autocomplete dropdown with PostgreSQL Trigram fuzzy search
- Keyboard navigation (Arrow keys, Enter, Escape)
- Link/unlink members to user accounts
- Email synchronization between linked entities
- WCAG 2.1 AA compliant (ARIA labels, keyboard accessibility)
- Bilingual UI (English/German)

### Technical Decisions

**1. Search Priority Logic**
Search query takes precedence over email filtering to provide better UX:
- User types → fuzzy search across all unlinked members
- Email matching only used for post-filtering when no search query present

**2. JavaScript Hook for Input Value**
Used minimal JavaScript (~6 lines) for reliable input field updates:
```javascript
// assets/js/app.js
window.addEventListener("phx:set-input-value", (e) => {
  document.getElementById(e.detail.id).value = e.detail.value
})
```
**Rationale:** LiveView DOM patching has race conditions with rapid state changes in autocomplete components. Direct DOM manipulation via `push_event` is the idiomatic LiveView solution for this edge case.

**3. Keyboard Navigation: Hybrid Approach**
Implemented keyboard accessibility with **mostly Server-Side + minimal Client-Side**:

```elixir
# Server-Side: Navigation and Selection (~45 lines)
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
  # Focus management on server
  new_index = min(current + 1, max_index)
  {:noreply, assign(socket, focused_member_index: new_index)}
end
```

```javascript
// Client-Side: Only preventDefault for Enter in forms (~13 lines)
Hooks.ComboBox = {
  mounted() {
    this.el.addEventListener("keydown", (e) => {
      const isDropdownOpen = this.el.getAttribute("aria-expanded") === "true"
      if (e.key === "Enter" && isDropdownOpen) {
        e.preventDefault()  // Prevent form submission
      }
    })
  }
}
```

**Rationale:** 
- Server-Side handles all navigation logic → simpler, testable, follows LiveView best practices
- Client-Side only prevents browser default behavior (form submit on Enter)
- Latency (~20-50ms) is imperceptible for keyboard events without DB queries
- Follows CODE_GUIDELINES "Minimal JavaScript Philosophy"

**Alternative Considered:** Full Client-Side with JavaScript Hook (~80 lines)
- ❌ More complex code
- ❌ State synchronization between client/server
- ✅ Zero latency (but not noticeable in practice)
- **Decision:** Server-Side approach is simpler and sufficient

**4. Fuzzy Search Implementation**
Combined PostgreSQL Full-Text Search + Trigram for optimal results:
```sql
-- FTS for exact word matching
search_vector @@ websearch_to_tsquery('simple', 'greta')
-- Trigram for typo tolerance
word_similarity('gre', first_name) > 0.2
-- Substring for email/IDs
email ILIKE '%greta%'
```

### Key Learnings

#### 1. Ash `manage_relationship` Internals
**Critical Discovery:** During validation, relationship data lives in `changeset.relationships`, NOT `changeset.attributes`:

```elixir
# During validation (manage_relationship processing):
changeset.relationships.member = [{[%{id: "uuid"}], opts}]
changeset.attributes.member_id = nil  # Still nil!

# After action completes:
changeset.attributes.member_id = "uuid"  # Now set
```

**Solution:** Extract member_id from both sources:
```elixir
defp get_member_id_from_changeset(changeset) do
  case Map.get(changeset.relationships, :member) do
    [{[%{id: id}], _opts}] -> id           # New link
    _ -> Ash.Changeset.get_attribute(changeset, :member_id)  # Existing
  end
end
```

**Impact:** Fixed email validation false positives when linking user+member with identical emails.

#### 2. LiveView + JavaScript Integration Patterns

**When to use JavaScript:**
- ✅ Direct DOM manipulation (autocomplete, input values)
- ✅ Browser APIs (clipboard, geolocation)
- ✅ Third-party libraries
- ✅ Preventing browser default behaviors (form submit, scroll)

**When NOT to use JavaScript:**
- ❌ Form submissions
- ❌ Simple show/hide logic
- ❌ Server-side data fetching
- ❌ Keyboard navigation logic (can be done server-side efficiently)

**Pattern:**
```elixir
socket |> push_event("event-name", %{key: value})
```
```javascript
window.addEventListener("phx:event-name", (e) => { /* handle */ })
```

**Keyboard Events Pattern:**
For keyboard navigation in forms, use hybrid approach:
- Server handles navigation logic via `phx-window-keydown`
- Minimal hook only for `preventDefault()` to avoid form submit conflicts
- Result: ~13 lines JS vs ~80 lines for full client-side solution

#### 3. PostgreSQL Trigram Search
Requires `pg_trgm` extension with GIN indexes:
```sql
CREATE INDEX members_first_name_trgm_idx 
  ON members USING GIN(first_name gin_trgm_ops);
```
Supports:
- Typo tolerance: "Gret" finds "Greta"
- Partial matching: "Mit" finds "Mitglied"  
- Substring: "exam" finds "example.com"

#### 4. Server-Side Keyboard Navigation Performance
**Challenge:** Concern that server-side keyboard events would feel laggy.

**Reality Check:**
- LiveView roundtrip: ~20-50ms on decent connection
- Human perception threshold: ~100ms
- Result: **Feels instant** in practice

**Why it works:**
```elixir
# Event handler only updates index (no DB queries)
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
  new_index = min(socket.assigns.focused_member_index + 1, max_index)
  {:noreply, assign(socket, focused_member_index: new_index)}
end
```
- No database queries
- No complex computations
- Just state updates → extremely fast

**When to use Client-Side instead:**
- Complex animations (Canvas, WebGL)
- Real-time gaming
- Continuous interactions (drag & drop, drawing)

**Lesson:** Don't prematurely optimize for latency. Server-side is simpler and often sufficient.

#### 5. Test-Driven Development for Bug Fixes
Effective workflow:
1. Write test that reproduces bug (should fail)
2. Implement minimal fix
3. Verify test passes
4. Refactor while green

**Result:** 355 tests passing, 100% backend coverage for new features.

### Files Changed

**Backend:**
- `lib/membership/member.ex` - `:available_for_linking` action with fuzzy search
- `lib/mv/accounts/user/validations/email_not_used_by_other_member.ex` - Relationship change extraction
- `lib/mv_web/live/user_live/form.ex` - Event handlers, state management

**Frontend:**
- `assets/js/app.js` - Input value hook (6 lines) + ComboBox hook (13 lines)
- `lib/mv_web/live/user_live/form.ex` - Keyboard event handlers, focus management
- `priv/gettext/**/*.po` - 10 new translation keys (DE/EN)

**Tests (NEW):**
- `test/membership/member_fuzzy_search_linking_test.exs`
- `test/accounts/user_member_linking_email_test.exs`
- `test/mv_web/user_live/form_member_linking_ui_test.exs`

### Deployment Notes
- **Assets:** Requires `cd assets && npm run build`
- **Database:** No migrations (uses existing indexes)
- **Config:** No changes required

---

## Conclusion

This project demonstrates a modern Phoenix application built with:
- ✅ **Ash Framework** for declarative resources and policies
- ✅ **Phoenix LiveView 1.1.0-rc.3** for real-time, server-rendered UI
- ✅ **Tailwind CSS + DaisyUI** for rapid UI development
- ✅ **PostgreSQL** with advanced features (full-text search, UUIDv7)
- ✅ **Multi-strategy authentication** (Password + OIDC)
- ✅ **Complex business logic** (bidirectional email sync)
- ✅ **Flexible data model** (EAV pattern with union types)

**Key Achievements:**
- 🎯 9+ sprints completed
- 🚀 100+ pull requests merged
- ✅ Core features implemented (CRUD, search, auth, sync, membership fees, roles & permissions)
- ✅ Membership fees system (types, cycles, settings)
- ✅ Role-based access control (RBAC) with 4 permission sets
- ✅ Member field visibility settings
- ✅ Sidebar navigation (WCAG 2.1 AA compliant)
- 📚 Comprehensive documentation
- 🔒 Security-focused (audits, validations, policies)
- 🐳 Docker-ready for self-hosting

**Next Steps:**
- ✅ ~~Implement roles & permissions~~ - RBAC system implemented (2026-01-08)
- Add payment tracking
- ✅ ~~Improve accessibility (WCAG 2.1 AA)~~ - Keyboard navigation implemented
- Member self-service portal
- Email communication features

---

## Recent Updates (2025-12-02 to 2026-01-13)

### Membership Fees System Implementation (2025-12-11 to 2025-12-26)

**PR #283:** *Membership Fee - Database Schema & Ash Domain Foundation* (closes #275)
- Created `Mv.MembershipFees` domain
- Added `MembershipFeeType` resource with intervals (monthly, quarterly, half_yearly, yearly)
- Added `MembershipFeeCycle` resource for individual billing cycles
- Database migrations for membership fee tables

**PR #284:** *Calendar Cycle Calculation Logic* (closes #276)
- Calendar-based cycle calculation module
- Support for different intervals
- Cycle start/end date calculations
- Integration with member joining dates

**PR #290:** *Cycle Generation System* (closes #277)
- Automatic cycle generation for members
- Cycle regeneration when fee type changes
- Integration with member lifecycle hooks
- Actor-based authorization for cycle operations

**PR #291:** *Membership Fee Type Resource & Settings* (closes #278)
- Membership fee type CRUD operations
- Global membership fee settings
- Default fee type assignment
- `include_joining_cycle` setting

**PR #294:** *Cycle Management & Member Integration* (closes #279)
- Member-fee type relationship
- Cycle status tracking (unpaid, paid, suspended)
- Member detail view integration
- Cycle regeneration on fee type change

**PR #304:** *Membership Fee 6 - UI Components & LiveViews* (closes #280)
- Membership fee type management LiveViews
- Membership fee settings LiveView
- Cycle display in member detail view
- Payment status indicators

### Custom Fields Enhancements (2025-12-11 to 2026-01-02)

**PR #266:** *Implements search for custom fields* (closes #196)
- Custom field search in member overview
- Integration with full-text search
- Custom field value filtering

**PR #301:** *Implements validation for required custom fields* (closes #274)
- Required custom field validation
- Form-level validation
- Error messages for missing required fields

**PR #313:** *Fix hidden empty custom fields* (closes #282)
- Fixed display of empty custom fields
- Improved custom field visibility logic

### UI/UX Improvements (2025-12-03 to 2025-12-16)

**PR #240:** *Implement dropdown to show/hide columns in member overview* (closes #209)
- Field visibility dropdown
- User-specific field selection
- Integration with global settings

**PR #247:** *Visual hierarchy for fields in member view and edit form* (closes #231)
- Improved field grouping
- Visual hierarchy improvements
- Better form layout

**PR #250:** *UX - Avoid opening member by clicking the checkbox* (closes #233)
- Checkbox click handling
- Prevented accidental navigation
- Improved selection UX

**PR #259:** *Fix small UI issues* (closes #220)
- Various UI bug fixes
- Accessibility improvements

**PR #293:** *Small UX fixes* (closes #281)
- Additional UX improvements
- Polish and refinement

**PR #319:** *Reduce member fields* (closes #273)
- Removed unnecessary member fields
- Streamlined member data model
- Migration for field removal

### Roles and Permissions System (2026-01-06 to 2026-01-08)
- ✅ **RBAC Implementation Complete** - Member Resource Policies (#345)
  - Four hardcoded permission sets: `own_data`, `read_only`, `normal_user`, `admin`
  - Role-based access control with database-backed roles
  - Member resource policies with scope filtering (`:own`, `:linked`, `:all`)
  - Authorization checks via `Mv.Authorization.Checks.HasPermission`
  - System role protection (cannot delete critical roles)
  - Comprehensive test coverage

### Actor Handling Refactoring (2026-01-09)
- ✅ **Consistent Actor Access** - `current_actor/1` helper function
  - Standardized actor access across all LiveViews
  - `ash_actor_opts/1` helper for consistent authorization options
  - `submit_form/3` wrapper for form submissions with actor
  - All Ash operations now properly pass `actor` parameter
  - Error handling improvements (replaced bang calls with proper error handling)

### Internationalization Improvements (2026-01-13)
- ✅ **Complete German Translations** - All UI strings translated
  - CI check for empty German translations in lint task
  - Standardized English `msgstr` entries (all empty for consistency)
  - Corrected language headers in `.po` files
  - Added missing translations for error messages

### Code Quality Improvements (2026-01-13)
- ✅ **Error Handling** - Replaced `Ash.read!` with proper error handling
- ✅ **Code Complexity** - Reduced nesting depth in `UserLive.Form`
- ✅ **Test Infrastructure** - Role tag support in `ConnCase`

### CSV Import Feature (2026-01-13)
- ✅ **CSV Templates** - Member import templates (#329)
  - German and English CSV templates
  - Template files in `priv/static/templates/`

### Sidebar Implementation (2026-01-12)
- ✅ **Sidebar Navigation** - Replaced navbar with sidebar (#260)
  - Standard-compliant sidebar with comprehensive tests
  - DaisyUI drawer pattern implementation
  - Desktop expanded/collapsed states
  - Mobile overlay drawer
  - localStorage persistence for sidebar state
  - WCAG 2.1 Level AA compliant

### Member Field Settings (2026-01-12, PR #300, closes #223)
- ✅ **Member Field Visibility Configuration** - Global settings for field visibility
  - JSONB-based visibility configuration in Settings resource
  - Per-field visibility toggle (show/hide in member overview)
  - Atomic updates for single field visibility changes
  - Integration with member list overview
  - User-specific field selection (takes priority over global settings)
  - Custom field visibility support
  - Default visibility: all fields visible except `exit_date` (hidden by default)
  - LiveComponent for managing member field visibility in settings page

---

---

## Recent Updates (2026-01-13 to 2026-01-27)

### Groups Feature Implementation (2026-01-27)

**PR #378:** *Add groups resource* (closes #371)
- Created `Mv.Membership.Group` resource with name, slug, description
- Created `Mv.Membership.MemberGroup` join table for many-to-many relationship
- Automatic slug generation from name (immutable after creation)
- Case-insensitive name uniqueness via LOWER(name) index
- Database migration: `20260127141620_add_groups_and_member_groups.exs`

**PR #382:** *Groups Admin UI* (closes #372)
- Groups management LiveViews (`/groups`)
- Create, edit, delete groups with confirmation
- Member count display per group
- Add/remove members from groups
- Groups displayed in member overview and detail views
- Filter and sort by groups in member list

**Key Features:**
- Many-to-many relationship: Members can belong to multiple groups
- Groups searchable via member search vector (full-text search)
- CASCADE delete: Removing member/group removes associations
- Unique constraint prevents duplicate member-group associations

### CSV Import Feature Implementation (2026-01-27)

**PR #359:** *Implements CSV Import UI* (closes #335)
- Import/Export LiveView (`/import_export`)
- CSV file upload with auto-upload
- Real-time import progress tracking
- Error and warning reporting
- Chunked processing (200 rows per chunk)

**PR #394:** *Adds config for import limits* (closes #336)
- Configurable maximum file size (default: 10 MB)
- Configurable maximum rows (default: 1000)
- Configuration via `config :mv, csv_import: [max_file_size_mb: ..., max_rows: ...]`
- UI displays limits to users

**PR #395:** *Implements custom field CSV import* (closes #338)
- Support for importing custom field values via CSV
- Custom field mapping by slug or name
- Validation of custom field value types
- Error reporting with line numbers and field names
- CSV templates (German and English) available for download

**Key Features:**
- Member field import (email, first_name, last_name, etc.)
- Custom field value import (all types: string, integer, boolean, date, email)
- Error capping (max 50 errors per import to prevent memory issues)
- Async chunk processing with progress updates
- Admin-only access (requires `:create` permission on Member resource)

### Page Permission Router Plug (2026-01-27)

**PR #390:** *Page Permission Router Plug* (closes #388)
- `MvWeb.Plugs.CheckPagePermission` plug for page-level authorization
- Route-based permission checking
- Automatic redirects for unauthorized access
- Integration with permission sets (own_data, read_only, normal_user, admin)
- Documentation: `docs/page-permission-route-coverage.md`

**Key Features:**
- Page-level access control before LiveView mount
- Permission set-based route matrix
- Redirect targets for different permission levels
- Public paths (login, OIDC callbacks) excluded from checks

### Resource Policies Implementation (2026-01-27)

**PR #387:** *CustomField Resource Policies* (closes #386)
- CustomField resource policies with actor-based authorization
- Admin-only create/update/destroy operations
- Read access for authenticated users
- No system-actor fallback (explicit actor required)

**PR #377:** *CustomFieldValue Resource Policies* (closes #369)
- CustomFieldValue resource policies
- own_data permission set: can create/update own linked member's custom field values
- Admin and normal_user: full access
- Bypass read rule for CustomFieldValue pattern (documented)

**PR #364:** *User Resource Policies* (closes #363)
- User resource policies with scope filtering
- own_data: can read/update own user record
- Admin: full access
- Email change validation for linked members

### System Actor Improvements (2026-01-27)

**PR #379:** *Fix System missing system actor in prod and prevent deletion*
- System actor user creation in migrations
- Block update/destroy on system-actor user
- System user handling in UserLive forms
- Normalize system actor email

**PR #361:** *System Actor Mode for Systemic Flows* (closes #348)
- System actor pattern for systemic operations
- Email synchronization uses system actor
- Cycle generation uses system actor
- Documentation: `docs/roles-and-permissions-architecture.md` (Authorization Bootstrap Patterns)

**PR #367:** *Remove NoActor bypass*
- Removed NoActor bypass to prevent masking authorization bugs
- All tests now require explicit actor
- Exception: AshAuthentication bypass tests (conscious exception)

### Email Sync Fixes (2026-01-27)

**PR #380:** *Fix email sync (user->member) when changing password and email*
- Email sync when admin sets password via `admin_set_password`
- Bidirectional email synchronization improvements
- Validation fixes for linked user-member pairs

### UI/UX Improvements (2026-01-27)

**PR #389:** *Change Logo* (closes #385)
- Updated application logo
- Logo display in sidebar and navigation

**PR #362:** *Add boolean custom field filters to member overview* (closes #309)
- Boolean custom field filtering in member list
- Filter by true/false values
- Integration with existing filter system

### Test Performance Optimization (2026-01-27)

**PR #384:** *Minor test refactoring to improve on performance* (closes #383)
- Moved slow tests to nightly test suite
- Optimized policy tests
- Reduced test complexity in seeds tests
- Documentation: `docs/test-performance-optimization.md`

**Key Changes:**
- Fast tests (standard CI): Business logic, validations, data persistence
- Slow tests (nightly): Performance tests, large datasets, query optimization
- UI tests: Basic HTML rendering, navigation, translations

---

**Document Version:** 1.5  
**Last Updated:** 2026-01-27  
**Maintainer:** Development Team  
**Status:** Living Document (update as project evolves)

