wannpassts/README.md
Tronax cbc03c56bf
feat: add PWA support and mobile-responsive layouts
- Add web app manifest, maskable/apple-touch icons, and a production-only
  service worker that caches the app shell for offline start
- Improve mobile UX: responsive breakpoints from ~320px, 44px touch
  targets, safe-area insets, dvh heights, 16px inputs to avoid iOS zoom
- Serve .webmanifest with correct content type in the Go static handler
- Document PWA behavior and mobile features in the README
2026-08-25 19:12:12 +02:00

151 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WannPassts
**Deine freien Zeiten geteilt per Link. Ohne deine Termine preiszugeben.**
WannPassts ist eine Self-Hosted-Buchungs-App (à la Cal.com / Calendly, aber privat):
Du verbindest deine Kalender, teilst einen Link, und andere sehen nur **frei / belegt**
und stellen Buchungsanfragen. Termintitel, Orte und Beschreibungen verlassen deine
Kalender nie.
## Features
- 🔐 **Registrierung/Login** (JWT, bcrypt-Passwort-Hashes)
- 📆 **Kalender-Connect:**
- **Google Kalender** per OAuth nutzt die FreeBusy-API, die *strukturell* keine Termindetails liefert
- **iCloud** & andere (Fastmail, Nextcloud, …) per **CalDAV** iCloud mit App-spezifischem Passwort
- **Beliebige ICS-/webcal-Links** (z. B. Google-„Private Adresse“, Ferien-/Schichtpläne)
- 🔗 **Öffentliche Buchungsseite** (`/b/<slug>`): freie Slots im von dir definierten Zeitfenster, Wochentage, Slot-Raster und Dauern **niemals Termindetails**
- 📨 **Buchungsanfragen** annehmen/ablehnen; angenommene Zeiten werden belegt und verhindern Doppelbuchungen
- ⚙️ Buchungsregeln: Zeitfenster, Slot-Raster (1060 Min), Dauern, Wochentage, Horizont (1120 Tage), Zeitzone
- 🧊 **Liquid-Glass-UI** mit **Dark-/Light-Mode**: Umschalter (Auto / Dunkel / Hell) auf jeder Seite, automatische Systemerkennung (`prefers-color-scheme`) inkl. Live-Wechsel, Auswahl wird pro Browser gespeichert
- 📱 **Voll mobilfähig**: responsive Layouts ab ~320 px, 44px-Touch-Targets, kein iOS-Fokus-Zoom (16px-Inputs), Safe-Area-Unterstützung (Notch), `dvh`-Höhen, `touch-action` ohne Doppeltipp-Zoom
- 📲 **PWA**: installierbar (Android/Desktop: „App installieren“, iOS: „Zum Home-Bildschirm“), Manifest mit Maskable-Icons, App-Shortcuts; Service Worker cached die App-Shell → Startseite funktioniert offline (API-Aufrufe brauchen natürlich Netz)
- 🔒 Provider-Tokens werden **AES-256-GCM-verschlüsselt** in der SQLite-DB gespeichert
## Tech Stack
| Teil | Technologie |
| -------- | ------------------------------------------------------------------ |
| Frontend | Vue 3 + TypeScript, Vite, Pinia, Vue Router (kein UI-Framework) |
| Backend | Go, chi, JWT, SQLite (modernc, CGO-frei), x/oauth2, go-webdav |
## Schnellstart (Entwicklung)
```bash
# Terminal 1 Backend (http://localhost:8080)
cd backend
cp .env.example .env # optional anpassen; ohne .env läuft es mit Defaults
go run ./cmd/server
# Terminal 2 Frontend (http://localhost:5173, Proxied /api → 8080)
cd frontend
npm install
npm run dev
```
Dann http://localhost:5173 öffnen, Konto erstellen, Kalender verbinden, Link teilen. 🎉
## Produktion (ein einziger Go-Prozess)
```bash
cd frontend && npm run build && cd ..
cd backend
STATIC_DIR=../frontend/dist \
APP_URL=https://deine-domain.de \
FRONTEND_URL=https://deine-domain.de \
JWT_SECRET=$(openssl rand -hex 32) \
ENCRYPTION_KEY=$(openssl rand -hex 32) \
go run ./cmd/server
```
Der Go-Server liefert dann das Frontend (`dist/`) **und** die API über einen Port.
### PWA-Hinweise
- Der Service Worker wird nur im Production-Build registriert (`npm run build` + Auslieferung über `STATIC_DIR`), nicht im Vite-Dev-Modus.
- Die Cache-Version des Service Workers wird pro Build automatisch neu gesetzt (Build-Zeitstempel) — nach einem Deploy holen Clients die neue Version und alte Caches werden geräumt.
- `/api/*` wird niemals gecached; offline funktioniert die App-Shell (Startseite/Navigation), Kalender- und Buchungsdaten benötigen eine Verbindung.
- Icons liegen als SVG-Quellen in `frontend/public/icons/` und werden per `rsvg-convert` zu PNGs konvertiert (Neuerzeugung: siehe Skript unten im Abschnitt „Tests“).
## Kalender-Anbindung einrichten
### Google
1. [Google Cloud Console](https://console.cloud.google.com): Projekt anlegen, **Google Calendar API** aktivieren.
2. OAuth-Zustimmungsbildschirm (External) einrichten, Scope `calendar.readonly`.
3. **OAuth-Client-ID** (Webanwendung) erstellen; als autorisierte Redirect-URI
`{APP_URL}/api/calendars/google/callback` eintragen (lokal: `http://localhost:8080/api/calendars/google/callback`).
4. `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET` in `backend/.env` setzen.
Beim Verbinden wird nur der **Primärkalender** angebunden; gelesen wird ausschließlich Free/Busy.
### iCloud
1. Auf [appleid.apple.com](https://appleid.apple.com/account/manage) → *Anmeldung und Sicherheit***App-spezifisches Passwort** erzeugen.
2. In WannPassts: *Kalender → iCloud / CalDAV*:
- Server: `https://caldav.icloud.com`
- Benutzername: Apple-ID (E-Mail)
- Passwort: das App-spezifische Passwort (nicht das normale!)
3. Der erste Kalender mit Terminen wird automatisch erkannt (Kalender-Pfad ist optional manuell überschreibbar).
### Andere (Fastmail, Nextcloud, GMX, …)
Gleiche CalDAV-Maske mit dem jeweiligen CalDAV-Server des Anbieters, oder beliebige **ICS-Links** abonnieren.
## Datenschutz-Design
- Besucher sehen ausschließlich **Start/Ende von busy-Zeiträumen** die API hat gar keine Felder für Titel & Co.
- Google: FreeBusy-API liefert von sich aus nur Belegungszeiträume.
- CalDAV/ICS: Termine werden serverseitig auf `DTSTART/DTEND` reduziert, abgesagte und „Verfügbar“-Termine (TRANSPARENT) werden ignoriert.
- Tokens/Passwörter der Verbindungen liegen verschlüsselt (AES-GCM aus `ENCRYPTION_KEY`) in der DB.
- Sync erfolgt bedarfsgesteuert (max. alle 5 Min) beim Aufruf der Buchungsseite plus manueller Button.
## Bekannte Grenzen (MVP)
- **Kein E-Mail-Versand** Anfragen/Freigaben erscheinen nur im Dashboard (Mail-Versand via SMTP wäre der nächste Ausbauschritt).
- **ICS-Abos** expandieren Serientermine nicht**; CalDAV und Google tun dies serverseitig korrekt.
- **Ausstehende (pending) Anfragen blockieren noch keine Zeit** erst angenommene. Bei zwei parallelen Anfragen für denselben Slot gewinnt, wer zuerst angenommen wird (Kollision wird beim Annehmen geprüft).
- Ein Google-Konto verbindet aktuell den Primärkalender; weitere Kalender lassen sich als ICS-Abo hinzufügen.
## Projektstruktur
```
backend/
cmd/server/ # Einstieg
internal/config/ # Env-/Secret-Handling
internal/db/ # SQLite-Schema & Queries
internal/crypto/ # AES-GCM für gespeicherte Provider-Tokens
internal/calendar/ # Provider: google.go, caldav.go, ics.go (+ Syncer)
internal/httpapi/ # Router, Middleware, Handler (auth, calendars, bookings, public)
frontend/
src/lib/ # api-Client, Typen, Zeitzonen-Helfer
src/stores/ # Pinia: auth, toast
src/views/ # Landing, Login, Register, Dashboard, Booking (/b/:slug)
src/components/ # GlassCard, ShareLink, Sektionen (Kalender/Anfragen/Einstellungen)
src/styles/main.css # Liquid-Glass-Dark-Designsystem
```
## API-Überblick
| Methode | Pfad | Auth | Zweck |
| ------------------- | --------------------------------- | ---- | ---------------------------------------- |
| POST | `/api/auth/register` \| `/login` | | Konto / JWT |
| GET/PATCH | `/api/me` | ✅ | Profil & Buchungsregeln |
| POST | `/api/me/slug` | ✅ | Neuen Buchungslink erzeugen |
| GET/DELETE | `/api/calendars…` | ✅ | Verbinden (google/caldav/ics), verwalten, sync |
| GET | `/api/public/{slug}` | | Buchungsseite: Regeln + busy-Zeiträume |
| POST | `/api/public/{slug}/bookings` | | Buchungsanfrage stellen |
| GET | `/api/bookings`, `…/accept\|decline` | ✅ | Anfragen verwalten |
## Tests
```bash
cd backend && go test ./... # ICS-Parser, Dauer-Handling, Interval-Clamping
```
PWA-Icons neu erzeugen (benötigt `rsvg-convert`):
```bash
cd frontend/public/icons
rsvg-convert -w 192 -h 192 -o icon-192.png icon.svg
rsvg-convert -w 512 -h 512 -o icon-512.png icon.svg
rsvg-convert -w 512 -h 512 -o icon-maskable-512.png icon-maskable.svg
rsvg-convert -w 180 -h 180 -o apple-touch-icon.png apple-touch-icon.svg
```