100 Commits
Author SHA1 Message Date
patrickandClaude Sonnet 5.5 7e165d37b6 feat(deploy): deploy.sh + lokale Host-Konfig, server-update/setup_server übernehmen Unit und sudoers aus dem Repo
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 09:40:35 +01:00
patrickandClaude Sonnet 5.5 628955560a chore(scripts): Server-Hosts per TM_DEV_HOST/TM_PROD_HOST/TM_SSH_USER konfigurierbar (Defaults unverändert)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 09:35:44 +01:00
patrickandClaude Sonnet 5.5 e766974018 fix(service): ProtectHome entfernt (asyncpg prüft ~/.postgresql)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 00:21:19 +01:00
patrickandClaude Sonnet 5.5 c02338eb7a fix(security): Dienst als Systemuser auf 127.0.0.1, TLS-Erneuerung per sudo
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 00:20:26 +01:00
patrickandClaude Sonnet 5.5 8a55e183fd test(ha): Token deaktivierter Firma liefert 404
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 00:17:59 +01:00
patrickandClaude Sonnet 5.5 b6eda4e2d5 fix(security): Krankheitsdaten im Kalender maskieren, Art.-15-Export nur HR/Admin, Login-Enumeration, Session-Invalidierung bei Passwortwechsel
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 23:05:46 +01:00
patrickandClaude Sonnet 5.5 8672f92ee5 fix(security): Audit-Funde M-1,M-2,M-3,M-6,M-7,N-1..N-3 (Client-IP-Trust, AuditLog company_id, Token-Feeds, Mass-Assignment, Kiosk-Heartbeat)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 23:04:07 +01:00
patrickandClaude Sonnet 5.5 a3470a135c fix(security): PDF-Export ohne Ressourcenzugriff, HTML-Escaping, CSV/XLSX Formula-Injection
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 23:02:39 +01:00
patrickandClaude Sonnet 5.5 8dfee59bf0 feat(portal): Token-Verwaltung für das Buchhaltungsportal in den Firmeneinstellungen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 22:26:34 +01:00
patrickandClaude Sonnet 5.5 43cdece01f docs: DEVLOG-Einträge ergänzt
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:55:35 +01:00
patrickandClaude Sonnet 5.5 73784248d4 test(busylight): Pull-Token deaktivierter Firma liefert 401
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:55:35 +01:00
patrickandClaude Sonnet 5.5 cd7aaa9ce2 fix(busylight): Pull-Token deaktivierter Firmen ablehnen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:53:51 +01:00
patrickandClaude Sonnet 5.5 f0b436c91f feat(portal): read-only Firmensummen für das Buchhaltungsportal (Token-Endpunkt)
GET /portal/summary liefert Resturlaub und Überstundenguthaben als Firmensummen, keine Einzelpersonen und keine
Krankheitsdaten. Firmen-Token (SHA-256) wie beim Busylight, Verwaltung unter /companies/me/portal-token, Migration 0049.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 19:40:49 +01:00
patrickandClaude Sonnet 5.5 80cee20fe5 docs: DEVLOG-Einträge ergänzt
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:57:21 +01:00
patrickandClaude Sonnet 5.5 c9f2725186 docs: Home-Assistant-Status in api.md
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:38:25 +01:00
patrickandClaude Sonnet 5.5 ed5c52d8b3 feat(ha): read-only Arbeitszeit-Status für Home Assistant (Token-URL)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:33:54 +01:00
patrickandClaude Sonnet 5.5 2f75ebc5a7 docs: DEVLOG-Einträge ergänzt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:25:37 +01:00
patrickandClaude Sonnet 5.5 0635bb5b8a feat(frontend): Geräte-ID-QR für Ed25519-Kiosk + TLS-Warnung bei Bearer
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:24:22 +01:00
patrickandClaude Sonnet 5 1d163cc299 fix(security): Audit-Nachzug – Rate-Limit, X-Real-IP-Vertrauen, Indizes
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Security-Audit 2026-09-10 (siehe Ed25519-Reinstall-Commits):
- Rate-Limit 10/min auf POST /kiosk/my-qr-token (Q-1) – kompromittiertes
  Employee-JWT konnte sonst unbegrenzt QR-Login-Tokens erzeugen.
- _get_client_ip vertraut X-Real-IP nur noch, wenn die Verbindung selbst
  von 127.0.0.1 kommt (I-1) – sonst könnte ein Client den Header selbst
  setzen und die Kiosk-IP-Whitelist umgehen.
- Migration 0047: Unique-Index auf kiosk_devices.device_token_hash (B-2,
  war Seq-Scan pro Bearer-Request), Index auf time_entries.kiosk_device_id
  (K-1). RLS auf kiosk_devices bereits seit Migration 0024 vorhanden,
  K-1-Verdacht einer RLS-Lücke damit ausgeräumt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 15:33:12 +02:00
patrickandClaude Sonnet 5 7873236de3 feat(time): kiosk_device_id an TimeEntry – dokumentiert Stempel-Standort
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Bisher wurde bei Kiosk-Stempelungen nur die grobe Quelle ("kiosk")
gespeichert, nicht welches Gerät/welcher Standort. Bei mehreren Eingängen/
Filialen war nicht nachvollziehbar, wo ein Mitarbeiter sich eingestempelt
hat. Migration 0046: time_entries.kiosk_device_id (FK kiosk_devices,
SET NULL). Wird nur beim Einstempeln gesetzt (Ursprung des Eintrags), nicht
bei Aus-/Pause-Aktionen überschrieben. TimeEntryOut.kiosk_device_id neu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 15:06:15 +02:00
patrickandClaude Sonnet 5 71db3bdd57 feat(kiosk): QR-Login-Token-Endpunkt für Employee-App
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
POST /kiosk/my-qr-token (normale JWT-Auth) – Mitarbeiter fordert einen 5min
gültigen, einmalig einlösbaren QR-Token an, den die Employee-App als QR-Code
zeigt. Kiosk scannt ihn (POST /kiosk/auth/qr, bereits vorhanden) und meldet
den Mitarbeiter an. Backend-Funktion (generate_qr_token) existierte schon,
war aber nie über einen Endpunkt erreichbar. Test für den kompletten Ablauf
inkl. Einmaligkeit (zweiter Versuch mit selbem Token → 401).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 14:47:45 +02:00
patrickandClaude Sonnet 5 62a19d17e8 feat(frontend): Bearer-Kiosk-Gerät + QR-Code-Einrichtung im Portal
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
KioskDevicesPage: Modus-Umschalter im Create-Modal (Ed25519 PC-Terminal vs.
Bearer/VPN-Tablet). Bearer-Modus ruft neuen POST /kiosk/devices/bearer auf,
zeigt Token danach einmalig als QR-Code (JSON {url, token}, scanbar von der
Kiosk-App) + Klartext mit Kopier-Button. Kein Public-Key-Feld im Bearer-Modus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 13:23:56 +02:00
patrickandClaude Sonnet 5 d80fc59f17 feat(kiosk): Bearer-Geräte-Erstellung im Portal (für QR-Einrichtung)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Neuer Endpunkt POST /kiosk/devices/bearer (Admin-JWT statt CLI) – erzeugt
Token serverseitig, gibt ihn nur in dieser einen Antwort im Klartext
zurück (KioskBearerDeviceCreated{device, token}), DB speichert nur den
Hash. Ermöglicht QR-Code-Einrichtung direkt aus dem Portal statt CLI-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 13:22:02 +02:00
patrickandClaude Sonnet 5 3c427dcb15 feat(frontend): Kiosk-App-Download in ProfilePage (nur Admins)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Analog zur Mobile-App-Sektion, aber auf COMPANY_ADMIN/SUPER_ADMIN
beschränkt – Kiosk-Geräte werden von Admins per CLI eingerichtet, keine
Endnutzer-Selbstbedienung wie bei der Employee-App. Liest
kiosk-version.txt aus dem gleichen static/downloads/-Verzeichnis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 13:16:13 +02:00
patrickandClaude Sonnet 5 370358d117 fix(cli): RLS-Refresh-Bug in kiosk add/rotate-key
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
session.refresh() nach commit scheitert an RLS (SET LOCAL app.bypass_rls
verfällt mit dem commit, siehe project_rls_bypass_commit_pitfall) – trat
beim ersten echten Bearer-Geräte-Test auf ("Could not refresh instance").
Beide refresh()-Aufrufe entfernt, nicht nötig: alle in der Erfolgsmeldung
angezeigten Felder sind bereits im Python-Objekt gesetzt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 13:12:17 +02:00
patrickandClaude Sonnet 5 c15fe81498 fix(tests): RLS-Bypass + Refresh-Fehler in Bearer-Auth-Tests
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Direkte DB-Inserts über db_session brauchen SET LOCAL app.bypass_rls='on'
pro Transaktion (verfällt nach commit, siehe [[project_rls_bypass_commit_pitfall]]).
db_session.refresh() nach commit schlug aus demselben Grund fehl – entfernt,
nicht nötig. Bestehender test_heartbeat_missing_headers erwartete 422,
liefert jetzt korrekt 401 (Header sind seit Bearer-Auth-Modus optional).
229/229 Tests grün auf 137.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 02:16:34 +02:00
patrickandClaude Sonnet 5 f4ff11a80e feat(kiosk): Bearer-Token-Auth für Android-Tablets hinter VPN
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Neuer, einfacherer Geräte-Auth-Modus (key_algorithm="bearer_vpn") als
Alternative zum bestehenden Ed25519-Request-Signing. Bestehende PC-Terminals
(key_algorithm="ed25519") bleiben unverändert – verify_kiosk_request()
verzweigt jetzt am Authorization-Header: "Bearer <token>" → einfacher Pfad
(nur Token-Hash + Status + IP-Whitelist, kein Timestamp/Nonce/Signatur),
sonst weiterhin die 4 X-Kiosk-*-Header wie bisher.

Hintergrund: Terminals+Tablets laufen jetzt hinter WireGuard-VPN, die
ursprüngliche MITM-Härtung (Ed25519-Signing) ist für Tablets keine
Kernanforderung mehr. Ermöglicht ein deutlich einfacheres Flutter-Kiosk-App-
Projekt ohne Client-seitige Krypto.

- Migration 0045: kiosk_devices.device_token_hash
- backend/cli.py: `kiosk add --auth-mode bearer` generiert Token, gibt es
  einmalig aus (analog Enrollment-Setup-URL bisher)
- 4 neue Tests in test_kiosk_security.py (gültig/ungültig/IP-Whitelist/revoked)

Vollständiger Plan: /home/sysops/.claude/plans/staged-marinating-dove.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 02:13:16 +02:00
patrickandClaude Sonnet 5 d5b2fc75d6 fix(kiosk): TOCTOU-Race beim QR-Login (Audit-Finding K-1)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
login_qr() las den Redis-Token per GET und löschte ihn danach separat per
DELETE – zwei parallele Requests mit demselben QR-Token konnten beide vor
dem Löschen lesen und je eine Session erzeugen (Doppel-Login statt echtem
Einmal-Token). Fix: GETDEL (atomar seit Redis 6.2, Server läuft 8.0.2).

K-2 (Cross-Tenant-Redeem) aus demselben Audit betrifft keinen realen
Code-Pfad – company_id-Vergleich in login_qr() war bereits korrekt, und die
Kiosk-Geräte-Enrollment "QR-Redeem"-Annahme existiert nicht als HTTP-Flow
(Enrollment läuft ausschließlich über authentifizierte CLI). Verifiziert
vor Weiterplanung der Android-Tablet-Kiosk-Architektur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 01:54:16 +02:00
patrickandClaude Sonnet 5 2e27a7fcae fix(time): Mikrosekunden bei Zeitstempeln kappen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
start_time/end_time/break_start wurden mit vollen Mikrosekunden gespeichert
(datetime.now(timezone.utc).time() ohne microsecond=0), was Clients ohne
Mikrosekunden-Parsing brechen konnte (siehe Mobile-App-Crash). Sekunden-
Genauigkeit reicht für Zeiterfassung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-09 23:59:25 +02:00
patrickandClaude Sonnet 5 4398632880 feat(frontend): Mobile-App-Version im Download-Bereich anzeigen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Liest version.txt aus dem gleichen static/downloads/-Verzeichnis wie die
APK und zeigt sie neben "Mobile App (Android)" in der ProfilePage. Muss bei
jedem APK-Deploy manuell mitgepflegt werden (kein automatischer Abgleich
mit der mobilen App-Version).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-09 23:53:23 +02:00
patrickandClaude Sonnet 5 fd752878b0 test: P1-Coverage für Scheduler-Idempotenz, Bradford-Fensterrand, FZA-Faktor+Kappung, Cross-Company-RLS
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-03 02:10:19 +02:00
patrickandClaude Sonnet 5 9afc29a8bb fix(redis): Timeouts für Async-Pool + Start-Health-Check-Log
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
get_async_redis() hatte keine socket_connect_timeout/socket_timeout/
max_connections – ein gehängtes Redis hätte Requests unbegrenzt
blockiert statt schnell zu failen. Zusätzlich einmaliger Ping im
Lifespan-Startup, nur zur Log-Sichtbarkeit (blockiert Boot nicht).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-03 01:58:33 +02:00
patrickandClaude Sonnet 5 107cba99f9 fix(redis): INCR+EXPIRE atomar via Pipeline (MULTI/EXEC)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Login-, TOTP- und Kiosk-PIN-Lockout zählten Fehlversuche mit separaten
INCR/EXPIRE-Calls. Zwischen beiden konnte der Fail-Counter-Key kurzzeitig
ohne TTL bestehen bleiben (Crash-Fenster) - nicht atomar. Jetzt per
redis.pipeline(transaction=True) als MULTI/EXEC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-03 00:12:44 +02:00
patrickandClaude Sonnet 5 56f6f16e27 fix(redis): Pool-Rollout vervollständigen + TOTP-Lockout fail-closed mit 503
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Redis-Review deckte auf, dass der Pool-Fix vom letzten Commit nur
totp_login/kiosk_security erreichte. Login/Refresh (auth_service.py) und
PIN/NFC/QR-Kiosk-Login (kiosk_auth_service.py) öffneten weiterhin pro
Request eine neue aioredis-Verbindung. Zusätzlich nutzten
kiosk_session_service.py und public_stamp_session_service.py den
*sync* Redis-Client aus async-Code – blockierender Socket-Call im
Event-Loop bei jedem Kiosk-/Stempel-Request.

Alle auf get_async_redis() umgestellt. TOTP-Lockout wirft jetzt 503
statt eines ungefangenen 500 bei Redis-Ausfall (RedisError explizit
gefangen, eigene HTTPExceptions unberührt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-03 00:09:35 +02:00
patrickandClaude Sonnet 5 5ba5a99e02 docs: README erstellt, Debian statt Ubuntu korrigiert
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
README.md war leer (0 Zeilen). Projektübersicht, Setup, Deploy-Workflow
ergänzt. Server läuft tatsächlich auf Debian 13, nicht Ubuntu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-03 00:03:06 +02:00
patrickandClaude Sonnet 5 ac496a5f9b fix(absences): POST /absences/ reicht warnings jetzt an den Client durch
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
create_absence() gibt (absence, warnings) zurück, der Router verwarf die
Warnungen bisher (im Unterschied zu approve_absence, das sie via
AbsenceApproveOut.warnings durchreicht). Neues AbsenceCreateOut-Schema
analog dazu. Regressionstest über Überschneidungs-Warnung.

Gefunden während Coverage-Arbeit an absence_service.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:54:02 +02:00
patrickandClaude Sonnet 5 3cf396b972 fix(tests): Testdaten für Jahreswechsel-Regressionstest korrigieren
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
01.01.2028 war ein Samstag – zählt schon als Wochenende, testete den
Fix damit nicht wirklich. 2028/2029-Grenze (Neujahr = Montag) verwendet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:51:46 +02:00
patrickandClaude Sonnet 5 548738c9ea fix(absences): Feiertage über Jahreswechsel korrekt aus working_days rausrechnen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
_get_holiday_dates() fragte nur PublicHoliday für data.start_date.year ab.
Ein Antrag über den Jahreswechsel (z.B. 28.12.-01.01.) rechnete den
Neujahrsfeiertag im Folgejahr daher nicht heraus -> Mitarbeiter verlor
einen Urlaubstag, den er nicht verbraucht hat. Neuer year_to-Parameter
deckt den Jahres-Range ab, beide Aufrufstellen (create_absence,
update_absence) angepasst. Regressionstest ergänzt.

Gefunden während Coverage-Arbeit an absence_service.py/report_service.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:50:28 +02:00
patrickandClaude Sonnet 5 dd044793c6 fix(tests): compare UUID objects not str in DATEV coverage test
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
report_service.datev_monthly_report compares user.company_id (uuid.UUID)
against the passed company_id with != — a plain str from the JSON API
response never equals the UUID, so it always looked like a mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:34:12 +02:00
patrickandClaude Sonnet 5 55d54e0ff0 fix(tests): expire_all() before DATEV bypass query to avoid stale identity map
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:32:02 +02:00
patrickandClaude Sonnet 5 21b65087d3 fix(tests): re-set RLS bypass per transaction in coverage gap tests
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
SET LOCAL app.bypass_rls only holds for the current transaction; a prior
commit() silently re-enabled RLS, causing db.get(User/Company, ...) to
return None on the raw db_session (no app.company_id context).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:31:20 +02:00
patrickandClaude Sonnet 5 dc4982bb37 fix(tests): year-boundary + response-shape issues in coverage gap tests
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
create_absence's warnings are discarded by the router (only approve_absence
passes them through AbsenceOut.warnings) — test now calls the service
directly and documents this as a separate finding. DATEV test kept within
a single year since absence_service._get_holiday_dates only looks up
holidays for start_date.year (cross-year ranges miss the boundary holiday).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:30:35 +02:00
patrickandClaude Sonnet 5 cbb5f467fe test: gezielte Coverage-Tests für kritische Pfade in absence_service/report_service
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Deckt Carryover-Expiry (Integrationspfad), kombinierte Teilzeit+Pro-rata-
Berechnung, FZA-Rückbuchung via Cancellation-Request-Flow, Zwei-Stufen-
Genehmigung inkl. Doppelgutschrift-/Schwellwert-Grenzfall, DATEV-Export
(Feiertag vs. Urlaub) und §3b-Zuschlagskategorisierung (Nachtschicht über
Mitternacht, Sonntag+Feiertag).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:28:34 +02:00
patrickandClaude Sonnet 5 b7bec79284 fix(setup): nginx-Template im Installer auf Prod-Stand bringen
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
setup_server.sh schrieb eine veraltete Minimal-nginx-Config ohne
/redoc, /static/-Alias (nötig für lokale Swagger/ReDoc-Assets) und
Security-Header. Bei einem Neu-Setup wäre /docs sofort wieder kaputt
gewesen. Jetzt 1:1 an die auf 137/164 laufende Config angeglichen.
Zusätzlich inkonsistente Step-Nummerierung ([1/6]...[5/7]) korrigiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:17:27 +02:00
patrickandClaude Sonnet 5 aba0b53449 fix(docs): StaticFiles-Mount-Pfad korrigieren (static statt app/static)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Vorheriger Commit hat nur die Dateien verschoben, main.py zeigte noch
auf den alten Pfad app/static.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:12:48 +02:00
patrickandClaude Sonnet 5 4e30c55b6c fix(docs): Swagger-Assets nach backend/static/ verschoben (nginx-Alias)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Server-nginx hat bereits ein /static/-Alias auf backend/static/ (Uploads),
nicht backend/app/static/. Assets lagen am falschen Pfad -> 404 hinter nginx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:12:28 +02:00
patrickandClaude Sonnet 5 9f9dfb5be3 fix(docs): Swagger/ReDoc-Assets lokal statt von cdn.jsdelivr.net
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
/docs blieb leer wenn das CDN vom Client-Netzwerk aus nicht erreichbar
war (Firewall/Proxy) – Backend lieferte korrektes HTML, aber Swagger-UI-
JS/CSS und ReDoc-JS kamen nicht an. Jetzt unter app/static/swagger-ui/
gebündelt und über eigene /docs+/redoc-Routen ausgeliefert (nur Dev,
Production weiterhin ohne interaktive Docs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:11:08 +02:00
patrickandClaude Sonnet 5 c1572c397d docs(api): 7 fehlende Router nachgezogen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
hours_payouts, public_stamp, ical, reseller, tenants,
special_assignments, tls_admin – seit dem letzten Doku-Update
(2026-05-24) neu dazugekommen. projects.py bewusst ausgelassen
(toter Code, nicht registriert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 23:05:51 +02:00
patrickandClaude Sonnet 5 9bdce25187 test(coverage): fail-under=50 Gate + XML-Report
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Baseline liegt bei 55% (verifiziert auf 137). Gate mit 5%-Puffer,
kein CI-Job dafür – Tests brauchen echtes Postgres+Redis mit RLS-Setup,
läuft nur remote (SSH), nicht in Gitea Actions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 22:52:42 +02:00
patrickandClaude Sonnet 5 538cbb0437 test(coverage): pytest-cov einbauen (term-missing Report)
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
Kein --cov-fail-under noch gesetzt, erst Baseline auf 137 ermitteln.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 22:47:31 +02:00
patrickandClaude Sonnet 5 ad0bc62b21 ci(frontend): Build-Job als Gate im Security-Workflow ergänzen
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Frontend Build (tsc + vite) (push) Canceled after 0s
npm ci + npm run build (tsc -b + vite build) laufen jetzt bei jedem
Push/PR und blockieren bei TS-/Build-Fehlern statt unbemerkt zu brechen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 22:46:57 +02:00
patrickandClaude Sonnet 5 c733ddfe40 fix(redis): gepoolten async-Redis-Client statt Connect/Close pro Request
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
TOTP-Login und Kiosk-Nonce-Check öffneten/schlossen bisher pro Request eine
neue aioredis-Verbindung. Neuer get_async_redis()-Pool in core/redis.py wird
von beiden Stellen genutzt, sauberer Shutdown im FastAPI-Lifespan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 22:29:08 +02:00
patrickandClaude Sonnet 5 52ecd9e5ce chore(routers): toten Router absence.py entfernen
159 Zeilen toter Code, weder in main.py registriert noch importiert.
Absence-Logik lebt in routers/absences.py + services/absence_service.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 22:27:24 +02:00
patrickandClaude Sonnet 5 b8073fa133 docs(devlog): Sessions vom 2026-08-05 bis 2026-09-02 nachtragen
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Security Audit / Python Dependency Audit (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
2026-09-02 15:08:54 +02:00
patrickandClaude Sonnet 5 81d58f720e feat(kiosk): PC-Terminal-Frontend für NFC-Login + Stempeln
Security Audit / Python Dependency Audit (push) Canceled after 0s
Security Audit / Node.js Dependency Audit (push) Canceled after 0s
Bisher zeigte KioskStampPage nur einen Status ("Kiosk-Modus aktiv"), es gab
keine Möglichkeit sich einzuloggen oder tatsächlich zu stempeln. Jetzt:

- NFC-Erfassung ohne Fokus-Anforderung: globaler keydown-Listener erkennt den
  Tastatur-Emulations-Output günstiger USB-HID-RFID-Reader (Zeichen-Burst
  <300ms + Enter) und unterscheidet ihn von echter Tastatureingabe
- PIN-Pad nach Kartenerkennung (Pflicht-Zweitfaktor, siehe 3650da8/K-3-Fix)
- Nach Login: Stempel-Aktionen je nach aktuellem Status (Ein/Aus/Pause
  starten/beenden) über die neuen /kiosk/stamp/*-Endpunkte (2ce363c)
- Automatischer Reset zur Kartenauflege-Ansicht nach jeder Aktion

Alle Requests laufen weiter transparent durch den bestehenden Ed25519-
signierenden ServiceWorker (kiosk-sw.js, kein Adapter nötig).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 13:37:06 +02:00
patrickandClaude Sonnet 5 21c7c0bd14 test(kiosk): E2E-Test für Login + Stempel-Flow, Cross-Device-Session-Check
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 13:34:39 +02:00
patrickandClaude Sonnet 5 2ce363c422 feat(kiosk): Stempel-Endpunkte für Kiosk-Sessions (fehlten bisher komplett)
Login (PIN/NFC/QR/Liste) lieferte einen session_token, aber es gab keinen
Endpunkt der ihn tatsächlich zum Stempeln nutzt (X-Kiosk-Session-Token war
nur als Konzept in kiosk_session_service dokumentiert, nirgends verdrahtet).

Neu: POST /kiosk/stamp/{in,out,break-start,break-end,status}. Gerät wird
weiterhin per Ed25519 verifiziert (verify_kiosk_request), zusätzlich validiert
_user_from_session() dass der session_token zu genau diesem Gerät gehört und
der User noch aktiv/in der richtigen Firma ist. Reuse von time_service
(stamp_in/out/break_start/break_end/get_today), source=KIOSK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 13:32:46 +02:00
patrickandClaude Sonnet 5 3650da8022 fix(kiosk): NFC-Login verlangt Pflicht-PIN als Zweitfaktor (Security-Audit K-3)
Bisher genügte die reine NFC-UID zum Einstempeln. Getestete Reader-Hardware
(günstiger USB-HID-RFID-Leser, EM4100 125kHz) liefert nur eine unverschlüsselte,
trivial klonbare Chip-ID - identisch zum in security_audit_kiosk_qr_nfc_2026_05_26
(K-3) beschriebenen Risiko, das bisher offen war.

- login_nfc() verlangt jetzt PIN, nutzt denselben Brute-Force-Lockout wie
  login_pin (keyed auf nfc_uid statt Personalnummer)
- Neuer Endpunkt POST /users/{id}/kiosk-nfc (Admin/HR) zum Zuordnen einer
  Karte zu einem Mitarbeiter - existierte bisher gar nicht, kiosk_nfc_uid
  war nur im Model vorhanden, nirgends setzbar
- Company-interner Unique-Check (eine Karte = ein Mitarbeiter)

Kein bestehendes Frontend nutzt NFC-Login bisher, daher kein Breaking Change.
Höhere Sicherheitsstufe (NTAG424 SUN, klon-resistent) bleibt vorgemerkt für
späteren Hardware-Wechsel (aktueller Reader kann keine Kryptografie).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 13:24:19 +02:00
patrickandClaude Sonnet 5 cfa707d8f9 fix(time): break_minutes DB-Default ergänzen + Retention-Tests korrigiert
Gleiches Muster wie hours_payouts.status (7166bbb): Model hatte nur
Python-default, kein server_default -> Migration 0044 zieht DB-Default
nach. Retention-Tests nutzten fehlenden Wert bei Raw-SQL-Insert und
fehlenden RLS-Bypass zwischen den Transaktionsgrenzen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 09:06:59 +02:00
patrickandClaude Sonnet 5 5a9e432a80 feat(dsgvo): Löschkonzept/Aufbewahrungsfristen (Auto-Purge)
Neuer retention_service.py: Lohn-/zeitrelevante Daten (time_entries,
hours_payouts) werden nach konfigurierbarer Frist gelöscht
(company.settings.retention_lohn_years, Default 10 Jahre). Technische
Tabellen mit fester Frist: audit_logs (3 Jahre), abgelaufene
sessions/password_resets (sofort).

Täglicher Scheduler-Job (03:00 Uhr, Redis-Tageslock analog Reminder-Jobs)
plus manuelle Trigger: POST /companies/me/run-retention-purge
(COMPANY_ADMIN/HR, nur eigene Firma) und POST /admin/run-retention-purge
(SUPER_ADMIN, global inkl. technischer Tabellen).

Letzter offener Punkt aus dem DSGVO-Löschkonzept (Art. 15/17 bereits erledigt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 09:04:27 +02:00
patrickandClaude Sonnet 5 7166bbb5aa fix(payouts): status-Spalte mit echtem DB-Default statt nur Python-Default
Model hatte nur default= (ORM-seitig), kein server_default. Test-DB nutzt
Base.metadata.create_all statt Alembic-Migrationen -> Spalte war dort NOT
NULL ohne DB-Default, raw-SQL-Inserts (test_rls.py) schlugen fehl. Prod-DB
hat den Default bereits aus Migration 0042 - hier nur Model nachgezogen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 00:30:20 +02:00
patrickandClaude Sonnet 5 456ca1526b fix(security): RLS für special_assignments nachziehen (DSGVO)
special_assignments (Migration 0029) hatte keine Row-Level-Security in
Produktion, obwohl company_id/user_id geführt werden - Tests liefen
trotzdem grün, da conftest.py die Policy bereits simuliert hatte
(gefunden durch postgres-expert-Review). Analog 0039 (hours_payouts)
nachgezogen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gis16MnuwkYcivLrSxK1pD
2026-08-27 00:20:00 +02:00
patrickandClaude Sonnet 5 23f8e634d1 feat(dsgvo): Art. 15 Auskunftsrecht – strukturierter Datenexport pro User
GET /users/{id}/export-data liefert alle personenbezogenen Daten
(Profil, Zeiterfassung, Abwesenheiten, Kommentare, Urlaubs-/Überstunden-
konto, Auszahlungen, AuditLog-Einträge über den User) als JSON-Download.
Eigenes Konto immer erlaubt, fremde Konten nur HR/Admin/Manager der
eigenen Firma. Ergänzt die bestehende Art.-17-Anonymisierung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
2026-08-07 08:19:15 +02:00
patrickandClaude Sonnet 5 59a0305a75 feat(reports): DATEV-Monatsblatt-Button im Zeiterfassungs-Tab
Neuer Button "DATEV-Monatsblatt" neben CSV/Excel/PDF im time-Tab von
ReportsPage. Nutzt den bestehenden Mitarbeiter-Filter + den Monat aus
dateFrom, lädt GET /reports/datev-monthly/export als PDF herunter.
Manager müssen einen einzelnen Mitarbeiter wählen (kein Sammel-Export
möglich, da 1 Blatt = 1 Mitarbeiter/Monat).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
2026-08-06 22:25:45 +02:00
patrickandClaude Sonnet 5 8a2f512112 fix(reports): DATEV-Export nutzt falschen FK-Namen (absence_type_id statt type_id)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
2026-08-06 22:20:50 +02:00
patrickandClaude Sonnet 5 1dcc04ae07 feat(reports): DATEV-Monatsblatt-Export (Blanko-Vorlage) für Steuerberater
Neuer PDF-Export GET /reports/datev-monthly/export: ein Blatt pro
Mitarbeiter/Monat im Layout der DATEV-Stundenaufzeichnungs-Vorlage
(Beginn/Pause/Ende/Dauer + K/U/UU/F/SA/SU-Kürzel + Bemerkungen +
Summe + Unterschriftsfelder). Führt time_entries, Absences und
Feiertage pro Kalendertag zusammen.

Außerdem: Mustervorlagen für neue Backend-Module (Model/Schema/Router/
Migration/Test) und eine Frontend-Page-Vorlage, abgeleitet vom
hours_payouts-Modul als aktuellstem sauberen Muster.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
2026-08-06 22:19:54 +02:00
patrickandClaude Sonnet 5 f3ed234e56 feat(admin): SUPER_ADMIN TLS-Zertifikat-Status + Renewal-Trigger
Neuer Router /admin/tls (SUPER_ADMIN only, AuditLog, Rate-Limit 5/hour):
- GET  /admin/tls/status         – erkennt proxy/certbot/internal-Modus,
  liest Ablaufdatum via openssl x509 -enddate
- POST /admin/tls/renew/certbot  – ruft setup-tls.sh <domain> auf
- POST /admin/tls/renew/internal – ruft setup-tls-internal.sh <hostname> [ip]
  auf, reloaded nginx danach

Läuft mit den Root-Rechten des bestehenden timemaster.service (User=root,
unverändert) - Angriffsfläche dadurch begrenzt auf SUPER_ADMIN-Auth +
Domain/Hostname-Validierung (Regex, kein Shell-Interpolieren, subprocess
mit Argument-Liste statt shell=True).

Frontend: neuer Tab "Server / TLS" in TenantsPage – Status-Anzeige +
zwei Formulare (öffentlich/intern).

3 neue Tests in test_tls_admin.py (Rollen-Gate, Status im Testcontext,
Input-Validierung).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 20:29:47 +02:00
patrickandClaude Sonnet 5 f354bddd1e feat(tls): optionales Setup für interne CA vorbereitet
setup-tls-internal.sh: Alternative zu setup-tls.sh für rein internes Netz
ohne öffentliches DNS. Baut einmalig eine interne Root-CA + signiert
Server-Zertifikate damit (openssl, kein Let's Encrypt). Root-CA muss auf
Clients importiert werden, danach keine Browser-Warnung mehr für alle
künftigen intern signierten Server. Nicht ausgeführt, bleibt inaktiv bis
manuell gestartet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 19:36:19 +02:00
patrickandClaude Sonnet 5 6104a67c9e feat(tls): optionales certbot-Setup vorbereitet (Option A)
setup-tls.sh: auf Server ausführbares, manuelles Skript für direktes
Let's-Encrypt-Zertifikat via certbot --nginx. Bleibt ungenutzt solange
der vorgeschaltete Proxy TLS+Domain übernimmt (aktueller Stand) -
nginx.conf bleibt deshalb bewusst HTTP-only, certbot würde die
443-Erweiterung selbst in /etc/nginx/ einfügen, nicht ins Repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 19:34:25 +02:00
patrickandClaude Sonnet 5 2046f6475b fix(security): python-jose CVE-2024-33663 durch pyjwt ersetzt, Deps gepinnt
- security.py/dependencies.py/auth.py: jose -> pyjwt (unmaintained,
  Algorithm-Confusion-CVE). API-kompatibel (jwt.encode/decode gleich).
- requirements.txt: alle Versionen gepinnt (waren >=, jetzt == anhand
  aktueller 137-Installation) fuer reproduzierbare Deploys.
- nginx.conf: /docs + /openapi.json nur noch aus LAN erreichbar (waren
  oeffentlich, API-Struktur-Leak).

fail2ban auf 137+164 installiert (sshd + nginx-badbots + timemaster-auth
Jails), Configs nicht im Repo (Server-only, /etc/fail2ban/).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 19:10:28 +02:00
patrickandClaude Sonnet 5 7c24887ec1 fix(hours_payouts): RLS-Bypass-Falle bei mid-request commit
_build_out() lief nach db.commit() -> app.bypass_rls verfällt mit
Transaktion, Post-Commit-Reads sahen keine Zeilen mehr. _build_out()
jetzt vor commit aufgerufen (create/request/approve/reject/cancel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTxkZEUdfgMxZvHPiZJ8bV
2026-08-05 18:56:55 +02:00
patrickandClaude Opus 4.8 eb8dace0fd feat(overtime): Auszahlungs-Anträge durch Mitarbeiter (opt-in pro Firma)
Mitarbeiter beantragt Überstunden-Auszahlung, HR genehmigt/lehnt ab.
Firmen-Opt-in payout_request_enabled (Default aus - nur HR-Direktbuchung).
Saldo-Abzug erst bei Genehmigung; Reject/Cancel bucht nichts.

- Migration 0042: hours_payouts.status/decided_by/decided_at/rejection_reason
  + companies.payout_request_enabled (nur Spalten, RLS unveraendert)
- Router: POST /hr/payouts/request|{id}/approve|reject|cancel; list status-Filter;
  HR-Direktbuchung bleibt (status approved)
- Frontend: PayoutRequestCard (Selbstbedienung in AbsencesPage), HR-Page
  Status-Spalte + Genehmigen/Ablehnen, CompanySettings-Toggle
- 4 pytest-Cases

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:43:26 +02:00
patrickandClaude Opus 4.8 87806b4501 fix(deploy): server-update.sh re-exec nach git pull (Self-Edit-Falle)
Skript zog sich per pull selbst neu, waehrend die alte Version noch lief →
Health-Check nutzte alten Code. Nach pull jetzt exec der frischen Version
(Marker _SU_REEXEC verhindert Doppel-Pull).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:47:58 +02:00
patrickandClaude Opus 4.8 a772785add fix(deploy): server-update.sh Health-Check mit Retry (Worker-Startzeit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:33:59 +02:00
patrickandClaude Opus 4.8 8ae792fd90 chore(deploy): entkoppelte Self-Update-Skripte pro Server
Jeder Server aktualisiert sich eigenstaendig aus dem Repo – kein
Cross-Server-SSH, kein "both". 137 und 164 komplett getrennt.

- server-update.sh: laeuft AUF dem Server (git pull --ff-only → alembic
  upgrade head → restart → health). Guard: bricht ab wenn venv fehlt.
- push-frontend.sh: lokal, baut Frontend + rsync dist/ an EINEN Server
  (Node laeuft nicht auf den Servern, dist ist gitignored). Guard gegen
  Ausfuehrung auf einem Zielserver.
- deploy.sh + update.sh stillgelegt (Abbruch-Stubs): deploy.sh rsyncte den
  Arbeitsbaum in den Server-git-Baum und verschmutzte ihn (git pull brach);
  update.sh koppelte beide Server ("both").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:29:43 +02:00
patrickandClaude Opus 4.8 d13350b38b feat(ical): abonnierbarer read-only Kalender-Feed pro Nutzer
Token-gescoper iCal-Feed (/absences/ical/<token>.ics), abonnierbar in
Outlook/Apple/Google. Anders als der CalDAV-Client (Push nach Nextcloud)
pollt der Kalender die URL selbst. Feed zeigt nur die eigenen bestätigten
Abwesenheiten des Token-Inhabers.

- users.ical_token_hash (SHA-256, rotierbar) + Migration 0041 (nur Spalte,
  keine RLS-Aenderung; users-Policy deckt neue nullable Spalte ab)
- Router ical.py: oeffentlicher Feed (kein JWT) + Token-Verwaltung
  POST/GET/DELETE /users/me/ical-token (authentifiziert)
- ProfilePage: Sektion "Kalender-Abo (iCal)" mit Erzeugen/Rotieren/
  Deaktivieren, URL-Anzeige einmalig + Kopieren
- test_ical.py: Token-Lifecycle + oeffentlicher Feed (3 Tests)

Deployed auf 137 (Migration 0041, 196/196 Tests gruen). 164 ausstehend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:12:05 +02:00
patrickandClaude Opus 4.8 d27be740a0 fix(payouts): 500 in GET /hr/payouts – computed Felder als optional
user_name/created_by_name sind Pflichtfelder, die der Router erst NACH
model_validate(payout) setzt – pydantic v2 schlägt aber schon bei der
Validierung fehl (Feld fehlt am ORM-Objekt). Default "" ergänzt; der
Router überschreibt die Werte wie bisher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:35:40 +02:00
patrickandClaude Opus 4.8 c497a14446 refactor(ui): Verwaltung-Dropdown aufgelöst – Berichte flach, Rest in /settings
Verwaltung-Dropdown entfernt. Berichte wandert flach in die Topbar (HR+).
Mitarbeiter/Sondervertretungen/Auszahlung als Gruppe "Personal" und Mandanten
als Gruppe "Mandant" auf der /settings-Kachelseite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:27:51 +02:00
patrickandClaude Opus 4.8 933d369dc4 feat(ui): Navigation neu strukturiert (flache Topbar + Verwaltung-Menü + /settings)
Topbar nur noch täglich genutzte Punkte (Dashboard/Zeit/Abwesenheiten/Kalender);
HR/Admin-Funktionen im neuen "Verwaltung"-Dropdown. Zahnrad führt auf eigene
/settings-Seite mit Kategorie-Kacheln statt 11er-Flachliste. Zentrale navConfig.ts
für PRIMARY/MANAGEMENT/SETTINGS_GROUPS; Rollen-Gating unverändert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 22:07:34 +02:00
patrickandClaude Opus 4.8 c122ab6fd7 feat(dsgvo): Art. 17 Anonymisierung von Mitarbeiterkonten
Statt Hard-Delete (kollidiert mit Lohn-/Steuer-Aufbewahrung) werden bei
DSGVO-Löschanfrage die personenbezogenen Felder der bestehenden User-Zeile
überschrieben; referenzierte Daten (Zeit/Abwesenheit/Auszahlung/AuditLog)
bleiben entkoppelt erhalten.

- users.anonymized_at (Migration 0040); UserOut.anonymized_at.
- user_service.anonymize_user: überschreibt Name/E-Mail (eindeutig anon-<id>@deleted.local),
  entfernt Auth-Material (Passwort/PIN/QR/NFC/TOTP/LDAP-DN), is_active=False, löscht Sessions,
  AuditLog 'user_anonymized'. Personalnummer bleibt reserviert (kein Personenbezug mehr).
  Selbst-Anonymisierung (400), Doppelanonymisierung/Reaktivierung anonymisierter Konten (409).
- POST /users/{id}/anonymize (COMPANY_ADMIN/SUPER_ADMIN, mandantengeprüft).
- UsersPage: "Anonymisieren"-Button mit Sicherheitsabfrage + "Anonymisiert"-Badge.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:31:14 +02:00
patrickandClaude Opus 4.8 3034c6c55a fix(security/dsgvo): RLS für hours_payouts nachgezogen (Mandantentrennung)
hours_payouts war die einzige firmenbezogene Tabelle ohne Row-Level-Security
(Migration 0030 hatte keinen RLS-Block). Die Endpunkte filtern zwar applikativ
nach company_id (kein akutes Leck), aber das DB-seitige Schutznetz – das im
ganzen System (FORCE RLS, 0024/0034) die Mandantentrennung garantiert – fehlte.

Migration 0039 aktiviert ENABLE+FORCE RLS + company_id-Policies (analog 0024).
conftest.py-RLS-Replik + neuer Cross-Tenant-Test test_rls_hours_payouts_tenant_isolation.
Verifiziert auf 137+164 (rls=True, force=True, 4 Policies). 191/191 Tests grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:06:19 +02:00
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