Initialize repo and add session changes (2025-09-08)

This commit is contained in:
Auto Commit
2025-09-08 14:03:43 +02:00
commit 44ab0a534b
327 changed files with 40952 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
# ADR-0006: Tenant Administration as Store-Ready PWA
- Status: Accepted
- Date: 2025-09-08
- Deciders: Product, Engineering
- Related: PRP Addendum 2025-09-08 (Tenant Admin PWA)
## Context
The original PRP envisioned tenant administration via a Filament panel. We want a dedicated, installable experience for tenant admins and the ability to distribute through Google Play and Apple App Store. We also want a cleaner API-first separation and mobile-friendly capabilities (push, offline, background sync).
## Decision
- Implement a separate React/Vite PWA for tenant admins ("Tenant Admin PWA").
- Distribute via:
- Android: Trusted Web Activity (TWA), bound to `admin.<platform-domain>` via Digital Asset Links, or Capacitor when native plugins are necessary.
- iOS: Capacitor wrapper for App Store distribution.
- Keep Super Admin as a Filament 4 web panel only.
- Expose all tenant features through `/api/v1/tenant/*`, authenticated using Authorization Code + PKCE and refresh tokens. Tokens include `tenant_id` and roles. Enforce tenant isolation with global scopes and policies.
- MVP billing uses event credits; subscriptions are deferred.
## Consequences
- Filament resources for tenant admins in PRP are deprecated as implementation guidance. They remain as field/validation reference only.
- Backend must provide complete API coverage for tenant use cases and implement token-based auth with refresh and rotation.
- Mobile packaging CI is added (TWA/Capacitor), including assetlinks.json, fastlane lanes, and privacy manifests.
## Implementation Notes
- Repo layout additions:
- `apps/admin-pwa` (React/Vite)
- `packages/mobile` (Capacitor + TWA wrappers)
- Existing `apps/guest-pwa` and Filament-based Super Admin remain.
- Security:
- PKCE, refresh tokens, and secure storage. Rate limits per tenant and device. Audit logs for sensitive actions and impersonation.
- Offline:
- Service Worker with background sync; conflict resolution with ETag/If-Match.
## Alternatives Considered
- Keep tenant admin in Filament: faster initially but not store-distributable and poorer mobile UX.
- Native apps: higher cost and longer timeline; PWA + thin wrappers meet requirements.

View File

@@ -0,0 +1,16 @@
# Session Changes — 2025-09-08
Summary
- Split PRP into docs/prp/* and added addendum + ADR for Tenant Admin PWA.
- Guest PWA: routes/components scaffold, bottom nav, header + settings sheet, theme toggle; polling hooks; upload (client compress <1.5 MB), offline queue + BG sync; gallery filters + lightbox; likes API/UI; SW runtime cache.
- Super Admin (Filament 4): resources for Tenants, Events, Photos, Legal Pages; dashboard widgets; photo moderation; event toggle; join link + selfhosted QR.
- CSV imports + templates for Emotions and Tasks with de/en localization; forms updated to JSON casts.
- Backend public API: stats/photos with ETag/304; upload endpoint; photo like.
- Tenant Admin PWA: auth shell (token), events list/create/edit, event detail (stats, QR, invite), photo moderation.
- Migrations: tenants table; users.tenant_id/role; events.tenant_id; model casts/relations added.
- Artisan: media:backfill-thumbnails; tenant:add-dummy; tenant:attach-demo-event.
Notes
- Security hardening intentionally minimal per instruction (token login for tenant admin).
- QR codes generated server-side via simple-qrcode.
- No secrets committed. Local gogs.ini used only for pushing to Gogs.

View File

@@ -0,0 +1,45 @@
# PRP Addendum (2025-09-08): Tenant Admin PWA
This addendum supersedes tenant-facing Filament guidance in `fotospiel_prp.md`. Super Admin remains Filament (web-only). Tenant administration now lives in a separate, store-ready PWA.
## Summary
- Separate React/Vite PWA for tenant admins.
- Distribution: Android via TWA, iOS via Capacitor; PWA install (A2HS) supported.
- API-first backend: `/api/v1/tenant/*` endpoints cover all tenant operations.
- Auth: Authorization Code + PKCE + refresh tokens; access token includes `tenant_id` and roles.
- Tenancy: global scope + policies; host-based resolution remains for guest PWA.
- Billing: Event credits MVP; subscriptions deferred.
## Architecture Changes
- Replace tenant Filament panel with PWA + API.
- Add `BelongsToTenant` trait and composite uniques including `tenant_id`.
- Introduce `apps/admin-pwa` and `packages/mobile` directories; keep `apps/super-admin` for Filament.
## Mobile Packaging
- Android (TWA): bind to `admin.<platform-domain>` with `/.well-known/assetlinks.json`.
- iOS (Capacitor): native wrapper, push notifications, secure storage.
## Offline & Sync
- Service Worker caches app shell and essentials.
- Background sync queues mutations; conflicts resolved via ETag/If-Match.
## API Surface (Tenant)
- Auth: `/oauth/authorize` (PKCE), `/oauth/token`, `/oauth/token/refresh`.
- Entities: events, galleries, members, uploads, settings, purchases.
- Conventions: pagination, filtering, 429 rate limits, trace IDs in errors.
## Security
- Token storage in Keychain/Keystore (mobile) and IndexedDB (web) with rotation.
- Audit logs for destructive actions and impersonation.
## Migration Notes
- Treat Filament tenant resources in PRP as deprecated examples. Use them to inform field definitions and validation only.
- Future task: convert `fotospiel_prp.md` to UTF-8 and merge this addendum into the main PRP.

View File

@@ -0,0 +1,15 @@
# 01 — Architecture Overview
- Backend: Laravel 12 (PHP 8.3), API-first.
- Super Admin: Filament 4 web panel (platform operations only).
- Tenant Admin: Separate React/Vite PWA, store-ready (Android TWA, iOS Capacitor). See ADR-0006.
- Guest PWA: React/Vite, anonymous session for attendees.
- Storage: S3-compatible object storage, signed URLs, CDN in front.
- Queues: Redis + Horizon for media processing and async jobs.
- DB: Single database, row-level multi-tenancy using `tenant_id` and policies.
Components
- API (`/api/v1`) with OAuth2 PKCE for tenant apps; session auth for Super Admin.
- Media pipeline: upload -> scan -> transform (EXIF/orientation/sizes) -> store -> CDN.
- Notifications: Web Push (VAPID); Capacitor push for iOS wrapper when needed.
- Observability: request ID, structured logs, audit trails for admin actions.

21
docs/prp/02-tenancy.md Normal file
View File

@@ -0,0 +1,21 @@
# 02 — Tenancy Model
Approach
- Single database, row-level scoping via `tenant_id` on tenant-owned tables.
- Global scope (BelongsToTenant trait) for all tenant-owned models; bypass for Super Admin.
- Policies enforce `tenant_admin` and `member` roles; guest upload uses signed, limited-scope tokens.
Keys & Indexes
- Composite uniques include `tenant_id` (e.g., `events`: unique `tenant_id, slug`).
- Foreign keys cascade/delete or null-on-delete based on data retention needs.
Tenant Resolution
- Tenant Admin PWA: resolve from authenticated token claim (`tenant_id`).
- Guest PWA/custom domains: resolve from host/subdomain; map to event and tenant.
Impersonation
- Super Admin can impersonate tenant users; all actions audited with actor + target + reason.
Backups & Export
- Backups include tenant partitions by `tenant_id`.
- Export endpoints provide per-tenant data bundles (photos metadata + links), respecting rate limits.

30
docs/prp/03-api.md Normal file
View File

@@ -0,0 +1,30 @@
# 03 — API Contract
- Base URL: `/api/v1`
- Auth
- Tenant apps: OAuth2 Authorization Code + PKCE, refresh tokens.
- Super Admin: session-authenticated Filament (web only).
- Common
- Pagination: `page`, `per_page` (max 100).
- Errors: `{ error: { code, message, trace_id }, details?: {...} }`.
- Rate limits: per-tenant and per-device for tenant apps; 429 with `x-rate-limit-*` headers.
Key Endpoints (abridged)
- Auth: `/oauth/authorize`, `/oauth/token`, `/oauth/token/refresh`.
- Tenants (Super Admin only): list/read; no create via API for MVP.
- Events (tenant): CRUD, publish, archive; unique by `tenant_id + slug`.
- Photos (tenant): signed upload URL, create, list, moderate, feature.
- Emotions & Tasks: list, tenant overrides; task library scoping.
- Purchases & Ledger: create purchase intent, webhook ingest, ledger list.
- Settings: read/update tenant theme, limits, legal page links.
Guest Polling (no WebSockets in v1)
- GET `/events/{slug}/stats` — lightweight counters for Home info bar.
- Response: `{ online_guests: number, tasks_solved: number, latest_photo_at: ISO8601 }`.
- Cache: `Cache-Control: no-store`; include `ETag` for conditional requests.
- GET `/events/{slug}/photos?since=<ISO8601|cursor>` — incremental gallery refresh.
- Response: `{ data: Photo[], next_cursor?: string, latest_photo_at: ISO8601 }`.
- Use `If-None-Match` or `If-Modified-Since` to return `304 Not Modified` when unchanged.
Webhooks
- Payment provider events, media pipeline status, and deletion callbacks. All signed with shared secret per provider.

View File

@@ -0,0 +1,213 @@
# 04 — Data Model & Portable Migrations
Use Laravel Schema builder; avoid database-specific ENUM/DDL. Composite unique indexes include `tenant_id` for tenant-owned data.
## Core (Tenants, Users, Events, Settings, Analytics, Purchases, Ledger)
```php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
// Tenants
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->nullable()->unique();
$table->string('contact_name');
$table->string('contact_email');
$table->string('contact_phone')->nullable();
$table->integer('event_credits_balance')->default(1);
$table->timestamp('free_event_granted_at')->nullable();
$table->integer('max_photos_per_event')->default(500);
$table->integer('max_storage_mb')->default(1024);
$table->json('features')->nullable();
$table->timestamp('last_activity_at')->nullable();
$table->timestamps();
});
// Users: tenancy + role (portable — avoid DB ENUM)
Schema::table('users', function (Blueprint $table) {
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('role', 32)->default('tenant_user')->index();
});
// Events: include tenant_id and composite unique
Schema::table('events', function (Blueprint $table) {
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->unique(['tenant_id', 'slug']);
});
// System settings
Schema::create('system_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->text('description')->nullable();
$table->boolean('is_public')->default(false);
$table->timestamps();
});
// Platform analytics
Schema::create('platform_analytics', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('metric_name', 100);
$table->bigInteger('metric_value');
$table->date('metric_date');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['metric_date', 'metric_name']);
$table->index(['tenant_id', 'metric_date']);
});
// Event purchases (event credits)
Schema::create('event_purchases', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('events_purchased')->default(1);
$table->decimal('amount', 10, 2);
$table->string('currency', 3)->default('EUR');
$table->string('provider', 32); // app enum: app_store|play_store|stripe|paypal
$table->string('external_receipt_id')->nullable();
$table->string('status', 16)->default('pending'); // app enum
$table->timestamp('purchased_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'purchased_at']);
});
// Credits ledger
Schema::create('event_credits_ledger', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->integer('delta');
$table->string('reason', 32); // app enum
$table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
$table->text('note')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'created_at']);
});
```
## Domain (Event Types, Events, Emotions, Tasks, Photos, Likes)
```php
use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
// Event types (global)
Schema::create('event_types', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->string('slug', 100)->unique();
$table->string('icon', 64)->nullable();
$table->json('settings')->nullable();
$table->timestamps();
});
// Events (tenant-scoped)
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->json('name');
$table->date('date');
$table->string('slug');
$table->json('description')->nullable();
$table->json('settings')->nullable();
$table->foreignId('event_type_id')->constrained('event_types');
$table->boolean('is_active')->default(true);
$table->string('default_locale', 5)->default('de');
$table->timestamps();
$table->unique(['tenant_id', 'slug']);
});
// Emotions (global library)
Schema::create('emotions', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->string('icon', 50);
$table->string('color', 7);
$table->json('description')->nullable();
$table->integer('sort_order')->default(0);
$table->boolean('is_active')->default(true);
});
// Pivot: emotion x event_type
Schema::create('emotion_event_type', function (Blueprint $table) {
$table->foreignId('emotion_id')->constrained('emotions')->cascadeOnDelete();
$table->foreignId('event_type_id')->constrained('event_types')->cascadeOnDelete();
$table->primary(['emotion_id', 'event_type_id']);
});
// Tasks (with optional tenant/event scoping)
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('emotion_id')->constrained('emotions');
$table->foreignId('event_type_id')->nullable()->constrained('event_types')->nullOnDelete();
$table->json('title');
$table->json('description');
$table->string('difficulty', 16)->default('easy'); // app enum
$table->json('example_text')->nullable();
$table->integer('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
$table->string('scope', 16)->default('global'); // global|tenant|event
$table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete();
$table->timestamps();
$table->unique(['tenant_id', 'emotion_id', 'title']);
});
// Photos
Schema::create('photos', function (Blueprint $table) {
$table->id();
$table->foreignId('event_id')->constrained('events')->cascadeOnDelete();
$table->foreignId('emotion_id')->constrained('emotions');
$table->foreignId('task_id')->nullable()->constrained('tasks')->nullOnDelete();
$table->string('guest_name');
$table->string('file_path');
$table->string('thumbnail_path');
$table->unsignedInteger('likes_count')->default(0);
$table->boolean('is_featured')->default(false);
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['event_id', 'created_at']);
});
// Photo likes
Schema::create('photo_likes', function (Blueprint $table) {
$table->id();
$table->foreignId('photo_id')->constrained('photos')->cascadeOnDelete();
$table->string('guest_name');
$table->string('ip_address', 45)->nullable();
$table->timestamp('created_at')->useCurrent();
$table->unique(['photo_id', 'guest_name', 'ip_address']);
});
```
## Legal Pages
```php
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;
Schema::create('legal_pages', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete();
$table->string('slug', 32); // imprint|privacy|terms|custom
$table->json('title');
$table->json('body_markdown');
$table->string('locale_fallback', 5)->default('de');
$table->unsignedInteger('version')->default(1);
$table->timestamp('effective_from')->nullable();
$table->boolean('is_published')->default(false);
$table->timestamps();
$table->unique(['slug', 'tenant_id', 'version']);
});
```
## Notes
- Prefer app-level enums (string columns + validation) over DB `ENUM`.
- Use `cascadeOnDelete()` only where child data must be removed with parent; otherwise `nullOnDelete()`.
- Every tenant-owned table should include `tenant_id` and appropriate composite indexes.

View File

@@ -0,0 +1,10 @@
# 05 — Super Admin (Filament)
Scope
- Platform operations only: tenants, legal pages, global settings, billing insights, monitoring.
- No tenant admin functions; those live in the Tenant Admin PWA.
Notes
- Keep to Filament 4 confirmed APIs; avoid pseudo namespaces.
- Impersonation must create an audit trail with actor, target, timestamp, reason.
- Read-only mode switch for maintenance; reflected in API `Retry-After` headers.

View File

@@ -0,0 +1,22 @@
# 06 — Tenant Admin PWA (Store-Ready)
Packaging
- Android: Trusted Web Activity (TWA) bound to `admin.<domain>`; fallback Capacitor if native plugins needed.
- iOS: Capacitor wrapper with push notifications and secure storage.
- Installable PWA (A2HS) with offline and background sync.
Auth & Tenancy
- OAuth2 Authorization Code + PKCE; refresh tokens; secure storage (Keychain/Keystore).
- Tokens carry `tenant_id` and roles; backend enforces scoping.
Capabilities
- Manage events, galleries, members, settings, legal pages, purchases.
- Notifications: Web Push (Android TWA) and Capacitor push (iOS).
- Conflict handling: ETag/If-Match; audit changes.
Distribution & CI
- Play: assetlinks.json at `/.well-known/assetlinks.json`.
- App Store: fastlane lanes; privacy manifests.
- Version alignment with backend; feature flags synced on login.
See also: docs/adr/ADR-0006-tenant-admin-pwa.md

View File

@@ -0,0 +1,153 @@
# 07a — Guest PWA Routes & Components
This scaffold describes recommended routes, guards, directories, and components for the Guest PWA. It is framework-leaning (React Router v6 + Vite), but adaptable.
Routing Principles
- Event routes require an event token (from QR/PIN). Guard redirects to Landing when missing/invalid.
- Use route-level code splitting for camera/lightbox/slideshow.
- Prefer modal routes (photo detail) layered over the gallery.
Route Map (proposed)
- `/` — Landing (QR/PIN input; deep-link handler)
- `/setup` — Profile Setup (name/avatar; skippable)
- `/e/:slug` — Home/Feed (default gallery view + info bar)
- `/e/:slug/tasks` — Task Picker (random/emotion)
- `/e/:slug/tasks/:taskId` — Task Detail (card)
- `/e/:slug/upload` — Upload Picker (camera/library + tagging)
- `/e/:slug/queue` — Upload Queue (progress/retry)
- `/e/:slug/gallery` — Gallery index (alias of Home or dedicated page)
- `/e/:slug/photo/:photoId` — Photo Lightbox (modal over gallery)
- `/e/:slug/achievements` — Achievements (optional)
- `/e/:slug/slideshow` — Slideshow (optional, read-only)
- `/settings` — Settings (language/theme/cache/legal)
- `/legal/:page` — Legal pages (imprint/privacy/terms)
- `*` — NotFound
Guards & Loaders
- `EventGuard` — verifies event token in storage; attempts refresh; otherwise redirects to `/`.
- `PrefetchEvent` — loads event metadata/theme on `:slug` routes.
- `OfflineFallback` — surfaces offline banner and queues mutations.
Suggested Directory Structure
```
apps/guest-pwa/
src/
routes/
index.tsx // router config + guards
pages/
LandingPage.tsx
ProfileSetupPage.tsx
HomePage.tsx
TaskPickerPage.tsx
TaskDetailPage.tsx
UploadPage.tsx
UploadQueuePage.tsx
GalleryPage.tsx
PhotoLightbox.tsx // modal route
AchievementsPage.tsx
SlideshowPage.tsx
SettingsPage.tsx
LegalPage.tsx
NotFoundPage.tsx
components/
Header.tsx
InfoBar.tsx
BottomNav.tsx
QRPinForm.tsx
CTAButtons.tsx
EmotionPickerGrid.tsx
TaskCard.tsx
CameraPicker.tsx // photos only; no video capture
UploadPreviewList.tsx
UploadQueueList.tsx
GalleryMasonry.tsx
PhotoCard.tsx
FiltersBar.tsx
Toast.tsx
stores/
useEventStore.ts // tenant/event token, theme
useProfileStore.ts // name/avatar (local)
useUploadQueue.ts // IndexedDB-backed queue
services/
apiClient.ts // fetch wrapper + trace id
eventsApi.ts // GET event meta
photosApi.ts // list/finalize/like
uploadService.ts // request signed URL, do PUT, finalize; photo only
sw.ts // service worker register helpers
hooks/
useOnline.ts
useA2HS.ts
usePollStats.ts // polls /events/:slug/stats every 10s
usePollGalleryDelta.ts // polls /events/:slug/photos?since=...
i18n/
de.json
en.json
main.tsx
App.tsx
```
Router Sketch (React Router v6)
```tsx
import { createBrowserRouter } from 'react-router-dom';
import EventGuard from './routes/EventGuard';
import PrefetchEvent from './routes/PrefetchEvent';
export const router = createBrowserRouter([
{ path: '/', element: <LandingPage /> },
{ path: '/setup', element: <ProfileSetupPage /> },
{
path: '/e/:slug',
element: (
<EventGuard>
<PrefetchEvent>
<HomeLayout />
</PrefetchEvent>
</EventGuard>
),
children: [
{ index: true, element: <HomePage /> },
{ path: 'tasks', element: <TaskPickerPage /> },
{ path: 'tasks/:taskId', element: <TaskDetailPage /> },
{ path: 'upload', element: <UploadPage /> },
{ path: 'queue', element: <UploadQueuePage /> },
{ path: 'gallery', element: <GalleryPage /> },
{ path: 'photo/:photoId', element: <PhotoLightbox /> },
{ path: 'achievements', element: <AchievementsPage /> },
{ path: 'slideshow', element: <SlideshowPage /> },
],
},
{ path: '/settings', element: <SettingsPage /> },
{ path: '/legal/:page', element: <LegalPage /> },
{ path: '*', element: <NotFoundPage /> },
]);
```
Component Checklist
- Layout
- `Header`, `InfoBar` (X Gäste online • Y Aufgaben gelöst), `BottomNav`, `Toast`.
- Entry
- `QRPinForm` (QR deep link or PIN fallback), `ProfileForm` (name/avatar).
- Home/Feed
- `CTAButtons` (Random Task, Emotion Picker, Quick Photo), `GalleryMasonry`, `FiltersBar`, `PhotoCard`.
- Tasks
- `EmotionPickerGrid`, `TaskCard` (shows duration, group size, actions).
- Capture/Upload (photos only)
- `CameraPicker`, `UploadPreviewList`, `UploadQueueList`.
- Photo View
- `PhotoLightbox` (modal), like/share controls, emotion tags.
- Settings & Legal
- `SettingsPage` sections, `LegalPage` renderer.
State & Data
- TanStack Query for server data (events, photos); optimistic updates for likes.
- Zustand store for local-only state (profile, queue, banners).
- IndexedDB for upload queue; CacheStorage for shell/assets.
- Polling: focus-aware intervals (10s stats, 30s gallery); use document visibility to pause; backoff on failures.
Accessibility & Performance
- Focus management on modal open/close; trap focus.
- Color contrast and minimum tap target sizes (44px).
- Code-split camera/lightbox/slideshow; prefetch next gallery page.
Out of Scope
- Video capture/upload is not supported.

119
docs/prp/07-guest-pwa.md Normal file
View File

@@ -0,0 +1,119 @@
# 07 — Guest PWA
Goal
- Delight guests with a frictionless, installable photo experience that works offline, respects privacy, and requires no account.
Non-Goals (MVP)
- No comments or chat. No facial recognition. No public profiles. No videos.
Personas
- Guest (attendee) — scans QR, uploads photos, browses and likes.
- Host (tenant) — optionally shares event PIN with guests; moderates via Tenant Admin PWA.
Top Journeys
- Join: Scan QR → Open event → Accept terms → Optional PIN → Land on Gallery.
- Upload: Add photos → Review → Submit → Background upload → Success/Retry.
- Browse: Infinite gallery → Filter (emotion/task) → Open photo → Like/Share → Back.
Core Features
- Event access
- Access token embedded in QR/link (short-lived, event-scoped); optional PIN challenge.
- Token refresh if event remains open; server can revoke/close event at any time.
- Offline-first
- App shell + last N gallery items cached; uploads queued with Background Sync.
- Conflict-safe retries; user can pause/cancel queued uploads.
- Capture & upload
- Choose from camera or library; limit file size; show remaining upload cap.
- Client-side resize to sane max (e.g., 2560px longest edge); EXIF stripped client-side if available.
- Assign optional emotion/task before submit; default to “Uncategorized”.
- Gallery
- Masonry grid, lazy-load, pull-to-refresh; open photo lightbox with swipe.
- Like (heart) with optimistic UI; share system sheet (URL to CDN variant).
- Filters: emotion, featured, mine (local-only tag for items uploaded from this device).
- Safety & abuse controls
- Rate limits per device and IP; content-length checks; mime/type sniffing.
- Upload moderation state: pending → approved/hidden; show local status.
- Privacy & legal
- First run shows legal links (imprint/privacy); consent for push if enabled.
- No PII stored; guest name is optional free text and not required by default.
Screens
- Splash/Loading: event lookup + token validation; friendly skeleton.
- Terms & PIN: legal links, optional PIN input; remember choice per event.
- Gallery: grid of approved photos; toolbar with filter, upload, settings.
- Upload Picker: camera/library, selection preview, emotion/task tagging.
- Upload Queue: items with progress, retry, remove; background sync toggle.
- Photo Lightbox: zoom, like, share; show emotion tags.
- Settings: language, theme (system), clear cache, legal pages.
Wireframes
- See wireframes file at docs/wireframes/guest-pwa.md for low-fidelity layouts and flows.
Core Pages (Pflichtseiten)
- Landing / Einstieg
- Purpose: Entry to event via QR/PIN; lowest friction.
- UI: Single input (QR deep link preferred, fallback PIN field) and Join button.
- States: invalid/expired token, event closed, offline (allow PIN entry and queue attempt).
- Profil-Setup (Name/Avatar)
- Purpose: Optional personalization for likes/attribution.
- UI: Name text field, optional avatar picker; one-time before first entry to event.
- Behavior: Skippable; editable later in Settings.
- Startseite (Home/Feed)
- Purpose: Central hub; show event title/subheadline and CTAs.
- Header: Event name + subheadline (e.g., “Dein Fotospiel zur Hochzeit”).
- Info bar: “X Gäste online • Y Aufgaben gelöst”.
- CTAs: „Aufgabe ziehen“ (random), „Wie fühlst du dich?“ (emotion picker), small link “Einfach ein Foto machen”.
- Content: Gallery/Feed with photos + likes.
- Aufgaben-Flow
- Aufgaben-Picker: Choose random task or emotion mood.
- Aufgaben-Detail (Karte): Task text, emoji tag, estimated duration, suggested group size; actions: take photo, new task (same mood), change mood.
- Foto-Interaktion
- Kamera/Upload: Capture or pick; progress + success message on completion; background sync.
- Galerie/Übersicht: Grid/Feed; filters: Neueste, Beliebt, Meine; Like hearts.
- Foto-Detailansicht: Fullscreen; likes/reactions; linked task + (optional) uploader name.
- Motivation & Spiel
- Achievements/Erfolge: Badges (e.g., Erstes Foto, 5 Aufgaben, Beliebtestes Foto); personal progress.
- Optionale Ergänzungen
- Slideshow/Präsentationsmodus: Auto-rotating gallery for TV/Beamer with likes/task overlay.
- Onboarding: 12 “So funktioniert das Spiel” hints the first time.
- Event-Abschluss: “Danke fürs Mitmachen”, summary stats, link/QR to online gallery.
Technical Notes
- Installability: PWA manifest + service worker; prompt A2HS on supported browsers.
- Storage: IndexedDB for queue + cache; `CacheStorage` for shell/assets.
- Background Sync: use Background Sync API when available; fallback to retry on app open.
- Accessibility: large tap targets, high contrast, keyboard support, reduced motion.
- i18n: default `de`, fallback `en`; all strings in locale files; RTL not in MVP.
- Media types: Photos only (no videos) — decision locked for MVP and v1.
- Realtime model: periodic polling (no WebSockets). Home counters every 10s; gallery delta every 30s with exponential backoff when tab hidden or offline.
API Touchpoints
- GET `/api/v1/events/{slug}` — public event metadata (when open) + theme.
- GET `/api/v1/events/{slug}/photos` — paginated gallery (approved only).
- POST `/api/v1/events/{slug}/photos` — signed upload initiation; returns URL + fields.
- POST (S3) — direct upload to object storage; then backend finalize call.
- POST `/api/v1/photos/{id}/like` — idempotent like with device token.
Limits (MVP defaults)
- Max uploads per device per event: 50
- Max file size (after client resize): 6 MB per photo
- Max resolution: 2560px longest edge per photo
Edge Cases
- Token expired/invalid → Show “Event closed/invalid link”; link to retry.
- No connectivity → Queue actions; show badge; retry policy with backoff.
- Storage full → Offer to clear cache or deselect files.
- Permission denied (camera/photos) → Explain and offer system shortcut.
Decisions
- Videos are not supported (capture/upload strictly photos).
Appendix: Page-to-Feature Mapping
- Landing: Access, validation, join; deep-link handling.
- Profile Setup: Local profile persistence; optional avatar assets.
- Home: Header, info bar, CTAs, feed preview.
- Tasks: Picker (random/emotion), detail card actions.
- Capture/Upload: Camera permissions, client resize, queue handling.
- Gallery: Filters, pagination, lightbox.
- Achievements: Local badges (MVP), server-backed later.
- Slideshow (optional): Read-only, auto-advance, idle-safe.

7
docs/prp/08-billing.md Normal file
View File

@@ -0,0 +1,7 @@
# 08 — Billing (MVP: Event Credits)
- Model: one-off purchases that grant event credits; no subscriptions in MVP.
- Tables: `event_purchases`, `event_credits_ledger` (see 04-data-model-migrations.md).
- Providers: Stripe (server-side checkout + webhooks); store receipts deferred.
- Idempotency: purchase intents keyed; ledger writes idempotent; retries safe.
- Limits: enforce `event_credits_balance >= 1` to create an event; ledger decrements on event creation.

View File

@@ -0,0 +1,8 @@
# 09 — Security & Compliance
- Roles: `super_admin`, `tenant_admin`, `member`; guest upload via signed tokens.
- Policies: all tenant-owned models gated; Super Admin bypass via explicit ability.
- Audit: record impersonation and destructive actions with actor, target, reason.
- Logging: structured, no PII; add request/trace IDs; redact secrets.
- GDPR: retention settings per tenant; deletion workflows; legal pages managed via CMS-like resource.
- Rate limits: per-tenant, per-user, per-device; protect upload and admin mutations.

View File

@@ -0,0 +1,7 @@
# 10 — Storage & Media Pipeline
- Storage: S3-compatible bucket with server-side encryption; path pattern `tenants/{tenant_uuid}/events/{event_uuid}/photos/{photo_uuid}/`.
- Variants: original + derived sizes in `variants/` with content hashing for cache-busting.
- Processing: queue jobs for EXIF strip, orientation, resize (multiple sizes), and virus scan; idempotent by UUID.
- Delivery: signed URLs for admin; CDN public for guest gallery variants.
- Deletion: soft-delete metadata, schedule object purge respecting retention policies.

7
docs/prp/11-ops-ci-cd.md Normal file
View File

@@ -0,0 +1,7 @@
# 11 — Ops, Environments, CI/CD
- Environments: local (Docker), staging, production. Feature flags managed in DB.
- CI: PHPStan/Larastan, PHPUnit/Pest, Pint, ESLint/TypeScript, build PWAs.
- Releases: semantic versioning; changelog from PRs; migration notes required.
- Observability: uptime checks, queue health (Horizon), error tracking.
- Mobile: fastlane lanes for Capacitor iOS; TWA build pipeline for Android.

6
docs/prp/12-i18n.md Normal file
View File

@@ -0,0 +1,6 @@
# 12 — Internationalization
- Default locale `de`, fallback `en`.
- Translatable fields stored as JSON (`name`, `description`, `title`, `body_markdown`).
- PWA copy managed via i18n files; glossary maintained centrally.
- Date/number formatting per locale; right-to-left not in MVP.

7
docs/prp/99-glossary.md Normal file
View File

@@ -0,0 +1,7 @@
# 99 — Glossary
- Tenant: Paying customer account (e.g., a couple, company).
- Event: A photo session context under a tenant (wedding, party, etc.).
- Guest PWA: Public attendee interface, anonymous by default.
- Tenant Admin PWA: Store-ready management app for tenants.
- Super Admin: Platform-level operator; Filament web panel only.

24
docs/prp/README.md Normal file
View File

@@ -0,0 +1,24 @@
# Event Photo Platform — Product Requirement Plan (PRP)
Status: Active (split version)
Date: 2025-09-08
This directory supersedes the legacy `fotospiel_prp.md`. Content is split into small, reviewable documents. See ADR-0006 for the Tenant Admin PWA decision.
- 01-architecture.md — System overview and components
- 02-tenancy.md — Multi-tenant model and enforcement
- 03-api.md — API-first contract and auth
- 04-data-model-migrations.md — Portable migration intent (Schema builder)
- 05-admin-superadmin.md — Super Admin web console (Filament)
- 06-tenant-admin-pwa.md — Store-ready Tenant Admin PWA
- 07-guest-pwa.md — Guest (event attendee) PWA
- 08-billing.md — Event credits MVP, ledger, purchases
- 09-security-compliance.md — RBAC, audit, GDPR
- 10-storage-media-pipeline.md — Object storage, processing, CDN
- 11-ops-ci-cd.md — CI, releases, environments
- 12-i18n.md — Languages, locales, copy strategy
- 99-glossary.md — Terms and roles
Notes
- The original `fotospiel_prp.md` remains as historical reference and will not be edited further.
- Any divergence should be resolved here; update ADRs when major decisions change.

View File

@@ -0,0 +1,185 @@
# Guest PWA — Wireframes (Low-Fi)
Navigation Flow
```mermaid
flowchart TD
A[Scan QR / Open Link] --> B[Token Validate]
B -->|ok| C[Terms / Optional PIN]
B -->|invalid| Z[Error: Event Closed]
C --> D[Gallery]
D --> E[Upload Picker]
E --> F[Upload Queue]
D --> G[Photo Lightbox]
D --> H[Filters]
D --> I[Settings]
F --> D
G --> D
```
Gallery (mobile)
```
+--------------------------------------------------+
| Toolbar: [Scan?] [Filter] [Upload] [Menu] |
+--------------------------------------------------+
| ▢ ▢▢ ▢ ▢▢▢ ▢▢ ▢ ▢▢ Masonry grid |
| ▢▢ ▢ ▢▢ ▢ ▢ ▢▢ ▢ |
| ▢ ▢▢ ▢ ▢▢ ▢▢ ▢ ▢ |
+--------------------------------------------------+
| Pull to refresh ▲ new items |
+--------------------------------------------------+
```
Upload Queue (mobile)
```
+---------------- Uploads -------------------------+
| [ ] IMG_1234.jpg 75% ||||||||||\____ [X] |
| [!] IMG_1235.jpg retry network error [↻] |
| [ ] IMG_0002.jpg queued |
| |
| Background sync: (On) • Pause • Clear all |
+--------------------------------------------------+
```
Lightbox
```
+------------------ Photo -------------------------+
| < back ❤ 128 likes Share ⤴ |
| |
| [ image ] |
| |
| Emotion: 🎉 Party Task: Jump shot |
+--------------------------------------------------+
```
States & Messages
- Empty gallery: “Be the first to upload a photo!” + CTA button.
- Event closed: explain and provide contact hint.
- Offline: banner “Youre offline — viewing cached photos. Uploads will resume automatically.”
Notes
- Keep wireframes platform-agnostic; exact visuals handled by design system.
- Photos only; video capture/upload is not supported.
Additional Screens (from PWA_Wireframes.txt)
Landing / Einstieg
```
+---------------- Landing -------------------------+
| Willkommen bei Fotochallenge 🎉 |
| |
| [ QR/PIN eingeben ] [ Event beitreten ] |
| |
| Fehler: Ungültiger Code / Event geschlossen |
+-------------------------------------------------+
```
Profil-Setup
```
+---------------- Profil --------------------------+
| Profil erstellen |
| [ Dein Name ] |
| ( ) Avatar wählen (Rund-Icons) |
| [ Starten ] [ Überspringen ] |
+--------------------------------------------------+
```
Home / Feed (mit Info-Bar)
```
+---------------- Home ----------------------------+
| Eventtitel |
| Dein Fotospiel zur Hochzeit |
| ———————————————————————————————— |
| X Gäste online ✅ Y Aufgaben gelöst |
| |
| [ Aufgabe ziehen ] [ Wie fühlst du dich? ] |
| (klein) Einfach ein Foto machen |
| |
| [Masonry Feed …] |
+--------------------------------------------------+
```
Aufgaben-Picker (Emotionen)
```
+-------------- Stimmung wählen -------------------+
| Albern Energetisch Herzlich Cool Gemeinsam |
| Kreativ Ruhig Mutig |
| [ Aufgabe anzeigen ] |
+--------------------------------------------------+
```
Aufgaben-Detail (Karte)
```
+---------------- Aufgabe -------------------------+
| Stimmung: Albern [X] |
| Aufgabe: Selfie in falsche Richtung |
| Dauer: <1 Min Gruppe: 25 |
| [ Los gehts ] [ Neue Aufgabe ] [ Stimmung ] |
+--------------------------------------------------+
```
Kamera / Upload
```
+--------------- Kamera/Upload --------------------+
| [ Kamera öffnen ] [ Foto hochladen ] |
| ✔ Foto erfolgreich hochgeladen |
| [ Galerie ansehen ] |
+--------------------------------------------------+
```
Galerie / Filter
```
+---------------- Galerie -------------------------+
| [ Neueste ] [ Beliebt ] [ Meine ] |
| ❤12 [Foto] |
| ❤️ 5 [Foto] |
| ❤21 [Foto] |
+--------------------------------------------------+
```
Foto-Detailansicht
```
+--------------- Foto-Detail ----------------------+
| [X] |
| [ Vollbild-Foto ] |
| ❤️ 23 🎉 4 Reaktionen |
| Aufgabe: Gruppenfoto (>=5) |
| Hochgeladen von: Lisa |
| [ ❤️ Like ] |
+--------------------------------------------------+
```
Erfolge / Achievements
```
+--------------- Erfolge --------------------------+
| Erstes Foto 5 Aufgaben Beliebtestes Foto |
| Gruppenfoto-König |
| Fortschritt: [██████▁▁▁] (6/10) |
+--------------------------------------------------+
```
Optionale: Slideshow & Abschluss
```
+------------- Slideshow (optional) ---------------+
| Vollbild-Wechsel alle 5s, Likes & Aufgabe-Overlay|
+--------------------------------------------------+
+--------------- Event-Abschluss ------------------+
| Danke fürs Mitmachen! |
| Fotos: 120 Aufgaben: 45 Likes: 900 |
| QR/Link zur Online-Galerie |
+--------------------------------------------------+
```