No description
Find a file
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
backend feat: add PWA support and mobile-responsive layouts 2026-08-25 19:12:12 +02:00
frontend feat: add PWA support and mobile-responsive layouts 2026-08-25 19:12:12 +02:00
.gitignore feat: add WannPassts booking app with Go backend and Vue frontend 2026-08-25 18:50:28 +02:00
README.md feat: add PWA support and mobile-responsive layouts 2026-08-25 19:12:12 +02:00

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)

# 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)

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: 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.comAnmeldung und SicherheitApp-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

cd backend && go test ./...   # ICS-Parser, Dauer-Handling, Interval-Clamping

PWA-Icons neu erzeugen (benötigt rsvg-convert):

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