Selbst gehostete Zeiterfassung und Urlaubsverwaltung für kleine und mittlere Betriebe. FastAPI, PostgreSQL, rootless Podman; Anmeldung über Keycloak/OIDC oder LDAP, Änderungsprotokoll mit Hash-Kette.
  • Python 85.1%
  • HTML 11.3%
  • CSS 1.5%
  • Jinja 1.2%
  • PLpgSQL 0.6%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-13 17:46:22 +01:00
.forgejo Spendenlink (Ko-fi) und Abschnitt 'Unterstützen' ergänzen 2026-11-04 09:28:32 +01:00
client Zeitgeber 0.22.4: Arbeitsort nach dem Gehen, Desktop-Client 0.1.12 2026-11-11 14:16:20 +01:00
deploy Zeitgeber 0.23.1: mehrere Vertretungen mit Bemerkung, Tagevorschau, Kollisionsprüfung, deaktivierte UCS-Konten 2026-11-13 17:46:22 +01:00
docs Zeitgeber 0.23.1: mehrere Vertretungen mit Bemerkung, Tagevorschau, Kollisionsprüfung, deaktivierte UCS-Konten 2026-11-13 17:46:22 +01:00
plugins/zg_caldav_push Zeitgeber 0.17.2 — Zeiterfassung und Urlaubsverwaltung 2026-10-27 08:20:07 +01:00
tests Zeitgeber 0.23.1: mehrere Vertretungen mit Bemerkung, Tagevorschau, Kollisionsprüfung, deaktivierte UCS-Konten 2026-11-13 17:46:22 +01:00
zeitgeber Zeitgeber 0.23.1: mehrere Vertretungen mit Bemerkung, Tagevorschau, Kollisionsprüfung, deaktivierte UCS-Konten 2026-11-13 17:46:22 +01:00
.gitignore Zeitgeber 0.17.2 — Zeiterfassung und Urlaubsverwaltung 2026-10-27 08:20:07 +01:00
alembic.ini Zeitgeber 0.17.2 — Zeiterfassung und Urlaubsverwaltung 2026-10-27 08:20:07 +01:00
LICENSE Zeitgeber 0.17.2 — Zeiterfassung und Urlaubsverwaltung 2026-10-27 08:20:07 +01:00
pyproject.toml Zeitgeber 0.23.1: mehrere Vertretungen mit Bemerkung, Tagevorschau, Kollisionsprüfung, deaktivierte UCS-Konten 2026-11-13 17:46:22 +01:00
README.md Arbeitsort auf der Startseite; Menüpunkt 'Wer ist wo' erschien nie 2026-11-05 13:09:19 +01:00

Zeitgeber

Selbst betriebene Zeiterfassung und Urlaubsverwaltung für kleine und mittlere Betriebe. Python 3.11+, FastAPI, SQLAlchemy, Jinja2, PostgreSQL; ausgeliefert als rootless Podman-Quadlet mit Caddy davor.

Entstanden ist das Ganze aus einem konkreten Bedarf: ein Haus mit knapp hundert Beschäftigten, das seine Zeiten in Excel-Mappen führte und eine Lösung brauchte, die im eigenen Netz läuft, an das vorhandene Keycloak und UCS-LDAP andockt und die Anforderungen des deutschen Arbeitszeitrechts von vornherein mitbringt. Es läuft seit September 2026 im Produktivbetrieb.

Was das heißt und was nicht. Die Anwendung ist in einem Betrieb erprobt, aber sie ist auf dessen Gegebenheiten hin gewachsen: deutsches Arbeitszeit- und Urlaubsrecht, deutschsprachige Oberfläche (die Mitarbeiteranleitung gibt es auch auf Englisch), Keycloak/OIDC und UCS-LDAP als Identitätsquellen, Feiertage nach Bundesland. Mandantenfähig ist sie nicht. Wer sie einsetzt, sollte Podman und Postgres selbst betreiben können — und sich die rechtlichen Vorgaben seines eigenen Standorts ansehen, statt den hier eingebauten Vorgaben zu vertrauen. Es gibt keine Gewähr, siehe LICENSE.

Rechtlicher Rahmen, der eingebaut ist: Mindestpausen nach § 4 ArbZG, Höchstarbeitszeit nach § 3 ArbZG, Krankheit im Urlaub nach § 9 BUrlG, Verfall von Übertragsurlaub nach § 7 Abs. 3 BUrlG, Attestpflicht nach § 5 EntgFG, Aufzeichnungspflicht nach § 16 ArbZG. Jeder dieser Punkte ist in den Richtlinien einstellbar, weil kein Betrieb sie gleich handhabt.

Nachweisbarkeit ist der Grund, warum es diese Anwendung und nicht eine Tabelle gibt: Hash-Kette über das Protokoll, Datenbank-Trigger für die Änderungshistorie, getrennte Datenbankrollen, ein täglicher Anker außerhalb der Datenbank. Wer seine eigenen Zahlen nachträglich glätten kann, hat keinen Nachweis, sondern eine Behauptung. Details unter „Sicherheit".

Der Name ist ein Platzhalter – ein grep -ri zeitgeber und ein Verzeichnis-Rename reichen zum Umbenennen.

Dokumentation

  • docs/wiki/Zeiterfassung-Mitarbeitende.md – Anleitung für Beschäftigte
  • docs/wiki/TimeTracking-Employees-EN.md – dieselbe auf Englisch
  • docs/wiki/Zeiterfassung-Personalabteilung.md – Anleitung für die Personalabteilung
  • docs/wiki/Zeiterfassung-Administration.md – Betrieb, Updates, Fehlersuche
  • docs/KEYCLOAK.md, docs/MIGRATIONEN.md, docs/ADDONS.md
  • deploy/ansible/ – die Rolle, mit der das Ganze ausgerollt wird

Die Anleitungen nennen durchweg Beispielnamen (zeitgeber.example.org, sso.example.org, dc=example,dc=org). Die sind beim Einrichten durch die eigenen zu ersetzen.

Was drin ist

  • Zeiterfassung – Kommen/Gehen per Stempeluhr, nachträgliche Erfassung und Korrektur, Pausen, Kategorien (Arbeit, Dienstreise, Schulung, Rufbereitschaft), Wochenansicht, CSV-Export, Gleitzeitsaldo gegen ein Arbeitszeitmodell mit Stichtag.
  • Mindestpausen nach § 4 ArbZG – über 6 h Arbeitszeit mindestens 30 min, über 9 h mindestens 45 min. Gezählt wird alles, was am Tag pausiert wurde: eingetragene Pausen und Lücken zwischen zwei Buchungen. Fehlt etwas, wird die Differenz ergänzt und von der Arbeitszeit abgezogen. Schwellen und Dauern sind in den Richtlinien einstellbar.
  • Anmeldung über Keycloak – OIDC Authorization Code Flow mit PKCE gegen den Realm ucs, ID-Token lokal gegen die JWKS geprüft, Rollen aus den Claims, RP-initiated Logout. Der LDAP-Bind bleibt als Alternative (ZG_AUTH_BACKEND=ldap|both), der LDAP-Abgleich pflegt weiterhin die Stammdaten.
  • Schema-Migrationen mit Alembic – im Paket mitgeliefert, zeitgeber db upgrade.
  • Vier Rollen – Mitarbeiter, Teamleiter, Abteilungsleiter, Geschäftsführung, dazu ein davon unabhängiges Administrationsrecht für die IT.
  • Urlaubsanträge – Zeitraum, halbe Tage, Vertretung, Bemerkung, automatische Tagesberechnung ohne Feiertage und freie Wochentage, Überschneidungsprüfung, Kontostand mit Übertrag, Genehmigung durch die zuständige Führungskraft.
  • Kalender – ICS-Abo je Person und je Team (Token-URL, ohne Anmeldung abrufbar), Feiertage aus dem Paket holidays, Teamkalender als Monatsraster in der Oberfläche.
  • Abwesenheitsübersicht – firmenweit sichtbar, wer wann abwesend ist; Reichweite und Detailtiefe stellt die Personalabteilung ein, Krankheit erscheint standardmäßig ohne Grund.
  • Arbeitsort je Buchung – Büro, Homeoffice oder auswärts, direkt an der Stempeluhr auf der Startseite, im Tray-Client und über die API; ein Wechsel mitten am Tag stempelt nicht aus, sondern ändert den Ort der laufenden Buchung. Übersicht „Wer ist wo“ und farbige Punkte im Teamkalender, beides über eine eigene Richtlinie abschaltbar oder auf die Personalabteilung begrenzbar. Zwei Orte an einem Tag ergeben „gemischt“; im ICS-Feed steht der Ort bewusst nicht.
  • Krankmeldung mit AU-Verwaltung – Karenztage nach § 5 EntgFG einstellbar, Übersicht über fehlende Bescheinigungen, Krankmeldung durch die Personalabteilung oder die Person selbst.
  • Auswertung – Abwesenheitstage je Person und Art für einen frei wählbaren Zeitraum (Jahr, Halbjahr, Quartal, Monat oder von/bis), samt der Bemerkungen aus den Anträgen – von der Personalabteilung korrigierbar – und CSV-Export. Nur für Personalabteilung und Geschäftsführung.
  • Addons – Erweiterung über Entry-Points oder ein Plugin-Verzeichnis: eigene Routen, Menüpunkte, Templates, Tabellen, Abwesenheitsarten, Ereignis-Hooks. Siehe docs/ADDONS.md.
  • Tray-Client für Ubuntu und Windows – Kommen, Pause, Gehen aus dem Infobereich, Anmeldung über ein Gerätetoken statt Browsersitzung (siehe client/README.md).
  • JSON-API unter /api/v1 für Terminals, Home Assistant oder Skripte, OpenAPI unter /api/docs.

Schnellstart auf dem Laptop

python3 -m venv .venv && . .venv/bin/activate
pip install -e .
export ZG_DATABASE_URL="sqlite:///./zeitgeber.db"
export ZG_AUTH_BACKEND=ldap ZG_LDAP_ENABLED=false   # lokale Konten statt Keycloak
export ZG_SECRET_KEY="$(openssl rand -hex 32)"
zeitgeber demo-data          # Migration + Testbenutzer, Passwort jeweils: zeitgeber
zeitgeber serve --reload     # http://127.0.0.1:8000

Gegen den echten Keycloak: ZG_AUTH_BACKEND=oidc, Issuer, Client-ID und Secret setzen (siehe docs/KEYCLOAK.md) und mit zeitgeber oidc-test prüfen, bevor der Browser dran ist.

Testkonten: kproske (Geschäftsführung + Administration), tleiter (Teamleiter), aleiter (Abteilungsleiter), mmuster, jschmidt (Mitarbeiter).

Produktivbetrieb mit Podman und Caddy

Vorgesehen ist der Host time unter dem bestehenden User zeiterfassung, rootless.

# 1. Image bauen (bewusst von Hand, keine .build-Unit im Bootpfad)
podman build -t localhost/zeitgeber:latest -f deploy/Containerfile .

# 2. Konfiguration und Verzeichnisse
install -m 600 deploy/env.example ~/zeitgeber.env && vi ~/zeitgeber.env
mkdir -p ~/plugins ~/tls ~/log
cp deploy/Caddyfile ~/Caddyfile
# Zertifikat und Key der internen CA nach ~/tls legen

# 3. DB-Passwort als Secret
printf '%s' 'starkes-passwort' | podman secret create zeitgeber-db-password -

# 4. Quadlet-Units
mkdir -p ~/.config/containers/systemd
cp deploy/quadlet/* ~/.config/containers/systemd/
# .service/.timer sind keine Quadlet-Units, die gehoeren woanders hin:
mkdir -p ~/.config/systemd/user && cp deploy/systemd/* ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user start zeitgeber-db zeitgeber-app zeitgeber-caddy
systemctl --user enable --now zeitgeber-ldapsync.timer

# 5. Schema, Grunddaten, erster Abgleich
# init-db macht db upgrade und legt danach Abwesenheitsarten und Feiertage an.
# Ohne diesen Schritt sind die Auswahlfelder für Urlaub und Krankmeldung leer.
podman exec zeitgeber-app zeitgeber init-db
podman exec zeitgeber-app zeitgeber ldap-sync    # optional, Stammdaten aus dem UCS

Die Datenbank läuft vorerst als eigener Postgres im selben Pod. Ein späterer Umzug auf das Betriebs-Cluster ist ein pg_dump/pg_restore plus geänderte ZG_DATABASE_URL – die Anwendung selbst merkt davon nichts.

Praktische Punkte, die hier schon berücksichtigt sind:

  • Environment=TZ=Europe/Berlin in jeder Unit – Timezone= reicht nicht, wenn das Image selbst TZ setzt.
  • PodmanArgs=--no-hosts plus NetworkAlias= – sonst fängt die Debian-Zeile 127.0.1.1 <fqdn> aus der Host-/etc/hosts den Namen ab.
  • Kein AutoUpdate=registry auf der Datenbank: das Rollback von podman auto-update dreht nur das Image zurück, nie das Schema.
  • Das Image wird von Hand gebaut und per Image=localhost/... referenziert, damit beim Boot kein Build vor dem Netz startet.
  • Für systemctl --user nach su - zeiterfassung müssen XDG_RUNTIME_DIR und DBUS_SESSION_BUS_ADDRESS gesetzt sein – am einfachsten per machinectl shell oder loginctl enable-linger zeiterfassung und ssh zeiterfassung@time.

Anmeldung und Rollen

Regelweg ist Keycloak (Realm ucs); die Einrichtung des Clients samt Rollen-Mapper steht in docs/KEYCLOAK.md. Die Rollen kommen aus den Claims realm_access.roles, resource_access.zeitgeber.roles und groups – damit funktionieren sowohl in Keycloak gepflegte Realm-Rollen als auch die aus dem UCS-LDAP föderierten Gruppen:

Rolle / Gruppe Rolle in der Anwendung Rechte
zeit_geschaeftsfuehrung Geschäftsführung sieht und genehmigt alles
zeit_abteilungsleiter Abteilungsleiter alle Teams der eigenen Abteilung
zeit_teamleiter Teamleiter eigenes Team
zeit_users Mitarbeiter eigene Daten
zeit_hr – Personalabteilung: alle Abwesenheiten, Richtlinien, Krankmeldungen
zeit_admins – Administrationsoberfläche, unabhängig von der Rolle

Zuordnung frei konfigurierbar über ZG_OIDC_ROLE_MAP (bzw. ZG_LDAP_ROLE_MAP beim LDAP-Weg). Wer mehrere hat, bekommt die höchste. ZG_OIDC_LOGIN_ROLE legt fest, wer sich überhaupt anmelden darf. Prüfen ohne Browser:

zeitgeber oidc-test                      # Discovery, Endpunkte, Redirect-URI
zeitgeber oidc-test --token "eyJ..."     # Claims und abgeleitete Rolle
zeitgeber ldap-test kproske --password   # nur für den LDAP-Weg

Beim LDAP-Abgleich gilt: hat sich der Benutzer über Keycloak angemeldet, pflegt der Abgleich nur noch Name, Mail, Abteilung und den Aktiv-Status – Rolle und Adminrecht bleiben in der Hand von Keycloak, damit sich beide Quellen nicht gegenseitig überschreiben.

Der Genehmigungsweg ergibt sich aus Team- und Abteilungsleitung (in der Verwaltung gepflegt): Mitarbeiter → Teamleiter → Abteilungsleiter → Geschäftsführung. Fehlt eine Stufe, rutscht der Antrag automatisch eine Ebene höher. Eigene Anträge kann niemand selbst genehmigen.

Analyse: was die bestehenden Open-Source-Systeme machen

Für die Konzeption habe ich mir die verbreiteten Systeme angesehen. Kurzfassung, jeweils mit dem, was ich übernommen habe:

Kimai (PHP/Symfony, AGPL) ist der Platzhirsch der Projektzeiterfassung: Kunden, Projekte, Aktivitäten, Stundensätze, Rechnungen, Reports und eine ausgereifte Plugin-Architektur über Symfony-Bundles. Kimai bietet daneben Auswertungen nach Benutzern, Kunden, Projekten, Aktivitäten, Tags und Zeiträumen sowie das Teilen von Statistiken über geheime URLs. Übernommen: das Bundle-Prinzip als Vorbild für das Addon-System, die Trennung von Erfassung und Auswertung, geheime Token-URLs für Kalenderfeeds. Nicht übernommen: das Projekt-/Kunden- und Abrechnungsmodell – für die interne Anwesenheitserfassung ist es Ballast, gehört aber als Addon nachrüstbar, deshalb sind Kategorie und Notiz am Zeiteintrag schon vorhanden.

urlaubsverwaltung / zeiterfassung (Java/Spring Boot, das läuft aktuell auf time) ist fachlich am nächsten dran. Dort steckt die rechtliche Begründung im Produkt selbst: das EuGH-Urteil von Mai 2019 (Rechtssache C-55/18, "CCOO") verpflichtet Arbeitgeber, ein objektives, verlässliches und zugängliches System zur Messung der täglichen Arbeitszeit einzurichten; Pausen sind nach § 4 ArbZG zu erfassen; Berichte lassen sich als CSV exportieren; Arbeitszeitmodelle werden je Person mit frei wählbarem Startdatum gepflegt, und Bundesland samt Feiertagsregelung ist pro Person einstellbar. Genau diese vier Punkte stecken hier im Datenmodell: WorkSchedule mit valid_from, subdivision pro Benutzer, Pausenminuten am Eintrag, CSV-Export. Aus der Urlaubsverwaltung stammt außerdem die Idee frei wählbarer Abwesenheitsarten neben dem Erholungsurlaub – Fortbildung, Kur, Home-Office – sowie die Teamübersicht über das ganze Jahr: hier als Tabelle AbsenceType und als Teamkalender umgesetzt.

solidtime (Laravel/Vue, AGPL) zeigt, wie weit man mit einer modernen Oberfläche kommt. Es zielt auf Freelancer und Agenturen mit Projekten, Aufgaben, Kunden, abrechenbaren Sätzen, mehreren Organisationen sowie Rollen und Berechtigungen, dazu Import aus Toggl und Clockify. Übernommen: der Import-Gedanke – der CSV-Export ist absichtlich so gebaut, dass er sich wieder einlesen lässt. Nicht übernommen: SPA mit Build-Chain. Server-gerendertes HTML plus etwas JavaScript ist hier weniger Wartungslast.

Traggo (Go, GPL) ist der Gegenentwurf: keine Aufgaben, nur getaggte Zeitspannen, bewusst maximal anpassbar über Tags. Für eine gesetzeskonforme Anwesenheitserfassung zu frei, das Tag-Prinzip ist aber ein guter Kandidat für ein späteres Addon (Kostenstellen, Projekte).

Was allen dreien fehlt und hier von Anfang an drin ist: LDAP-Anmeldung als Standardweg statt als Zusatz, der deutsche Genehmigungsweg über drei Führungsebenen und ICS-Feeds ohne Cloud-Zwang.

Abwesenheiten und Krankmeldungen

Unter Wer ist da (/abwesenheiten) sieht jede angemeldete Person, wer im laufenden Monat abwesend ist. Was dort steht, entscheidet die Personalabteilung unter /personal/richtlinien — ohne Neustart, ohne Zugriff auf den Container:

  • Reichweite: ganze Firma, eigene Abteilung oder nur das eigene Team.
  • Detailtiefe je Abwesenheitsart: Grund im Klartext, nur „abwesend“, oder gar nicht anzeigen. Urlaub und Fortbildung stehen im Klartext, alles Gesundheitsbezogene auf „abwesend“ — die eigene Abwesenheit sieht jeder immer im Klartext.
  • Obergrenze über alle Arten hinweg, mit der sich die ganze Übersicht in einem Griff anonymisieren lässt.
  • Ob beantragte, noch nicht genehmigte Abwesenheiten mitlaufen, und ob die Vertretung genannt wird.

Krankheit wird nicht beantragt, sondern gemeldet und gilt sofort. Unter /personal/krankmeldungen trägt die Personalabteilung Meldungen ein, die telefonisch kommen, verlängert laufende Fälle und hakt eingegangene Bescheinigungen ab. Ob eine AU nötig ist, ergibt sich aus der Richtlinie:

  • AU nötig ab Tag — Vorgabe 3. Die ersten drei Tage sind „krank ohne AU“, ab dem vierten wird das Attest verlangt (§ 5 EntgFG). 1 verlangt es ab dem ersten Tag, 0 schaltet die Pflicht ab.
  • Kalender- oder Arbeitstage für diese Zählung.
  • Warnschwelle: ab wie vielen Tagen ohne AU im Jahr die Personalansicht die Person hervorhebt.
  • Ob Mitarbeitende sich selbst krankmelden dürfen oder nur die Personalabteilung meldet.

Verlängert sich ein Fall über die Schwelle hinaus, wird die AU-Pflicht automatisch nachgezogen. Diagnosen erfasst die Anwendung bewusst nicht — nur den Zeitraum und ob der Nachweis vorliegt.

Stempeln vom Arbeitsplatz

Unter Geräte legt jede Person selbst Token an — eines je Rechner, jederzeit einzeln zurückziehbar. Gespeichert wird nur der Hash, angezeigt wird das Token genau einmal. Damit meldet sich der Client an:

zeitgeber-client --einrichten --server https://zeitgeber.example.org --token zg_...
zeitgeber-client                       # Symbol im Infobereich
zeitgeber-client --stempeln kommen     # auch ohne Oberfläche, z. B. für Tastenkürzel

Die Pause ist bewusst keine geschätzte Minutenzahl: Pause beendet den laufenden Eintrag, Weiter beginnt einen neuen, und die Lücke dazwischen zählt als Pause. Reicht die Summe aller Pausen eines Tages nicht an die gesetzliche Mindestpause heran, wird die Differenz ergänzt und von der Arbeitszeit abgezogen.

Aufbau und Verpackung stehen in client/README.md, inklusive Autostart unter Ubuntu und einer einzelnen EXE für Windows.

Struktur

zeitgeber/
  config.py          Einstellungen (ZG_*-Umgebungsvariablen)
  models.py          Datenmodell
  rbac.py            Sichtbarkeiten und Genehmigungswege
  security.py        Session-Cookie, lokale Passwörter (scrypt)
  plugins.py         Addon-System
  bootstrap.py       Schema und Grunddaten
  cli.py             zeitgeber <befehl>
  migrate.py         Alembic-Aufrufe aus der Anwendung
  migrations/        Alembic-Umgebung und Revisionen
  auth/              OIDC (Keycloak), LDAP-Bind, Provisionierung, Abgleich
  services/          Arbeitszeit, Urlaub, Krankmeldung, Richtlinien, ICS, Feiertage
  web/               FastAPI-App, Routen, Templates, CSS
deploy/              Containerfile, Caddyfile, Quadlet-Units, env-Beispiel
client/              Tray-Client für Ubuntu und Windows
plugins/             Beispiel-Addon (CalDAV-Übertragung)
docs/                Keycloak, Migrationen, Addon-Handbuch, offene Punkte
tests/               pytest (Urlaubslogik, Arbeitszeit, ICS, OIDC, Anmeldeweg)

Benachrichtigungen und Genehmigungsweg

Wird ein Antrag eingereicht, bekommt die zuständige Führungskraft eine Mail mit Betreff „ hat Abwesenheit beantragt", dem Zeitraum, der Art und einem Link auf /genehmigungen. Nach der Entscheidung geht eine Mail an die antragstellende Person. Beides lässt sich mit ZG_NOTIFY_ON_REQUEST und ZG_NOTIFY_ON_DECISION abschalten; ohne ZG_SMTP_HOST versendet die Anwendung gar nichts. Schlägt der Versand fehl, wird das protokolliert — der Antrag bleibt davon unberührt.

Testen:

podman exec zeitgeber-app zeitgeber mail-test du@example.org

Vertretung. Ist die zuständige Führungskraft selbst abwesend und hat in ihrem Antrag eine Vertretung angegeben, darf diese Person in der Zeit entscheiden. Die Benachrichtigung geht dann an die Vertretung, die Abwesende bekommt sie zur Kenntnis. Die Regel hängt am Antrag und nicht an einem Feld am Konto — sie gilt damit genau für den Zeitraum der Abwesenheit und muss nirgends wieder abgeschaltet werden. Ist auch die Vertretung weg, wird deren Vertretung herangezogen (maximal drei Stufen). Findet sich niemand, bleibt der Antrag liegen, bis die Führungskraft zurück ist.

Wer ist zuständig? Vorrang hat der im Verzeichnis gepflegte Vorgesetzte: der LDAP-Abgleich liest das Attribut aus ZG_LDAP_ATTR_MANAGER (Vorgabe manager, ein DN) und löst es auf ein Konto auf. Ist dort nichts hinterlegt, greift die Organisation — Mitarbeiter zur Teamleitung, Teamleitung zur Abteilungsleitung, Abteilungsleitung zur Geschäftsführung. In der Verwaltung lässt sich der Vorgesetzte je Person auch von Hand setzen; der nächste Abgleich überschreibt das nicht, solange im Verzeichnis nichts steht.

Stammdaten per CSV

Arbeitszeitmodelle, Urlaubsansprüche, Personalnummern und Eintrittsdaten kommen aus keinem Verzeichnisfeld — bei mehr als einer Handvoll Konten lohnt der Umweg über eine Tabelle:

podman exec zeitgeber-app zeitgeber personal-export > personal.csv
# von der Personalabteilung ausfüllen lassen, dann:
podman exec -i zeitgeber-app zeitgeber personal-import --trocken < personal.csv
podman exec -i zeitgeber-app zeitgeber personal-import < personal.csv

Die Datei ist semikolongetrennt und öffnet sich direkt in Excel. Nur kennung ist Pflicht, leere Felder bleiben unverändert — die Tabelle muss also nicht vollständig sein.

Spalte Bedeutung
kennung Benutzername, Pflicht
mo di mi do fr sa so Sollstunden je Wochentag — hier steht die Teilzeit
wochenstunden Abkürzung: verteilt auf Mo–Fr und schlägt die Einzeltage
gueltig_ab Stichtag des Modells; ein neues Datum legt ein weiteres an
urlaubsanspruch uebertrag korrektur Urlaubskonto des Jahres
eintritt personalnummer bundesland Stammdaten

Zahlen dürfen deutsch geschrieben werden (7,5), Datumsangaben als 01.04.2026 oder 2026-04-01. --trocken zeigt Zeile für Zeile, was sich ändern würde, ohne zu speichern. Fehlerhafte Zeilen werden einzeln abgewiesen und benannt, die übrigen laufen durch.

Wer freitags nie da ist, bekommt 8;8;8;8;0; Werkstudenten mit 20 Stunden entweder 4;4;4;4;4 oder einfach wochenstunden=20. Die Personalnummer holt sich der LDAP-Abgleich inzwischen selbst aus employeeNumber, sofern noch keine gesetzt ist.

Umstieg aus den Excel-Zeitnachweisen

Die bisherigen Mappen (eine Datei je Person, ein Blatt je Monat) lassen sich übernehmen:

Am einfachsten machen das die Mitarbeitenden selbst: Excel-Import in der Seitenleiste, Datei hochladen, Ergebnis prüfen, übernehmen. Der Trockenlauf ist voreingestellt. Die Personalabteilung kann dort auch für andere importieren und die Selbstbedienung nach der Umstellung unter Richtlinien abschalten.

Über die Kommandozeile geht es ebenfalls:

podman exec -i zeitgeber-app zeitgeber excel-import /tmp/kproske.xlsx \
    --kennung kproske --trocken
podman exec -i zeitgeber-app zeitgeber excel-import /tmp/kproske.xlsx \
    --kennung kproske --ab 01.01.2026 --startsaldo 12,5

Gelesen werden Spalte B (Datum), E (ein), F (aus) und G (Pause), dazu die Kennbuchstaben aus der Legende der Mappe:

Zelle Bedeutung
U in Arbeitszeit ein ganzer Tag Urlaub
U in Pause halber Tag Urlaub, die gebuchte Zeit bleibt erhalten
K in Arbeitszeit ein ganzer Tag krank
K in Pause Teil des Tages krank
S in Arbeitszeit ein ganzer Tag Schulung → Fortbildung

Zusammenhängende Abwesenheiten derselben Art werden zu einem Antrag zusammengefasst, nicht als zwanzig Einzeltage angelegt. Schichten über Mitternacht werden erkannt. Der Import ist wiederholbar: Tage mit vorhandenem Eintrag werden übersprungen, es entstehen keine Dubletten. Tage mit Kommen ohne Gehen werden gemeldet und übersprungen — die gehören von Hand nachgetragen.

Zum Gleitzeitsaldo: Die Anwendung rechnet den Saldo aus den erfassten Tagen. Importierst du erst ab einem Stichtag, fehlt alles davor. --startsaldo setzt deshalb einen Anfangswert, ab dem gerechnet wird — den Übertrag aus dem Vormonat liest der Import aus der Mappe und nennt ihn im Bericht. Dasselbe geht über die Spalten gleitzeit_saldo und gleitzeit_ab im CSV-Import.

Wichtig dabei: Arbeitstage ohne Buchung zählen als volles Minus. Importiere deshalb ganze Monate, nicht einzelne Wochen.

Konten pflegen

Ein Konto entsteht bei der ersten Anmeldung über Keycloak oder beim LDAP-Abgleich. Die Spalte Quelle in der Verwaltung zeigt, woher es stammt:

  • oidc — über Keycloak angemeldet. Rolle und Rechte kommen aus den Claims; ein früher gesetztes lokales Passwort wird bei der Anmeldung entfernt, damit kein zweiter, unsichtbarer Anmeldeweg bestehen bleibt.
  • ldap — aus dem Abgleich entstanden, hat sich noch nie angemeldet.
  • local — Notfallkonto mit eigenem Passwort. Der LDAP-Abgleich lässt es unangetastet, auch wenn im Verzeichnis jemand gleichen Namens existiert.

Löschen in der Verwaltung folgt einer Regel: wer noch keine Zeiten und keine Abwesenheiten hat, wird wirklich gelöscht; sobald Daten vorliegen, wird nur deaktiviert. Zeitdaten sind nach § 16 ArbZG zwei Jahre aufzubewahren. Das eigene Konto und das letzte Konto mit Verwaltungsrecht sind gesperrt. Teams und Abteilungen lassen sich löschen, die Zuordnungen der Betroffenen werden dabei aufgehoben.

Sicherheit

  • Anmeldung: OIDC mit PKCE, State- und Nonce-Prüfung, ID-Token gegen die JWKS des Realms verifiziert (Signatur, iss, aud, exp, nonce), eigene CA konfigurierbar.
  • Sessions: signiertes Cookie (itsdangerous), HttpOnly, SameSite=Lax, Secure bei HTTPS.
  • CSRF-Token in jedem Formular, geprüft an jeder schreibenden Route.
  • Passwörter nur für lokale Notfallkonten, gehasht mit scrypt.
  • Kalender-URLs enthalten ein 32-Byte-Token und lassen sich in der Oberfläche erneuern.
  • Gerätetoken werden nur als SHA-256-Hash gespeichert, das Präfix dient dem Nachschlagen; jedes Token gilt für genau ein Konto und ist einzeln zurückziehbar.
  • Änderungen an Zeiten und Anträgen landen im Audit-Log.
  • 2FA und Passwortrichtlinien liegen bei Keycloak, die Anwendung kennt keine Passwörter der Mitarbeiter.
  • Offen und bewusst nicht drin: Verschlüsselung einzelner Felder, API-Token für Maschinen.

Lizenz

MIT — siehe LICENSE. Copyright 2026 Kai Proske · Linux360.

Die Fußzeile in der Oberfläche nennt Urheber, Lizenz und Version. Der Lizenztext und der Urhebervermerk gehören nach den Bedingungen der MIT-Lizenz in jede Kopie und in jeden nennenswerten Teil der Software.