mitgliederverwaltung/docs/development-progress-log.md
Moritz 6a9229c54f
All checks were successful
continuous-integration/drone/push Build is passing
chore: update docs
2026-01-13 23:38:15 +01:00

53 KiB

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
  2. Setup and Foundation
  3. Major Features Implementation
  4. Implementation Decisions
  5. Build and Deployment
  6. Testing Strategy
  7. Common Issues and Solutions
  8. Future Improvements
  9. 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.

Historical context:

1. Phoenix Project Initialization (Sprint 0)

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.

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:

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:

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


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
# 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 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:

# 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.

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 for decision tree and sync rules.

4. CustomFieldValue System (EAV Pattern)

Implementation: Entity-Attribute-Value pattern with union types

# 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:

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)

-- 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):

# 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

# 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

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.

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 and README.md - Testing SSO.

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 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

For detailed guidelines, see 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.

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.


Testing Strategy

Test Coverage Areas

1. Unit Tests (Domain Logic)

Example: Member Email Validation

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

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

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

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

Test Data Management

Seed Data:

  • Admin user: admin@mv.local / testpassword
  • Sample members: Hans Müller, Greta Schmidt, Friedrich Wagner
  • Linked accounts: Maria Weber, Thomas Klein
  • CustomFieldValue types: String, Date, Boolean, Email

Test Helpers:

# 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.


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:

# 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/rauthy/callback
  4. OIDC_CLIENT_SECRET in .env
  5. App restarted after .env update

Debug:

# 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:

-- 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:

# 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:

# 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:

# 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:

# 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

  1. Email Communication 📧

    • Send emails to members
    • Email templates
    • Bulk email (with consent)
  2. Member Self-Service 👤

    • Members update own data
    • Online application
    • Profile management
  3. Advanced Filtering 🔍

    • Multi-field filters
    • Saved filter presets
    • Export filtered results
  4. Accessibility Improvements

    • WCAG 2.1 AA compliance
    • Screen reader optimization
    • Keyboard navigation
    • High contrast mode

Low Priority

  1. Document Management 📄

    • Attach files to members
    • Document templates
    • Digital signatures
  2. Reporting & Analytics 📊

    • Membership statistics
    • Payment reports
    • Custom reports
  3. 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

## 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

# 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):

# 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:

# 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:

# 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:

# 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:

Phoenix Framework:

Tailwind CSS:

PostgreSQL:


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 " 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:

// 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:

# 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
// 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:

-- 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:

# 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:

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:

socket |> push_event("event-name", %{key: value})
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

Requires pg_trgm extension with GIN indexes:

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:

# 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

Document Version: 1.4
Last Updated: 2026-01-13
Maintainer: Development Team
Status: Living Document (update as project evolves)