diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e4a8910 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Dienstplan-Pro — Team-Release v1.0 Konfiguration +# Diese Datei ist eine VORLAGE. Echte Secrets niemals committen. + +# ── Admin (Pflicht) ── +# Fehlt/ungültig → Server startet NICHT (Fail-Fast). Bestehende Daten werden diesem Admin zugeordnet. +ADMIN_EMAIL=admin@deine-domain.de + +# ── App ── +APP_BASE_URL=https://bonus.pixel-by-design.de +PORT=3000 +DATA_DIR=/data + +# ── Sitzungen / Tokens ── +SESSION_TTL_DAYS=30 +SESSION_IDLE_HOURS=8 +TOKEN_TTL_MIN=30 + +# ── Rate-Limit (E-Mail primär; IP großzügig wegen Klinik-NAT) ── +RATE_LIMIT_EMAIL=5 +RATE_LIMIT_WINDOW_MIN=15 +RATE_LIMIT_IP=50 + +# ── SMTP (Magic-Link-Versand). Ohne SMTP_HOST → Link erscheint nur in der Server-Konsole. ── +# WICHTIG: SPF + DKIM beim Provider einrichten, sonst landen Links im Spam. +SMTP_HOST=smtp.deine-domain.de +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=dienstplan@deine-domain.de +SMTP_PASS= +SMTP_FROM=Dienstplan-Pro + +# ── Nur lokaler HTTP-Dev: Session-Cookie ohne Secure-Flag. In Produktion NICHT setzen. ── +# COOKIE_INSECURE=true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e75bb9f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## v1.0.0 — Team-Release (2026-07-07) + +### Neu — Mehrbenutzer +- **Magic-Link-Login** (passwortlos): Kollegen melden sich mit freigeschalteter Arbeits-E-Mail an; Login-Link scanner-sicher (Bestätigungsseite gegen Mail-Prefetch), Sitzung mit absoluter + Inaktivitäts-Frist, Logout. +- **Getrennte Datenbasis pro Nutzer** (server-erzwungen, `user_id` nur aus der Session — Anti-IDOR). Auch der Admin sieht nur seine eigenen Daten. +- **Admin-Nutzerverwaltung**: E-Mails freischalten / auflisten / entfernen mit Last-Admin-Schutz; Notzugangs-Link (audit-geloggt). +- **Migration** der bisherigen globalen Daten auf den Admin — atomar, idempotent, Fail-Fast bei fehlender `ADMIN_EMAIL`. + +### Sicherheit +- Login-Token & Session-IDs nur als SHA-256-Hash gespeichert; Rohwert nur im Link/Cookie. +- `foreign_keys=ON` + `ON DELETE CASCADE`; httpOnly/Secure/SameSite=Lax-Cookies (secure-by-default). +- Rate-Limit (E-Mail primär, IP großzügig), neutrale Auth-Antworten, atomarer Token-Claim (TOCTOU-frei). +- Minimales, PII-freies Audit-Log (Login/Logout/Admin-Aktionen/Fehlversuche) mit pseudonymer `user_id`. + +### Unverändert übernommen +- Server-Persistenz (SQLite/WAL) + tägliches Online-Backup + Offline-fähiger Sync. +- Foto-Import (Vision-LLM) und 3-Varianten-Bonusberechnung (NRW Psychiatrie 2011). + +### Bewusst nicht in v1 +- DSGVO-Rechtstexte/Löschkonzept (internes Team, Risiko akzeptiert), Passwort-Login, gemeinsame Datenbasis. diff --git a/CLAUDE.md b/CLAUDE.md index bf043e6..e498a65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,34 +54,28 @@ Script loading order in `index.html` is critical: ## Business Logic -### Qualifying Days (WE/Feiertag) +### Tag-Klassifizierung (Slot pro Dienst) -A day is "qualifying" (eligible for higher bonus rate) if ANY of: -- **Weekend**: Friday (5), Saturday (6), or Sunday (0) -- **Public Holiday**: Any NRW state holiday -- **Day Before Holiday**: The calendar day preceding a public holiday +Jeder Dienst bekommt genau einen Slot (`variants.js` → `classify`): +- **fr**: echter Freitag · oder Tag vor einem Mo–Do-Feiertag +- **sa**: echter Samstag · oder Sandwich-Tag (Feiertag UND Tag davor) +- **so**: echter Sonntag · oder Mo–Do-Feiertag (ohne Sandwich) +- **weekday**: Mo–Do ohne Feiertagsbezug -### Bonus Calculation Rules +Echte Fr/Sa/So gewinnen immer. Sätze: `fr/sa/so` = 450 €, `weekday` = 250 €; Anteile 0,5 oder 1,0. -``` -Constants: -- RATE_NORMAL = 250€ (normal weekday rate) -- RATE_WEEKEND = 450€ (qualifying day rate) -- MIN_QUALIFYING_DAYS = 2.0 (threshold) -- DEDUCTION_AMOUNT = 2.0 (deducted from qualifying days) +### Bonus-Berechnung (3 Varianten — `variants.js`, NICHT mehr das alte 2.0-Schwellen-Modell) -Algorithm: -1. Count qualifying days (Friday, Sat, Sun, holidays, day-before-holiday) -2. Count normal days (Mon-Thu, not holiday-related) -3. If qualifyingDays < 2.0: NO BONUS (total = 0€) -4. If qualifyingDays >= 2.0: - - Deduct 2.0 from qualifying days (Friday priority) - - Bonus = (normalDays × 250€) + (remainingQualifyingDays × 450€) -``` +`calculator.js` läuft alle drei Varianten und nimmt die mit dem höchsten Betrag (Gleichstand → niedrigste Variantennummer). Werte normal (Urlaub = halbiert): +- **V1** — greift wenn `fr+so ≥ 1` UND `weekday ≥ 3`; Abzug 1 aus fr+so (Fr-Priorität) + 3 weekday; `sa` wird voll bezahlt. +- **V2** — greift wenn `sa ≥ 1` UND `weekday ≥ 2`; Abzug 1 sa + 2 weekday; `fr`/`so` werden voll bezahlt. +- **V3** — greift wenn `fr+sa+so ≥ 2`; Abzug 2 aus dem Pool (Reihenfolge fr → so → sa); `weekday` wird voll bezahlt. -### Friday Priority Deduction +**Urlaubsmodus** (Flag pro Mitarbeiter/Monat) halbiert alle Schwellen und Abzüge. Maßgeblich ist `variants.js`; die In-App-Doku (Einstellungen → Berechnungsregeln) ist aktuell. -When deducting the 2.0 qualifying days, Fridays are deducted first before Saturday/Sunday/holidays. This is tracked via `qualifyingDaysFriday` and `qualifyingDaysOther` in the calculator. +### Mehrbenutzer (v1.0) + +Seit dem Team-Release ist die App hinter Magic-Link-Login; Daten sind pro Nutzer (`user_id`) getrennt. Backend in `server/` (auth.js, index.js, mailer.js, ratelimit.js, audit.js). Konfiguration via Env-Variablen — siehe `.env.example` (u.a. `ADMIN_EMAIL` Pflicht/Fail-Fast, SMTP, Session-Fristen). ### Duty Shares diff --git a/README.md b/README.md index 00da5ad..2bfd580 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,18 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta - ✅ **Dienstplanung**: Dienste für beliebige Monate eintragen (ganze und halbe Dienste) - ✅ **Automatische Feiertagserkennung**: NRW-Feiertage 2025-2030 - ✅ **Bonusberechnung**: Automatische Berechnung nach festgelegten Regeln +- ✅ **Team-Login (v1.0)**: Passwortlose Anmeldung per Magic-Link; jeder Nutzer hat eine eigene, getrennte Datenbasis +- ✅ **Server-Persistenz + Offline-Sync**: Daten liegen serverseitig (SQLite) und synchronisieren; LocalStorage als Offline-Cache - ✅ **Datenexport/Import**: JSON-Export für Backup und Migration -- ✅ **LocalStorage**: Alle Daten werden lokal im Browser gespeichert - ✅ **Responsive Design**: Funktioniert auf Desktop und Mobilgeräten +## Team-Betrieb (v1.0) + +- **Anmelden:** freigeschaltete Arbeits-E-Mail eingeben → Login-Link per Mail → bestätigen. Kein Passwort. Sitzung ~30 Tage (bzw. 8 h Inaktivität). +- **Admin** (per `ADMIN_EMAIL` beim ersten Start angelegt) verwaltet unter **Einstellungen → Konto & Team**, wer teilnehmen darf, und behält den Team-Blick; **reguläre Nutzer** erfassen nur ihre eigene Person. +- **Datentrennung** ist serverseitig erzwungen — niemand sieht fremde Daten. +- Konfiguration: siehe `.env.example`. SMTP mit SPF/DKIM einrichten, sonst landen Login-Links im Spam. + ## Berechnungsregeln ### Qualifizierende Tage (WE/Feiertag) @@ -19,13 +27,8 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta - **Feiertage**: Alle gesetzlichen Feiertage in NRW - **Tag vor Feiertag**: Der Tag vor einem gesetzlichen Feiertag -### Bonusberechnung -1. **Schwellenwert**: Mindestens **2.0 qualifizierende Tage** im Monat erforderlich -2. **Abzug**: Bei Erreichen des Schwellenwerts werden **2.0 qualifizierende Tage** abgezogen (Freitag-Priorität) -3. **Vergütung**: - - Normale Tage: **250€** pro Tag - - Qualifizierende Tage (WE/Feiertag): **450€** pro Tag - - Halbe Dienste: Jeweils die Hälfte +### Bonusberechnung (NRW Psychiatrie 2011 — 3 Varianten) +Jeder Dienst wird in einen Slot klassifiziert (`fr`/`sa`/`so` = **450 €**, `weekday` = **250 €**; inkl. Feiertags-Verschiebung). Der Bonus wird über **drei Varianten** (V1/V2/V3) berechnet — es gewinnt die mit dem höchsten Betrag; bei Gleichstand die niedrigste Variantennummer. Der **Urlaubsmodus** halbiert Schwellen und Abzüge. Die vollständigen Regeln (Schwellen/Abzüge je Variante) stehen in der App unter **Einstellungen → Berechnungsregeln**. ### Beispiel Mitarbeiter hat im Monat: