_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
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
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
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
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
/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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- Ü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>
- 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>
- 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>
- 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>
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>
- 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>