API-Dokumentation

1. Grundlagen

StatusBedeutung
200OK
201angelegt; Body ist der neue Datensatz
204erledigt, kein Body (Löschen, Logout)
400ungültige Eingabe (Meldung im Feld fehler)
401nicht angemeldet
403angemeldet, aber nicht erlaubt (kein Admin, gesperrt, E-Mail unbestätigt)
404gibt es nicht oder gehört einem anderen User
409Konflikt: Name/E-Mail schon vergeben, Buchungen überschneiden sich, Kategorie hat noch Buchungen, es läuft nichts zum Stoppen
429zu viele Anmeldeversuche (Auth-Endpunkte: 20 pro Minute und IP)

2. Auth

POST/api/register

Legt ein unbestätigtes Konto an und schickt einen sechsstelligen Code per Mail (24 h gültig). Der erste echte User wird Admin.

{"email": "anna@example.org", "username": "anna", "passwort": "mindestens8"}

Regeln: gültige E-Mail; Nutzername 3–32 Zeichen aus A–Z a–z 0–9 . _ -; Passwort 8 Zeichen bis 72 Bytes.
201 {"meldung": "...", "email": "anna@example.org", "mail_gesendet": true} · 400 ungültig · 409 vergeben. mail_gesendet: false heißt: Versand nicht eingerichtet oder fehlgeschlagen, der Code steht im Server-Log.

POST/api/confirm

Bestätigt die E-Mail-Adresse und meldet direkt an (setzt das Cookie).

{"email": "anna@example.org", "code": "123456"}

200 Konto (wie /api/me) · 400 Code falsch oder abgelaufen · 403 gesperrt.

POST/api/resend-confirmation
{"email": "anna@example.org"}

Schickt einen neuen Code, falls ein unbestätigtes Konto existiert. Antwortet immer gleich mit 200, damit sich nicht testen lässt, welche Adressen registriert sind.

POST/api/login
{"kennung": "anna oder anna@example.org", "passwort": "..."}

200 Konto, setzt das Cookie (30 Tage) und speichert IP und Zeitpunkt · 401 Name/E-Mail oder Passwort falsch · 403 gesperrt, oder {"fehler": "...", "grund": "email_unbestaetigt", "email": "..."}.

POST/api/logout

Beendet die Sitzung und löscht das Cookie. 204, auch ohne Anmeldung.

GET/api/me

Der angemeldete User. impersonation ist gesetzt, wenn ein Admin gerade diese Ansicht nutzt.

{"id": 3, "username": "anna", "email": "anna@example.org", "is_admin": true, "impersonation": null}
POST/api/forgot-password
{"email": "anna@example.org"}

Schickt einen Link /login.html?reset=TOKEN (1 h gültig, einmal benutzbar). Antwortet immer gleich mit 200.

POST/api/reset-password
{"token": "TOKEN aus dem Link", "passwort": "neuesPasswort"}

Setzt das neue Passwort, beendet alle Sitzungen des Users und bestätigt die E-Mail-Adresse. 200 · 400 Token ungültig/abgelaufen oder Passwort zu kurz.

3. Kategorien

Jedes neue Konto bekommt sieben Startkategorien (Arbeit, Reisezeit, Körperzeugs, Mit Leuten treffen, Todo-Liste bearbeiten, Computer/Server/Netzwerk, Was anderes). Eine Kategorie:

{"id": 1, "name": "Arbeit", "beschreibung": "Beruf, Arbeit (Karlsruhe)", "farbe": "#0d6efd", "sortierung": 1, "archiviert": false}

Regeln: Name 1–60 Zeichen, pro User eindeutig (ohne Rücksicht auf Groß-/Kleinschreibung, sonst 409); Beschreibung bis 200 Zeichen; Farbe #rrggbb. Archiviert heißt: nicht mehr im Umschalter, kein Umschalten darauf (400), alte Buchungen bleiben in der Auswertung.

GET/api/kategorien

Alle Kategorien des Users in ihrer Reihenfolge, archivierte eingeschlossen.

POST/api/kategorien
{"name": "Lesen", "beschreibung": "", "farbe": "#198754"}

Hängt die Kategorie ans Ende. 201 · 400 · 409 Name vergeben.

PUT/api/kategorien/:id
{"name": "Lesen", "beschreibung": "Bücher, Artikel", "farbe": "#198754", "archiviert": false}

Überschreibt Name, Beschreibung, Farbe und Archiv-Status. 200 · 400 · 404 · 409.

PUT/api/kategorien/reihenfolge
{"ids": [3, 1, 2, 4, 5, 6, 7]}

Setzt die Reihenfolge; die Liste muss jede Kategorie des Users genau einmal enthalten (sonst 400). 200 die neue Liste.

DELETE/api/kategorien/:id

Nur ohne Buchungen. 204 · 404 · 409 hat Buchungen – stattdessen archivieren.

4. Status, Umschalten, Stopp

GET/api/status

Was gerade läuft. letzte ist die zuletzt beendete Buchung, wenn nichts läuft (für „Weiter“); server_zeit erlaubt dem Client, seine Uhr abzugleichen.

{"laufend": {"id": 41, "kategorie_id": 1, "start": "2026-11-08T07:40:00Z", "ende": null, "notiz": ""},
 "kategorie": {"id": 1, "name": "Arbeit", ...},
 "letzte": null,
 "server_zeit": "2026-11-08T09:12:31Z"}
POST/api/umschalten
{"kategorie_id": 2, "start": "2026-11-08T09:00:00Z", "notiz": ""}

Beendet die laufende Buchung zum Zeitpunkt start und beginnt eine neue. start ist optional (Standard: jetzt) – für einen vergessenen Wechsel oder offline gesammelte Wechsel, die nachgereicht werden. Läuft schon dieselbe Kategorie (und keine Notiz), ändert sich nichts. 200 wie /api/status · 400 unbekannte/archivierte Kategorie, Start in der Zukunft · 409 Start liegt vor dem Beginn der laufenden Buchung oder überschneidet eine andere.

POST/api/stopp
{"ende": "2026-11-08T17:30:00Z"}

Beendet die laufende Buchung; Body optional (Standard: jetzt). 200 wie /api/status · 409 es läuft nichts.

5. Buchungen

{"id": 41, "kategorie_id": 1, "start": "2026-11-08T07:40:00Z", "ende": "2026-11-08T11:15:00Z", "notiz": "Sprint-Planung"}

ende: null = läuft. Notiz bis 500 Zeichen.

GET/api/buchungen?von=…&bis=…

Alle Buchungen, die (teilweise) in [von, bis) liegen, nach Start sortiert – auch eine, die am Vortag begann. Beide Parameter Pflicht; Spanne höchstens max_monate. 200 · 400.

POST/api/buchungen
{"kategorie_id": 3, "start": "2026-11-07T12:15:00Z", "ende": "2026-11-07T13:00:00Z", "notiz": ""}

Trägt eine Buchung nach. Ohne ende wird sie zur laufenden (nur möglich, wenn nichts läuft und nichts danach kommt). 201 · 400 · 409 Überschneidung – dann steht die kollidierende Buchung in konflikt_mit:

{"fehler": "überschneidet sich mit einer anderen Buchung", "konflikt_mit": {"id": 17, "kategorie_id": 5, "start": "...", "ende": "...", "notiz": ""}}
PUT/api/buchungen/:id
{"kategorie_id": 2, "start": "2026-11-07T06:30:00Z", "ende": "2026-11-07T07:40:00Z", "notiz": "", "nachbarn_anpassen": true}

Überschreibt die Buchung. Mit nachbarn_anpassen wandern direkt angrenzende Buchungen mit: endete die vorherige genau am alten Start, endet sie am neuen; begann die nächste genau am alten Ende, beginnt sie am neuen. Würde eine Nachbarbuchung dadurch keine Dauer mehr haben, kommt 409 und nichts wird geändert. 200 · 400 · 404 · 409.

DELETE/api/buchungen/:id

204 · 404. Die Lücke bleibt bestehen.

GET/api/buchungen/export.csv?von=…&bis=…

Die Buchungen des Zeitraums als CSV (UTF-8 mit BOM, Semikolon, Dezimalkomma) zum Öffnen in einer Tabellenkalkulation. Spalten: id;kategorie;start_utc;ende_utc;dauer_minuten;notiz.

6. Auswertung

GET/api/auswertung?von=…&bis=…

Zeit je Kategorie in [von, bis). Buchungen über den Rand zählen nur mit ihrem Anteil im Zeitraum, die laufende bis jetzt. anteil ist der Prozentsatz an der erfassten Zeit; Kategorien ohne Zeit fehlen; sortiert nach Dauer absteigend.

{
  "von": "2026-11-01T23:00:00Z", "bis": "2026-11-08T23:00:00Z",
  "posten": [
    {"kategorie_id": 1, "name": "Arbeit", "farbe": "#0d6efd", "archiviert": false,
     "sekunden": 106800, "stunden": 29.67, "anteil": 38.2, "buchungen": 8},
    ...
  ],
  "erfasst_sekunden": 279840,
  "zeitraum_sekunden": 584400,
  "nicht_erfasst_sekunden": 304560
}

zeitraum_sekunden reicht höchstens bis jetzt (die Zukunft kann noch nicht erfasst sein); nicht_erfasst_sekunden ist die Zeit ohne Buchung und zählt nicht in die Prozente.

7. Admin

Nur für Admins (sonst 403). Die verändernden Aktionen lehnen das eigene Konto mit 400 ab. Alle Aktionen werden im Server-Log protokolliert.

GET/api/admin/users
[{"id": 3, "username": "anna", "email": "anna@example.org", "erstellt_am": "...", "is_admin": true,
  "email_bestaetigt": true, "gesperrt": false,
  "letzte_login_ip": "192.168.1.20", "letzter_login_am": "2026-11-07T18:49:00Z", "anzahl_buchungen": 412}]
POST/api/admin/users/:id/lock

Sperrt den User und beendet seine Sitzungen sofort. 200 {"id": 4, "gesperrt": true}.

POST/api/admin/users/:id/unlock

200 {"id": 4, "gesperrt": false}.

POST/api/admin/users/:id/reset-password

Schickt dem User einen Reset-Link. {"mail_gesendet": true, "meldung": "..."}; kann die Mail nicht verschickt werden: {"mail_gesendet": false, "meldung": "...", "link": "https://..."}.

DELETE/api/admin/users/:id

Löscht den User mit allen Buchungen, Kategorien und Sitzungen. 204 · 404.

POST/api/admin/users/:id/impersonate

Ersetzt die Sitzung des Admins durch eine Sitzung als dieser User. Die Sitzung merkt sich den Admin (impersonated_by); /api/me zeigt ihn unter impersonation. 200 Konto des Users.

POST/api/impersonate/stop

Zurück zur eigenen Admin-Sitzung (aus der Impersonation heraus aufrufen). 200 Konto des Admins · 400 keine Impersonation aktiv.

Server-Einstellungen

Änderbar ohne Neustart; Änderungen werden in config.yaml geschrieben (das Favicon als favicon.svg in den Datenordner). Schlüssel: base_url, max_monate (12–1200, längster Zeitraum für Auswertung, Buchungsliste und CSV), zeptomail_api_key, zeptomail_api_url (https), zeptomail_from_address, zeptomail_from_name, tracking_html (freies HTML für den <head>, höchstens 20 KB, wird unverändert in jede Seite eingebaut), app_name (1–40 Zeichen), primaerfarbe (#rrggbb) und favicon_svg (SVG-Text, höchstens 100 KB, ohne Skripte und externe Verweise; leer = Standard-Favicon). Öffentlich abrufbar: /favicon.svg, /favicon.png und /marke.css.

GET/api/admin/einstellungen
{
  "werte": {
    "max_monate": {"wert": "600", "gesetzt": false, "standard": "600"},
    "zeptomail_api_key": {"wert": "••••ABCD", "gesetzt": true, "standard": ""},
    ...
  },
  "nur_lesen": {"addr": ":80", "db": "/app/data/zeiterfassung.db", "cookie_secure": "false", "proxy_header": "X-Forwarded-For"},
  "config_pfad": "/app/config.yaml"
}

standard ist der eingebaute Wert, auf den „zurücksetzen“ zurückfällt (er wird dann so in config.yaml geschrieben). Der API-Key wird nie im Klartext geliefert, nur seine letzten vier Zeichen.

PUT/api/admin/einstellungen
{"setzen": {"max_monate": 240, "zeptomail_api_key": "..."}, "zuruecksetzen": ["base_url"]}

Nicht genannte Einstellungen bleiben unverändert. Alles oder nichts: 400 mit {"fehler": "...", "felder": {"max_monate": "muss eine ganze Zahl zwischen 12 und 1200 sein"}}, wenn ein Wert ungültig ist. 200 wie GET.

POST/api/admin/einstellungen/testmail

Schickt mit den aktuellen Einstellungen eine Mail an den angemeldeten Admin. 200 · 400 kein API-Key hinterlegt · 502 ZeptoMail hat abgelehnt (Grund in fehler).

8. Sonstiges und PWA

GET/health

{"status": "ok"}, wenn die Datenbank erreichbar ist. Ohne Anmeldung, für Docker-Healthchecks.

GET/manifest.webmanifest

Web-App-Manifest mit Name, Primärfarbe und Icons aus dem Erscheinungsbild. Dazu /icons/icon-192.png, /icons/icon-512.png und /icons/maskable-512.png (aus dem Favicon gerendert) sowie der Service Worker /sw.js.