add claude.md

This commit is contained in:
2026-03-31 17:03:43 +02:00
parent 93748ba594
commit e79826d37c

227
CLAUDE.md Normal file
View File

@@ -0,0 +1,227 @@
# Qaffee by Aquantico Projektübersicht
> **Wichtig:** Diese Datei bei jeder relevanten Änderung aktuell halten —
> neue Endpoints, Datenbankmigrationen, Routen, Komponenten oder Architekturentscheidungen
> gehören hier dokumentiert.
## Zweck
Digitale Strichliste für eine gemeinsame Kaffeeküche. Mehrere Firmen teilen sich eine Küche;
ein Anbieter (Aquantico) verwaltet Produkte und Firmen. Mitarbeiter strichen ihren Konsum
auf einem iPad, Firmen und der Anbieter werten den Verbrauch monatlich aus.
---
## Stack
| Schicht | Technologie |
|---|---|
| Backend | Quarkus 3.17.7, Java 21, RESTEasy Reactive, Hibernate ORM Panache, Flyway |
| Frontend | SvelteKit 2, Svelte 5, TypeScript, adapter-node |
| Datenbank | MariaDB 11.4 |
| Deployment | Docker Compose |
| iPad | PWA (manifest.json, apple-mobile-web-app-capable) |
---
## Projektstruktur
```
Strichliste/
├── backend/ Quarkus-Backend
│ ├── Dockerfile Multi-Stage Build (maven → jre)
│ ├── pom.xml Quarkus 3.17.7, flyway-mysql Dependency nötig!
│ └── src/main/
│ ├── java/de/strichliste/
│ │ ├── dto/ Record-DTOs für alle API-Requests/Responses
│ │ ├── entity/ JPA-Entities (PanacheEntityBase + IDENTITY-Strategy!)
│ │ ├── filter/ AuthFilter (JAX-RS NameBinding, Token via ?token=)
│ │ └── resource/
│ │ ├── PublicResource GET/POST für iPad (kein Auth)
│ │ ├── CompanyAdminResource COMPANY_ADMIN Rolle
│ │ └── ProviderAdminResource PROVIDER_ADMIN Rolle
│ └── resources/
│ ├── application.properties
│ └── db/migration/ Flyway SQL-Migrationen (V1V4)
├── frontend/ SvelteKit-Frontend
│ ├── Dockerfile Multi-Stage Build (node build → node run)
│ ├── src/
│ │ ├── hooks.server.ts Proxy: /api/* → http://backend:8080 (kein CORS nötig)
│ │ ├── app.html / app.css Globales Layout & CSS-Variablen (Aquantico-Farben)
│ │ ├── lib/
│ │ │ ├── api/client.ts Typisierter API-Client (alle Endpoints)
│ │ │ └── components/
│ │ │ ├── Header.svelte Globaler Header mit Aquantico-Logo & Zurück-Button
│ │ │ ├── MonthPicker.svelte Monat Jahr Picker (DE), ersetzt input[type=month]
│ │ │ └── EmojiPicker.svelte emoji-picker-element Web Component Wrapper
│ │ └── routes/
│ │ ├── +page.svelte iPad: Firmen-Auswahl
│ │ ├── company/[id]/ iPad: Mitarbeiter-Auswahl
│ │ ├── company/[id]/tally/ iPad: Produkt wählen → Strich → zurück zu /
│ │ ├── admin/company/ Firmen-Admin (Mitarbeiter, Monatsauswertung, Logo)
│ │ └── admin/provider/ Anbieter-Admin (Firmen, Produkte, Report, Links)
│ └── static/
│ ├── aquantico-logo.png Originaldatei: AquanticoLogo_Horizontal.png
│ └── manifest.json PWA-Manifest (Name: "Qaffee by Aquantico")
├── db/
│ ├── backup.sh mysqldump-Cronjob, 7 Tage Rotation
│ └── seed.sql Platzhalter (Schema via Flyway, nicht hier)
├── docker-compose.yml
└── backups/ Automatische DB-Dumps (gitignore)
```
---
## Datenmodell
```
company id, name, active, logo (MEDIUMBLOB), logo_content_type, created_at
employee id, company_id, first_name, last_name, active, created_at
product id, name, price_cents (INT), icon_placeholder (Emoji-Zeichen), active, created_at
tally_entry id, employee_id, product_id, price_cents_at_booking, month_key (YYYY-MM), created_at
access_link id, token (64 chars), role (COMPANY_ADMIN|PROVIDER_ADMIN), company_id, description, active
```
**Wichtige Konventionen:**
- Alle Entities erben von `PanacheEntityBase<Long>` mit `@GeneratedValue(strategy = IDENTITY)`**nicht** `PanacheEntity` (würde SEQUENCE verwenden, MariaDB-inkompatibel)
- Preise immer in **Cent** (Integer), nie als Decimal
- `price_cents_at_booking` in `tally_entry` sichert historische Preise — Buchungen sind unveränderlich
- `active`-Flag statt DELETE (Soft Delete) bei Company, Employee, Product, AccessLink
- `month_key` (z.B. `2026-03`) als denormalisiertes Feld für schnelle Monatsabfragen
---
## Flyway-Migrationen
| Version | Inhalt |
|---|---|
| V1 | Initiales Schema (alle Tabellen) |
| V2 | Seed-Daten (Beispielfirmen, Produkte, Tokens) |
| V3 | `logo`, `logo_content_type` in `company` |
| V4 | `price_cents_at_booking` in `tally_entry` |
Neue Migrationen immer als `V5__beschreibung.sql` anlegen — niemals bestehende Migrationen ändern.
---
## Auth-Konzept
- **iPad-Oberfläche** (`/`, `/company/*`): komplett öffentlich, kein Token
- **Admin-Bereiche**: Token als Query-Parameter `?token=xyz` oder Header `X-Auth-Token`
- Token wird gegen `access_link`-Tabelle geprüft (`AuthFilter.java`, JAX-RS `@NameBinding`)
- Rollen: `COMPANY_ADMIN` (sieht nur eigene Firma), `PROVIDER_ADMIN` (sieht alles)
- Tokens werden vom Anbieter-Admin generiert (`POST /api/admin/provider/access-links`)
**Seed-Tokens (Entwicklung):**
- `provider-admin-token` → Anbieter-Admin
- `company1-admin-token` → Firmen-Admin für Firma 1
- `company2-admin-token` → Firmen-Admin für Firma 2
---
## API-Übersicht
### Öffentlich (kein Auth)
```
GET /api/companies Aktive Firmen
GET /api/companies/{id}/employees Aktive Mitarbeiter einer Firma
GET /api/companies/{id}/logo Firmenlogo (Binär)
GET /api/products Aktive Produkte
POST /api/tally Strich erfassen {employeeId, productId}
GET /api/tally/monthly/{employeeId} Monatsübersicht (?month=YYYY-MM)
```
### Firmen-Admin (`?token=…`, Rolle: COMPANY_ADMIN)
```
GET/POST /api/admin/company/employees
PUT /api/admin/company/employees/{id}
PUT /api/admin/company/employees/{id}/toggle
POST /api/admin/company/logo (multipart/form-data)
DELETE /api/admin/company/logo
GET /api/admin/company/report (?month=YYYY-MM)
GET /api/admin/company/report/employee/{id}
```
### Anbieter-Admin (`?token=…`, Rolle: PROVIDER_ADMIN)
```
GET/POST /api/admin/provider/companies
PUT /api/admin/provider/companies/{id}
PUT /api/admin/provider/companies/{id}/toggle
GET/POST /api/admin/provider/products
PUT /api/admin/provider/products/{id}
PUT /api/admin/provider/products/{id}/toggle
GET /api/admin/provider/report (?month=YYYY-MM)
GET/POST /api/admin/provider/access-links
```
---
## Frontend-Routen
| URL | Seite | Zugang |
|---|---|---|
| `/` | Firmen-Auswahl | öffentlich (iPad) |
| `/company/[id]` | Mitarbeiter-Auswahl | öffentlich (iPad) |
| `/company/[id]/tally?employee=[id]` | Produkt wählen | öffentlich (iPad) |
| `/admin/company?token=…` | Firmen-Admin | COMPANY_ADMIN Token |
| `/admin/provider?token=…` | Anbieter-Admin | PROVIDER_ADMIN Token |
---
## CSS-Variablen (Aquantico-Branding)
```css
--color-navy: #0d2440 /* Header-Hintergrund */
--color-bg: #081829 /* Seitenhintergrund */
--color-bg-secondary: #0d2440
--color-bg-card: #122e52 /* Kacheln */
--color-primary: #00b4c8 /* Teal (Aquantico) */
--color-teal: #00b4c8
--color-warning: #ffa502 /* "Qaffee"-Schriftzug im Header */
--color-text-muted: #8aaabf
--color-border: #1a3a5c
```
---
## Bekannte Stolperfallen
- **PanacheEntity vs PanacheEntityBase**: Immer `PanacheEntityBase<Long>` + `@GeneratedValue(strategy = IDENTITY)` verwenden. `PanacheEntity` nutzt SEQUENCE, das auf MariaDB nicht existiert.
- **flyway-mysql**: Muss explizit in `pom.xml` stehen (`org.flywaydb:flyway-mysql`), sonst erkennt Flyway MariaDB nicht.
- **CORS**: Im Backend deaktiviert (`quarkus.http.cors=false`). Alle API-Anfragen laufen über den SvelteKit-Server-Proxy (`hooks.server.ts`), der `Origin`-Header würde sonst 403 auslösen.
- **`icon_placeholder`**: Enthält seit Einführung des EmojiPickers direkte Emoji-Zeichen (z.B. `☕`). Legacy-Produkte aus Seed-Daten haben noch Schlüsselwörter (`coffee`, `tea`, `chocolate`) — `LEGACY_ICON_MAP` in der Tally-Seite behandelt das.
- **MariaDB-Version**: Muss `11.4` sein (LTS). `11.8` wird von Flyway nicht unterstützt.
- **Docker-Proxy**: `API_URL=http://backend:8080` (privat, nicht PUBLIC_) in docker-compose.yml für den SvelteKit-Server.
---
## Lokale Entwicklung (ohne Docker)
```bash
# MariaDB lokal starten (z.B. via Docker)
docker run -d -p 3306:3306 -e MARIADB_DATABASE=strichliste \
-e MARIADB_USER=strichliste -e MARIADB_PASSWORD=strichliste \
-e MARIADB_ROOT_PASSWORD=root mariadb:11.4
# Backend
cd backend
./mvnw quarkus:dev
# Frontend (Vite-Proxy leitet /api → localhost:8080)
cd frontend
npm run dev
```
---
## Deployment
```bash
docker compose up --build # Alles neu bauen und starten
docker compose up --build frontend # Nur Frontend neu bauen
docker compose down -v # Alles stoppen + Volumes löschen (DB-Reset)
```
Backups liegen automatisch in `backups/` (täglicher mysqldump, 7 Tage Rotation).