# MyTime MyTime ist eine persönliche Web-App zur Urlaubsplanung und Überstunden-Erfassung. Sie läuft als einzelner Docker-Dienst mit SQLite und ist für den Zugriff im Heimnetz gedacht. Es gibt keine Anmeldung: **Port niemals ins öffentliche Internet weiterleiten.** ## Schnellstart (fertiges Image) Voraussetzungen: Docker Engine, Docker Compose Plugin und Zugriff auf die private Gitea-Container-Registry. Einmalig anmelden; Docker fragt interaktiv nach dem Token mit Paket-Leserecht: ```bash docker login gitea.lokcal.de -u ``` Dann im geklonten Repository: ```bash cp .env.example .env docker compose pull docker compose up -d ``` `compose.yaml` verwendet standardmäßig das fertige Image `gitea.lokcal.de/loki/mytime:latest` und baut nichts lokal. Der Host-Port kommt aus `.env` (`APP_PORT=3100` in der Vorlage); erreichbar ist die App dann unter `http://:3100`. Der Status lässt sich mit `curl http://127.0.0.1:3100/api/health` prüfen. ### Lokal entwickeln (Image selbst bauen) Für Änderungen am Quellcode die separate Dev-Datei verwenden: ```bash cp .env.example .env docker compose -f compose.dev.yaml up --build -d ``` `compose.dev.yaml` baut aus dem lokalen Verzeichnis. Daten und Sicherungen bleiben in beiden Varianten in `./data` und `./backups` erhalten. Beim ersten Start in **Einstellungen → Arbeitszeit** die Sollstunden für alle sieben Wochentage eintragen. Freie Tage erhalten `0`; der Knopf „40-h-Woche übernehmen“ füllt Mo–Fr mit 8 Stunden. Danach „Speichern“. ## Erste Einrichtung Öffne **Einstellungen → Arbeitszeit**. Trage Sollstunden für Montag bis Sonntag ein; `0` bedeutet arbeitsfrei. „40-h-Woche übernehmen“ füllt Montag bis Freitag mit je 8 Stunden und Samstag/Sonntag mit 0. Einzelne Tage kannst du anschließend anpassen. Die angezeigte Wochensumme dient zur Kontrolle. Speichern. Unter **Einstellungen → Urlaub & Bundesland** stellst du den Jahresanspruch, den Übertrag und das Bundesland ein. Standard-Bundesland ist Hamburg. Änderungen am Urlaubsanspruch gelten für das im Hauptbereich ausgewählte Jahr; das Bundesland gilt app-weit. ## Bedienung Die Hauptnavigation enthält **Übersicht**, **Zeiterfassung**, **Feiertage**, **Verlauf** und **Einstellungen**. Der Hell-/Dunkelmodus wird über das Sonne-/Mond-Symbol im oberen MyTime-Balken umgeschaltet und pro Gerät gespeichert. ### Arbeitszeit erfassen 1. In **Zeiterfassung** das Datum, Arbeitsbeginn, Arbeitsende und Pause eintragen. Die Pause ist standardmäßig 30 Minuten; `0` ist erlaubt. 2. **Speichern**. Positive Zeit über dem Soll des jeweiligen Wochentags wird automatisch gutgeschrieben. Minderstunden werden nicht automatisch abgezogen. 3. Manuelle Überstunden oder Abbau buchst du in **Übersicht**. Abbau ist nur möglich, wenn der Überstundenstand dadurch nicht negativ wird; eine Notiz ist optional. 4. Krankheitstage meldest du ebenfalls in **Zeiterfassung**: Datum wählen, ganzen oder halben Tag sowie optional eine Notiz eintragen und „Krank melden“. Nur geplante Arbeitstage (keine Wochenenden/Feiertage) sind meldbar. Eine bestehende Meldung lässt sich dort auch wieder löschen. ### Urlaub buchen und stornieren - In **Übersicht** Start- und Enddatum sowie ganzen oder halben Tag wählen und buchen. Wochenenden, Tage ohne Sollstunden und Feiertage im ausgewählten Bundesland zählen nicht als Urlaubstage. - Auf dem Desktop kannst du den Zeitraum in der Jahrestabelle ziehen. Auf einem Touch-Gerät einen Kalendertag etwa eine halbe Sekunde gedrückt halten, loslassen und den Endtag antippen. Ein normaler Tipp öffnet das Tagesfenster. - Das Tagesfenster zeigt Buchungen und Details des Tages. Dort kannst du Urlaub buchen oder eine bestehende Buchung mit Sicherheitsabfrage stornieren. - Unter **Verlauf → Urlaubsbuchungen** findest du Buchungen aller Jahre. Häkchen auswählen und mehrere Buchungen gemeinsam stornieren; der CSV-Export lädt die Liste herunter. - Stornierte Einträge landen im **Papierkorb**. Dort kannst du sie wiederherstellen oder nach einer zweiten Bestätigung endgültig löschen. - Die Buchung ist auf den verfügbaren Rest beschränkt: Jahresanspruch + Übertrag − bereits verbrauchte Urlaubstage. Bei einem Zeitraum über den Jahreswechsel wird jedes Jahr einzeln geprüft. - Der Übertrag eines Jahres läuft am **31.03. des Folgejahres** ab. Kurz vorher und danach zeigt die Übersicht einen Hinweis; Salden und Buchungen bleiben unverändert. ### Hauptbereiche - **Übersicht:** Salden, Buchungsformulare und Jahrestabelle. Die Jahrestabelle ist gedreht aufgebaut: Tage als Zeilen, Monate als Spalten. Urlaub ist grün, Krankheit violett, Feiertage rot; die Legende erklärt die Markierungen. Beim Ziehen bleibt der gewählte Zeitraum markiert und kann im Popup direkt gebucht werden. - **Feiertage → Feiertage:** Feiertage des ausgewählten Bundeslands, monatsweise aufgelistet. Ein Feiertag lässt sich anklicken, um zu seinem Datum in der Übersicht zu springen. - **Feiertage → Brückentage:** gespeicherte Empfehlungen von ferien-feiertage.de, nach Datum sortiert und nach Monaten gruppiert. Monatsübergreifende Tage sind in beiden Mini-Kalendern markiert. „Übernehmen“ bucht eine Empfehlung nach Bestätigung direkt; bereits gebuchte oder nicht mehr verfügbare Vorschläge werden ausgeblendet. - **Verlauf → Jahresübersicht:** zwölf Mini-Kalender und Monatssummen. Der Jahresumschalter gilt nur hier und springt beim Verlassen des Tabs wieder auf das App-Jahr zurück. Export als PDF. - **Verlauf → Überstundenverlauf:** automatische und manuelle Buchungen aller Jahre, als CSV exportierbar. - **Verlauf → Urlaubsbuchungen:** Monatsverteilung als Balkendiagramm mit Gesamt- und Durchschnittswerten, darunter die Buchungen aller Jahre mit Mehrfachauswahl zum Stornieren; als CSV exportierbar. - **Verlauf → Jahresvergleich:** Anspruch, Verbrauch, Resturlaub, Krankheitstage sowie automatische, manuelle und gesamte Überstunden für Vorjahr und App-Jahr. - **Einstellungen → Sicherung:** Sicherungsstände ansehen und über „Jetzt sichern“ sofort eine Sicherung erstellen. ## Installation auf einem Server MyTime kann auf dem Server selbst gebaut werden oder als fertiges Image aus der Gitea-Registry laufen. ### Empfohlen: fertiges Image Voraussetzungen: - Docker Engine und Docker Compose Plugin - Git-Zugriff auf das private Gitea-Repository - Leseberechtigung für `gitea.lokcal.de/loki/mytime` in der Container-Registry Einmalig am Server an der Registry anmelden (Token mit Paket-Leserecht verwenden; nicht in eine Shell-History oder Datei schreiben): ```bash docker login gitea.lokcal.de -u ``` Repository klonen, Port konfigurieren und Image starten: ```bash git clone https://gitea.lokcal.de/loki/mytime.git cd mytime cp .env.example .env # APP_PORT bei Bedarf in .env anpassen, z. B. auf 3100 docker compose pull docker compose up -d curl http://127.0.0.1:3100/api/health ``` Auf einem neuen Server ohne Portkonflikt kann `APP_PORT` in `.env` auf `3000` gesetzt werden. Datenbank und Backups liegen in `./data` und `./backups` neben der Compose-Datei. **Aktualisieren:** ```bash cd mytime git pull docker compose pull docker compose up -d --force-recreate curl http://127.0.0.1:3100/api/health ``` ### Image-Build in Gitea Actions `.gitea/workflows/docker.yml` führt bei Pushes auf `main` Tests, Skript-Tests, TypeScript-Prüfung und Produktions-Build aus. Nur wenn alles erfolgreich ist, wird das Image als `latest` und mit Commit-SHA in die Registry gepusht. Ein Gitea-Actions-Runner und die Container-Registry müssen aktiviert sein. Der Workflow verwendet `ACTION_BOT` als Registry-Token; alternativ können `REGISTRY_USER` und `REGISTRY_TOKEN` als Repository-Secrets gesetzt werden. Der Token braucht Paket-Schreibrecht. Zum lokalen Bauen und Entwickeln siehe `compose.dev.yaml` im Schnellstart. ## Brückentage-Daten aktualisieren Die App enthält Scraping-Daten für alle 16 Bundesländer. Sie werden beim normalen Betrieb nicht aus dem Internet geladen; die App funktioniert damit offline. Auf einem Rechner mit Internetzugang im Repository ausführen: ```bash npm ci npm run scrape:bridge npm test -- --run npm run test:scripts npx tsc --noEmit ``` Standardmäßig lädt das Skript das laufende und das nächste Jahr und erhält bereits gespeicherte Jahre. Optional lassen sich Jahre und Bundesländer angeben: ```bash npm run scrape:bridge -- --years=2028,2029,2030 --states=HH,BY ``` Danach Änderungen committen und nach Gitea pushen. Die Action baut ein aktualisiertes Image. Für nicht gespeicherte Jahre zeigt die App einen Hinweis statt lokal berechneter Empfehlungen. ## Datensicherung und Wiederherstellung Die App erstellt wöchentlich automatisch eine konsistente SQLite-Sicherung und behält höchstens fünf Dateien in `./backups`. In **Einstellungen → Sicherung** kann zusätzlich jederzeit eine Sicherung manuell erstellt und der Sicherungsstand kontrolliert werden. Nicht mehr benötigte Sicherungen lassen sich dort nach einer Bestätigung einzeln löschen. Bei aktiver Anmeldung ist der Bereich nur für Administratoren sichtbar. ### Sicherung zurückspielen 1. Im Verzeichnis mit der Compose-Datei den Dienst stoppen: ```bash docker compose stop app ``` Bei lokaler Entwicklung stattdessen `docker compose -f compose.dev.yaml stop app` verwenden. 2. WAL-Seitendateien der aktuellen Datenbank entfernen, dann Sicherung einspielen: ```bash rm -f data/app.sqlite3-wal data/app.sqlite3-shm cp backups/.sqlite3 data/app.sqlite3 ``` 3. Dienst wieder starten: ```bash docker compose start app ``` Bei lokaler Entwicklung stattdessen `docker compose -f compose.dev.yaml start app` verwenden. 4. `http://:/api/health` prüfen und anschließend Salden und Buchungen in der App kontrollieren. Wichtig: Die App verwendet SQLite im **WAL-Modus**. Deshalb vor dem Restore immer stoppen und die alten `-wal`- und `-shm`-Dateien löschen; sonst können alte Transaktionen mit der eingespielten Sicherung vermischt werden. ## Sicherheit und Daten - MyTime hat standardmäßig keine Anmeldung (`AUTH_MODE=off`). Nur im Heimnetz betreiben, keinen Router-Port ins Internet weiterleiten und den Port in der Firewall auf das LAN begrenzen. Wer die optionale Anmeldung einschaltet (siehe unten), betreibt den Dienst trotzdem nur im Heimnetz. - Der Container startet und läuft als Non-Root-User `node` (UID:GID `1000:1000`); `compose.yaml` und `compose.dev.yaml` erzwingen das zusätzlich per `user: "1000:1000"`. Einmalige Migration bestehender Daten: `sudo chown -R 1000:1000 data backups`. - Datenbank: `./data/app.sqlite3`; Sicherungen: `./backups/`. - Daten und Sicherungen sind in `.gitignore` ausgeschlossen und werden nicht mit dem Quellcode veröffentlicht. ## Anmeldung (optional) Standardmäßig ist die Anmeldung ausgeschaltet und die App verhält sich wie bisher. Mit `AUTH_MODE=password` oder `AUTH_MODE=sso` stehen alle Seiten und API-Routen nur noch nach Anmeldung zur Verfügung (Ausnahmen: `/api/auth/*` und der Docker-Healthcheck `/api/health`). 1. `.env.example` nach `.env` kopieren und anpassen: | Variable | Bedeutung | | --- | --- | | `AUTH_MODE` | `off` (Standard), `password` oder `sso`. | | `BETTER_AUTH_SECRET` | Pflicht bei aktiver Anmeldung, mindestens 32 Zeichen (`openssl rand -base64 32`). | | `BETTER_AUTH_URL` | Pflicht bei aktiver Anmeldung, z. B. `http://:3100`. | | `OIDC_DISCOVERY_URL` | Nur `sso`: Discovery-URL des Anbieters, z. B. `https://auth.lan/application/o/mytime/.well-known/openid-configuration`. | | `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` | Nur `sso`: Client-Daten aus dem Anbieter (Authentik, Keycloak, Zitadel …). | 2. Dienst neu starten (`docker compose up -d` bzw. `docker compose -f compose.dev.yaml up -d`). 3. **Erste Anmeldung** (`password`): Die leere Datenbank verlangt beim ersten Aufruf „Erste Anmeldung" – Name, E-Mail und Passwort anlegen. Das erste Konto wird **Admin** und erbt ggf. vorhandene Daten aus der Zeit ohne Anmeldung. Weitere Konten legt danach nur ein Admin an (Einstellungen → Benutzer). 4. **SSO** (`sso`): Auf der Anmeldeseite erscheint „Mit SSO anmelden". Die App leitet zum OIDC-Anbieter weiter und legt beim ersten Login das Benutzerkonto automatisch an (das erste Konto wird Admin). ### Rollen und Rechte | Rolle | Darf | | --- | --- | | `benutzer` | Eigene Daten lesen und buchen (Zeiterfassung, Urlaub als **Anfrage**, Krankmeldung), eigene Einstellungen und eigenes Konto (Passwort, E-Mail) pflegen. | | `teamleiter` | Alles von `benutzer`, zusätzlich fremde Einstellungen lesen/ändern sowie der **Team-Tab**: Matrix-Kalender aller Benutzer mit Resturlaub, Anfragen genehmigen/ablehnen. | | `admin` | Alles von `teamleiter`, zusätzlich **Benutzerverwaltung** (anlegen, Rolle wechseln, löschen) und **Sicherung**. | ### Genehmigungs-Workflow 1. Normale Benutzer planen Urlaub als **Anfrage** (`beantragt`): Badge in den Buchungen, gestrichelte Kalendertage, eigene Sammelzeile im Team-Tab – zählt noch **nicht** in den Resturlaub, blockiert aber dieselben Tage für eigene weitere Anfragen. 2. Teamleiter/Admins sehen offene Anfragen oben im Team-Tab und **genehmigen** oder **lehnen ab**. Genehmigen prüft den Anspruch frisch (422, falls inzwischen überzogen). 3. **Bestätigt** zählt in den Saldo (Papierkorb bei Storno wie bisher); **abgelehnt** bleibt mit Badge sichtbar und ist löschbar. Eigene Anfragen lassen sich jederzeit spurlos stornieren. Hinweise: - HTTP im Heimnetz genügt; sichere Cookies sind bewusst nicht erzwungen (`useSecureCookies` aus). Trotzdem gilt: kein Internet-Expose, LAN-Begrenzung bleibt. - Abmelde-Icon steht im Kopf neben dem Theme-Schalter. Eine abgelaufene Sitzung führt zurück zur Anmeldung statt zur Fehlerwand. - Sitzungs-, Benutzer- und Kontodaten liegen als Tabellen `session`, `user`, `account`, `verification` in derselben `app.sqlite3` und werden mitgesichert. Alle Zeitdaten sind benutzerbezogen (`user_id`); der Selbstschutz verhindert, das eigene Konto oder den letzten Admin zu löschen. - Einstellungen → Konto: Passwort ändern (aktuelles nötig) und E-Mail ändern (gilt sofort, ohne Bestätigungs-Mail – kein SMTP im Heimnetz). ## Entwicklung und Tests Voraussetzungen: Node.js 22 und npm. ```bash npm ci npm run dev npm test -- --run npm run test:scripts npx tsc --noEmit npm run build ```