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>
18 KiB
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.pybackend/app/models/work_schedule.pybackend/app/schemas/time_entry.pybackend/app/routers/time_entries.pybackend/app/services/time_service.py(inkl. ArbZG-Prüfung)backend/migrations/versions/0002_time_entries.pybackend/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.pybackend/app/routers/kiosk.pybackend/app/services/kiosk_service.pyAuth-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-DefaultAbsenceType.certificate_after_daysals Per-Typ-Override (Override gewinnt)- Auto-Berechnung
certificate_required_bybeim 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 eingegangenGET /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 CounterGET /users/by-personnel/{number}– Lookup für externe IntegrationenGET /users/import-template.csv– Vorlage für Bulk-ImportPOST /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 /usersQuery-Paramsearchfiltert 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-MappingemployeeNumber
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 AnzeigeCompanySettingsPage: Toggles Pflicht/Modus/Präfix/CounterReportsPage+ 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 FALSEcompanies.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:
KioskDeviceerweitern:status: enum('pending','approved','revoked')(löstis_activeab)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 | Noneclient_version: str | Nonecurrent_user_id: uuid | null(wer aktuell eingestempelt ist – DSGVO: pro Firma deaktivierbar)offline_queue_size: int default 0ip_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 trueCompany.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ätsX-Kiosk-Timestamp: Unix-Sekunden (max 30s Drift; Server-Time autoritativ)X-Kiosk-Nonce: UUID, einmalig (Replay-Schutz)X-Kiosk-Signature: Base64(Ed25519-Signatur überMETHOD + 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):
- Vor Ort/Remote: IT-Person erzeugt auf Kiosk-Gerät ein Ed25519-Keypair (entweder im Browser via WebCrypto oder per
ssh-keygen -t ed25519lokal). Private Key bleibt am Gerät. - Public Key wird an Admin gesendet (E-Mail/Chat reicht – ist public)
- Admin SSH'ed auf Server, registriert per CLI:
timemaster kiosk add --pubkey ... - CLI gibt Setup-URL + Geräte-ID zurück → Admin schickt URL an IT-Person
- Kiosk-Browser öffnet Setup-URL → bindet bestehenden privaten Key an Geräte-ID, signiert ersten Test-Request
- Admin sieht Gerät in WebGUI als
pending→ Klick „Approve" → ab jetzt aktiv - 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/heartbeatalle 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 vonis_active(durchstatusersetzt)- 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:
- Backend: Ed25519-Verifizierung + CLI-Tool + Migration (kein Frontend)
- Frontend Kiosk-Mode: ServiceWorker + WebCrypto + Setup-Flow + Offline-Queue
- Frontend Verwaltung: KioskDevicesPage-Erweiterung + Health-Badge im Layout
- 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
HTTPExceptionmit sprechendemdetail - Tokens nie im Klartext in DB – immer
hash_token()auscore/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 0003–0016: 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"