# Pocketwatch Go Ein schlanker, self-hosted Zeiterfasser in Go – funktional an `winnicodes/pocketwatch` angelehnt, aber ohne React, Node.js, PHP oder Nginx im Anwendungs-Stack. Die Anwendung besteht aus einem Go-Binary, eingebettetem HTML/CSS/Vanilla-JavaScript und SQLite über `modernc.org/sqlite` (pure Go, kein CGO notwendig). ## Enthalten - Start-/Stop-Timer mit Live-Anzeige - Laufender Timer überlebt Reloads und Gerätewechsel - Kunde + Tätigkeit, Kunden-Autocomplete nach letzter Nutzung - Tages- und Wochensumme - Verlauf gruppiert nach Tagen - Suche über Kunde und Tätigkeit - Zeitraumfilter: Tag, Woche, Monat, Jahr, alle - Zeitraum vor/zurück schalten - Einträge nachtragen, bearbeiten und löschen - Rundung 1–60 Minuten, normal oder immer aufwärts - CSV-Export (Semikolon + UTF-8 BOM für Excel) - PDF-Export ohne PDF-Framework - Service-Bericht als PDF pro Kunde mit Tätigkeitsübersicht, Auftrag/Ticket, Ansprechpartner, Einsatzort, Bemerkungen und Unterschriftsfeldern für Kunde/Techniker - Export wahlweise für aktuelle Ansicht oder freien Datumsbereich, auf-/absteigend und kompakt - Responsive Desktop-/Mobile-Oberfläche - Mobile Kompaktansicht: startet mit dem Timer im Fokus; Verlauf/Übersicht und Einstellungen werden separat über die untere Navigation geöffnet - Mehrbenutzerbetrieb mit strikt getrennten Daten - Admin-/Benutzerrollen - Benutzer anlegen, deaktivieren und Passwörter zurücksetzen - Eigenes Passwort ändern - Keine öffentliche Registrierung nach der Ersteinrichtung - Sessions in SQLite, HttpOnly-Cookie, CSRF-Token - Login-Rate-Limit - CSP und weitere Security-Header - SQLite WAL, Foreign Keys, Busy Timeout - Healthcheck unter `/healthz` - Docker/Compose, unprivilegierter Runtime-Benutzer, alle Linux-Capabilities entfernt ## Abhängigkeiten Zur Laufzeit gibt es nur eine externe Go-Abhängigkeit: ```text modernc.org/sqlite ``` Alles andere verwendet die Go-Standardbibliothek. Das Frontend hat **keine** npm-/Node-Abhängigkeiten und keinen separaten Build-Schritt. ## Schnellstart mit Docker Compose ```bash docker compose up -d --build ``` Danach: ```text http://localhost:8080 ``` Beim ersten Aufruf erscheint die Ersteinrichtung. Der erste Account wird Administrator. Die Daten liegen im Docker-Volume `pocketwatch-data` in `/data/pocketwatch.db`. Das Image enthält außerdem einen Docker-Healthcheck gegen `/healthz`. Bei einem Host-Bind-Mount statt eines Named Volumes muss das Zielverzeichnis für UID/GID `10001` schreibbar sein. ## Lokal ohne Docker Voraussetzungen: - Go 1.26+ Dann: ```bash go mod tidy DATA_DIR=./data APP_ADDR=:8080 go run ./cmd/pocketwatch ``` Oder: ```bash make run ``` ## Konfiguration | Variable | Default | Bedeutung | |---|---:|---| | `APP_ADDR` | `:8080` | Listen-Adresse des HTTP-Servers | | `DATA_DIR` | `/data` | Verzeichnis für `pocketwatch.db` | | `COOKIE_SECURE` | `false` | Auf `true` setzen, wenn die App ausschließlich über HTTPS erreichbar ist | ### Hinter Reverse Proxy / HTTPS Wenn z. B. Caddy, Traefik oder nginx TLS terminiert: ```yaml environment: COOKIE_SECURE: "true" ``` Die App setzt selbst keine CORS-Header. API und UI sind als Same-Origin-Anwendung gedacht. ## Datenmodell SQLite enthält vier Kernbereiche: - `users` – Accounts, Rollen, Aktivstatus - `sessions` – gehashte Session-Tokens + CSRF-Token - `entries` – Zeiteinträge, immer mit `user_id` - `user_settings` – persönliche Rundungs-, Export- und Anzeigeeinstellungen Ein partieller Unique-Index stellt sicher, dass pro Benutzer höchstens ein laufender Timer existiert. Zeitpunkte werden als Unix-Millisekunden gespeichert. Die Web-Oberfläche verwendet die lokale Browser-Zeitzone; Exporte verwenden die persönliche IANA-Zeitzone, z. B. `Europe/Berlin`. ## Sicherheit ### Passwörter Passwörter werden mit PBKDF2-HMAC-SHA256, zufälligem Salt und 310.000 Iterationen gespeichert. Die Implementierung nutzt `crypto/pbkdf2`, `crypto/sha256` und `crypto/rand` aus der Go-Standardbibliothek von Go 1.26. ### Sessions - 256-Bit zufällige Session-Tokens - nur SHA-256-Digest des Session-Tokens in SQLite - HttpOnly-Cookie - SameSite=Lax - optional `Secure` - serverseitiges Ablaufdatum - Sessions werden beim Deaktivieren eines Benutzers oder Passwort-Reset invalidiert ### CSRF Schreibende API-Aufrufe benötigen zusätzlich ein zufälliges, sitzungsgebundenes `X-CSRF-Token`. ### Mandantentrennung Jede SQL-Operation auf Zeiten enthält die `user_id` aus der authentifizierten Session. IDs aus einem anderen Benutzerkonto reichen daher nicht aus, um fremde Einträge zu lesen oder zu verändern. ## Mobile Kompaktansicht Auf Smartphones (bis 820 px Breite) ist standardmäßig die **Kompaktansicht** aktiv. Dabei verschwindet die Desktop-Kopfleiste und die Timer-Erfassung nutzt nahezu den gesamten verfügbaren Bildschirm. Kunde, Tätigkeit, Timer, Start/Stop und die Tages-/Wochensummen bleiben direkt sichtbar. Die untere mobile Navigation trennt die Bereiche bewusst: - **Timer** – konzentrierte Zeiterfassung - **Übersicht** – Verlauf, Suche, Filter, Nachtragen und Export - **Mehr** – Einstellungen, Benutzerverwaltung (für Admins) und Abmelden Die Einstellung **„Kompaktansicht auf Mobilgeräten“** kann pro Benutzer deaktiviert werden. Sie wird in SQLite in `user_settings.mobile_compact` gespeichert. Bestehende Datenbanken werden beim Start automatisch um die neue Spalte ergänzt; der Standardwert ist aktiviert. ## Service-Berichte Im Exportdialog steht zusätzlich **„Service-Bericht für Unterschrift“** zur Verfügung. Der Bericht verwendet den gewählten Exportzeitraum und filtert anschließend exakt auf einen Kunden. Dadurch können keine Tätigkeiten anderer Kunden versehentlich in denselben unterschreibbaren Bericht geraten. Der Service-Bericht enthält: - Kunde und Ansprechpartner - Einsatzort - Auftrags-/Ticketnummer - Betreff sowie optionale Zusammenfassung/Bemerkungen - chronologische Tätigkeiten mit Datum, Uhrzeit und gerundeter Dauer - Gesamtdauer - Ort und Berichtsdatum - getrennte Unterschriftsfelder für Kunde und Techniker Die zusätzlichen Berichtsdaten werden per `POST /api/service-report.pdf` übertragen und nicht als Query-Parameter in der Download-URL abgelegt. Es werden keine Service-Berichte oder Unterschriften dauerhaft in SQLite gespeichert; der PDF-Export wird bei Bedarf erzeugt. ## Backup Wegen WAL sollte die Datenbank nicht blind während Schreibzugriffen als einzelne Datei kopiert werden. Der einfachste konsistente Weg bei Docker Compose: ```bash docker compose stop pocketwatch docker run --rm \ -v pocketwatch-go_pocketwatch-data:/data:ro \ -v "$PWD:/backup" \ alpine:3.22 \ tar czf /backup/pocketwatch-backup.tgz -C /data . docker compose start pocketwatch ``` Der konkrete Volume-Name kann je nach Compose-Projektname abweichen (`docker volume ls`). ## Projektstruktur ```text pocketwatch-go/ ├── cmd/pocketwatch/main.go ├── internal/app/ │ ├── auth.go │ ├── db.go │ ├── export.go │ ├── server.go │ ├── *_test.go │ └── web/ │ ├── index.html │ ├── login.html │ ├── app.css │ ├── app.js │ └── login.js ├── Dockerfile ├── docker-compose.yml ├── Makefile └── go.mod ``` ## Tests ```bash go test ./... ``` Enthalten sind u. a. Tests für PBKDF2, Rundungslogik und den minimalen PDF-Writer. ## Bewusste Unterschiede zum ursprünglichen Pocketwatch Diese Implementierung übernimmt das Produktkonzept und die wichtigsten Bedienabläufe, ist aber technisch ein Neuaufbau: - SQLite statt JSON-Dateien - Go statt PHP - Vanilla JS statt React/Vite/Tailwind - kein Node.js im Build oder Betrieb - Login und Mehrbenutzerbetrieb - Adminverwaltung - serverseitige Datenisolation - CSRF- und Session-Schutz - Healthcheck und Security-Header Die Oberfläche orientiert sich am dunklen, kompakten Amber-Design des Originals, ist aber kein 1:1 kopierter Frontend-Quellcode. Sie ist derzeit bewusst deutschsprachig; das Datenmodell hält die Spracheinstellung bereits für eine spätere vollständige Lokalisierung vor. ## Inspiration Inspiriert von [`winnicodes/pocketwatch`](https://github.com/winnicodes/pocketwatch), das als minimalistischer self-hosted Zeiterfasser unter MIT veröffentlicht ist.