Commit Graph
62 Commits
Author SHA1 Message Date
patrickandClaude Opus 4.8 2b74f95e68 fix(security): K-5 – LDAP/CalDAV/SMTP-Verschlüsselung auf zentralen Datenschlüssel
ldap_service/caldav_service/email_service hatten je eine eigene _fernet()/
_decrypt_password(), die am JWT-SECRET_KEY hing. Damit honorierten sie das in
core/crypto bereits umgesetzte SECRET_KEY_DATA NICHT – eine JWT-Key-Kompromittierung
hätte weiterhin LDAP-/CalDAV-/SMTP-Passwörter offengelegt.

Jetzt delegieren alle drei an core.crypto.get_fernet_key()/decrypt_value. Da auf
beiden Servern SECRET_KEY_DATA nicht gesetzt ist, ist die Ableitung heute identisch
(sha256(SECRET_KEY)) → bestehende verschlüsselte Werte bleiben lesbar, kein Bruch.
Sobald SECRET_KEY_DATA gesetzt wird, gilt die Trennung dann konsistent für ALLE
Datenverschlüsselung (TOTP + LDAP + CalDAV + SMTP).

190/190 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:56:01 +02:00
patrickandClaude Opus 4.8 408b2c4c7a feat: agent-04 – Team-Dashboard für Manager/HR im Frontend angebunden
Das Backend liefert /dashboard/team (team_dashboard) bereits, die DashboardPage
lud aber nur /dashboard/me und /dashboard/company → Manager/HR sahen nur das
Mitarbeiter-Dashboard. Jetzt:

- Manager-Rollen (MANAGER/HR/COMPANY_ADMIN/SUPER_ADMIN) laden /dashboard/team.
- Neue Sektion "Team heute": KPIs (anwesend / abwesend / nicht eingestempelt /
  Mitarbeiterzahl), ausstehende Genehmigungen als Quick-Links, Team-Tabelle
  (Status-Badge, Einstempelzeit, Stunden heute).

Reine Frontend-Anbindung – Backend, Reports und Exports (PDF/CSV/XLSX) waren
bereits vollständig (24 Report-Tests grün).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:27:48 +02:00
patrickandClaude Opus 4.8 6fcd6783ad fix: AuditLogPage – React-key am Listen-Fragment (Konsolen-Warnung)
agent-06 Audit-Log war bereits vollständig implementiert (Backend-Router mit
Filtern + Dropdown-Endpunkten, AuditLogPage mit Filter/Tabelle/JSON-Diff/CSV).
Hier nur die fehlende key-Prop am map-Fragment auf React.Fragment umgestellt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:23:18 +02:00
patrickandClaude Opus 4.8 eab41ede69 feat: agent-12 – Zwei-Stufen-Genehmigung für Abwesenheiten
Optionale zweistufige Freigabe (Feature-Parität mit Urlaubsverwaltung,
Second-Stage-Authority), ohne SSO:

- Firmen-Opt-in companies.two_stage_approval_enabled + two_stage_min_days
  (nur Anträge ab X Arbeitstagen brauchen Stufe 2; 0 = alle).
- Ablauf PENDING → FIRST_APPROVED → APPROVED: erste Stufe durch Manager-Rollen,
  finale Stufe nur HR/Admin und zwingend eine ANDERE Person als Stufe 1.
- Urlaubs-/FZA-Abzug, CalDAV-Sync und Vertreter-Mail erst bei finaler Genehmigung.
  Ablehnen in beiden Stufen möglich; Eigentümer darf FIRST_APPROVED noch stornieren.
- pending_days, Kalender und Reminder-Digest berücksichtigen FIRST_APPROVED.
- Neuer Status-Wert + absences.first_approved_by; System-Kommentar bei Stufe 1.

Frontend: CompanySettingsPage (Toggle + Schwellwert), AbsencesPage
("Endgültig genehmigen"/Ablehnen für HR/Admin ≠ Erstgenehmiger, Status-Badge).

Migration 0038. 190/190 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:17:32 +02:00
patrickandClaude Opus 4.8 2f110df619 feat: agent-11 PR3 – Scheduler + Erinnerungs-Mails + Notification-Prefs
Geplante Erinnerungen (Feature-Parität mit Urlaubsverwaltung):

- APScheduler (AsyncIOScheduler) in der FastAPI-Lifespan; tägliche Jobs ab
  settings.reminder_hour. Redis-Tageslock gegen Doppelversand bei mehreren
  Prozessen; jeder Job mit eigener Session + RLS-Bypass.
- Drei Jobs (auch einzeln aufrufbar): offene Anträge an Genehmiger,
  Resturlaub-Verfall-Vorwarnung an Mitarbeiter, fehlende AU an HR.
- Pro-User notification_prefs (JSONB, opt-out); GET/PATCH /users/me/notification-prefs
  + ProfilePage-UI; Vertreter-Mail respektiert die Prefs.
- Manueller Trigger POST /companies/me/run-reminders (Admin) – gleiche Logik,
  firmen-scoped (testbar ohne Warten).
- Bugfix: GET-/PATCH-Urlaubskonto (update_balance) nutzte nicht existente
  Felder (base_days/carried_over_days/ip_address) → korrigiert auf
  entitled_days/carried_over/ip + company_id; available_days ergänzt.

Migration 0037 (users.notification_prefs). 188/188 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:21:14 +02:00
patrickandClaude Opus 4.8 e8bed43570 feat: agent-11 PR2 – Urlaubsanspruch (Verfall scharf, Pro-rata, Teilzeit)
Korrektheit der Urlaubskonten (Feature-Parität mit Urlaubsverwaltung):

- Verfall scharfgeschaltet: neuer effektiv verfügbarer Saldo (available_days)
  schließt verfallenen, noch nicht verbrauchten Resturlaub aus; Konto-Warnung
  beim Antrag nutzt jetzt available statt remaining. (Verfallsdatum bleibt in
  company.settings, UI bereits vorhanden.)
- Anteilige Berechnung (Zwölftel) im Ein-/Austrittsjahr anhand neuer Felder
  users.entry_date / exit_date; opt-in pro Firma (vacation_prorate_first_year).
- Teilzeit: Anspruch optional aus Arbeitstagen/Woche des WorkSchedule abgeleitet
  (vacation_from_schedule), Basis vacation_default_days.
- _get_or_create_balance berechnet den Grundanspruch jetzt frisch
  (_compute_entitlement); _carryover_expired/effective_available als Service-API,
  Router delegiert.

Frontend: CompanySettingsPage (Jahresurlaub + Pro-rata- und Teilzeit-Toggles),
UsersPage (Ein-/Austrittsdatum im Edit-Modal), AbsencesPage ("Verfügbar" + Hinweis
bei verfallenem Resturlaub).

Migration 0036. 183/183 Tests grün. Deployed auf 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:03:42 +02:00
patrickandClaude Opus 4.8 6fa66b8c13 feat: agent-11 PR1 – Vertretung, Storno-Re-Genehmigung, Kommentare
Abwesenheits-Modul abgerundet (Feature-Parität mit Urlaubsverwaltung):

- Vertretung: Overlap-Warnung beim Anlegen, E-Mail an Vertretung bei
  Genehmigung, GET /absences/?as_substitute=true, neuer schlanker
  GET /users/colleagues (alle Rollen, RLS-gefenced) für die Auswahl;
  Vertreter-Dropdown + Anzeige in der Liste.
- Stornierung mit Re-Genehmigung: neuer Status CANCELLATION_REQUESTED,
  POST /absences/{id}/request-cancellation; Manager genehmigt/lehnt über
  bestehende approve/reject ab (Urlaub + FZA-Rückbuchung via _apply_cancellation).
- Kommentare: Model AbsenceComment (company_id-RLS), GET/POST comments,
  System-Kommentare bei Statuswechsel, AbsenceCommentsModal.
- Fix: CalDAV fire-and-forget nutzte die Request-Session weiter (in Tests
  geteilt -> "another operation in progress"); jetzt sync_*_bg mit eigener
  Session + RLS-Bypass.

Migration 0035. 178/178 Tests grün. Deployed auf 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:47:47 +02:00
patrickandClaude Opus 4.8 be22a51805 docs: DEVLOG – agent-10 Reseller/Mandantenübersicht
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:44:48 +02:00
patrickandClaude Opus 4.8 9e1ba2bd91 feat: Mandant/Reseller ohne E-Mail anlegbar (Temp-Passwort statt Einladung)
- admin_email bzw. reseller email optional; ohne E-Mail wird interne Login-
  Kennung (<name>@<slug>.local) + einmaliges Temp-Passwort erzeugt, Account
  sofort aktiv, kein Mailversand
- TenantOut/ResellerOut: initial_password (einmalig) ergänzt
- LoginRequest.email: str statt EmailStr (Kennung muss kein zustellbares
  Postfach sein; .local-Domains sind sonst nicht einloggbar)
- Frontend: E-Mail-Felder optional, CredDialog zeigt Login + Temp-Passwort einmalig
- Test: Anlage ohne E-Mail + Login mit generierten Zugangsdaten; 173/173 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:34:23 +02:00
patrickandClaude Opus 4.8 fcda813ba6 feat: Reseller-Rolle + SUPER_ADMIN-Mandantenübersicht
Mandantenfähigkeit ausgebaut:
- Neue Rolle RESELLER (company_id NULL); companies.reseller_id + is_active
- RLS-Erweiterung (Migration 0034): companies/users zusätzlich auf app.reseller_id
  gefenced → Reseller sieht/verwaltet DB-seitig nur eigene Firmen, keine
  personenbezogenen Zeit-/Abwesenheitsdaten (DSGVO: nur Verwaltung)
- get_current_user setzt app.reseller_id + Bypass aus für RESELLER
- tenant_service: Firma + Erst-Admin (Einladung), Übersicht mit Kennzahlen
- Router /reseller/* (Self-Service) und /admin/* (SUPER_ADMIN: Mandanten + Reseller)
- Login-Sperre bei deaktiviertem Mandanten
- Frontend: TenantsPage (/admin/tenants), eigene ResellerCompaniesPage (/reseller),
  rollenbasierte Login-Weiterleitung, Nav "Mandanten" für SUPER_ADMIN
- 4 neue Tests inkl. Cross-Reseller-RLS-Isolation; 172/172 grün

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:15:51 +02:00
patrickandClaude Opus 4.8 da4745013e feat: QR-Stempeln als eigener Menüpunkt + Tablet-Link
QR-Stempeln aus CompanySettingsPage in eigene Seite /settings/qr-stamp
ausgelagert, eigener Nav-Eintrag 'QR-Stempeln' (COMPANY_ADMIN/SUPER_ADMIN).
Toggle speichert jetzt eigenständig (PATCH public_stamp_enabled). Neuer
Tablet-Link-Bereich: Direkt-URL kopieren + 'Auf diesem Gerät öffnen' zum
dauerhaften Einrichten eines Tablets am Eingang (gleicher Token wie Handy-QR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:10:48 +02:00
patrickandClaude Opus 4.8 031d7e36f3 fix: QR-Stempel-Status korrekt – Status vor mid-request commit ermitteln
public_action/auth riefen db.commit() vor _status() auf. SET LOCAL
app.bypass_rls gilt nur pro Transaktion; nach dem Commit filterte RLS
(mangels app.company_id auf der öffentlichen Route) alle Zeilen weg, sodass
open immer False und today leer war. Status jetzt vor dem Commit ermitteln.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:03:49 +02:00
patrickandClaude Opus 4.8 994c9809aa docs: DEVLOG für QR-Stempel-Feature
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:55:49 +02:00
patrickandClaude Opus 4.8 707b73b0c5 feat: Statischer firmenweiter QR-Code für mobiles Ein-/Ausstempeln
Mitarbeiter scannen einen am Eingang ausgehängten QR-Code mit dem Privat-Handy
(/stamp?t=<token>), melden sich per Personalnummer + PIN an und stempeln ein/aus.

Eigener öffentlicher Endpunkt-Pfad, da der Kiosk-PIN-Login Ed25519-Geräte-
Signaturen verlangt, die ein Privat-Handy nicht hat.

Backend:
- Company.public_stamp_enabled (opt-in, default OFF) + rotierbares
  public_stamp_token_hash (SHA-256) + created_at; Migration 0033
- Router /time/public: company/auth/action (slowapi-Limits, AuditLog)
- kiosk_auth_service.login_pin_public() reused PIN-Lockout, keyed auf
  (public:company_id, personnel_number)
- public_stamp_session_service: 120s Redis-Kurz-Session
- Admin-Token-Endpunkte in companies.py (GET/rotate/DELETE)

Frontend:
- Public-Route /stamp (PublicStampPage)
- Stempel-PIN-Verwaltung in ProfilePage (reused POST /users/{id}/kiosk-pin)
- QR-Generierung/Druck/Toggle in CompanySettingsPage

Sicherheit: schwächer als Kiosk (keine Geräte-Signatur/Nonce/IP-Whitelist),
bewusster BYOD-Komfort-Tradeoff; Schutz über PIN + Lockout + opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:58:38 +02:00
patrickandClaude Sonnet 4.6 6261e572c5 fix: Redis-Lockout-Keys in Test-Setup bereinigen
Login/PIN/TOTP-Lockout- und burned_token-Keys aus vorherigen
Test-Runs werden beim Session-Setup gelöscht, damit Tests
nicht durch Zustand aus früheren Runs fehlschlagen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:17:48 +02:00
patrickandClaude Sonnet 4.6 738e940268 fix: Refresh-Endpoint bevorzugt Body-Token über Cookie (Token-Rotation Test)
Body-Token hat Vorrang wenn explizit angegeben — verhindert dass
httpx-Cookie-Jar im Test den alten Token mit dem neuen Cookie überschreibt.
Browser-Clients senden keinen Body, nutzen weiterhin Cookie.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:14:44 +02:00
patrickandClaude Sonnet 4.6 b59c5ca52a security: N-1 uvicorn proxy-headers + N-2 Token-Reuse-Detection + N-3 XSS-Audit + N-4 Token-URL-Fragment + N-5 pip-audit CI
N-1: uvicorn --proxy-headers --forwarded-allow-ips=127.0.0.1
- timemaster.service: proxy-headers Flag gesetzt (beide Server)

N-2: Refresh-Token Re-Use-Detection
- auth_service.py: verbrauchter Token-Hash 48h in Redis (burned_token:<hash>)
- Bei erneutem Einsatz: alle Sessions invalidieren + AuditLog + HTTP 401

N-3: dangerouslySetInnerHTML-Audit
- Kein Vorkommen im Frontend gefunden — sauber

N-4: Reset/Invite-Token als URL-Fragment statt Query-Parameter
- email_service.py: ?token= → # (Fragment wird nicht in Referer gesendet)
- ResetPasswordPage.tsx: useSearchParams → window.location.hash.slice(1)
- Token-Lebensdauern geprüft: Reset 1h, Invite 7d — OK

N-5: Gitea CI Security-Workflow
- .gitea/workflows/security.yml: pip-audit + npm audit
- Trigger: push/PR auf main + wöchentlich montags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:55:41 +02:00
patrickandClaude Sonnet 4.6 1bcb09a822 security: H-1 settings-Whitelist + H-5 UUID-Guard + H-6 DNS-Pinning + H-7 Heartbeat-Timing
H-1: company.settings als typisiertes Sub-Schema
- schemas/company.py: CompanySettingsUpdate mit extra=forbid
- Nur bekannte Keys (carryover_expires_month/day) erlaubt
- Unbekannte Keys → HTTP 422

H-5: SQL-Injection defensiv absichern
- dependencies.py: UUID-Round-Trip str(_uuid.UUID(...)) + Sicherheitskommentar

H-6: CalDAV DNS-Rebinding-Schutz
- caldav_service.py: PinnedIPTransport — IP einmal auflösen, beim Request fixieren
- _validate_caldav_url gibt aufgelöste IP zurück
- Alle HTTP-Methoden nutzen PinnedIPTransport

H-7: Heartbeat-Timestamp nach Route-Logik
- kiosk_security.py: last_heartbeat_at-Update aus Dependency entfernt
- kiosk_service.py: Update erst in process_heartbeat() nach erfolgreicher Auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:35:18 +02:00
patrickandClaude Sonnet 4.6 721d33a04c security: M-2 HttpOnly-Cookie + M-4 TrustedHost-Warning + M-5 TOTP-Lockout + M-7 zentraler get_client_ip()
M-2: Refresh-Token als HttpOnly SameSite=Strict Cookie
- auth.py: _set_refresh_cookie/_delete_refresh_cookie Helpers
- Alle Auth-Endpoints (login, totp/login, refresh, logout) nutzen Cookie
- schemas/auth.py: refresh_token in Request/Response optional
- AuthContext.tsx: kein refresh_token in localStorage
- api/client.ts: credentials:include, kein Token-Body beim Refresh

M-4: TrustedHostMiddleware Warning in Production
- main.py: Startup-Warning wenn is_production + kein ALLOWED_HOSTS

M-5: TOTP-Fehlversuche Redis-Lockout
- auth.py: _check/_record/_clear_totp_lockout; 5 Versuche → 15 min Sperre

M-7: Zentraler get_client_ip()-Helper
- core/dependencies.py: get_client_ip() mit X-Real-IP → X-Forwarded-For → client.host
- hours_payouts.py, absences.py, busylight.py: request.client.host ersetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:25:24 +02:00
patrickandClaude Sonnet 4.6 9887a2a623 docs: DEVLOG aktualisiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:15:26 +02:00
patrickandClaude Sonnet 4.6 4cce09521e feat: FZA Einzelstunden + Security-Fixes (K-1–K-5, H-2–H-4, M-1/M-3/M-6)
FZA Einzelstunden:
- Absence.fza_hours (Numeric 5,2) — FZA in Stunden statt Tagen
- Migration 0032: fza_hours Spalte in absences
- AbsenceCreate/AbsenceOut Schema um fza_hours erweitert
- absence_service: _deduct/_refund_overtime nutzt fza_hours direkt wenn gesetzt
- Frontend: Tage/Stunden-Toggle im FZA-Antrag-Modal

Security K-1: Privilege Escalation via PATCH /users/{id}.role
- user_service: Whitelist für Rollenänderungen, SUPER_ADMIN nur durch SUPER_ADMIN
- Letzter COMPANY_ADMIN gegen Selbst-Demotion gesichert

Security K-2: Kiosk-IP-Whitelist hinter nginx
- kiosk_security: _get_client_ip() liest X-Real-IP statt request.client.host

Security K-3: Kiosk-PIN Brute-Force-Schutz
- kiosk_auth_service: Redis-Lockout nach 5 Fehlversuchen (15 min)

Security K-4: TOTP-Setup-Hijacking
- auth router: /totp/setup abgelehnt wenn TOTP bereits aktiv

Security K-5: Separater Fernet-Key
- config: SECRET_KEY_DATA Feld (optional, Fallback auf SECRET_KEY)
- crypto: get_fernet_key() mit Warning bei fehlendem SECRET_KEY_DATA

Security H-2: Vacation Balance nur HR/Admin
- absences router: PATCH /balance nur noch HR/COMPANY_ADMIN/SUPER_ADMIN + AuditLog

Security H-3: Rate-Limits auf /auth/refresh + /auth/logout
- auth router: 30/min auf refresh, 60/min auf logout

Security H-4: Login-Failure-Logging + Lockout
- auth_service: Redis-Counter, Lockout nach 10 Versuchen (15 min)
- AuditLog für login_success und login_failed

Security M-1: Nginx Security-Header
- nginx.conf: X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, X-XSS-Protection, Permissions-Policy

Security M-3: AuditLog bei Rollenänderungen
- user_service: action=role_changed mit old/new role

Security M-6: create_all nur in Development
- main.py: Base.metadata.create_all nur wenn not settings.is_production

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:13:42 +02:00
patrickandClaude Sonnet 4.6 a17f71475f chore: .claude/ aus Git-Tracking entfernen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:42:04 +02:00
patrickandClaude Sonnet 4.6 ead6e577a3 chore: CLAUDE.md aus Git-Tracking entfernen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 10:41:01 +02:00
patrickandClaude Sonnet 4.6 a64553e124 feat: Monatsansicht im /mobile Heute-Screen
- Toggle 'Heute / <Monatsname>' oben im Screen
- Monats-KPIs: Gesamtstunden, Arbeitstage, Ø pro Tag
- Tagesliste absteigend mit Datum, Uhrzeit, Status, Stunden
- Lazy-Load: Monatsdaten werden erst beim Wechsel geladen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:56:04 +02:00
patrickandClaude Sonnet 4.6 ecf4789c07 feat: Überstunden-Kappung + Jahresverfall pro Firma konfigurierbar
Backend:
- Company: overtime_cap_hours, overtime_expiry_enabled/month/day,
  overtime_max_carryover_hours
- OvertimeBalance: last_expiry_applied_at
- Migration 0031: neue Spalten in companies + overtime_balances
- _recalculate_overtime_balance: Kappung direkt nach Berechnung
- apply_overtime_expiry_if_needed(): lazy Verfall beim Balance-Abruf
- GET /absences/overtime-balance: prüft + wendet Verfall automatisch an
- POST /absences/overtime-balance/apply-expiry: manueller Trigger (Admin)

Frontend:
- CompanySettingsPage: neuer Block 'Überstunden-Konto'
  - Toggle Kappungsgrenze + Stunden-Input
  - Toggle Jahresverfall + Stichtag (Tag/Monat) + max. Übertrag
  - 'Verfall anwenden'-Button für Admins

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:48:30 +02:00
patrickandClaude Sonnet 4.6 3d62fc28d8 fix: Überstunden tages-weise berechnen statt Gesamtzeitraum
Vorher: worked_total vs expected_total über den gesamten Zeitraum
→ Urlaubs-/Kranktage senkten das Konto obwohl keine Überstunden gemacht wurden

Jetzt: pro Tag mit Eintrag wird worked_on_day vs scheduled_on_day verglichen
→ nur echte Mehrarbeit zählt als Überstunde

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:29:13 +02:00
patrickandClaude Sonnet 4.6 7a28727344 feat: Stunden-Auszahlungen in /mobile Profil-Screen
- Überstunden-Saldo (Gesamt/Entnommen/Verfügbar) als 3-spaltige Karte
- Letzte 5 Auszahlungen mit Stunden (lila), Abrechnungsmonat, Notiz
- Parallel-Load mit Overtime-Balance beim Profil-Laden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:22:08 +02:00
patrickandClaude Sonnet 4.6 df4b66e3ec feat: Stunden-Auszahlung Feature (/hr/payouts)
- Backend: Model HoursPayout, Schema, Router GET/POST/DELETE
- GET /hr/payouts: HR/Admin sehen alle, Employee/Manager nur eigene
- POST /hr/payouts: reduziert OvertimeBalance.taken_hours sofort
- DELETE /hr/payouts/{id}: storniert und bucht Stunden zurück
- AuditLog-Einträge bei Anlegen und Stornieren
- Migration 0030: hours_payouts Tabelle
- Frontend: /hr/payouts Seite (lila, 💸) mit Filter, Tabelle, Modal
- Modal zeigt verfügbares Überstundenguthaben + Warnung bei Überziehung
- Navigation: Stunden-Auszahlung (HR/COMPANY_ADMIN/SUPER_ADMIN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:17:52 +02:00
patrickandClaude Sonnet 4.6 6d5bbe27b2 fix: agent-08 Kiosk-Härtung + 24h-Zeiteintrag-Bug
- fix: worked_minutes nutzt jetzt Sekunden statt Minuten für Overnight-Vergleich
  (end < start statt end <= start) – verhindert 24h-Anzeige bei Schnell-Stempel
  in derselben Minute (z.B. 23:34:46 → 23:34:48)
- fix: _check_arbzg() gleicher Sec-basierter Fix
- fix: KioskDeviceStatus Enum values_callable → kiosk list crasht nicht mehr
- feat: kiosk rotate-key CLI-Kommando (Status→pending, Re-Enrollment)
- feat: Kiosk-Settings in CompanyOut/CompanyUpdate Schema (require_approval,
  track_current_user, heartbeat_interval_sec)
- feat: Kiosk-Terminal-Einstellungsblock in CompanySettingsPage (🖥️)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:42:08 +02:00
patrickandClaude Sonnet 4.6 46d4a93ed4 docs: ROADMAP.md angelegt – alle Features in Planung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:22:48 +02:00
patrickandClaude Sonnet 4.6 d0930ad6c2 feat: Sondervertretungen als eigene HR-Seite (/hr/special-assignments)
- Neue Seite SpecialAssignmentsPage mit Filter, Tabelle, Add/Edit-Modal
- Farbcodierung: Faktor >1.0 amber, <1.0 blau, =1.0 grau
- Monat-Filterung client-seitig, paralleles Laden in Batches
- Layout.tsx: Nav-Eintrag in Hauptnavigation
- App.tsx: Route /hr/special-assignments
- UsersPage: Sondervertretungs-Block aus Edit-Modal entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:13:03 +02:00
patrickandClaude Sonnet 4.6 69e6313c47 fix: require_role in special_assignments router ohne extra Depends() wrapping
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:59:16 +02:00
patrickandClaude Sonnet 4.6 02cea3f8e0 fix: migration 0029 enum DO-Block statt CREATE TYPE IF NOT EXISTS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:58:00 +02:00
patrickandClaude Sonnet 4.6 55a37aba3f fix: migration 0029 idempotent (IF NOT EXISTS für Enum + Tabelle)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:57:29 +02:00
patrickandClaude Sonnet 4.6 fa61f1acef feat: Sondervertretungs-Faktoren (special_assignments)
- Neues Model SpecialAssignment mit AssignmentMode (fza|payroll|both)
- CRUD-Endpunkte unter /users/{id}/special-assignments
- Payroll-Report: GET /reports/special-assignments/payroll?year=&month=
- Migration 0029: special_assignments Tabelle + btree_gist Overlap-Constraint
- _recalculate_overtime_balance berücksichtigt FZA-Faktoren
- Frontend: Sondervertretungs-Zeiträume im UsersPage Edit-Modal
- Frontend: ReportsPage neuer Tab 'Sondervertretungen' mit Payroll-Tabelle + CSV-Export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:55:47 +02:00
patrickandClaude Sonnet 4.6 aac9e9c559 fix: AuditLog bei FZA-Stornierung mit korrektem old_status und fza_hours_refunded-Flag
- old_value enthält den tatsächlichen Status vor der Stornierung (pending/approved)
- new_value enthält cancelled_by_name für bessere Lesbarkeit
- fza_hours_refunded: true wird gesetzt wenn ein genehmigter FZA storniert wurde

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:22:39 +02:00
patrickandClaude Sonnet 4.6 2889c85e57 test: test_cancel_absence an neue 200-Response angepasst (vorher 204)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:19:09 +02:00
patrickandClaude Sonnet 4.6 5a8686935f test: FZA tests korrigiert (can_manual_time_entry, TimeEntryWithWarnings, CalDAV race fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:18:00 +02:00
patrickandClaude Sonnet 4.6 3426c3337c feat: Freizeitausgleich-Lücken geschlossen (Gap 1-3) + konfigurierbare Schwellwerte
Gap-1: Überziehschutz für Überstundenkonto
  - Company.overtime_overdraft_allowed (default: true) – blockiert FZA wenn deaktiviert
  - Company.overtime_warning_threshold_hours (default: 0) – Warnung wenn Konto unter Schwelle fällt
  - warnings[] jetzt in approve_absence Response (AbsenceApproveOut)
  - Migration 0028_overtime_fza_config.py

Gap-2: total_hours wird bei Zeiteintrag-Genehmigung neu berechnet
  - time_service.approve_entry() ruft _recalculate_overtime_balance() auf
  - last_calculated Timestamp wird gesetzt

Gap-3: Stornierung genehmigter FZA-Anträge bucht taken_hours zurück
  - _refund_overtime() Helfer hinzugefügt
  - cancel_absence() erlaubt jetzt HR/Admin auch genehmigte Abwesenheiten zu stornieren
  - DELETE /absences/{id} gibt jetzt AbsenceOut zurück (statt 204)
  - Mitarbeiter können genehmigte FZA-Anträge nicht selbst stornieren (409)

Frontend:
  - CompanySettingsPage: neuer Abschnitt 'Freizeitausgleich' mit Toggle + Schwellwert-Eingabe

Tests: backend/tests/test_fza.py mit 6 Tests (alle 3 Gaps)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:08:03 +02:00
patrickandClaude Sonnet 4.6 a70dfc1c3a fix: ternäre Button-Kette in MobileStampScreen korrigiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:53:43 +02:00
patrickandClaude Sonnet 4.6 66bf1b0686 feat: Admin-Toggle für mobile Zeiterfassung
Backend:
- Company.mobile_stamping_enabled (BOOLEAN DEFAULT TRUE)
- CompanyOut + CompanyUpdate: neues Feld
- Migration 0027: companies.mobile_stamping_enabled

Frontend Desktop (CompanySettingsPage):
- Abschnitt 'Mobile-Ansicht' mit Toggle-Switch
- Speichert via PATCH /companies/me

Frontend Mobile (MobileStampScreen):
- Lädt mobile_stamping_enabled aus GET /companies/me
- Deaktiviert: Hinweis-Banner statt Buttons
  ('Einstempeln nicht verfügbar – bitte Kiosk/Desktop nutzen')
- Aktiviert: normales Verhalten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:52:46 +02:00
patrickandClaude Sonnet 4.6 96a4e728d6 feat: Abwesenheiten-Screen in Mobile-App
- MobileAbsencesScreen.tsx:
  - Urlaubskonto-Karte (verbleibende Tage + Fortschrittsbalken)
  - Liste eigener Abwesenheiten (aktuell/geplant + vergangen)
  - Farbpunkt pro Abwesenheitstyp, Status-Badge
  - Bottom-Sheet Modal: Antrag stellen oder Krank melden
  - Start-/Enddatum-Picker, Typ-Auswahl, optionale Notiz
  - SICK-Typ → quick-sick Endpoint, sonst POST /absences/
- MobileBottomNav: 4. Tab 'Urlaub' (war 3 Tabs)
- MobilePage: Screen 'absences' eingebunden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:31:00 +02:00
patrickandClaude Sonnet 4.6 070f86e183 fix: mobile/tablet Geräteerkennung in LoginPage → Redirect zu /mobile/login
- useEffect prüft Bildschirmbreite (<1024px) + User-Agent (Mobi/Android/iPad/iPhone)
- Tablet/Handy wird automatisch zu /mobile/login weitergeleitet
- Desktop bleibt auf /login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:22:11 +02:00
patrickandClaude Sonnet 4.6 299cc52d76 feat: mobile Login-Seite /mobile/login
- MobileLoginPage.tsx: touch-optimiertes Login-Formular
  - E-Mail + Passwort mit großen Touch-Targets (min-h-[52px])
  - TOTP-Flow: nach erstem Login automatisch 6-stelliges Code-Feld
  - Numerische Tastatur (inputMode=numeric) für TOTP-Eingabe
  - Fehlerbehandlung + Ladezustand
  - Link zur Desktop-Version
- MobilePage: Redirect zu /mobile/login statt /login
- App.tsx: Route /mobile/login registriert (kein Layout-Wrapper)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:17:32 +02:00
patrickandClaude Sonnet 4.6 df2909aa78 fix: auto-refresh access token on 401 in API client
- Neuer refreshAccessToken()-Helper: POST /auth/refresh → neuer access_token
- Bei 401-Response: Token refreshen, Request automatisch wiederholen
- Parallele Requests: nur ein Refresh gleichzeitig (_refreshing-Promise)
- Refresh fehlgeschlagen → localStorage löschen + Redirect zu /login
- Gilt für alle API-Aufrufe (Desktop + Mobile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:12:50 +02:00
patrickandClaude Sonnet 4.6 ed37d7dca0 security: 9 Findings aus Security-Audit behoben (CRITICAL + HIGH + MEDIUM)
CRITICAL:
- C-1: LDAP tls_verify Default False → True (MITM-Schutz)
- C-2: TOTP-Secret Fernet-verschlüsselt in DB (statt Plaintext)
  - core/crypto.py: encrypt_value() / decrypt_value() helper
  - Migration 0026: totp_secret VARCHAR(64→500), ldap tls_verify default=true
  - _totp_plain() helper mit Legacy-Fallback für bestehende Werte

HIGH:
- H-1: Kiosk Nonce-Cache asyncio.Lock (Race Condition behoben)
- H-2: File-Upload-Limit 10 MB (import_kimai.py + users.py CSV-Import)
- H-3: CORS allow_methods/allow_headers explizit eingeschränkt (war *)
- H-4: TrustedHostMiddleware aktiviert wenn ALLOWED_HOSTS gesetzt

MEDIUM:
- M-1: IP-Logging nutzt X-Forwarded-For hinter nginx-Proxy
- M-4: Audit-Log für password_changed, totp_enabled, totp_disabled
- M-5: CalDAV verify_ssl in Production erzwungen (_effective_verify_ssl)

152/152 Tests grün

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:45:09 +02:00
patrickandClaude Sonnet 4.6 98c2d69306 docs: CalDAV-Konfiguration und CALDAV_ALLOWED_CIDRS in deployment.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:04:20 +02:00
patrickandClaude Sonnet 4.6 73029a0912 feat: CALDAV_ALLOWED_CIDRS Whitelist für interne CalDAV/Nextcloud-Server
Interne Nextcloud-Instanzen im LAN können jetzt per .env-Variable
von der SSRF-Blockliste ausgenommen werden.

Beispiel in .env:
CALDAV_ALLOWED_CIDRS=192.168.1.0/24,10.10.5.50/32

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:53:22 +02:00
patrickandClaude Sonnet 4.6 4d5dfe8244 feat: agent-02-kiosk Phase 2A - Auth endpoints (PIN/NFC/QR/List)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:49:09 +02:00
patrickandClaude Sonnet 4.6 fab28468c4 feat: agent-02-kiosk Phase 1 - NFC UID migration + session service
- Migration 0025: kiosk_nfc_uid column on users table with partial unique index per company
- User model: kiosk_nfc_uid field after personnel_number
- New service: kiosk_session_service.py (Redis-based 15min sessions)
- New core module: app/core/redis.py (sync Redis client with ping-test)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:45:47 +02:00
patrickandClaude Sonnet 4.6 43ae6f76a2 fix(security): SSRF-Schutz für CalDAV-URLs
Neue _validate_caldav_url() Funktion in caldav_service.py:
- Prüft Schema (nur http/https erlaubt)
- Blockiert private IP-Ranges (RFC 1918, Loopback, Link-local)
- DNS-Auflösung + Prüfung der aufgelösten IP (DNS-Rebinding-Schutz)
- Blockiert: 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, fc00::/7 etc.

Validierung in allen drei HTTP-Helpers (_http_put, _http_delete, _http_propfind)
sowie beim Speichern in caldav.py Router (company + user config) – doppelter Schutz.

Getestet: 8 böse URLs geblockt, 2 legitime URLs erlaubt (10/10 OK)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:39:59 +02:00
patrickandClaude Sonnet 4.6 b547cdda33 feat(kiosk): Stufe 3 – ServiceWorker, WebCrypto Setup-Flow, Kiosk-UI, 15 Security-Tests
3A – Frontend Kiosk-Modus:
- public/kiosk-sw.js (NEU, 187 Zeilen): ServiceWorker signiert alle /api/v1/kiosk/
  Requests automatisch mit Ed25519. Keypair-Generierung intern (non-extractable),
  Speicherung in IndexedDB. BroadcastChannel-Leader-Election für Heartbeat.
- KioskSetupPage.tsx (NEU, 307 Zeilen): Enrollment-Flow unter /kiosk/setup.
  Keypair-Generierung via WebCrypto im ServiceWorker, Public Key als PEM anzeigen.
  Browser-Kompatibilitäts-Check (Ed25519 ab Chrome 113+).
- KioskStampPage.tsx (NEU, 348 Zeilen): Kiosk-UI unter /kiosk.
  Live-Uhr mit Server-Zeit-Offset, Heartbeat-Loop 30s, Online/Offline-Indikator.
- App.tsx: /kiosk und /kiosk/setup Routen außerhalb ProtectedRoute

3B – Tests:
- tests/test_kiosk_security.py (NEU, 387 Zeilen): 15/15 Tests grün
  Abgedeckt: gültige Signatur, falscher Key, Replay-Schutz, Timestamp-Drift,
  Future-Timestamp, pending/revoked Device, unbekanntes Gerät, fehlende Header,
  Lifecycle-Tests, heartbeat_status nach Heartbeat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:23:03 +02:00
patrickandClaude Sonnet 4.6 02b3fa3869 feat(kiosk): Stufe 2 – Ed25519-Auth, CLI-Tool, neue KioskDevicesPage
2A – Backend Ed25519-Verifizierung:
- app/core/kiosk_security.py (NEU): verify_kiosk_request() Dependency
  - Timestamp-Check (30s Drift), Nonce-Cache (Redis/In-Memory), IP-Whitelist
  - Ed25519-Signatur über METHOD+PATH+TIMESTAMP+NONCE+sha256(BODY)
  - PEM + OpenSSH Key-Format unterstützt
- app/routers/kiosk.py: approve/revoke Endpunkte, POST /heartbeat (Ed25519-signiert)
- app/services/kiosk_service.py: token-basierte Methoden entfernt, approve/revoke/heartbeat
- app/schemas/kiosk.py: KioskDeviceOut mit heartbeat_status, HeartbeatRequest/Response

2B – CLI-Tool:
- cli.py (NEU, 529 Zeilen): Typer-CLI mit kiosk add/list/approve/revoke/info
  - Public-Key-Fingerprint (SHA256), Rich-Tabellen, CIDR-Validierung
  - Direkter DB-Zugriff mit RLS-Bypass

2C – Frontend:
- KioskDevicesPage.tsx: Zwei-Tab-Layout (Wartet/Aktiv), Status-Ampel,
  Auto-Refresh 30s, Ed25519-Workflow (kein Token mehr)
- Layout.tsx: KioskHealthBadge (online/total, 30s Refresh, nur COMPANY_ADMIN)

requirements.txt: typer>=0.12.0, rich>=13.7.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:13:46 +02:00
patrickandClaude Sonnet 4.6 e5ccb46172 feat(kiosk): Migration 0021 – Ed25519-Auth, Status-Enum, Heartbeat, IP-Whitelist
Migration 0021_kiosk_security (eingeklinkt zwischen 0020 und 0022):
- kiosk_devices: token_hash + is_active → status enum(pending/approved/revoked)
- kiosk_devices: public_key, key_algorithm, enrollment_token_hash/expires_at
- kiosk_devices: last_heartbeat_at, client_version, offline_queue_size
- kiosk_devices: current_user_id (DSGVO), ip_whitelist (CIDR)
- companies: kiosk_require_approval, kiosk_track_current_user, kiosk_heartbeat_interval_sec

Model KioskDevice: komplett überarbeitet (KioskDeviceStatus Enum)
Model Company: 3 neue Kiosk-Felder

Bestehende Geräte: status=revoked (müssen neu enrolled werden)
Existing servers: SQL manuell angewendet (Alembic skip bei inserted migrations)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:08:33 +02:00
patrickandClaude Sonnet 4.6 027c4a4c00 feat: Live-Stempel-Uhr, Break-UI, Balance-Widget, Approval-Queue + PDF-Export (WeasyPrint)
Frontend (TimeTrackingPage):
- Live-Arbeitsuhr (HH:MM:SS) während eingestempelt
- Break-Start/End-Buttons mit laufender Pausenuhr
- Wochen-Balance-Widget (gearbeitet / erwartet / überstunden)
- Approval-Queue Tab für Manager/HR/Admin (pending entries genehmigen/ablehnen)

Backend (Reports):
- weasyprint>=61.0 in requirements.txt
- 3 neue PDF-Export-Tests (Zeit, Abwesenheit, Überstunden)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:59:32 +02:00
patrickandClaude Sonnet 4.6 267c4ce968 docs: vollständige Projektdokumentation hinzugefügt
- docs/api.md: komplette API-Referenz (1375 Zeilen, alle Endpunkte)
- docs/architecture.md: Tech-Stack, DB-Schema, RLS-Architektur, Auth-Flow
- docs/deployment.md: Setup, nginx, systemd, update.sh, Backup/Rollback
- docs/development.md: Dev-Setup, Test-Workflow, Code-Konventionen, Fallstricke

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:29:44 +02:00
patrickandClaude Sonnet 4.6 c53652884c fix: 8 pre-existing Test-Fehler behoben
1. Krankmeldungen an Wochenenden erlaubt (working_days=0 für SICK)
   Medizinisch korrekt: Krankmeldungen können auch Wochenendtage umfassen.
   Behebt: test_create_absence_sick_auto_approved, test_quick_sick_*,
           test_pull_includes_today_absence_with_category, test_sick_stats_*

2. Self-Approval-Schutz in Tests berücksichtigt
   abs_approver_headers / time_approver_headers: zweiter Admin je Company.
   Behebt: test_approve_absence, test_balance_deducted_after_approve, test_approve_entry

3. test_export_invalid_format: "pdf" → "xml" (pdf ist jetzt valides Format)

Ergebnis: 134/134 passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:51:50 +02:00
patrickandClaude Sonnet 4.6 5a1a7b4d79 fix: router db.refresh() nach commit bricht RLS-Kontext
SET LOCAL Werte (bypass_rls, company_id) sind transaktions-gebunden.
Nach db.commit() ist der Kontext weg – ein nachfolgendes db.refresh()
läuft in einer neuen Transaktion ohne RLS-Kontext und liefert 0 Rows.

Da expire_on_commit=False gesetzt ist, sind alle Instanz-Attribute
nach dem Commit bereits im Speicher vorhanden. Die expliziten
db.refresh()-Aufrufe nach db.commit() in allen Routers sind daher
redundant und wurden entfernt.

test_rls.py: 6 neue Tests beweisen DB-seitige Mandanten-Isolation.
conftest.py: _apply_rls() wendet RLS-Policies auf Test-DB an.
migrations/0024: korrigiert auf op.execute(text()) API.
migrations/env.py: SET LOCAL außerhalb Transaktion entfernt.

Ergebnis: 8 failed (pre-existing), 126 passed – identisch zur Baseline vor RLS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:34:48 +02:00
patrickandClaude Sonnet 4.6 4c6c1bf88e agent-rls: PostgreSQL Row Level Security für Mandanten-Isolation
- Migration 0024: RLS + FORCE RLS auf 18 Tabellen
  - Direkte company_id-Policies: users, departments, companies, absence_types,
    audit_logs, kiosk_devices, ldap_configs, smtp_configs, caldav_company_configs,
    work_schedules, overtime_balances
  - JOIN-Policies (user_id → company_id): absences, sessions, password_resets,
    time_entries, vacation_balances, caldav_user_configs
  - public_holidays ausgenommen (globale Referenztabelle)
- database.py: get_db setzt bypass_rls='on' als Default (Auth-Endpoints unverändert)
- dependencies.py: get_current_user setzt app.company_id + bypass_rls='off'
  für alle nicht-SUPER_ADMIN Rollen
- migrations/env.py: Alembic-Migrationen nutzen bypass_rls='on'
- tests/conftest.py: override_get_db setzt bypass_rls='on' für Test-Session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:57:58 +02:00
patrickandClaude Sonnet 4.6 247c372311 update.sh: switch backend sync to git pull instead of rsync
Servers now have git repos initialized tracking origin/main.
Step 3 does git pull --ff-only instead of rsync. Frontend dist
is still rsynced since it's gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:31:11 +02:00
patrickandClaude Sonnet 4.6 db5ac8d488 add deployment updater scripts
- update.sh: full deploy pipeline (tests → build → rsync → migrate → restart → health-check)
  supports --no-tests, --no-frontend, --server 137|164|both, --dry-run
  aborts before service restart if migration fails (DB protection)
- scripts/check_migrations.sh: compare local Alembic head vs. server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:16:24 +02:00
patrickandClaude Sonnet 4.6 0f3091aae9 agent-07 phase 2: fix test isolation + CSV import UI
- Fix conftest.py: commit after each request in override_get_db so
  preview_csv's rollback no longer wipes the shared registered_user
  (root cause of 401 cascade across test_user_import + test_personnel_number)
- Fix limiter.enabled=False in client fixture (blocks rate-limit 429)
- Fix user_import_service: allow reactivation when personnel number
  belongs to the same user being reactivated
- Fix test_personnel_number: use PATCH /companies/me (not /companies/{id})
  and add try/finally cleanup for personnel_number_required flag
- Frontend UsersPage: add CSV import modal with template download,
  preview/validation table, and guarded apply button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:07:32 +02:00