Files
timemaster/CLAUDE.md
T
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

18 KiB
Raw Blame History

TimeMaster Projektkontext für Claude Code

Was ist das?

Zeiterfassung & HR-Tool (ähnlich Timebutler.de). DSGVO-konform, EU-Server.

Tech-Stack

  • Backend: Python 3.12 · FastAPI · SQLAlchemy (async) · PostgreSQL 16 · Redis 7
  • Frontend: React 18 · TypeScript · Tailwind CSS
  • Deployment: Nativ auf Ubuntu (systemd + nginx) kein Docker in Phase 1
  • Tests: pytest + pytest-asyncio

Aktueller Stand

agent-01-auth (FERTIG)

  • FastAPI App, CORS, Health-Endpoint
  • JWT Access Token (30 min) + Refresh Token Rotation (30 Tage)
  • 5 Rollen: SUPER_ADMIN, COMPANY_ADMIN, HR, MANAGER, EMPLOYEE
  • require_role() FastAPI Dependency
  • Models: Company, Department, User, Session, PasswordReset, AuditLog
  • Routers: /api/v1/auth · /api/v1/users · /api/v1/companies
  • Services: auth_service, user_service, email_service
  • Alembic Migration: 0001_initial (alle Auth-Tabellen)
  • 20+ pytest Tests

🚧 agent-02-zeiterfassung (TODO Woche 3-4)

Dateien anlegen in:

  • backend/app/models/time_entry.py
  • backend/app/models/work_schedule.py
  • backend/app/schemas/time_entry.py
  • backend/app/routers/time_entries.py
  • backend/app/services/time_service.py (inkl. ArbZG-Prüfung)
  • backend/migrations/versions/0002_time_entries.py
  • backend/tests/test_time.py

API-Endpunkte:

POST   /api/v1/time/stamp-in
POST   /api/v1/time/stamp-out
POST   /api/v1/time/break-start
POST   /api/v1/time/break-end
GET    /api/v1/time/today
GET    /api/v1/time/entries
POST   /api/v1/time/entries
PATCH  /api/v1/time/entries/{id}
POST   /api/v1/time/entries/{id}/approve
POST   /api/v1/time/entries/{id}/reject
GET    /api/v1/time/balance/{user_id}

agent-03-abwesenheit (FERTIG siehe weiter unten)

🚧 agent-02-kiosk (TODO Woche 5, nach agent-02)

  • backend/app/models/kiosk_device.py
  • backend/app/routers/kiosk.py
  • backend/app/services/kiosk_service.py Auth-Methoden: PIN · NFC (Web NFC API) · QR (jsQR) · Mitarbeiter-Liste

agent-03-abwesenheit (FERTIG deployed)

  • Models: AbsenceType, Absence, VacationBalance, PublicHoliday, OvertimeBalance
  • Rollen-basierte Genehmigung (MANAGER/HR/ADMIN)
  • Urlaubskonto: Grundurlaub + Sondertage + Resturlaub (auto carry-over)
  • Frontend: AbsencesPage mit Liste-Tab + Jahresplaner (Person×Monat-Grid + Monats-Gantt)
  • TOTP/2FA: pyotp, QR-Setup, partial JWT Login-Flow, Profil-Seite
  • Migrationen: 0015 (TOTP), 0016 (special_days) deployed

agent-05-krankmeldung (FERTIG deployed 2026-05-06)

Erweiterung des Abwesenheits-Moduls speziell für Kranktage:

Backend:

  • Company.sick_note_required_after_days (Default 3) Firmen-Default
  • AbsenceType.certificate_after_days als Per-Typ-Override (Override gewinnt)
  • Auto-Berechnung certificate_required_by beim Anlegen einer SICK-Absence
  • POST /absences/quick-sick Sofort-Krankmeldung (auto-approved, nutzt ersten aktiven SICK-Typ)
  • PATCH /absences/{id}/certificate HR/Admin markiert Attest als eingegangen
  • GET /absences/sick-stats?user_id=&ref_date= Bradford-Faktor (rolling 12 Monate), Episoden, Tage, AU-überfällig

Frontend:

  • 🤒 "Krank melden" Schnell-Button im AbsencesPage-Header (orange, neben "+ Antrag stellen")
  • AU-überfällig-Badge in Liste (orange) + grüner "Attest ✓"-Badge wenn eingegangen
  • HR/Admin-Action "Attest erhalten" in Liste (sichtbar für HR/COMPANY_ADMIN/SUPER_ADMIN, nicht MANAGER)
  • ReportsPage: vierter Tab "Krankmeldungen" mit KPIs (⌀ Bradford, Σ Tage, Σ Episoden, AU offen) + sortierte Tabelle pro Mitarbeiter
  • Bradford-Heatmap aufgeschoben

Migration: 0022_sick_note_config.py companies.sick_note_required_after_days INTEGER DEFAULT 3 (chained on 0020, da 0017 nicht in der Alembic-Kette war)

Tests: 5 neue pytest-Cases in test_absences.py (quick_sick, cert auto-calc, type override, mark_certificate, bradford) alle grün

🚧 agent-06-auditlog (TODO)

AuditLog-Einträge sind bereits in der DB (Tabelle audit_logs, Model vorhanden seit agent-01). Fehlend: Backend-Endpunkt + Frontend-Seite für SUPER_ADMIN / COMPANY_ADMIN.

Backend:

  • Neuer Endpunkt: GET /audit-logs/ mit Filtern:
    • user_id, action, entity_type, date_from, date_to, limit, offset
    • Nur COMPANY_ADMIN/SUPER_ADMIN, company-isoliert
  • Felder die zurückgegeben werden: id, user_id, user_name, action, entity_type, entity_id, old_value, new_value, ip_address, created_at
  • Router: backend/app/routers/audit.py

Frontend:

  • Neue Seite: frontend/src/pages/AuditLogPage.tsx
  • Route: /audit-log (nur COMPANY_ADMIN, SUPER_ADMIN)
  • Filter: Zeitraum, Benutzer, Aktion, Entity-Typ
  • Tabelle: Zeitstempel, Benutzer, Aktion, Betroffenes Objekt, IP
  • Detail-Expand: old_value / new_value als JSON-Diff
  • Export als CSV
  • Navigation: Eintrag in Einstellungen-Dropdown in Layout.tsx

🚧 agent-04-dashboard (TODO Woche 6-7)

  • Mitarbeiter-, Manager-, Admin-Dashboard
  • Reports: Anwesenheit, Abwesenheit, Überstunden, Kranktage (Bradford-Faktor)
  • Export: PDF (WeasyPrint) · CSV · XLSX (openpyxl)

🚧 agent-07-personalnummer (Phase 1 deployed 2026-05-05; Phase 2-5 offen)

Eindeutige Personalnummer pro Mitarbeiter, in allen API-Antworten, Exports und Kiosk-Login nutzbar.

Backend:

  • User.personnel_number: str(50) | None Format nur Ziffern (^[0-9]+$), max 50 Stellen
  • Partial Unique Index: UNIQUE (company_id, personnel_number) WHERE personnel_number IS NOT NULL
  • Reservierung: Bei Deaktivierung bleibt Nr. am User, wird nie wieder vergeben
  • Company-Felder:
    • personnel_number_required: bool (default false → Pflicht erst nach Aktivierung in Firmen-Settings; gilt nur für neue User, Bestandsuser bleiben)
    • personnel_number_mode: enum('manual'|'auto') (default manual)
    • personnel_number_next: int (Counter, beginnt bei 1, 4-stellig zero-padded → 0001, kein Präfix wegen „nur Ziffern"-Regel)
  • Auto-Modus erlaubt manuelles Override durch Admin (Counter läuft normal weiter)
  • Race-Condition-Schutz: UPDATE companies SET personnel_number_next = personnel_number_next + 1 RETURNING ... (atomic)
  • Endpoints:
    • GET /users/next-personnel-number Vorschlag basierend auf Counter
    • GET /users/by-personnel/{number} Lookup für externe Integrationen
    • GET /users/import-template.csv Vorlage für Bulk-Import
    • POST /users/import (multipart CSV) Bulk-Import:
      • Doppelte E-Mail im Import (zwei Zeilen) → Fehler
      • E-Mail existiert aktiv → Fehler
      • E-Mail existiert aber User deaktiviert → Reaktivieren mit neuen Daten
      • Leere Personalnr. → Auto-Vergabe (auch im Manuell-Modus)
    • GET /users Query-Param search filtert auch Personalnr.
  • AuditLog bei jeder Personalnr.-Änderung
  • LDAP-Sync: bei Konflikt mit reservierter/vergebener Nr. → Fehler, LDAP-Wert wird verworfen (kein Override)
  • Personalnr. in CSV/XLSX/PDF-Exports (reports.py), CalDAV-Template-Platzhalter $personalnummer, LDAP-Sync-Mapping employeeNumber

Frontend:

  • UsersPage: Spalte "Pers.-Nr.", Suche schließt Personalnr. ein, Edit-Modal mit Live-Verfügbarkeitsprüfung
  • Invite-Modal: Eingabefeld + "Vorschlagen"-Button (Auto-Modus)
  • ProfilePage: Read-only Anzeige
  • CompanySettingsPage: Toggles Pflicht/Modus/Präfix/Counter
  • ReportsPage + Exports: Spalte ergänzt
  • CSV-Import-UI mit Vorlagen-Download und Validierungs-Vorschau

Migration: 0020_personnel_number.py

  • users.personnel_number VARCHAR(50) NULL + Partial Unique Index + CHECK constraint ~ '^[0-9]+$'
  • companies.personnel_number_required BOOLEAN DEFAULT FALSE
  • companies.personnel_number_mode VARCHAR(10) DEFAULT 'manual'
  • companies.personnel_number_next INTEGER DEFAULT 1

Tests:

  • pytest: Unit/Integration für Service-Logik, CSV-Import, Race-Condition (parallele Inserts)
  • Playwright-Setup wird im Rahmen von agent-08 aufgesetzt CSV-Upload-UI dann mit testen

🚧 agent-08-kiosk-haertung (TODO kritisch vor Produktiv-Einsatz)

MITM-resistente Absicherung des Kiosk-Modus. Aktueller Token-basierter Auth ist nicht ausreichend gegen TLS-Inspection-Proxies und Insider-Angriffe (siehe Security-Audit).

Architektur-Entscheidung: Browser bleibt Kiosk-Plattform native App mittelfristig, aber nicht in dieser Phase. Trust-Boundary: IT-Admin der Kiosk-Geräte.

Krypto-Entscheidung: Ed25519-Public-Key statt mTLS+HMAC Statt mTLS-Client-Zertifikate + HMAC-Shared-Secret nutzt jedes Kiosk-Gerät ein Ed25519-Keypair (gleicher Algo wie moderne SSH-Keys). Public Key wird beim Server registriert, Private Key bleibt non-extractable im Kiosk-Browser. Vorteile: kein CA-Management, kein Shared Secret, Remote-Enrollment trivial via CLI auf Server, einfacheres nginx (kein mTLS), kryptographisch gleichwertig oder besser.

Backend:

  • KioskDevice erweitern:
    • status: enum('pending','approved','revoked') (löst is_active ab)
    • public_key: text Ed25519 Public Key (PEM oder OpenSSH-Format)
    • key_algorithm: str default 'ed25519' (Vorbereitung für spätere Algos)
    • last_heartbeat_at: datetime | None
    • client_version: str | None
    • current_user_id: uuid | null (wer aktuell eingestempelt ist DSGVO: pro Firma deaktivierbar)
    • offline_queue_size: int default 0
    • ip_whitelist: text | null (CIDR-Liste, in Production verpflichtend)
    • enrollment_token_hash: str | None + enrollment_expires_at (Setup-URL, max 30 min nicht Token, nur Geräte-ID-Bindung)
  • Company.kiosk_require_approval: bool default true
  • Company.kiosk_track_current_user: bool default true (DSGVO-Opt-Out)

Sichere Stempel-Auth pro Request (neue Dependency verify_kiosk_request()):

  • TLS-Pflicht: nginx HTTPS-only + HSTS, App lehnt non-TLS in Production ab
  • HTTP-Header pro Request:
    • X-Kiosk-Key-Id: UUID des Geräts
    • X-Kiosk-Timestamp: Unix-Sekunden (max 30s Drift; Server-Time autoritativ)
    • X-Kiosk-Nonce: UUID, einmalig (Replay-Schutz)
    • X-Kiosk-Signature: Base64(Ed25519-Signatur über METHOD + PATH + TIMESTAMP + NONCE + sha256(BODY))
  • Backend verifiziert mit gespeichertem Public Key (Python: cryptography.hazmat.primitives.asymmetric.ed25519)
  • Nonce-Cache 60s in Redis (Replay-Window = Drift-Window)
  • IP-Whitelist-Check pro Gerät

CLI-Tool (neu: backend/cli.py, mit Typer):

# Auf Server ausführen (root@192.168.1.137)
timemaster kiosk add \
  --company "Acme GmbH" \
  --name "Eingang Berlin" \
  --location "Hauptgebäude" \
  --pubkey ~/.ssh/kiosk_berlin.pub \
  --ip-whitelist 10.0.0.0/24

# Output: device_id + Setup-URL für Kiosk-Browser
# https://timemaster.example.com/kiosk/setup?id=<uuid>&token=<one-time>

timemaster kiosk list [--company X] [--status pending|approved|revoked]
timemaster kiosk approve <device_id>
timemaster kiosk revoke <device_id>
timemaster kiosk rotate-key <device_id>   # Setup-URL für neuen Pubkey

CLI nutzt direkten DB-Zugriff über bestehende SQLAlchemy-Models, keine HTTP-API.

Enrollment-Flow (Remote-tauglich):

  1. Vor Ort/Remote: IT-Person erzeugt auf Kiosk-Gerät ein Ed25519-Keypair (entweder im Browser via WebCrypto oder per ssh-keygen -t ed25519 lokal). Private Key bleibt am Gerät.
  2. Public Key wird an Admin gesendet (E-Mail/Chat reicht ist public)
  3. Admin SSH'ed auf Server, registriert per CLI: timemaster kiosk add --pubkey ...
  4. CLI gibt Setup-URL + Geräte-ID zurück → Admin schickt URL an IT-Person
  5. Kiosk-Browser öffnet Setup-URL → bindet bestehenden privaten Key an Geräte-ID, signiert ersten Test-Request
  6. Admin sieht Gerät in WebGUI als pending → Klick „Approve" → ab jetzt aktiv
  7. Privater Key verlässt nie das Kiosk-Gerät, Server kennt nur Public Key

Optional Web-Enrollment-Variante (für lokale Filiale, Admin steht daneben):

  • Admin generiert in WebGUI Setup-URL → QR-Code → Kiosk scannt → Browser generiert Keypair → Public Key per HTTP-POST an Backend → Admin approved.

Key-Rotation: kein Auto-Rotate (Public Key altert nicht). Manueller Re-Enrollment via timemaster kiosk rotate-key falls Verdacht auf Kompromittierung.

Heartbeat & Liveness:

  • Endpoint POST /kiosk/heartbeat alle 30s (signiert wie Stempel-Requests sonst Fake-Online möglich)
  • Body: { uptime_seconds, current_user_id?, browser_version, queued_offline_entries, client_version }
  • Server-Status: online (<90s), stale (<5min), offline (älter)
  • Heartbeat-Intervall pro Firma einstellbar (default 30s, max 120s)

Kiosk-Frontend (separate Route /kiosk):

  • ServiceWorker übernimmt Ed25519-Signierung aller Requests
  • WebCrypto Ed25519 mit extractable: false (XSS kann Key nicht exfiltrieren)
  • Browser-Anforderung: Chrome 113+, Safari 16.4+, Firefox 130+ (alle ab 2023/2024)
  • Heartbeat-Loop alle 30s
  • Offline-Queue mit IndexedDB (Sync sobald wieder online) Queue-Größe an Server reportet
  • BroadcastChannel zur Tab-Koordination (nur ein Tab sendet Heartbeat)
  • Kiosk-Login per Personalnummer + PIN als zusätzliche Methode (nur Kiosk, nicht Web)
  • Server-Time wird im Heartbeat-Response zurückgeliefert → UI zeigt Server-Zeit, Stempel-Timestamp serverseitig erzwungen

Frontend-Verwaltung (KioskDevicesPage):

  • Tab "Wartet auf Freigabe" + Tab "Aktive Geräte" mit Live-Status (Auto-Refresh 30s)
  • Status-Ampel-Spalte (🟢/🟡/🔴), letzter Heartbeat, aktueller User, Client-Version
  • Public-Key-Fingerprint anzeigen (zur Verifikation)
  • Sortier-/Filter-Option "nur offline"
  • Layout.tsx: Health-Badge oben rechts für COMPANY_ADMIN + HR ("2/3 Kiosks online")

Anomalie-Detection (Phase 2 nach Grundgerüst):

  • Stempel-Frequenz-Threshold pro Gerät
  • IP-Sprung-Detection (Geo-Inkonsistenz)
  • Ungewöhnlicher User-Mix pro Gerät → AuditLog-Alarm

nginx-Konfiguration (nginx.conf): keine mTLS-Änderungen nötig. Standard-HTTPS + HSTS reicht Auth läuft komplett auf Application-Layer.

Migration: 0021_kiosk_security.py

  • kiosk_devices: neue Spalten + Drop von is_active (durch status ersetzt)
  • Bestehende Geräte → Default-Status revoked (müssen neu enrolled werden, sicher)
  • companies.kiosk_require_approval, kiosk_track_current_user, kiosk_heartbeat_interval_sec

Sicherheits-Bewertung (Ed25519-Variante):

Schutz Erreicht Restrisiko
Passiver MITM (mitlesen) TLS + Signaturen Public Key + signierte Requests sichtbar (nicht ausnutzbar)
Aktiver MITM (Replay/Modify) Signatur+Nonce+Timestamp
Shared-Secret-Diebstahl entfällt kein Secret
PrivKey-Diebstahl (XSS) non-extractable WebCrypto Key bleibt im Browser-Speicher
Enrollment-Phishing Public Key offen austauschbar
Remote-Filialen-Enrollment trivial via CLI
CA-Verlust = alle Kiosks tot entfällt keine CA
IT-Admin als Innentäter ⚠️ teilweise nur native App + Hardware-Keystore
Zeit-Manipulation am Kiosk Server-Time autoritativ
Browser-Cache-Clear → Re-Enroll ⚠️ ja manueller Re-Enroll nötig (akzeptiert)

Aufteilung in PRs:

  1. Backend: Ed25519-Verifizierung + CLI-Tool + Migration (kein Frontend)
  2. Frontend Kiosk-Mode: ServiceWorker + WebCrypto + Setup-Flow + Offline-Queue
  3. Frontend Verwaltung: KioskDevicesPage-Erweiterung + Health-Badge im Layout
  4. Phase 2: Anomalie-Detection

Wichtige Konventionen

Backend

  • Alle DB-Operationen async (SQLAlchemy AsyncSession)
  • Services enthalten Business-Logik, Router nur HTTP-Handling
  • Pydantic v2 Schemas mit model_validate() statt .from_orm()
  • Fehler immer als HTTPException mit sprechendem detail
  • Tokens nie im Klartext in DB immer hash_token() aus core/security.py
  • Kein Hard-Delete bei Users nur is_active = False

Sicherheit

  • Passwort-Validierung: min. 8 Zeichen, 1 Großbuchstabe, 1 Ziffer
  • Rate-Limiting für Auth-Endpunkte (TODO: slowapi einbauen)
  • Audit-Log bei allen sensitiven Aktionen

ArbZG-Regeln (für agent-02)

  • Max. 8h Arbeitszeit / Tag (Ausnahme bis 10h möglich)
  • Pause ab 6h: 30 min · ab 9h: 45 min
  • Mindestruhezeit zwischen Schichten: 11h
  • Warnung bei Überschreitung, kein hartes Blockieren

Datenbankschema Status

  • Migration 0001: companies, departments, users, sessions, password_resets, audit_logs
  • Migration 0002: time_entries, work_schedules (TODO)
  • Migration 00030016: absence_types, absences, vacation_balances, public_holidays, overtime_balance, totp, special_days
  • Migration 0017: (übersprungen Nummer war nie in der Alembic-Kette)
  • Migration 0018: kiosk_devices
  • Migration 0019: manual_time_entry_permission
  • Migration 0020: personnel_number (users + companies) (agent-07 Phase 1 deployed)
  • Migration 0021: kiosk_security (mTLS, HMAC, heartbeat, enrollment) TODO (agent-08)
  • Migration 0022: sick_note_config (companies.sick_note_required_after_days) (agent-05 deployed 2026-05-06)

Umgebung

Lokal (Claude Code läuft hier)

  • Entwicklung & Dateien bearbeiten: ~/projects/timemaster/
  • Tests lokal ausführen: pytest -v
  • Kein lokaler Python/Postgres nötig alles läuft auf dem Server

Deployment-Server

  • Host: root@192.168.1.137
  • Projektpfad: /opt/timemaster/
  • venv: /opt/timemaster/backend/venv/
  • Service: systemctl restart timemaster
  • Logs: journalctl -u timemaster -f

Deployment-Workflow (nach jeder Änderung)

# Dateien auf Server synchronisieren
rsync -avz --exclude='__pycache__' --exclude='*.pyc' --exclude='.env' \
  ~/projects/timemaster/backend/ \
  root@192.168.1.137:/opt/timemaster/backend/

# Migration ausführen (falls neue Alembic-Version)
ssh root@192.168.1.137 "cd /opt/timemaster/backend && source venv/bin/activate && alembic upgrade head"

# Service neu starten
ssh root@192.168.1.137 "systemctl restart timemaster"

# Logs prüfen
ssh root@192.168.1.137 "journalctl -u timemaster -n 50"

Erstes Setup auf dem Server (einmalig)

ssh root@192.168.1.137 "bash /opt/timemaster/setup_server.sh"