All checks were successful
release-tag / release-image (push) Successful in 2m6s
237 lines
8.2 KiB
Markdown
237 lines
8.2 KiB
Markdown
# 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.
|