Merge remote-tracking branch 'origin/main' into feature/308-web-form
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/production Build is failing

This commit is contained in:
Simon 2026-03-12 13:52:33 +01:00
commit 4af80a8305
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
16 changed files with 9624 additions and 9715 deletions

View file

@ -219,24 +219,8 @@ trigger:
- main - main
event: event:
- push - push
- tag
steps: steps:
- name: build-and-publish-container
image: plugins/docker
settings:
registry: git.local-it.org
repo: git.local-it.org/local-it/mitgliederverwaltung
username:
from_secret: DRONE_REGISTRY_USERNAME
password:
from_secret: DRONE_REGISTRY_TOKEN
auto_tag: true
auto_tag_suffix: ${DRONE_COMMIT_SHA:0:8}
when:
event:
- tag
- name: build-and-publish-container-branch - name: build-and-publish-container-branch
image: plugins/docker image: plugins/docker
settings: settings:
@ -256,6 +240,33 @@ steps:
depends_on: depends_on:
- check-fast - check-fast
---
kind: pipeline
type: docker
name: build-and-release
trigger:
event:
- tag
steps:
- name: build-and-publish-container
image: plugins/docker
settings:
registry: git.local-it.org
repo: git.local-it.org/local-it/mitgliederverwaltung
username:
from_secret: DRONE_REGISTRY_USERNAME
password:
from_secret: DRONE_REGISTRY_TOKEN
auto_tag: true
when:
event:
- tag
depends_on:
- check-fast
--- ---
kind: pipeline kind: pipeline
type: docker type: docker

View file

@ -61,7 +61,7 @@ We are building a membership management system (Mila) using the following techno
8. [Accessibility Guidelines](#8-accessibility-guidelines) 8. [Accessibility Guidelines](#8-accessibility-guidelines)
**Related documents:** **Related documents:**
- **UI / UX:** [`DESIGN_DUIDELINES.md`](../DESIGN_DUIDELINES.md) defines visual and interaction consistency: use of CoreComponents (no raw DaisyUI in views), page skeleton (`<.header>`, `mt-6 space-y-6`), **Back button left in header for edit/new forms** (§2.2), typography, buttons, forms, tables, flash/toast, and microcopy (e.g. German "du" and glossary). Follow "components first" and semantic variants instead of hard-coded colors. - **UI / UX:** [`DESIGN_GUIDELINES.md`](../DESIGN_GUIDELINES.md) defines visual and interaction consistency: use of CoreComponents (no raw DaisyUI in views), page skeleton (`<.header>`, `mt-6 space-y-6`), **Back button left in header for edit/new forms** (§2.2), typography, buttons, forms, tables, flash/toast, and microcopy (e.g. German "du" and glossary). Follow "components first" and semantic variants instead of hard-coded colors.
- **Vereinfacht API:** [`docs/vereinfacht-api.md`](docs/vereinfacht-api.md) describes the finance-contact sync (find by email filter, minimal create payload, no extra required member fields). - **Vereinfacht API:** [`docs/vereinfacht-api.md`](docs/vereinfacht-api.md) describes the finance-contact sync (find by email filter, minimal create payload, no extra required member fields).
--- ---
@ -1339,13 +1339,16 @@ dgettext("auth", "Sign in with email")
**Extract and Merge:** **Extract and Merge:**
```bash ```bash
# Extract new translatable strings # Extract new translatable strings and merge into existing .po files (recommended)
mix gettext.extract mix gettext.extract --merge
# Merge into existing translations # Alternative: extract only, then merge separately
mix gettext.extract
mix gettext.merge priv/gettext --on-obsolete=mark_as_obsolete mix gettext.merge priv/gettext --on-obsolete=mark_as_obsolete
``` ```
**Gettext merge workflow:** Prefer `mix gettext.extract --merge` so the `.pot` template is regenerated from source and merged into all locale `.po` files in one step. Edit only the `msgstr` values in `.po` files for translations; do not manually change source references, entry order, or the `.pot` file structure. If Git merge conflicts appear in `.po` or `.pot` files, resolve by removing conflict markers (keeping both sides where appropriate), then run `mix gettext.extract --merge`. If the `.pot` file is corrupted, delete it and run `mix gettext.extract --merge` to regenerate it from source.
### 3.13 Task Runner: Just ### 3.13 Task Runner: Just
**Common Commands:** **Common Commands:**

View file

@ -2,12 +2,12 @@
**Mila** — simple, usable, self-hostable membership management for small to mid-sized clubs. **Mila** — simple, usable, self-hostable membership management for small to mid-sized clubs.
[![Build Status](https://drone.dev.local-it.cloud/api/badges/local-it/mitgliederverwaltung/status.svg)](https://drone.dev.local-it.cloud/local-it/mitgliederverwaltung) [![Build Status](https://drone.cicd.local-it.cloud/api/badges/local-it/mitgliederverwaltung/status.svg)](https://drone.cicd.local-it.cloud/local-it/mitgliederverwaltung)
![License](https://img.shields.io/badge/license-AGPL--v3-blue) ![License](https://img.shields.io/badge/license-AGPL--v3-blue)
## 🚧 Project Status ## 🚧 Project Status
⚠️ **Early development** — not production-ready. Expect breaking changes. ⚠️ **First Version** — Expect breaking changes.
Contributions and feedback are welcome! Contributions and feedback are welcome!
## ✨ Overview ## ✨ Overview
@ -48,9 +48,10 @@ You can find our documentation for users here: https://wiki.local-it.org/s/mila-
- ✅ SSO via OIDC (works with Authentik, Rauthy, Keycloak, etc.) - ✅ SSO via OIDC (works with Authentik, Rauthy, Keycloak, etc.)
- ✅ Sidebar navigation (standard-compliant, accessible) - ✅ Sidebar navigation (standard-compliant, accessible)
- ✅ Global settings management - ✅ Global settings management
- 🚧 Self-service & online application - Self-service & online application
- ✅ Accessibility improvements (WCAG 2.1 AA compliant keyboard navigation) - ✅ Accessibility improvements (WCAG 2.1 AA compliant keyboard navigation)
- 🚧 Email sending - ✅ Email sending
- ✅ Integration of Accounting-Software ([Vereinfacht](https://github.com/vereinfacht/vereinfacht))
## 🚀 Quick Start (Development) ## 🚀 Quick Start (Development)
@ -173,12 +174,6 @@ The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/oidc/ca
## ⚙️ Configuration ## ⚙️ Configuration
- **Env vars:** see `.env.example` - **Env vars:** see `.env.example`
- `OIDC_CLIENT_SECRET` — secret for your OIDC client
- Database defaults (Docker Compose):
- Host: `localhost`
- Port: `5000`
- User/pass: `postgres` / `postgres`
- DB: `mila_dev`
## 🏗️ Architecture ## 🏗️ Architecture
@ -193,6 +188,8 @@ The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/oidc/ca
- `lib/mv_web/` — Phoenix controllers, LiveViews, components - `lib/mv_web/` — Phoenix controllers, LiveViews, components
- `lib/mv/` — Shared helpers and business logic - `lib/mv/` — Shared helpers and business logic
- `assets/` — Tailwind, JavaScript, static files - `assets/` — Tailwind, JavaScript, static files
- `test/` — All tests
📚 **Full tech stack details:** See [`CODE_GUIDELINES.md`](CODE_GUIDELINES.md) 📚 **Full tech stack details:** See [`CODE_GUIDELINES.md`](CODE_GUIDELINES.md)
📖 **Implementation history:** See [`docs/development-progress-log.md`](docs/development-progress-log.md) 📖 **Implementation history:** See [`docs/development-progress-log.md`](docs/development-progress-log.md)
@ -228,42 +225,19 @@ For testing the production Docker build locally:
# Copy template and edit # Copy template and edit
cp .env.example .env cp .env.example .env
nano .env nano .env
# Required variables:
SECRET_KEY_BASE=<your-generated-secret>
TOKEN_SIGNING_SECRET=<your-generated-secret>
DOMAIN=localhost # or PHX_HOST=localhost
# Optional OIDC configuration:
# OIDC_CLIENT_ID=mv
# OIDC_BASE_URL=http://localhost:8080/auth/v1
# OIDC_CLIENT_SECRET=<from-your-oidc-provider>
# OIDC_REDIRECT_URI is auto-generated as https://{DOMAIN}/auth/user/oidc/callback
# Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars):
# SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base
# TOKEN_SIGNING_SECRET_FILE=/run/secrets/token_signing_secret
# OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret
# DATABASE_URL_FILE=/run/secrets/database_url
# DATABASE_PASSWORD_FILE=/run/secrets/database_password
``` ```
3. **Start development environment** (for Rauthy): 3. **Start production environment:**
```bash
docker compose up -d
```
4. **Start production environment:**
```bash ```bash
docker compose -f docker-compose.prod.yml up docker compose -f docker-compose.prod.yml up
``` ```
5. **Database migrations run automatically** on app start. For manual migration: 4. **Database migrations run automatically** on app start. For manual migration:
```bash ```bash
docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate" docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate"
``` ```
6. **Access the production app:** 5. **Access the production app:**
- Production App: http://localhost:4001 - Production App: http://localhost:4001
- Uses same Rauthy instance as dev (localhost:8080) - Uses same Rauthy instance as dev (localhost:8080)
@ -286,9 +260,9 @@ For actual production deployment:
## 🤝 Contributing ## 🤝 Contributing
We welcome contributions! We welcome contributions!
- Open issues and PRs in this repo. - Open issues and PRs in this repo
- Please follow existing code style and conventions. - Please follow existing code style and conventions
- Expect breaking changes while the project is in early development. - Expect breaking changes while the project is in early development
## 📄 License ## 📄 License
@ -298,4 +272,4 @@ See the [LICENSE](LICENSE) file for details.
## 📬 Contact ## 📬 Contact
- Issues: [GitLab Issues](https://git.local-it.org/local-it/mitgliederverwaltung/-/issues) - Issues: [GitLab Issues](https://git.local-it.org/local-it/mitgliederverwaltung/-/issues)
- Community links: coming soon. - E-Mail: info@local-it.org

View file

@ -195,7 +195,7 @@
- Auto-dismiss: info/success 46s, warning 68s, error 812s; dismiss button kept for accessibility. - Auto-dismiss: info/success 46s, warning 68s, error 812s; dismiss button kept for accessibility.
- Implement via JS hook (e.g. `FlashAutoDismiss`) + `data-dismiss-ms` (or `data-kind`) on flash component; on timeout push `lv:clear-flash` and hide element. - Implement via JS hook (e.g. `FlashAutoDismiss`) + `data-dismiss-ms` (or `data-kind`) on flash component; on timeout push `lv:clear-flash` and hide element.
- LiveView: add shared `handle_event("lv:clear-flash", %{"key" => key}, socket)` (e.g. in `MvWeb` live_view quote) calling `clear_flash(socket, key)`. - LiveView: add shared `handle_event("lv:clear-flash", %{"key" => key}, socket)` (e.g. in `MvWeb` live_view quote) calling `clear_flash(socket, key)`.
- All flashes (including “Email copied”) use the same variants (info, success, warning, error); no special tone. See `DESIGN_DUIDELINES.md` §9. - All flashes (including “Email copied”) use the same variants (info, success, warning, error); no special tone. See `DESIGN_GUIDELINES.md` §9.
--- ---

View file

@ -442,7 +442,7 @@ defmodule MvWeb.GlobalSettingsLive do
</.form_section> </.form_section>
<%!-- Vereinfacht Integration Section --%> <%!-- Vereinfacht Integration Section --%>
<.form_section title={gettext("Vereinfacht Integration")}> <.form_section title={gettext("Accounting-Software (Vereinfacht) Integration")}>
<%= if @vereinfacht_env_configured do %> <%= if @vereinfacht_env_configured do %>
<p class="text-sm text-base-content/70 mb-4"> <p class="text-sm text-base-content/70 mb-4">
{gettext("Some values are set via environment variables. Those fields are read-only.")} {gettext("Some values are set via environment variables. Those fields are read-only.")}
@ -550,7 +550,7 @@ defmodule MvWeb.GlobalSettingsLive do
</.form> </.form>
</.form_section> </.form_section>
<%!-- OIDC Section --%> <%!-- OIDC Section --%>
<.form_section title={gettext("OIDC")}> <.form_section title={gettext("OIDC (Single Sign-On)")}>
<%= if @oidc_env_configured do %> <%= if @oidc_env_configured do %>
<p class="text-sm text-base-content/70 mb-4"> <p class="text-sm text-base-content/70 mb-4">
{gettext("Some values are set via environment variables. Those fields are read-only.")} {gettext("Some values are set via environment variables. Those fields are read-only.")}

View file

@ -28,15 +28,6 @@ defmodule MvWeb.ImportLive.Components do
"Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import." "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, because unknown data field columns will be ignored. Groups and membership fees are not supported for import."
)} )}
</p> </p>
<p class="text-sm">
<.link
href={~p"/settings#custom_fields"}
class="link"
data-testid="custom-fields-link"
>
{gettext("Manage Member Data")}
</.link>
</p>
</div> </div>
</div> </div>
""" """

View file

@ -230,7 +230,7 @@ defmodule MvWeb.MemberLive.Show do
<%!-- Custom Fields Section --%> <%!-- Custom Fields Section --%>
<%= if Enum.any?(@custom_fields) do %> <%= if Enum.any?(@custom_fields) do %>
<div> <div>
<.section_box title={gettext("Custom Fields")}> <.section_box title={gettext("Individual datafields")}>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<%= for custom_field <- @custom_fields do %> <%= for custom_field <- @custom_fields do %>
<% cfv = find_custom_field_value(@member.custom_field_values, custom_field.id) %> <% cfv = find_custom_field_value(@member.custom_field_values, custom_field.id) %>

View file

@ -2,7 +2,7 @@
<.header> <.header>
{gettext("Listing Roles")} {gettext("Listing Roles")}
<:subtitle> <:subtitle>
{gettext("Manage user roles and their permission sets.")} {gettext("Manage roles and their permission sets.")}
</:subtitle> </:subtitle>
<:actions> <:actions>
<%= if can?(@current_user, :create, Mv.Authorization.Role) do %> <%= if can?(@current_user, :create, Mv.Authorization.Role) do %>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -78,7 +78,7 @@ defmodule MvWeb.Components.MemberFilterComponentTest do
html = render(view) html = render(view)
# Should have both "Payments" and "Custom Fields" group labels # Should have both "Payments" and "Custom Fields" group labels
assert html =~ gettext("Payments") || html =~ "Payment" assert html =~ gettext("Payments") || html =~ "Payment"
assert html =~ gettext("Custom Fields") assert html =~ gettext("Individual datafields")
end end
test "renders only payment filter when no boolean custom fields exist", %{conn: conn} do test "renders only payment filter when no boolean custom fields exist", %{conn: conn} do

View file

@ -77,7 +77,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Edit mode: section titles must not reappear when modal opens (regression) # Edit mode: section titles must not reappear when modal opens (regression)
refute has_element?(view, "h2", "Member fields") refute has_element?(view, "h2", "Member fields")
refute has_element?(view, "h2", "Custom fields") refute has_element?(view, "h2", "Individual datafields")
# Should show correct member count (1 member) # Should show correct member count (1 member)
assert render(view) =~ "1 member has a value assigned for this datafield" assert render(view) =~ "1 member has a value assigned for this datafield"

View file

@ -238,7 +238,6 @@ defmodule MvWeb.ImportLiveTest do
assert has_element?(view, "[data-testid='import-page']") assert has_element?(view, "[data-testid='import-page']")
assert has_element?(view, "[data-testid='csv-upload-form']") assert has_element?(view, "[data-testid='csv-upload-form']")
assert has_element?(view, "[data-testid='start-import-button']") assert has_element?(view, "[data-testid='start-import-button']")
assert has_element?(view, "[data-testid='custom-fields-link']")
end end
test "template links and file input are present", %{conn: conn} do test "template links and file input are present", %{conn: conn} do

View file

@ -52,7 +52,7 @@ defmodule MvWeb.MemberLive.ShowTest do
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
# Custom Fields section should be visible # Custom Fields section should be visible
assert html =~ gettext("Custom Fields") assert html =~ gettext("Individual datafields")
# Custom field label should be visible # Custom field label should be visible
assert html =~ custom_field.name assert html =~ custom_field.name
@ -97,7 +97,7 @@ defmodule MvWeb.MemberLive.ShowTest do
{:ok, _view, html} = live(conn, ~p"/members/#{member}") {:ok, _view, html} = live(conn, ~p"/members/#{member}")
# Custom Fields section should be visible # Custom Fields section should be visible
assert html =~ gettext("Custom Fields") assert html =~ gettext("Individual datafields")
# Both field labels should be visible # Both field labels should be visible
assert html =~ field1.name assert html =~ field1.name