feat(superadmin): migrate internal docs from docusaurus to guava kb
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-07 09:58:39 +01:00
parent 1d2242fb4d
commit fb45d1f6ab
77 changed files with 3813 additions and 18636 deletions

View File

@@ -143,6 +143,7 @@ class User extends Authenticatable implements FilamentHasTenants, FilamentUser,
return match ($panel->getId()) {
'superadmin' => $this->isSuperAdmin(),
'superadmin-kb' => $this->isSuperAdmin(),
'admin' => $this->role === 'tenant_admin',
default => false,
};

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Providers\Filament;
use App\Filament\Pages\Auth\Login;
use App\Filament\SuperAdmin\Pages\Auth\EditProfile;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Panel;
use Filament\PanelProvider;
use Guava\FilamentKnowledgeBase\Plugins\KnowledgeBasePlugin;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class SuperAdminKbPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('superadmin-kb')
->path('super-admin/docs')
->viteTheme('resources/css/filament/superadmin-kb/theme.css')
->plugins([
KnowledgeBasePlugin::make(base_path('docs/superadmin-kb')),
])
->login(Login::class)
->profile(EditProfile::class, isSimple: false)
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->authGuard('super_admin');
}
}

View File

@@ -17,7 +17,9 @@ use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Support\Icons\Heroicon;
use Filament\View\PanelsRenderHook;
use Filament\Widgets;
use Guava\FilamentKnowledgeBase\Plugins\KnowledgeBaseCompanionPlugin;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
@@ -35,6 +37,7 @@ class SuperAdminPanelProvider extends PanelProvider
->default()
->id('superadmin')
->path('super-admin')
->viteTheme('resources/css/filament/superadmin/theme.css')
->topNavigation()
->colors([
'primary' => Color::Pink,
@@ -45,6 +48,10 @@ class SuperAdminPanelProvider extends PanelProvider
->navigationLabel('Log Viewer')
->navigationIcon(Heroicon::OutlinedDocumentText)
->navigationSort(20),
KnowledgeBaseCompanionPlugin::make()
->knowledgeBasePanelId('superadmin-kb')
->helpMenuRenderHook(PanelsRenderHook::TOPBAR_START)
->disableKnowledgeBasePanelButton(),
])
->navigationGroups([
NavigationGroup::make()

View File

@@ -5,6 +5,7 @@ return [
App\Providers\AuthServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\Filament\SuperAdminPanelProvider::class,
App\Providers\Filament\SuperAdminKbPanelProvider::class,
App\Providers\HorizonServiceProvider::class,
Stephenjude\FilamentBlog\FilamentBlogServiceProvider::class,
];

View File

@@ -11,6 +11,7 @@
"filament/filament": "~4.0",
"firebase/php-jwt": "^6.11",
"gboquizosanchez/filament-log-viewer": "*",
"guava/filament-knowledge-base": "^2.1",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/horizon": "^5.37",

333
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a4956012b0e374c8f74b61a892e6b984",
"content-hash": "710c9b2af62e3768484530129f86a63d",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -336,6 +336,60 @@
],
"time": "2025-11-24T14:40:29+00:00"
},
{
"name": "calebporzio/sushi",
"version": "v2.5.3",
"source": {
"type": "git",
"url": "https://github.com/calebporzio/sushi.git",
"reference": "bf184973f943216b2aaa8dbc79631ea806038bb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/calebporzio/sushi/zipball/bf184973f943216b2aaa8dbc79631ea806038bb1",
"reference": "bf184973f943216b2aaa8dbc79631ea806038bb1",
"shasum": ""
},
"require": {
"ext-pdo_sqlite": "*",
"ext-sqlite3": "*",
"illuminate/database": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"illuminate/support": "^5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"php": "^7.1.3|^8.0"
},
"require-dev": {
"doctrine/dbal": "^2.9 || ^3.1.4",
"orchestra/testbench": "3.8.* || 3.9.* || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
"phpunit/phpunit": "^7.5 || ^8.4 || ^9.0 || ^10.0 || ^11.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Sushi\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Caleb Porzio",
"email": "calebporzio@gmail.com"
}
],
"description": "Eloquent's missing \"array\" driver.",
"support": {
"source": "https://github.com/calebporzio/sushi/tree/v2.5.3"
},
"funding": [
{
"url": "https://github.com/calebporzio",
"type": "github"
}
],
"time": "2025-02-13T21:03:57+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -1909,6 +1963,90 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "guava/filament-knowledge-base",
"version": "2.1.2",
"source": {
"type": "git",
"url": "https://github.com/GuavaCZ/filament-knowledge-base.git",
"reference": "cf62a0e526407b80c4fbcd0be6a6af508460d53d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GuavaCZ/filament-knowledge-base/zipball/cf62a0e526407b80c4fbcd0be6a6af508460d53d",
"reference": "cf62a0e526407b80c4fbcd0be6a6af508460d53d",
"shasum": ""
},
"require": {
"calebporzio/sushi": "^2.5",
"filament/filament": "^4.0",
"illuminate/contracts": "^11.0|^12.0",
"league/commonmark": "^2.4",
"n0sz/commonmark-marker-extension": "^1.0",
"phiki/phiki": "^2.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.14.0",
"spatie/php-structure-discoverer": "^2.1",
"symfony/yaml": "^7.0"
},
"require-dev": {
"larastan/larastan": "^3.0",
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.0",
"orchestra/testbench": "^9.0|^10.0",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-arch": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"KnowledgeBase": "Guava\\FilamentKnowledgeBase\\Facades\\KnowledgeBase"
},
"providers": [
"Guava\\FilamentKnowledgeBase\\KnowledgeBaseServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Guava\\FilamentKnowledgeBase\\": "src/",
"Guava\\FilamentKnowledgeBase\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Lukas Frey",
"email": "lukas.frey@guava.cz",
"role": "Developer"
}
],
"description": "A filament plugin that adds a knowledge base and help to your filament panel(s).",
"homepage": "https://github.com/guava/filament-knowledge-base",
"keywords": [
"Guava",
"filament-knowledge-base",
"laravel"
],
"support": {
"issues": "https://github.com/GuavaCZ/filament-knowledge-base/issues",
"source": "https://github.com/GuavaCZ/filament-knowledge-base/tree/2.1.2"
},
"funding": [
{
"url": "https://github.com/GuavaCZ",
"type": "github"
}
],
"time": "2026-01-25T07:48:36+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.10.0",
@@ -4321,6 +4459,49 @@
],
"time": "2026-01-02T08:56:05+00:00"
},
{
"name": "n0sz/commonmark-marker-extension",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/noah1400/commonmark-marker-extension.git",
"reference": "acb34415b84f28cc360123eba53fbb4cc4418672"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/noah1400/commonmark-marker-extension/zipball/acb34415b84f28cc360123eba53fbb4cc4418672",
"reference": "acb34415b84f28cc360123eba53fbb4cc4418672",
"shasum": ""
},
"require": {
"league/commonmark": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^9.5.25"
},
"type": "commonmark-extension",
"autoload": {
"psr-4": {
"N0sz\\CommonMark\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A marker extension for CommonMark PHP implementation",
"homepage": "https://github.com/noah1400/commonmark-marker-extension",
"keywords": [
"commonmark",
"markdown",
"parser"
],
"support": {
"issues": "https://github.com/noah1400/commonmark-marker-extension/issues",
"source": "https://github.com/noah1400/commonmark-marker-extension/tree/1.0.1"
},
"time": "2022-09-27T20:19:00+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.11.0",
@@ -5237,6 +5418,77 @@
},
"time": "2022-03-07T12:52:04+00:00"
},
{
"name": "phiki/phiki",
"version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phikiphp/phiki.git",
"reference": "b16020573e9f4ad3c9d230c17ed4c84c15356e28"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phikiphp/phiki/zipball/b16020573e9f4ad3c9d230c17ed4c84c15356e28",
"reference": "b16020573e9f4ad3c9d230c17ed4c84c15356e28",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/commonmark": "^2.5.3",
"php": "^8.2",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"illuminate/support": "^11.45",
"laravel/pint": "^1.18.1",
"orchestra/testbench": "^9.15",
"pestphp/pest": "^3.5.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.0",
"symfony/var-dumper": "^7.1.6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Phiki\\Adapters\\Laravel\\PhikiServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Phiki\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ryan Chandler",
"email": "support@ryangjchandler.co.uk",
"homepage": "https://ryangjchandler.co.uk",
"role": "Developer"
}
],
"description": "Syntax highlighting using TextMate grammars in PHP.",
"support": {
"issues": "https://github.com/phikiphp/phiki/issues",
"source": "https://github.com/phikiphp/phiki/tree/v2.1.0"
},
"funding": [
{
"url": "https://github.com/sponsors/ryangjchandler",
"type": "github"
},
{
"url": "https://buymeacoffee.com/ryangjchandler",
"type": "other"
}
],
"time": "2026-01-20T21:26:48+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@@ -7024,6 +7276,85 @@
],
"time": "2025-11-24T15:57:48+00:00"
},
{
"name": "spatie/php-structure-discoverer",
"version": "2.3.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/php-structure-discoverer.git",
"reference": "552a5b974a9853a32e5677a66e85ae615a96a90b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/552a5b974a9853a32e5677a66e85ae615a96a90b",
"reference": "552a5b974a9853a32e5677a66e85ae615a96a90b",
"shasum": ""
},
"require": {
"illuminate/collections": "^11.0|^12.0",
"php": "^8.3",
"spatie/laravel-package-tools": "^1.92.7",
"symfony/finder": "^6.0|^7.3.5|^8.0"
},
"require-dev": {
"amphp/parallel": "^2.3.2",
"illuminate/console": "^11.0|^12.0",
"nunomaduro/collision": "^7.0|^8.8.3",
"orchestra/testbench": "^9.5|^10.8",
"pestphp/pest": "^3.8|^4.0",
"pestphp/pest-plugin-laravel": "^3.2|^4.0",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"spatie/laravel-ray": "^1.43.1"
},
"suggest": {
"amphp/parallel": "When you want to use the Parallel discover worker"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\StructureDiscoverer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"role": "Developer"
}
],
"description": "Automatically discover structures within your PHP application",
"homepage": "https://github.com/spatie/php-structure-discoverer",
"keywords": [
"discover",
"laravel",
"php",
"php-structure-discoverer"
],
"support": {
"issues": "https://github.com/spatie/php-structure-discoverer/issues",
"source": "https://github.com/spatie/php-structure-discoverer/tree/2.3.3"
},
"funding": [
{
"url": "https://github.com/LaravelAutoDiscoverer",
"type": "github"
}
],
"time": "2025-11-24T16:41:01+00:00"
},
{
"name": "spatie/shiki-php",
"version": "2.3.2",

View File

@@ -84,26 +84,6 @@ services:
start_period: 30s
restart: unless-stopped
docs-build:
image: node:22
working_dir: /var/www/html/docs/site
command:
- bash
- -lc
- npm install -g npm@11 && npm ci && npm run build
volumes:
- app-code:/var/www/html
env_file:
- path: .env
environment:
VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}
VITE_SENTRY_ENV: ${VITE_SENTRY_ENV:-}
VITE_SENTRY_RELEASE: ${VITE_SENTRY_RELEASE:-}
depends_on:
app:
condition: service_healthy
restart: "no"
photobooth-uploader-build:
image: mcr.microsoft.com/dotnet/sdk:10.0
working_dir: /var/www/html
@@ -144,15 +124,12 @@ services:
web:
image: nginx:1.27-alpine
depends_on:
docs-build:
condition: service_completed_successfully
app:
condition: service_healthy
volumes:
- app-code:/var/www/html:ro
- app-storage:/var/www/html/storage:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/.htpasswd-docs:/etc/nginx/.htpasswd-docs:ro
env_file:
- path: .env
networks:

View File

@@ -34,8 +34,6 @@ services:
volumes:
- app-code:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./docs/site/build:/var/www/docs-site:ro
- ./docker/nginx/.htpasswd-docs:/etc/nginx/.htpasswd-docs:ro
ports:
- "${APP_HTTP_PORT:-8080}:80"
restart: unless-stopped

View File

@@ -1,2 +0,0 @@
# Default development credentials (docs / changeme). Replace this file in production!
docs:$apr1$k78HFdNi$trMRjICSTj5cKRWqJOEcw1

View File

@@ -34,18 +34,6 @@ server {
fastcgi_buffers 8 16k;
}
location ^~ /internal-docs/ {
# Docusaurus static build lives in app-code volume at /var/www/html/docs/site/build
alias /var/www/html/docs/site/build/;
index index.html;
try_files $uri $uri/ /internal-docs/index.html;
auth_basic "Fotospiel Internal Docs";
auth_basic_user_file /etc/nginx/.htpasswd-docs;
add_header Cache-Control "private, no-store";
}
location ~* \.(jpg|jpeg|gif|png|css|js|ico|svg|webp|woff2?)$ {
expires 30d;
access_log off;

View File

@@ -1,6 +1,6 @@
# Archive
Legacy documents that are no longer part of the active Fotospiel specification live here to keep the primary `/docs` tree focused. Nothing inside this folder should be referenced by the Docusaurus site unless explicitly linked.
Legacy documents that are no longer part of the active Fotospiel specification live here to keep the primary `/docs` tree focused. Nothing inside this folder should be referenced by active internal documentation unless explicitly linked.
Current contents came from the repository root and include:

View File

@@ -26,7 +26,7 @@ docs/help/
├── guest/
└── templates/
```
- English and German live under their respective locale folders, which keeps slugs unique for the Docusaurus build (`/help/en/admin/...`, `/help/de/guest/...`).
- English and German live under their respective locale folders, which keeps slugs unique for help routing (`/help/en/admin/...`, `/help/de/guest/...`).
- Inside each locale folder, article filenames match their slug (e.g., `post-event-wrapup.md`). Paired locales keep the same slug and metadata.
- Every article includes the YAML front matter shown below so the apps and Filament know how to render status, owners, etc.

View File

@@ -117,11 +117,10 @@ Because the app image keeps the authoritative copy of the code, each container r
- Configure backups for the `storage` directories and database dumps.
- Hook into your observability stack (e.g., ship container logs to Loki or ELK).
## 9. Internal docs publishing
## 9. Internal docs access
- Build the static docs site locally or in CI via `./scripts/build-docs-site.sh` (runs `npm ci && npm run build` in `docs/site/` and outputs to `docs/site/build`).
- The Nginx container mounts that build directory and serves it at `/internal-docs/`. History fallback is already configured in `docker/nginx/default.conf`.
- Access is protected with HTTP Basic Auth. Update `docker/nginx/.htpasswd-docs` with real credentials (`htpasswd -c docker/nginx/.htpasswd-docs <user>`). The repo ships with a weak default for local use—never reuse it in production.
- Optionally run the build script inside your CI pipeline before deploying so the static output is always up to date. A typical GitHub Actions job would simply call the script after checkout and upload `docs/site/build` as an artifact or rsync it to the server.
- Internal operations/admin documentation is now delivered in Filament through the Guava Knowledge Base plugin.
- Access path: `/super-admin/docs` (same auth guard as SuperAdmin).
- No separate static docs build/container is required.
With the provided configuration you can bootstrap a consistent Docker-based deployment across environments while keeping queue workers, migrations, and asset builds manageable. Adjust service definitions as needed for staging vs. production.

View File

@@ -121,9 +121,8 @@ Only SuperAdmins should have access to these widgets. If you rotate the API key,
With this setup the Fotospiel team can manage deployments, restarts, and metrics centrally through Dokploy while Laravels scheduler and workers continue to run within the same infrastructure.
## 8. Internal docs publishing in Dokploy
## 8. Internal docs in Dokploy
- Build the static docs site during CI/CD by running `./scripts/build-docs-site.sh`. Upload the resulting `docs/site/build` directory as part of the deployment artifact or copy it into the Dokploy server before redeploying the stack.
- `docker-compose.dokploy.yml` mounts that directory into the Nginx container and exposes it at `/internal-docs/`, protected by HTTP Basic Auth.
- Update `docker/nginx/.htpasswd-docs` with production credentials (use `htpasswd -c docker/nginx/.htpasswd-docs <user>` locally, then redeploy). The repository ships with a weak default only for development—always override it in Dokploy.
- If Dokploy builds the image itself, add a post-build hook or automation step that runs the docs build and copies it into the shared storage volume before restarting the `web` service.
- Internal operations/admin documentation is served from the SuperAdmin panel itself via the Guava Knowledge Base.
- Access path: `/super-admin/docs` with normal SuperAdmin authentication (`super_admin` guard).
- No dedicated documentation build container or static docs artifact is required in Dokploy.

View File

@@ -78,7 +78,7 @@ If you skip source map upload, events still work; stack traces will be minified.
## 4) Docker/Dokploy wiring
- `docker-compose.dokploy.yml` now forwards `SENTRY_*` and `VITE_SENTRY_*` to `app`, workers, scheduler, and docs-build.
- `docker-compose.dokploy.yml` now forwards `SENTRY_*` and `VITE_SENTRY_*` to `app`, workers, scheduler, and build-time asset steps.
- Ensure the build container that runs `npm run build` sees `VITE_SENTRY_*` (set in Dokploy env UI or `.env` loaded by Dokploy).
- Redeploy stack after setting envs; no other container changes needed.

View File

@@ -115,7 +115,7 @@ Für OnCall/PlatformOps sollten mindestens folgende Zugänge eingerichtet
Bei einem Incident sollte die OnCallPerson immer vom **Betriebshandbuch** aus denken:
- Einstieg über `docs/ops/operations-manual.md` (DocusaurusStartseite).
- Einstieg über `docs/ops/operations-manual.md` (Knowledge-Base-Startseite im SuperAdmin-Panel).
- Je nach Symptome:
- **API-/Frontend-Probleme** → PublicAPIPlaybook (`ops/deployment/public-api-incident-playbook.md`), ggf. Marketing/GuestPWASpezifikationen in `docs/prp/` (in PRP, nicht im OpsBereich).
- **Upload/Storage-Probleme**`ops/media-storage-spec.md`, `ops/guest-notification-ops.md`.

View File

@@ -16,7 +16,7 @@ Ziel ist, dass du von hier aus schnell zu den relevanten Runbooks und Referenzen
- **Systemlandschaft (HighLevel)**
- Laravel App + Nginx + Redis + MySQL.
- AsyncPipeline: Queues (`default`, `media-storage`, `media-security`, `notifications`) und Horizon.
- Satelliten: PhotoboothFTP + ControlService, DocsSite (`/internal-docs`), Monitoring/Dokploy.
- Satelliten: PhotoboothFTP + ControlService, SuperAdmin Knowledge Base (`/super-admin/docs`), Monitoring/Dokploy.
- Externe Dienste: Lemon Squeezy (Billing), RevenueCat (MobileAbos), EMail Provider, Logging/Monitoring (Loki/Grafana o.ä.).
> TODO: Ergänze ein Architekturdiagramm aus Sicht des Betriebs (z.B. als PNG oder PlantUML) und verlinke es hier.

View File

@@ -1,4 +0,0 @@
# Local node dependencies and build artifacts for the docs site
node_modules
build
.docusaurus

View File

@@ -1,31 +0,0 @@
# Fotospiel Docs Site
This directory hosts a standalone [Docusaurus](https://docusaurus.io/) project that renders everything inside the main `/docs` tree as a browsable internal website. Keeping the static-site tooling here isolates all Node dependencies from the Laravel/Vite application.
## Structure
- `../` — existing Markdown sources (PRP, ops runbooks, etc.). These stay untouched.
- `./package.json` — dependencies and scripts for the docs site only.
- `./docusaurus.config.js` — points the docs plugin at `path: '../'` and excludes this `site/` directory.
- `./sidebars.js` — auto-generates the sidebar from the folder hierarchy.
- `./src/css/custom.css` — brand overrides for the default theme.
## Usage
```bash
cd docs/site
npm install
npm run start # Dev server at http://localhost:3100
npm run build # Outputs to docs/site/build
npm run serve # Serves built assets for preview
```
Because `routeBasePath` is `/`, the docs front page is the PRP index (or whichever document you place at `docs/README.md`). Update nav/footer links in `docusaurus.config.js` as needed.
## Deployment
1. `npm run build` creates the static site under `docs/site/build`.
2. Publish that directory to your static host (S3 + CloudFront, Dokploy static app, etc.).
3. Automate via CI by running installs/builds only inside this folder so the main app pipeline remains unchanged.
If you add new Markdown files anywhere under `/docs`, they automatically appear in the sidebar. To hide files, add ignore patterns to `include/exclude` in `docusaurus.config.js`.

View File

@@ -1,97 +0,0 @@
// @ts-check
const path = require('path');
const { themes } = require('prism-react-renderer');
const lightCodeTheme = themes.github;
const darkCodeTheme = themes.dracula;
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Fotospiel Ops & Product Docs',
tagline: 'Single source of truth for the platform',
url: 'https://fotospiel.app',
baseUrl: '/internal-docs/',
favicon: 'img/favicon.ico',
organizationName: 'fotospiel',
projectName: 'fotospiel-docs-site',
onBrokenLinks: 'warn',
trailingSlash: false,
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
markdown: {
hooks: {
onBrokenMarkdownLinks: 'warn',
},
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
path: path.resolve(__dirname, '..'),
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
include: ['**/*.md', '**/*.mdx'],
exclude: ['site/**', 'help/**', 'agents/**', 'content/**', 'archive/**', '**/_drafts/**'],
editUrl: undefined,
showLastUpdateAuthor: false,
showLastUpdateTime: false,
},
blog: false,
pages: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themes: [],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: 'Fotospiel Docs',
items: [
{
type: 'docSidebar',
sidebarId: 'docsSidebar',
position: 'left',
label: 'Documentation',
},
{
href: 'https://github.com/fotospiel',
label: 'Git',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Docs',
items: [
{
label: 'Architecture PRP',
to: '/',
},
{
label: 'Ops Playbooks',
to: '/ops',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Fotospiel. Internal use only.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;

18268
docs/site/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"name": "fotospiel-docs-site",
"private": true,
"version": "0.1.0",
"description": "Docusaurus wrapper for the Fotospiel internal documentation.",
"scripts": {
"start": "docusaurus start",
"dev": "docusaurus start --host 0.0.0.0 --port 3100",
"build": "docusaurus build",
"serve": "docusaurus serve",
"clean": "rimraf build .docusaurus"
},
"dependencies": {
"@docusaurus/core": "^3.4.0",
"@docusaurus/preset-classic": "^3.4.0",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"rimraf": "^5.0.7"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -1,114 +0,0 @@
/**
* Sidebar configuration for the Fotospiel docs site.
*/
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
docsSidebar: [
// Ops first: Betriebshandbuch + alle Runbooks
{
type: 'category',
label: 'Ops & Betrieb',
collapsed: false,
items: [
{
type: 'category',
label: 'Grundlagen',
collapsed: false,
items: [
'ops/operations-manual',
'ops/oncall-roles',
'ops/oncall-cheatsheet',
'ops/support-escalation-guide',
],
},
{
type: 'category',
label: 'Incidents & DR',
items: [
'ops/incidents-major',
'ops/backup-restore',
'ops/dr-tenant-event-restore',
'ops/dr-storage-issues',
],
},
{
type: 'category',
label: 'Medien & Upload',
items: [
'ops/media-storage-spec',
'ops/guest-notification-ops',
'ops/queue-workers',
'ops/howto-guest-upload-failing',
'ops/howto-photobooth-no-photos',
],
},
{
type: 'category',
label: 'Photobooth',
items: [
'ops/photobooth/README',
'ops/photobooth/control_service',
'ops/photobooth/ops_playbook',
],
},
{
type: 'category',
label: 'Billing',
items: [
'ops/billing-ops',
'ops/howto-tenant-package-not-active',
],
},
{
type: 'category',
label: 'DSGVO & Compliance',
items: [
'ops/compliance-dsgvo-ops',
'ops/howto-dsgvo-delete-photo',
'ops/howto-tenant-full-export',
],
},
{
type: 'category',
label: 'Deployment',
items: [
'ops/deployment/docker',
'ops/deployment/dokploy',
'ops/deployment/lokale-podman-adressen',
'ops/deployment/public-api-incident-playbook',
],
},
{
type: 'category',
label: 'Releases & Tests',
items: [
'ops/releases',
'testing/e2e',
],
},
{
type: 'category',
label: 'Monitoring & Diagramme',
items: [
'ops/monitoring-observability',
'ops/monitoring-glitchtip',
'ops/diagrams',
],
},
],
},
// Testing / Qualität
{
type: 'category',
label: 'Testing',
collapsed: true,
items: ['testing/e2e'],
},
],
};
module.exports = sidebars;

View File

@@ -1,18 +0,0 @@
:root {
--ifm-color-primary: #ff5a5f;
--ifm-color-primary-dark: #e44f54;
--ifm-color-primary-darker: #cc4449;
--ifm-color-primary-darkest: #b23a3f;
--ifm-color-primary-light: #ff7376;
--ifm-color-primary-lighter: #ff8c8f;
--ifm-color-primary-lightest: #ffa5a8;
--ifm-code-font-size: 95%;
}
.navbar__brand {
font-weight: 600;
}
.footer--dark {
background-color: #1b1f23;
}

View File

@@ -0,0 +1,14 @@
# Operations Hub
This section consolidates everything platform operators need: deployment guides, worker management, storage specs, photobooth ingest, and response playbooks. Use it as the entry point when publishing the docs site.
## Structure
- `deployment/` — Docker, Dokploy, and incident playbooks previously under `docs/deployment/`.
- `photobooth/` — FTP ingest service docs and ops playbooks.
- `media-storage-spec.md` — Upload/archival flow overview.
- `guest-notification-ops.md` — Push notification queue monitoring.
- `queue-workers.md` — Worker container instructions referencing scripts in `/scripts/`.
- `ai-magic-edits-ops.md` — AI Magic Edits Betrieb, Entitlements, Monitoring und Incident-Playbooks.
Future additions (e.g., escalations, on-call checklists, Terraform notes) should live here as well so all ops content remains in one location.

View File

@@ -0,0 +1,6 @@
---
title: Grundlagen
type: group
order: 10
icon: heroicon-o-book-open
---

View File

@@ -0,0 +1,167 @@
---
title: Betriebshandbuch & Ops-Startseite
---
Willkommen im Betriebshandbuch von Fotospiel. Dieses Dokument ist der Einstiegspunkt für alle, die die Plattform betreiben: InfrastrukturOps, OnCall, Support mit erweiterten Rechten und ProduktOwner, die Auswirkungen von Änderungen verstehen möchten.
Ziel ist, dass du von hier aus schnell zu den relevanten Runbooks und Referenzen springen kannst.
## 1. Systemübersicht & Verantwortlichkeiten
- **Rollen & Verantwortlichkeiten**
- Wer ist für welche Ebene zuständig? (AppVerfügbarkeit, Infrastruktur, Sicherheit, Abrechnung, Support.)
- Empfehlung: definiere mindestens _OnCall_, _PlattformOps_ und _Support (2nd Level)_ als feste Rollen diese Seite ist für alle drei.
- **Systemlandschaft (HighLevel)**
- Laravel App + Nginx + Redis + MySQL.
- AsyncPipeline: Queues (`default`, `media-storage`, `media-security`, `notifications`) und Horizon.
- Satelliten: PhotoboothFTP + ControlService, SuperAdmin Knowledge Base (`/super-admin/docs`), Monitoring/Dokploy.
- Externe Dienste: Lemon Squeezy (Billing), RevenueCat (MobileAbos), EMail Provider, Logging/Monitoring (Loki/Grafana o.ä.).
> TODO: Ergänze ein Architekturdiagramm aus Sicht des Betriebs (z.B. als PNG oder PlantUML) und verlinke es hier.
## 1.1 SuperadminKontrollfläche & ZugriffsMatrix
Die SuperadminKonsole ist für operative Kontrolle und Eskalation gedacht nicht für tägliche TenantArbeit. Ziel ist eine minimale, aber vollständige Kontrollfläche.
**Minimaler Control Surface (Superadmin)**
- **TenantLifecycle & Limits:** Aktivieren/Sperren, GracePeriode, Löschung/Anonymisierung, Limits (Fotos/Event, Storage), AuditTimeline.
- **Commercial & Billing:** Pakete/Addons, TenantPakete, Käufe/History, Gutscheine/Coupons.
- **EventOversight:** Events/Fotos global, ModerationsQueues, TenantFeedback.
- **Plattform & Compliance:** Legal Pages, Datenexporte, AuditLog.
- **Infra & Storage:** Storage Targets, Photobooth Settings, Deployments/Logs.
**ZugriffsMatrix (Soll)**
| Bereich | Superadmin | TenantAdmin | Gast |
| --- | --- | --- | --- |
| TenantLifecycle & Limits | RW | R (own) | |
| TenantPakete & Billing | RW | R (own) | |
| Events/Photos (global) | RW | RW (own) | R/W (event scope) |
| Moderation/Feedback | RW | RW (own) | |
| Tasks/Emotions/EventTypes | RW | RW (own) | R (event scope) |
| Users (Platform) | RW | R (own) | |
| Legal/Content | RW | R | R (public) |
| Storage/Photobooth/Infra | RW | R | |
| AuditLog (AdminAktionen) | R | | |
**NichtZiele**
- Superadmin ersetzt keine TenantAdmins für Tagesgeschäft, nur Eskalation.
- Kein zusätzliches Tracking/PIILogging ohne PrivacyUpdate.
- Keine InfrastrukturMutation ohne explizite Freigabe.
## 1.2 SuperadminRoadmap (geplante Seiten & Zweck)
Diese Seiten sollen praktische Steuerung über TenantAdmins und die GuestExperience geben, ohne ins Tagesgeschäft abzurutschen.
- **ModerationQueue (GuestContent):** Flagged Fotos/Feedback sammeln, bulk hide/delete/resolve, sauberes Audit.
- **GuestPolicySettings:** DefaultToggles (Downloads/Sharing), RateLimits, RetentionDefaults, damit neue Events konsistent starten.
- **OpsHealthDashboard:** QueueBacklog, failed jobs, StorageSchwellen, UploadPipelineHealth auf einen Blick.
- **ComplianceTools:** DSGVOExportRequests und RetentionOverrides pro Tenant/Event.
- **SuperadminAuditLog:** Jede AdminAktion nachvollziehbar (ohne PIIPayloads).
- **TenantAnnouncements:** Zielgerichtete Hinweise/ReleaseNotes an TenantAdmins, inkl. Zeitplanung.
- **IntegrationsHealth:** StatusBoard für Lemon Squeezy/RevenueCat/Webhooks inkl. Störungen.
## 2. Deployments & Infrastruktur
Diese Kapitel erklären, wie die Plattform in Docker/Dokploy betrieben wird.
- **Docker-Deployment (ComposeStack)**
- `docs/ops/deployment/docker.md` Referenz für `docker-compose.yml`, Services, Volumes, Migrations und SchedulerSetup.
- **Dokploy-Deployment (PaaS)**
- `docs/ops/deployment/dokploy.md` Wie die gleichen Services als DokployComposeStacks betrieben werden, inkl. SuperAdminIntegration.
- **Join-Token-Analytics & Public API**
- `docs/ops/deployment/join-token-analytics.md` Konfiguration der AnalyticsPfade für JoinTokens.
- `docs/ops/deployment/public-api-incident-playbook.md` Runbook für PublicAPIStörungen (RateLimits, Abuse, Outages).
- **Lokale Podman/Dev-URLs**
- `docs/ops/deployment/lokale-podman-adressen.md` Übersicht über lokale Services/Ports bei PodmanSetups.
Fragen zur Infrastruktur (Netzwerk, TLS, DNS, Backups) sollten immer zuerst gegen diese Dokumente geprüft werden.
## 3. Queues, Storage & Medien-Pipeline
Fotos sind das Herz des Produkts entsprechend wichtig ist ein klarer Blick auf die MedienPipeline.
- **Queues & Worker**
- `docs/ops/queue-workers.md` Wie die WorkerContainer (`queue`, `media-storage-worker`, `media-security-worker`, `notifications`) gestartet, skaliert und überwacht werden.
- **Media Storage & Archivierung**
- `docs/ops/media-storage-spec.md` Detaillierte Beschreibung, wie Uploads in den „hot“Storage laufen, wie `event_media_assets` gepflegt werden und wie ArchiveJobs funktionieren.
- **Upload-Gesundheit & Notifications**
- `docs/ops/guest-notification-ops.md` Runbook für das NotificationCenter, PushRegistrierung und UploadHealthAlerts.
> TODO: Ergänze ein zentrales „Storage & Queues Monitoring“-Kapitel mit konkreten Schwellenwerten und Alarmierung (z.B. Einbindung von Horizon, RedisMonitoring, Log-Channels).
## 4. Photobooth-Pipeline
Die PhotoboothIntegration hat eigene Betriebsanforderungen:
- `docs/ops/photobooth/README.md` Überblick über PhotoboothSetup und Datenfluss.
- `docs/ops/photobooth/control_service.md` SteuerAPI (UserProvisionierung, Credentials, RateLimits).
- `docs/ops/photobooth/ops_playbook.md` Operatives Playbook für Aktivierung, Fehleranalyse und IncidentResponse rund um PhotoboothUploads.
> Prüfe vor großen Events mit gebuchten Photobooths diese Playbooks und stelle sicher, dass Volumes, Credentials und Scheduler korrekt konfiguriert sind.
## 5. Störungs- & Incident-Runbooks
Die folgenden Dokumente sind deine erste Anlaufstelle im IncidentFall:
- **Major Incidents & Eskalation**
- `docs/ops/incidents-major.md` genereller Rahmen (SEVLevels, Triage, Kommunikation, Postmortems) und Verweise auf die spezifischen Runbooks unten.
- **Public API Störungen**
- `docs/ops/deployment/public-api-incident-playbook.md` SchrittfürSchrittPlan bei Missbrauch, Fehlerspitzen oder Ausfällen der öffentlichen APIs.
- **Upload-/Medien-Probleme**
- `docs/ops/media-storage-spec.md` Referenz, welche Queues/Jobs beteiligt sind und wie man Fehlerzustände erkennt (z.B. lange „pending“-Assets, gescheiterte Archivierung).
- `docs/ops/guest-notification-ops.md` UploadAlerts und Gastbenachrichtigungen.
- **Photobooth-Incidents**
- `docs/ops/photobooth/ops_playbook.md` Vorgehen bei ausfallendem FTP, IngestFehlern oder Sicherheitsvorfällen (Credentials).
Zusätzlich gibt es kurze „Howto“-Runbooks für häufige Supportfälle:
- `docs/ops/howto-tenant-package-not-active.md` Zahlung erfolgreich, Paket nicht aktiv.
- `docs/ops/howto-guest-upload-failing.md` Gäste können nicht hochladen.
- `docs/ops/howto-photobooth-no-photos.md` PhotoboothUploads landen nicht im Event.
- `docs/ops/howto-dsgvo-delete-photo.md` DSGVOLöschung eines konkreten Fotos.
> TODO: Ergänze ein allgemeines „Major Incident“Kapitel (SEV1/2 Definition, Kommunikationskanäle, Vorlagen) und verlinke es hier.
## 6. Prozesse, Roadmap & Änderungen
Der Betreiber muss wissen, welche größeren Änderungen anstehen oder kürzlich live gegangen sind.
- **Roadmap & Epics**
- Aktive Epics und offene Arbeitspakete werden in bd gepflegt (`bd list --status=open`, `bd ready`).
- **Changes & Retro-Notizen**
- Lessons Learned, Follow-ups und Incident-Maßnahmen werden als bd-Issues oder Issue-Notizen festgehalten.
Als Betreiber lohnt es sich, vor größeren Deployments die offenen bd-Issues zu prüfen, um Seiteneffekte zu antizipieren.
## 7. Tests, Qualität & Releases
Stabile Releases sind Grundvoraussetzung für ruhigen Betrieb:
- **E2E-Tests & Qualität**
- `docs/testing/e2e.md` beschreibt, welche EndtoEndTests existieren und wie sie als SmokeSuite für Releases verwendet werden können.
- **Release-Prozess (Entwurf)**
- `docs/ops/releases.md` Checkliste für CIPipelines, StagingDeploy, ProdRollout, SmokeTests und RollbackÜberlegungen.
## 8. Nächste Schritte für das Betriebshandbuch
Die folgenden Kapitel sind als eigenständige Runbooks angelegt und sollten mit der Zeit weiter gefüllt werden:
- **Rollen & On-Call-Handbuch**
- `docs/ops/oncall-roles.md` definiert PlatformOps, OnCall, Support und Produktrollen sowie Eskalationswege.
- **On-Call Cheat Sheet**
- `docs/ops/oncall-cheatsheet.md` schnelle Übersicht über wichtige Kommandos, Logs und Dashboards für Incidents.
- **Support & Eskalation**
- `docs/ops/support-escalation-guide.md` beschreibt, welche Informationen Support von Kunden einsammeln sollte, bevor an Ops eskaliert wird.
- **Backup & Restore / Disaster Recovery**
- `docs/ops/backup-restore.md` Was gesichert werden muss, RestoreSzenarien und DRÜbungen.
- **DSGVO & Compliance-Operationen**
- `docs/ops/compliance-dsgvo-ops.md` Praktische Abläufe für Auskunfts/Löschanfragen, Retention und Dokumentation.
- **Billing & Zahlungs-Operationen**
- `docs/ops/billing-ops.md` Umgang mit Zahlungsproblemen, WebhookFehlern und PaketInkonsistenzen.
- **Monitoring & Observability**
- `docs/ops/monitoring-observability.md` Welche Signale, Metriken und Alerts es geben sollte.
- **Architektur-Diagramme**
- `docs/ops/diagrams.md` MermaidDiagramme für MediaPipeline und Checkout/BillingFlow.
Das Betriebshandbuch bleibt damit ein lebendes Dokument. Neue Runbooks sollten unter `docs/ops/` entstehen und hier verlinkt werden, damit Operatoren einen klaren Einstiegspunkt behalten.

View File

@@ -0,0 +1,125 @@
---
title: Rollen & On-Call-Handbuch
---
Dieses Dokument beschreibt, **wer** im Betrieb wofür zuständig ist und **wie** OnCallBereitschaft organisiert wird. Es ergänzt die technischen Runbooks um eine klare Verantwortungsebene.
> Hinweis: Konkrete Namen/Kontaktdaten sollten nicht in Git stehen, sondern getrennt (z.B. in einem internen Adressbuch oder PasswortSafe). Dieses Dokument definiert Rollen und Prozesse.
## 1. Rollenübersicht
### 1.1 Platform Ops
- Verantwortlich für:
- Infrastruktur (Docker/DokployStacks, Netzwerke, TLS, Backups).
- Technische Verfügbarkeit der Services (App, Queues, DB, Redis, Storage).
- Umsetzung und Pflege der Runbooks unter `docs/ops/`.
- Typische Aufgaben:
- Deployments koordinieren (`docker-compose`, Dokploy, Migrations).
- Monitoring/Alerting pflegen (`ops/monitoring-observability.md`).
- IncidentResponse bei SEV1/SEV2 (`ops/incidents-major.md`).
### 1.2 On-Call Engineer
- Rolle, die im wechselnden Turnus (z.B. wöchentlich) OnCall ist.
- Verantwortlich für:
- Reaktion auf laufende Alerts (Monitoring, Pager, ChatBots).
- Erstes Triage nach `ops/incidents-major.md`.
- Eskalation an weitere Rollen (z.B. Platform Ops, Produkt, Security).
- Voraussetzungen:
- Zugriff auf ProduktionsLogs, MonitoringDashboards, Dokploy/Horizon.
- Vertraut mit den wichtigsten Runbooks (PublicAPI, Storage, Photobooth, Billing).
### 1.3 Support / Customer Success
- Verantwortlich für:
- Kontakt mit Tenants (EMail/Telefon/Chat).
- Übersetzung technischer Probleme in Kundensprache.
- Sammeln aller relevanten Informationen, bevor an OnCall/Platform Ops eskaliert wird.
- Typische Aufgaben:
- Tickets aus dem HelpSystem triagieren.
- Proaktive Kommunikation bei Events („Wir haben ein UploadProblem identifiziert, wir arbeiten daran“).
### 1.4 Produkt / Engineering Leads
- Verantwortlich für:
- Entscheidungen bei FeatureFlags, Rollbacks, HotfixReleases.
- Priorisierung langfristiger Maßnahmen nach Incidents (bd-Issues als Roadmap/Backlog).
- Typische Aufgaben:
- Teilnahme an Postmortems.
- Freigabe von riskanteren Änderungen (z.B. große Migrations).
## 2. On-Call-Modell
### 2.1 Bereitschaftszeiten
Empfohlene Einteilung (anpassbar an dein Team):
- **Bürozeiten (z.B. 09:0017:00)**
- OnCall ist die jeweils zuständige PlatformOpsPerson des Tages.
- Reaktionsziel: 15 Minuten bei SEV1/2, 60 Minuten bei SEV3.
- **Außerhalb der Bürozeiten / Event-Spitzen**
- Optional: Rotierender OnCallDienst mit Rufbereitschaft.
- Reaktionsziel: nach individueller Vereinbarung (z.B. 3060 Minuten bei SEV1).
> Wenn ihr keinen formalen 24/7Dienst habt, sollte klar dokumentiert sein, **wann** keine garantierte Reaktionszeit besteht (z.B. nachts/wochenends) und wie das Kunden gegenüber kommuniziert wird.
### 2.2 Rotation & Übergabe
- OnCallRotation (z.B. wöchentlich) im Teamtool (Kalender/IssueTracker) pflegen.
- Vor Start einer Schicht:
- Offene Incidents und bekannte Problemzonen durchgehen.
- Sicherstellen, dass Aufrufwege funktionieren (Chat, Telefon, Pager).
- Nach Schicht:
- Kurze Übergabe an nächste OnCallPerson (offene Themen, laufende Beobachtungen).
## 3. Eskalationspfad bei Incidents
### 3.1 Standard-Eskalation (SEV-2/3)
1. OnCall nimmt Alert entgegen, prüft grob die Lage (`ops/incidents-major.md` → Triage).
2. Wenn Problem lösbar erscheint:
- Runbooks anwenden (z.B. PublicAPIPlaybook, MedienRunbook, PhotoboothOps).
- Kundenkommunikation via Support abstimmen.
3. Wenn unklar oder größer:
- PlatformOps bzw. Engineering Lead im Chat markieren.
- IncidentThread mit Statusupdates führen.
### 3.2 SEV-1 (kritisch)
1. OnCall ruft sofort **PlatformOps** und ggf. **ProduktLead** in den IncidentThread.
2. Falls nötig, mit Produkt die Entscheidung für:
- Rollback auf letztes Release,
- temporäre Abschaltung einzelner Features (FeatureFlags),
- Aktivierung einer MaintenanceSeite
treffen.
3. Support/Success informieren betroffene Tenants mit kurzem Status und ETA (auch wenn ETA noch grob ist).
## 4. Tools & Zugänge
Für OnCall/PlatformOps sollten mindestens folgende Zugänge eingerichtet und getestet sein:
- **Dokploy / Docker-Orchestrierung**
- Zugriff auf ComposeStacks, Logs, HealthChecks.
- **Horizon / Queue-Monitoring**
- Zugriff auf `/horizon` (nur für SuperAdmins).
- **Logs**
- Zentralisierte Logs (Loki/ELK) oder SSHZugriff zur Maschine mit `storage/logs`.
- **Monitoring/Alerts**
- Zugang zu Uptime/MonitoringService (StatusDashboard, AlertKonfiguration).
> Stelle sicher, dass OnCallPersonen ausprobiert haben, ob sie diese Tools tatsächlich erreichen können (VPN, 2FA, etc.), bevor eine Schicht beginnt.
## 5. Verbindung zu den Runbooks
Bei einem Incident sollte die OnCallPerson immer vom **Betriebshandbuch** aus denken:
- Einstieg über `docs/ops/operations-manual.md` (Knowledge-Base-Startseite im SuperAdmin-Panel).
- Je nach Symptome:
- **API-/Frontend-Probleme** → PublicAPIPlaybook (`ops/deployment/public-api-incident-playbook.md`), ggf. Marketing/GuestPWASpezifikationen in `docs/prp/` (in PRP, nicht im OpsBereich).
- **Upload/Storage-Probleme**`ops/media-storage-spec.md`, `ops/guest-notification-ops.md`.
- **Photobooth**`ops/photobooth/ops_playbook.md`.
- **Abrechnung**`ops/billing-ops.md`.
- **DSGVO-Fälle**`ops/compliance-dsgvo-ops.md`.
Dieses Dokument soll nicht alle technischen Details wiederholen, sondern sicherstellen, dass immer klar ist, **wer** reagiert und **welches** Runbook als nächstes geöffnet werden sollte.

View File

@@ -0,0 +1,46 @@
---
title: OnCall Cheat Sheet
---
Dieser Spickzettel ist für OnCallPersonen gedacht, die im Incident schnell handeln müssen. Er konzentriert sich bewusst auf die wichtigsten Kommandos, Dashboards und Checks.
## 1. Top10 Kommandos
- AppContainer Logs (Laravel / Horizon):
- `docker compose logs -f app`
- `docker compose logs -f horizon`
- QueueStatus:
- `php artisan queue:failed`
- `php artisan horizon:status`
- StorageHealth:
- `php artisan storage:monitor`
- `php artisan storage:check-upload-queues`
- DatenbankChecks (Beispiele):
- `php artisan tinker` → gezielte Queries zu `events`, `event_media_assets`, `checkout_sessions`.
## 2. Erstdiagnose bei „Nichts geht mehr“
- Statusseite / Monitoring prüfen (HTTPStatus, FehlerRate, QueueLänge).
- `docker compose ps` → welche Services sind „unhealthy“ oder down?
- Logs der auffälligen Services anschauen (App, Queue, DB, Nginx).
- Kurz festhalten:
- Wann trat das Problem auf?
- Betrifft es **alle** Tenants oder einzelne?
- Nur GuestPWA, nur TenantAdmin oder beides?
## 3. Wichtigste Dashboards (Beispiele)
- APIFehlerRate (5xx, 4xx für Public API).
- QueueBacklog (`default`, `media-storage`, `media-security`, `notifications`).
- ResponseTime Guest/TenantPWA.
- Lemon SqueezyWebhookFehler (falls im Monitoring abgebildet).
> Ergänze hier konkrete Links zu euren Grafana/DatadogDashboards, sobald diese stabil sind.
## 4. Wann eskalieren?
- SEV1: Plattform weitgehend nicht nutzbar (> 15 Minuten Ausfall, viele Tenants betroffen).
- SEV2: Kritische Kernfunktion (Uploads, Logins, Zahlungen) länger als 30 Minuten gestört.
- SEV3: Einzelne Tenants oder Funktionen, Workaround vorhanden.
Siehe auch `docs/ops/incidents-major.md` für detaillierte SEVDefinitionen und Kommunikationsregeln.

View File

@@ -0,0 +1,48 @@
---
title: Support → Ops Eskalationsleitfaden
---
Dieses Dokument beschreibt, welche Informationen der Support einsammeln sollte, bevor ein Ticket an Ops eskaliert wird. Ziel: weniger PingPong, schnellere Lösung.
## 1. Pflichtinfos pro Ticket
- **TenantID** bzw. TenantSlug.
- **EventID** bzw. EventSlug.
- **Zeitstempel** der Beobachtung (lokale Zeit + Zeitzone).
- **Betroffene User**:
- GastSession ID (falls verfügbar).
- EMail (für TenantAdmins).
- **Umgebung**:
- Browser + Version.
- Betriebssystem / Device.
- Mobil / Desktop.
- **Screenshots / Screenrecording**:
- Fehlermeldungen.
- UIZustand (z.B. Upload hängt bei 90 %).
## 2. Typische Fälle & Zusatzinfos
- **Upload schlägt fehl**
- URL des JoinLinks.
- Anzahl betroffener Gäste (einige / viele / alle).
- Grobe Dateigröße (Handyfoto, stark komprimiert, RAW etc.).
- **PhotoboothFotos fehlen**
- Name/Typ der Photobooth.
- Zeitpunkt der letzten sichtbaren Fotos.
- Ob die Photobooth selbst Fehler anzeigt.
- **Paket nicht aktiv / Limits falsch**
- Bestellnummer / Lemon SqueezyCheckoutID (falls vorhanden).
- Zeitpunkt der Zahlung.
- Welches Paket wurde erwartet?
## 3. Wie an Ops übergeben?
- Ticket im Tracker mit Label „ops“ versehen.
- Kurzes Summary in ein bis zwei Sätzen:
- „Gäste können seit 18:30 Uhr im Event XYZ keine Fotos hochladen. Fehler: Upload fehlgeschlagen.“
- Alle oben genannten Pflichtinfos als strukturierte Liste ergänzen.
Siehe auch:
- `docs/ops/oncall-roles.md`
- `docs/ops/oncall-cheatsheet.md`

View File

@@ -0,0 +1,22 @@
# Admin Issue Resolution (Ops Playbook)
Internal troubleshooting guide for superadmins and on-call.
## Upload incidents
| Symptom | Likely cause | First action |
| --- | --- | --- |
| Queue stuck >10 min | Workers stalled or storage pressure | Check queue workers and storage health; see `docs/ops/queue-workers.md` and `docs/ops/dr-storage-issues.md` |
| Guests blocked | Per-device limits reached | Confirm limits and whether exceptions are allowed |
| Thumbnails missing | Backfill jobs stalled | Run `php artisan media:backfill-thumbnails --tenant=XYZ` |
## Access issues
- **Admin cannot log in**: verify invite acceptance, check SSO mapping if enforced, re-send invite.
- **Guest cannot join**: confirm event is published and the join link is current.
## Billing and quota blocks
- Check Lemon Squeezy / RevenueCat status dashboards.
- Confirm webhook freshness and retry failures if needed.
## Communications
- Use the support escalation guide at `docs/ops/support-escalation-guide.md` for customer comms.
- Log all actions and timestamps in a bd issue.

View File

@@ -0,0 +1,34 @@
# Live Ops Control (Ops Playbook)
Use this playbook when supporting an event in real time. This is internal guidance for superadmins/on-call.
## Scope
- Moderation queues and Live Show queues.
- High-volume events with potential backlog or device failures.
- Incident response when content safety or performance is at risk.
## Baseline checks
1. Confirm event status and moderation mode.
2. Verify queue counts and recent upload rate.
3. Check if any trusted devices are bypassing review.
## Triage workflow
- **Queue backlog** (>25 items or >10 min):
- Increase moderation staffing.
- Tighten upload visibility rules.
- Reduce Live Show effects or layout to lower throughput pressure.
- **Offensive content reported**:
- Hide the item, capture evidence, notify duty officer.
- Confirm the report appears in the audit log.
- **Live Show empty**:
- Confirm correct show link and moderation mode.
- Check whether items are waiting in the queue.
## Escalation
- Reliability on-call for queue or processing failures.
- Legal duty officer for sensitive content handling.
- Customer Success for comms to organizers.
## After action
- Capture timeline and actions in a bd issue.
- Add follow-ups for any repeated failure modes.

View File

@@ -0,0 +1,6 @@
---
title: Incidents & DR
type: group
order: 20
icon: heroicon-o-exclamation-triangle
---

View File

@@ -0,0 +1,78 @@
---
title: Major Incidents & Eskalation
---
Diese Seite beschreibt, wie du bei größeren Störungen (SEV1/SEV2) vorgehst. Sie ergänzt die spezifischen Runbooks (Public API, MedienPipeline, Photobooth) um einen einheitlichen Rahmen.
## 1. Incident-Klassifikation
- **SEV1 (kritisch)**
- Gäste können nicht mehr hochladen ODER keine Events/Galerien mehr öffnen.
- Tenant Admins können sich nicht einloggen oder keine Kernaktionen ausführen (Events verwalten, Medien moderieren).
- Datenverlust oder potenzieller Datenverlust (z.B. Löschjob auf falscher StorageEbene).
- **SEV2 (hoch)**
- Teilweise Degradation (z.B. PhotoboothUploads hängen, PublicAPI stark verlangsamt, eine Region betroffen).
- Kritische BackgroundJobs (Archivierung, AV/EXIFScans, ZahlungsWebhooks) stauen sich, ohne dass Gäste sofort komplett blockiert sind.
- **SEV3 (mittel)**
- Einzelne Features gestört (NotificationCenter, JoinTokenAnalytics, einzelne AdminViews).
- Workaround möglich (z.B. manuelle Nacharbeit durch Support).
> Wichtig: Jede Störung, die einen zahlenden Eventkunden am Tag des Events blockiert, sollte mindestens als SEV2, ggf. als SEV1 eingeordnet werden.
## 2. Erstmaßnahmen (Triage)
1. **Scope bestimmen**
- Welche Benutzer sind betroffen? (Alle Gäste, einzelne Tenants, nur Photobooth, nur Admins?)
- Betrifft es nur eine Umgebung (staging vs. production)?
2. **Schnell-Checks**
- Status von App, Queue, Redis, DB (Docker/DokployÜbersicht prüfen).
- Horizon/Queues: sind relevante Queues leer, wachsend, „stuck“? Gibt es viele Failed Jobs?
- Logs für relevante Kanäle: `storage/logs/laravel.log`, spezielle Channels wie `storage-jobs`, `notifications`, `billing`, Nginx/ProxyLogs.
- Monitoring: externe UptimeChecks / Dashboards (z.B. PublicAPI Latenz, ErrorRate).
3. **Einordnung & Eskalation**
- SEV1/2: OnCall informieren (Pager/Chat), IncidentKanal im Teamchat eröffnen.
- SEV3: Im IssueTracker erfassen, ggf. gebündelt mit anderen Findings.
Nutze bei PublicAPIProblems zusätzlich das `docs/ops/deployment/public-api-incident-playbook.md`.
## 3. Standard-Runbooks nach Bereich
- **Public API / Gast-Zugriff**
- Siehe `docs/ops/deployment/public-api-incident-playbook.md`.
- Typische Auslöser: Peaks, Abuse, externe Integrationen, Ratenlimits.
- **Medien-Pipeline / Uploads**
- Siehe `docs/ops/media-storage-spec.md` und `docs/ops/guest-notification-ops.md`.
- Fälle: Uploads bleiben im Pending, Archivjobs laufen nicht, Speicherkapazität erreicht, Gäste bekommen „Uploads hängen noch…“.
- **Photobooth**
- Siehe `docs/ops/photobooth/ops_playbook.md`.
- Fälle: FTP nicht erreichbar, Ingest nicht laufend, falsche Credentials, SecurityVorfälle.
- **Abrechnung & Billing**
- Siehe `docs/ops/billing-ops.md`.
- Fälle: Lemon Squeezy/RevenueCatWebhookFehler, falsche PaketZustände, doppelte/fehlende Buchungen.
Dieses Dokument verweist immer nur auf die jeweils tieferen Runbooks bei konkreten Problemen gehst du dort in die Details.
## 4. Kommunikation
- **Intern (Team)**
- Eröffne einen dedizierten IncidentThread im Chat (mit Zeitstempel, SEVLevel, Kurzbeschreibung).
- Halte dort Statusupdates fest (z.B. „17:05 UploadQueue entstaut, weitere Beobachtung 30 min“).
- Notiere bewusst Entscheidungen (z.B. „19:10 Feature X temporär deaktiviert“, „19:25 Rollback auf vorheriges Release“).
- **Extern (Kunden)**
- Ab SEV2: Überlege einen kurzen Statushinweis (StatusSeite oder manuelle Kommunikation an direkt betroffene Tenants).
- Bei Incidents während Events: Koordiniere mit Success/Support, um proaktiv auf Tenant Admins zuzugehen.
> TODO: Falls du eine StatusSeite oder automatisierte EMails hast, dokumentiere hier, wie und wann sie ausgelöst werden.
## 5. Nachbereitung (Postmortem)
Nach einem SEV1/2 Incident:
1. **Fakten sammeln** (Timeline, betroffene Tenants/Events, konkrete Auswirkungen).
2. **Ursache** (Root Cause) möglichst präzise identifizieren auch dann, wenn direkt „nur“ Symptome gefixt wurden.
3. **Kurzfristige Maßnahmen** (Hotfixes, KonfigAnpassungen, zusätzliche Checks).
4. **Langfristige Maßnahmen** als bd-Issues festhalten (inkl. Link zum Incident).
5. **Dokumentation aktualisieren**
- Relevante Runbooks (dieses Dokument, PublicAPIRunbook, StorageSpec, BillingOps, etc.) mit neuen Learnings ergänzen.
Ziel ist, dass die TimetoDetect und TimetoResolve für ähnliche Probleme in Zukunft sinkt.

View File

@@ -0,0 +1,93 @@
---
title: Backup & Restore / Disaster Recovery
---
Dieses Dokument beschreibt, was gesichert werden sollte, wie Backups geprüft werden und wie ein Restore im Notfall abläuft.
## 1. Was muss gesichert werden?
- **Datenbank**
- MySQLDatenbank (alle Schemas/Tables des FotospielBackends).
- Enthält Tenants, Events, FotosMetadaten, JoinTokens, Abrechnungsdaten, Logs (soweit in DB).
- **Medienspeicher**
- HotStorage: Pfade unter `storage/app/private` / `storage/app/public` oder konfigurierten „hot“Disks.
- Archivspeicher: Buckets/Disks, in denen `event_media_assets` mit Status `archived` liegen.
- **Konfiguration**
- `.env` Dateien (ohne sie in Git zu speichern), DokployComposeKonfigurationen, Secrets für externe Dienste.
- Optional: HorizonKonfiguration, MonitoringDashboards.
> TODO: Füge hier konkrete Pfade/BucketNamen und die verwendeten BackupTools (z.B. `mysqldump`, S3Snapshots, DokployBackups) ein.
## 2. Backup-Strategie
- **Datenbank-Backups**
- Frequenz: mindestens täglich (idealerweise alle 46 Stunden für ProduktionsDB).
- Aufbewahrung: z.B. 730 Tage, mit OffSiteKopie.
- Prüfschritte:
- Dump/Backupfile auf Plausibilität (Größe, letzte Änderung).
- Regelmäßige TestRestores in eine StagingDB:
- Beispiel (einfacher Dump auf Host Pfade/Passwörter an Umgebung anpassen):
- `mysqldump -h 127.0.0.1 -u fotospiel -p fotospiel > fotospiel-$(date +%F).sql`
- Restore in temporäre DB (z.B. `fotospiel_restore`) und kurze Stichproben:
- `mysql -h 127.0.0.1 -u fotospiel -p fotospiel_restore < fotospiel-YYYY-MM-DD.sql`
- **Medien-Backups**
- HotStorage:
- Snapshot/IncrementalBackup der StorageVolumes oder S3Buckets.
- Archive:
- Sicherstellen, dass ArchivBackups nicht versehentlich durch LifecyclePolicies gelöscht werden, bevor gesetzliche Retention erfüllt ist.
- **Konfig-Backups**
- `.env` und Secrets nur verschlüsselt speichern (z.B. in einem SecretsManager, nicht in KlartextBackups).
- Dokploy/ComposeKonfiguration versionieren (Git) und zusätzlich sicher exportieren.
## 3. Restore-Szenarien
### 3.1 Einzelner Tenant/Event defekt
1. Reproduzieren, ob der Fehler rein logisch (Datenkonsistenz) oder physisch (Fehlender MedienBlob) ist.
2. **DBRestore (punktuell)**:
- Wenn möglich, nur relevante Tabellenbereiche (z.B. `tenants`, `events`, `photos`, `event_media_assets`, `event_packages`) aus Backup in eine temporäre DB laden.
- Differenzanalyse: welche Daten fehlen/fehlerhaft? Manuell oder via Skript zurückspielen.
3. **Medien-Check**
- Fehlende Dateien im Hot/ArchiveStorage identifizieren (z.B. per `event_media_assets` Pfade + `Storage::disk()->exists`).
- Wenn Dateien im Backup vorhanden, gezielt an den richtigen Pfad zurückkopieren.
> Diese Schritte sollten zuerst in einer StagingUmgebung eingeübt werden, bevor sie in Produktion angewendet werden.
### 3.2 Betriebsweite Störung (DB/Storage Verlust)
1. **DB wiederherstellen**
- Leere Datenbank aufsetzen, letztes konsistentes Backup einspielen.
2. **Storage wiederherstellen**
- HotStorageBackup auf Volumes/Buckets zurückspielen (z.B. DockerVolume `app-storage` oder zugeordneten Bucket).
- ArchivBuckets ggf. unverändert lassen, sofern noch intakt.
3. **App & Queues**
- App mit readonly/maintenanceFlag starten, Queues gestoppt lassen.
- Konsistenzprüfungen (z.B. stichprobenartige Tenants, Events, Medien, Abrechnungsdaten).
4. **Queues wieder freigeben**
- Wenn die wichtigsten Funktionen wieder intakt sind, Queues/Horizon graduell zuschalten.
> TODO: Ergänze konkrete Kommandos (Migrationsstatus prüfen, HealthChecks) und definierte RTO/RPOZiele.
## 4. Tests & DR-Übungen
- Mindestens 12 Mal pro Jahr einen vollständigen Restore in einer separaten Umgebung durchspielen:
- DBBackup einspielen.
- MedienBackups anbinden.
- Eine Handvoll Tenants/Events kompletter durchklicken (Upload, Galerie, AdminFunktionen).
- Ergebnisse als bd-Issue dokumentieren (z.B. „DRÜbung 2026Q1“ mit Learnings).
## 5. Verantwortlichkeiten
- **Backup-Ownership**: Wer stellt sicher, dass Backups laufen und testweise wiederhergestellt werden?
- **DR-Ownership**: Wer führt die DRÜbungen durch und wer entscheidet im Ernstfall über Failover/Restore?
Diese Punkte sollten mit konkreten Namen/Rollen befüllt werden, damit im Ernstfall keine Unklarheiten bestehen.
## 6. Ergänzende DR-Playbooks
Spezielle DRSzenarien sind in separaten Runbooks beschrieben:
- `docs/ops/dr-tenant-event-restore.md` Vorgehen bei versehentlich gelöschten oder beschädigten Tenants/Events.
- `docs/ops/dr-storage-issues.md` Vorgehen bei Hot/ArchiveStorageProblemen (voll, hängende Archivierung, fehlende Medien).
Dieses Dokument bleibt die HighLevelÜbersicht für konkrete Fälle solltest du immer auch die entsprechenden Playbooks konsultieren.

View File

@@ -0,0 +1,96 @@
---
title: DR-Playbook Tenant/Event versehentlich gelöscht
---
Dieses Playbook beschreibt, wie du vorgehst, wenn ein Tenant oder Event versehentlich gelöscht oder stark beschädigt wurde. Es baut auf den allgemeinen Hinweisen aus `docs/ops/backup-restore.md` auf.
> Wichtig: Diese Schritte sollten zuerst in einer **Staging-Umgebung** geübt werden. In Produktion nur nach klarer Freigabe und mit sauberer Dokumentation anwenden.
## 1. Schadensbild erfassen
Bevor du irgendetwas wiederherstellst:
- **Was genau ist betroffen?**
- Nur ein Event (z.B. versehentlich im Admin archiviert/gelöscht)?
- Mehrere Events eines Tenants?
- Der komplette Tenant (inkl. Benutzer, Events, Pakete)?
- **Welche Daten fehlen/fehlerhaft?**
- Fehlen nur Metadaten (Events, Fotos, Pakete) oder auch MedienDateien?
- Gibt es noch Spuren im Admin/UI (z.B. leere Übersichten, aber Logs mit Fehlermeldungen)?
- **Zeitfenster eingrenzen**
- Wann wurde der Fehler bemerkt?
- Wann war der Zustand sicher noch korrekt (z.B. vor letztem Deploy / gestern Abend)?
Diese Informationen bestimmen, welches Backup verwendet werden sollte.
## 2. Logische vs. physische Schäden unterscheiden
- **Logischer Schaden**
- Falsche Flags (Status falsch, Event „archiviert“ statt „aktiv“).
- Inkompatible PaketZuweisungen, aber Daten sind noch vorhanden.
- Lösbare Fälle oft ohne Restore durch gezielte Updates / AdminUI.
- **Physischer Schaden**
- Reihen in KernTabellen gelöscht (z.B. `events`, `photos`, `event_media_assets`, `tenants`).
- MedienDateien im Storage gelöscht/überschrieben.
Nur bei physischen Schäden ist ein Restore aus Backup nötig. Logische Schäden sollten möglichst mit minimalinvasiven Korrekturen behoben werden.
## 3. Vorgehen bei einzelnen Events
### 3.1 Datenbank Event-Datensätze identifizieren
1. **Event-IDs ermitteln**
- Aus Logs, alten Links, Metriken oder Backups.
2. **Querverweise prüfen**
- `events` (Basisdaten), `photos`, `event_media_assets`, `event_packages`, ggf. `event_join_tokens`.
3. **Temporäre Restore-DB nutzen**
- Erzeuge eine temporäre Datenbank (z.B. `fotospiel_restore`) und spiele den relevanten BackupDump ein.
- Dort die betroffenen EventDatensätze suchen.
### 3.2 Selektiver Restore von Event-Daten
Empfohlenes Muster:
- In **Restore-DB**:
- Exportiere alle relevanten Zeilen für das Event (z.B. `events`, `photos`, `event_media_assets`) in SQL/CSV.
- In **Produktions-DB**:
- Prüfe, ob IDs kollidieren (z.B. neue Events seit dem Backup).
- Freie IDs und referentielle Integrität beachten; wenn IDs bereits vergeben sind, ist ein reiner Import meist nicht möglich → dann manuelle Rekonstruktion (neues Event + Medien erneut verknüpfen).
> Dieses Playbook beschreibt bewusst kein generisches „SQL-Skript“, weil die tatsächliche Struktur und IDs von der aktuellen Migration abhängen. Ziel ist, die **Vorgehensweise** zu standardisieren, nicht ein unüberlegtes MassenUpdate.
### 3.3 Medien-Dateien
1. In der RestoreUmgebung prüfen, welche Pfade `event_media_assets` für das Event referenzieren.
2. Im BackupStorage nach diesen Pfaden suchen (Hot und ggf. ArchivBucket).
3. Fehlende Dateien in das produktive StorageVolume/Bucket an den erwarteten Pfad kopieren.
Wenn die Medien physisch nicht mehr vorhanden sind, ist nur eine teilweise Rekonstruktion möglich (z.B. Thumbnails ohne Originale) das sollte mit dem Tenant klar kommuniziert werden.
## 4. Vorgehen bei Tenant-weiten Fehlern
Wenn ein kompletter Tenant versehentlich gelöscht wurde (inkl. Benutzer/Events):
1. **Einordnung**
- Handelt es sich um einen isolierten Tenant oder könnten mehrere betroffen sein (z.B. durch fehlerhaftes Skript)?
2. **Restore-Strategie wählen**
- _Variante A: Partial Restore_ nur die Tabellenzeilen zum Tenant aus der BackupDB in die ProduktionsDB zurückführen.
- _Variante B: Backup-Spiegel_ Tenant + zugehörige Medien in eine separate Umgebung wiederherstellen und dem Kunden dort einen temporären Zugang geben.
3. **Risikoabwägung**
- Partial Restore in eine laufende ProduktionsDB trägt höhere Risiken (Kollisionsgefahr mit neuen Daten).
- SpiegelVariante ist operativ aufwändiger, kann aber sicherer sein, wenn viele neue Daten seit dem Backup hinzugekommen sind.
> Welche Variante gewählt wird, sollte von PlatformOps + Produkt gemeinsam entschieden werden.
## 5. Kommunikation & Dokumentation
- **Mit dem betroffenen Tenant**
- Ehrlich kommunizieren, was passiert ist, was wiederherstellbar ist und welches Risiko ein Restore birgt.
- Zeitrahmen und mögliche Einschränkungen klar benennen.
- **Intern**
- Den gesamten Prozess in einem bd-Issue oder im Ticketing festhalten:
- Was, wann, warum schief ging.
- Welche RestoreSchritte durchgeführt wurden.
- Welche Verbesserungen künftig notwendig sind (z.B. bessere Schutzmechanismen, zusätzliche Bestätigungen beim Löschen).
Dieses Playbook ist bewusst höherlevelig gehalten; spezifische SQL oder ToolSnippets sollten ergänzend in einem internen Notizsystem oder als separate Anhänge gepflegt werden, sobald eure BackupPipelines stabil etabliert sind.

View File

@@ -0,0 +1,73 @@
---
title: DR-Playbook Storage-Probleme (Hot/Archive)
---
Dieses Playbook beschreibt, wie du vorgehst, wenn es Probleme mit dem MedienStorage gibt z.B. HotStorage voll, Archivierung bleibt hängen oder viele Assets im Status `failed`.
## 1. Symptome & erste Checks
Typische Symptome:
- Gäste können keine Fotos mehr hochladen (Fehlermeldungen im UploadFlow).
- Tenant Admins sehen „fehlende Medien“ oder sehr langsame Galerien.
- `event_media_assets` enthält viele Einträge mit Status `pending` oder `failed`.
- Logs enthalten Hinweise auf fehlgeschlagene Archivierungen oder fehlende Dateien.
Erste Checks:
- StorageUsage der HotVolumes/Buckets prüfen (DockerVolume, S3Dashboard o.ä.).
- `EventMediaAsset`Status stichprobenartig prüfen (`hot`, `archived`, `pending`, `failed`).
- QueueLängen und Fehler in `media-storage` und `media-security` via Horizon und Logs.
## 2. Hot-Storage voll oder kurz vor Limit
1. **Warnungen bestätigen**
- System/ProviderWarnungen (z.B. 90 % voll) bestätigen.
- Prüfen, ob `storage:monitor` oder ähnliche Kommandos bereits Alerts ausgelöst haben.
2. **Sofortmaßnahmen**
- Archivierung priorisieren: sicherstellen, dass `storage:archive-pending` regelmäßig läuft und die `media-storage`Queue abgearbeitet wird.
- Temporäre Limits erhöhen, falls Provider dies erlaubt (z.B. S3Bucket praktisch „unbegrenzt“ vs. lokaler Disk).
3. **Aufräumen**
- Alte Caches/Thumbnails, die problemlos neu generiert werden können, ggf. gezielt löschen.
- Keine unüberlegten `rm -rf` Aktionen auf dem Storage immer mit klarer Strategie arbeiten.
## 3. Archivierung hängt oder schlägt häufig fehl
1. **Queue-Status prüfen**
- `media-storage` QueueLänge, Failed Jobs in Horizon prüfen.
- LogChannel `storage-jobs` nach Fehlermeldungen durchsuchen.
2. **Fehlerbilder auswerten**
- Typische Ursachen:
- Netzwerk/CredentialProbleme beim Zugriff auf den ArchivBucket.
- Zeitüberschreitungen bei sehr großen Medien.
- Inkonsistente `EventMediaAsset`Einträge (Pfad nicht mehr vorhanden, falscher DiskKey).
3. **Abhilfe**
- Netzwerk/Credentials fixen (z.B. S3Keys, Endpoints, Rechte).
- Problematische Assets gezielt in den Logs identifizieren und manuell nachziehen (Kopie auf ArchiveDisk, Status auf `archived` setzen, FehlerFeld leeren).
- Wenn viele Assets betroffen sind, lieber ein dediziertes Skript/Job bauen als adhoc SQL.
## 4. Fehlende oder beschädigte Medien-Dateien
Wenn `EventMediaAsset`Einträge existieren, die zu nicht mehr vorhandenen Dateien zeigen:
1. **Umfang ermitteln**
- Stichproben auf Basis der Fehlerlogs oder per BatchCheck (z.B. ein ArtisanCommand, das `exists()` prüft).
2. **Backup-Sicht**
- Prüfen, ob die Dateien noch im Backup vorhanden sind (Hot/ArchiveBackups).
3. **Wiederherstellung**
- Fehlende Dateien an den erwarteten Pfad im Storage kopieren (Hot oder Archive).
- `EventMediaAsset`Status und Timestamps ggf. aktualisieren (`hot` vs. `archived`).
Wenn keine Backups existieren, bleibt nur, die betroffenen Assets sauber als „nicht mehr verfügbar“ zu kennzeichnen und die Nutzer entsprechend zu informieren.
## 5. Nach einem Storage-Incident
- **Monitoring schärfen**
- Schwellwerte in `storage-monitor` anpassen (Warnung/Kritisch), Alerts für Queues/Storage erweitern.
- **Kapazitätsplanung**
- Erkenntnisse über Medienwachstum nutzen, um frühzeitig auf größere Volumes/Buckets oder häufigere Archivierung umzusteigen.
- **Dokumentation**
- Incident und Maßnahmen als bd-Issue dokumentieren.
- Dieses Playbook aktualisieren, wenn neue Muster entdeckt wurden.
Dieses Playbook ist eng mit `docs/ops/media-storage-spec.md` und `docs/ops/monitoring-observability.md` verknüpft. Nutze diese Dokumente für Detailinformationen zu Queues, Thresholds und StorageTargets.

View File

@@ -0,0 +1,6 @@
---
title: Medien & Upload
type: group
order: 30
icon: heroicon-o-cloud-arrow-up
---

View File

@@ -0,0 +1,101 @@
# Media Storage Ops Specification
## Purpose
This document explains how customer photo uploads move through the Fotospiel platform, which services handle each stage, and what operators must monitor. It complements the PRP storage chapters by focusing on day-to-day operational expectations.
## High-Level Flow
1. **Upload (web / API containers)**
- Guest/Tenant requests hit the Laravel app container.
- `EventStorageManager::getHotDiskForEvent()` resolves the current *hot* storage target for the event (usually the local disk mounted at `/var/www/html/storage/app/private`).
- Controllers call `Storage::disk($hotDisk)->putFile("events/{event}/photos", $file)` and immediately write the original file plus a generated thumbnail to the hot disk.
- `EventMediaAsset` rows are created with `status = hot`, capturing disk key, relative path, checksum, size, and variant metadata.
- Every upload also dispatches `ProcessPhotoSecurityScan` to the `media-security` queue for antivirus and EXIF stripping.
2. **Metadata & accounting**
- `photos` table stores user-facing references (URLs, thumbnail URLs, guest metadata).
- The linked `event_media_assets` records keep canonical storage information and are used later for archival/restores.
- `media_storage_targets` and `event_storage_assignments` tables describe which disk key is considered “hot” vs “archive” per event.
3. **Asynchronous archival**
- The scheduler (either `php artisan schedule:work` container or host cron) runs `storage:archive-pending`.
- That command finds events whose galleries expired or were manually archived and dispatches `ArchiveEventMediaAssets` jobs onto the `media-storage` queue.
- `ArchiveEventMediaAssets` streams each file from the current disk to the archive disk resolved by `EventStorageManager::getArchiveDiskForEvent()`, updates the asset row to `status = archived`, and optionally deletes the hot copy.
- Archive storage usually maps to an object store bucket using the prefix `tenants/{tenant_uuid}/events/{event_uuid}/photos/{photo_uuid}/` (see `docs/prp/10-storage-media-pipeline.md`).
4. **Cold storage / restore**
- When an event is re-opened or media needs to be rehydrated, jobs mark affected assets `restoring` and copy them back to a hot disk.
- Restores reuse the same `event_media_assets` bookkeeping so URLs and permissions stay consistent.
## Container & Service Responsibilities
| Component | Role |
| --- | --- |
| `app` (Laravel FPM) | Accepts uploads, writes to the hot disk, and records metadata. |
| `media-storage-worker` | Runs `/scripts/queue-worker.sh media-storage`; consumes archival/restoration jobs and copies data between disks. Shares the same `app-code` volume so it sees `/var/www/html/storage`. |
| `queue` workers | Default queue consumers for non-storage background jobs. |
| `media-security-worker` | Processes `ProcessPhotoSecurityScan` jobs (antivirus + EXIF scrub). |
| `scheduler` | Runs `php artisan schedule:work`, triggering `storage:archive-pending`, `storage:monitor`, queue health checks, etc. |
| `horizon` | Optional dashboard / supervisor for queue throughput. |
| Redis | Queue backend for all worker containers. |
## Key Commands & Jobs
| Command / Job | Description |
| --- | --- |
| `storage:archive-pending` | Scans for expired/archived events and dispatches `ArchiveEventMediaAssets` jobs (`media-storage` queue). |
| `storage:monitor` | Aggregates capacity/queue stats and emails alerts when thresholds are exceeded (`config/storage-monitor.php`). |
| `media:backfill-thumbnails` | Regenerates thumbnails for existing assets; useful before enabling archival on migrated data. |
| `ProcessPhotoSecurityScan` | Runs antivirus/EXIF stripping for a photo; default queue is `media-security`. |
| `ArchiveEventMediaAssets` | Copies hot assets to the archive disk, updates statuses, and deletes hot copies if configured. |
## Configuration Reference
| Setting | Location | Notes |
| --- | --- | --- |
| Default disk | `.env` `FILESYSTEM_DISK` & `config/filesystems.php` | Hot uploads default to `local` (`/var/www/html/storage/app/private`). |
| Storage targets | `media_storage_targets` table | Each row stores `key`, `driver`, and JSON config; `EventStorageManager` registers them as runtime disks. |
| Security queue | `.env SECURITY_SCAN_QUEUE` & `config/security.php` | Defaults to `media-security`. |
| Archive scheduling | `config/storage-monitor.php['archive']` | Controls grace days, chunk size, locking, and dispatch caps. |
| Queue health alerts | `config/storage-monitor.php['queue_health']` | Warning/critical thresholds for `media-storage` and `media-security` queues. |
| Checksum alerts | `config/storage-monitor.php['checksum_validation']` | Enables checksum verification alerts and thresholding window. |
| Container volumes | `docker-compose.yml` | `app`, workers, and scheduler share the `app-code` volume so `/var/www/html/storage` is common. |
## Operational Checklist
- **Before enabling archival in a new environment**
- Seed storage targets: `php artisan db:seed --class=MediaStorageTargetSeeder`.
- Run migrations so `event_media_assets` and related tables exist.
- Backfill existing photos into `event_media_assets` (custom script or artisan command) so archival jobs know about historical files.
- **Monitoring**
- Watch `storage:monitor` output (email or logs) for capacity warnings on hot disks.
- Use Horizon or Redis metrics to verify `media-storage` queue depth; thresholds live in `config/storage-monitor.php`.
- Review `/var/www/html/storage/logs/storage-jobs.log` (if configured) for archival failures.
- Checksum mismatches (hot→archive) are flagged by `storage:monitor` using `checksum_validation` thresholds.
- Ensure `media-security` queue stays below critical thresholds so uploads arent blocked awaiting security scans.
- **Troubleshooting uploads**
- Confirm hot disk is mounted and writable (`/var/www/html/storage/app/private/events/...`).
- Verify `media_storage_targets` contains an active `is_hot=true` entry; `EventStorageManager` falls back to default disk if none is assigned.
- Check Redis queue lengths; stalled `media-security` jobs prevent photos from being marked clean.
- **Troubleshooting archival**
- Run `php artisan storage:archive-pending --event=<ID> --force` to enqueue a specific event.
- Tail `docker compose logs -f media-storage-worker` for copy failures.
- Verify archive disk credentials (e.g., S3 keys) via `media_storage_targets.config`; missing or invalid settings surface as job failures with `status=failed`.
- If hot copies must remain, dispatch `ArchiveEventMediaAssets` with `$deleteSource=false` (custom job call).
- **Restoring media**
- Assign a hot storage target if none is active (`EventStorageManager::ensureAssignment($event)`).
- Dispatch a restore job or manually copy assets back from the archive disk, updating `event_media_assets.status` to `hot` or `restoring`.
## Related Documentation
- `docs/prp/10-storage-media-pipeline.md` — canonical architecture diagram for storage tiers.
- `docs/ops/queue-workers.md` — how to run `media-storage` and `media-security` workers (scripts live in `/scripts/`).
- `docs/ops/deployment/docker.md` / `docs/ops/deployment/dokploy.md` — container topology and volumes.
- `config/security.php`, `config/storage-monitor.php`, and `config/filesystems.php` for runtime knobs.
Keep this spec updated whenever the storage pipeline, queue names, or archive policies change so ops can quickly understand the flow end-to-end.

View File

@@ -0,0 +1,67 @@
## Guest Notification & Push Ops Guide
This runbook explains how to keep the guest notification centre healthy, roll out web push, and operate the new upload health alerts.
### 1. Database & config prerequisites
1. Run the latest migrations so the `push_subscriptions` table exists:
```bash
php artisan migrate --force
```
2. Generate VAPID keys (using `web-push` or any Web Push helper) and store them in the environment:
```
PUSH_ENABLED=true
PUSH_VAPID_PUBLIC_KEY=<base64-url-key>
PUSH_VAPID_PRIVATE_KEY=<base64-url-key>
PUSH_VAPID_SUBJECT="mailto:ops@example.com"
```
3. Redeploy the guest PWA (Vite build) so the runtime config exposes the new keys to the service worker.
### 2. Queue workers
Push deliveries are dispatched on the dedicated `notifications` queue. Ensure one of the queue workers listens to it:
```bash
/var/www/html/scripts/queue-worker.sh default,notifications
```
If Horizon is in use just add `notifications` to the list of queues for at least one supervisor. Monitor `storage/logs/notifications.log` (channel `notifications`) for transport failures.
### 3. Upload health alerts
The `storage:check-upload-queues` command now emits guest-facing alerts when uploads stall or fail repeatedly. Schedule it every 5 minutes via cron (see `cron/upload_queue_health.sh`) or the Laravel scheduler:
```
*/5 * * * * /var/www/html/cron/upload_queue_health.sh
```
Tune thresholds with the `STORAGE_QUEUE_*` variables in `.env` (see `.env.example` for defaults). When an alert fires, the tenant admin toolkit also surfaces the same issues.
### 4. Manual API interactions
- Register push subscription (from browser dev-tools):
```
POST /api/v1/events/{token}/push-subscriptions
Headers: X-Device-Id
Body: { endpoint, keys:{p256dh, auth}, content_encoding }
```
- Revoke subscription:
```
DELETE /api/v1/events/{token}/push-subscriptions
Body: { endpoint }
```
- Inspect per-guest state:
```bash
php artisan tinker
>>> App\Models\PushSubscription::where('event_id', 123)->get();
```
### 5. Smoke tests
After enabling push:
1. Join a published event, open the notification centre, and enable push (browser prompt must appear).
2. Trigger a host broadcast or upload-queue alert; confirm the browser shows a native notification and that the notification drawer refreshes without polling.
3. Temporarily stop the upload workers to create ≥5 pending assets; re-run `storage:check-upload-queues` and verify guests receive the “Uploads werden noch verarbeitet …” message.
Document any deviations in a bd issue note for future regressions.

View File

@@ -0,0 +1,131 @@
## Docker Queue & Horizon Setup
This directory bundles ready-to-use entrypoint scripts and deployment notes for running Fotospiels queue workers inside Docker containers. The examples assume you already run the main application in Docker (e.g. via `docker-compose.yml`) and share the same application image for workers. Queue entrypoints now live in `/scripts/` inside the container so every service can execute the same shell scripts.
### 1. Prepare the application image
Make sure the worker scripts are copied into the image and marked as executable:
```dockerfile
# Dockerfile (excerpt)
COPY scripts /var/www/html/scripts
RUN chmod +x /var/www/html/scripts/*.sh
```
If you keep the project root mounted as a volume during development the `chmod` step can be skipped because the files will inherit host permissions.
### 2. Queue worker containers
Add one or more worker services to `docker-compose.yml`. The production compose file in the repo already defines `queue` and `media-storage-worker` services that call these scripts; the snippet below shows the essential pattern if you need to tweak scaling.
```yaml
services:
queue-worker:
image: fotospiel-app # reuse the main app image
restart: unless-stopped
depends_on:
- redis # or your queue backend
environment:
APP_ENV: ${APP_ENV:-production}
QUEUE_CONNECTION: redis
QUEUE_TRIES: 3 # optional overrides
QUEUE_SLEEP: 3
command: >
/var/www/html/scripts/queue-worker.sh default
media-storage-worker:
image: fotospiel-app
restart: unless-stopped
depends_on:
- redis
environment:
APP_ENV: ${APP_ENV:-production}
QUEUE_CONNECTION: redis
QUEUE_TRIES: 5
QUEUE_SLEEP: 5
command: >
/var/www/html/scripts/queue-worker.sh media-storage
media-security-worker:
image: fotospiel-app
restart: unless-stopped
depends_on:
- redis
environment:
APP_ENV: ${APP_ENV:-production}
QUEUE_CONNECTION: redis
QUEUE_TRIES: 3
QUEUE_SLEEP: 5
command: >
/var/www/html/scripts/queue-worker.sh media-security
```
Scale workers by increasing `deploy.replicas` (Swarm) or adding `scale` counts (Compose v2).
> **Heads-up:** Guest push notifications are dispatched on the `notifications` queue. Either add that queue to the default worker (`queue-worker.sh default,notifications`) or create a dedicated worker so push jobs are consumed even when other queues are busy.
### 3. Optional: Horizon container
If you prefer Horizons dashboard and auto-balancing, add another service:
```yaml
services:
horizon:
image: fotospiel-app
restart: unless-stopped
depends_on:
- redis
environment:
APP_ENV: ${APP_ENV:-production}
QUEUE_CONNECTION: redis
command: >
/var/www/html/scripts/horizon.sh
```
Expose Horizon via your web proxy and protect it with authentication (the app already guards `/horizon` behind the super admin panel login if configured).
### 4. Environment variables
- `QUEUE_CONNECTION` — should match the driver configured in `.env` (`redis` recommended).
- `QUEUE_TRIES`, `QUEUE_SLEEP`, `QUEUE_TIMEOUT`, `QUEUE_MAX_TIME` — optional tuning knobs consumed by `queue-worker.sh`.
- `STORAGE_ALERT_EMAIL` — enables upload failure notifications introduced in the new storage pipeline.
- `SECURITY_SCAN_QUEUE` — overrides the queue name for the photo antivirus/EXIF worker (`media-security` by default).
- Redis / database credentials must be available in the worker containers exactly like the web container.
### 5. Bootstrapping reminder
Before starting workers on a new environment:
```bash
php artisan migrate
php artisan db:seed --class=MediaStorageTargetSeeder
```
Existing assets should be backfilled into `event_media_assets` with a one-off artisan command before enabling automatic archival jobs.
### 6. Monitoring & logs
- Containers log to STDOUT; aggregate via `docker logs` or a centralized stack.
- Horizon users can inspect `/horizon` for queue lengths and failed jobs.
- With plain workers run `php artisan queue:failed` (inside the container) to inspect failures and `php artisan queue:retry all` after resolving issues.
### 7. Rolling updates
When deploying new code:
1. Build and push updated app image.
2. Run migrations & seeders.
3. Recreate worker/horizon containers: `docker compose up -d --force-recreate queue-worker media-storage-worker horizon`.
4. Tail logs to confirm workers boot cleanly and start consuming jobs.
### 8. Running inside Dokploy
If you host Fotospiel on Dokploy:
- Create separate Dokploy applications for each worker type using the same image and command snippets above (`queue-worker.sh default`, `media-storage`, etc.).
- Attach the same environment variables and storage volumes defined for the main app.
- Use Dokploys one-off command feature to run migrations or `queue:retry`.
- Expose the Horizon service through the Dokploy HTTP proxy (or keep it internal and access via SSH tunnel).
- Enable health checks so Dokploy restarts workers automatically if they exit unexpectedly.
These services can be observed, redeployed, or reloaded from Dokploys dashboard and from the SuperAdmin integration powered by the Dokploy API.

View File

@@ -0,0 +1,81 @@
---
title: How-to Gäste können nicht hochladen
---
Dieses Howto beschreibt, wie du vorgehst, wenn Gäste melden, dass sie keine Fotos mehr hochladen können (Fehler im UploadFlow oder „hängenbleibende“ Uploads).
## 1. Problem eingrenzen
Fragen an den Tenant/Support:
- Betrifft es **alle** Gäste oder nur einzelne?
- Betrifft es **alle** Events oder nur ein bestimmtes Event?
- Welche Fehlermeldung erscheint im GuestFrontend (so genau wie möglich, gerne mit Screenshot)?
- Seit wann tritt das Problem auf? (Zeitfenster)
Diese Informationen bestimmen, ob du in Richtung API/RateLimit, Storage/Queues oder EventKonfiguration schauen musst.
## 2. Basischecks API & App
1. **Public-API Status**
- Teste manuell einen Upload gegen ein TestEvent oder reproduziere das Problem mit dem betroffenen JoinToken.
- Achte auf HTTPStatuscodes im BrowserNetworkTab (4xx vs. 5xx).
2. **App- / Deployment-Status**
- Prüfe in Docker/Dokploy, ob App/Queue/Redis/DBContainer gesund sind.
- Schaue in `storage/logs/laravel.log` nach offensichtlichen Exceptions rund um das gemeldete Zeitfenster.
Wenn die PublicAPI generell 5xx liefert, greift eher das PublicAPIIncidentPlaybook (`docs/ops/deployment/public-api-incident-playbook.md`).
## 3. Queues & Upload-Health
Wenn das Problem hauptsächlich Uploads betrifft (andere Funktionen laufen):
1. **Queue-Längen prüfen**
- In Horizon:
- `media-storage`, `media-security` und ggf. `notifications` QueueLängen ansehen.
- In Logs:
- Warnungen aus `storage:check-upload-queues` oder `storage-jobs` suchen.
2. **Upload-Health-Command**
- Sicherstellen, dass `storage:check-upload-queues` regelmäßig läuft (Cron / Scheduler).
- Manuell ausführen (in der AppContainerShell):
```bash
php artisan storage:check-upload-queues
```
- Ausgaben/Logs prüfen:
- Meldungen zu „stalled“ Uploads, Events mit dauerhaft vielen PendingAssets.
## 4. Storage & Limit-Probleme
1. **Hot-Storage-Füllstand**
- Prüfen, ob das StorageVolume/Bucket nahe an 100 % ist (siehe `docs/ops/dr-storage-issues.md`).
- Wenn ja:
- Archivierung beschleunigen (`storage:archive-pending` verifizieren).
- Kurzfristig Speicher vergrößern oder Caches aufräumen.
2. **Paket-/Limit-Prüfungen**
- Wenn nur bestimmte Events betroffen sind:
- PaketLimits des Events prüfen (z.B. max_photos/max_guests).
- EventStatus (abgelaufen/archiviert?) prüfen.
- Logs können Fehlercodes liefern wie „photo_limit_exceeded“ diese deuten auf bewusst ausgelöste LimitSperren hin, nicht auf technische Fehler.
## 5. Typische Muster & Gegenmaßnahmen
- **Hohe Fehlerrate beim Upload (5xx)**
- Hinweis auf API/BackendProblem:
- Siehe PublicAPIRunbook und AppLogs (Datenbank/RedisFehler, Timeouts).
- **Uploads bleiben „ewig“ auf „wird verarbeitet“**
- Queues laufen nicht oder `media-storage`/`media-security` steckt fest:
- Horizon prüfen, ob WorkerContainer laufen.
- Ggf. Worker neu starten und Failed Jobs analysieren.
- **Nur ein Event betroffen, andere funktionieren**
- Meist Limit oder KonfigThema (Paket voll, Galerie abgelaufen, Event deaktiviert).
- TenantAdminUI prüfen: EventStatus, PaketStatus, Data Lifecycle Einstellungen.
## 6. Kommunikation
- **An Tenant/Support zurückmelden**:
- Was war die Ursache? (z.B. PaketLimit, temporäre Überlastung, StorageKnappheit).
- Was wurde getan? (z.B. Paket angepasst, Queues neu gestartet, Storage erweitert).
- Ob und wie der Tenant/gäste weiteres tun müssen (z.B. Seite neu laden, später erneut probieren).
Für tiefere Ursachen rund um Storage siehe `docs/ops/media-storage-spec.md` und `docs/ops/dr-storage-issues.md`.

View File

@@ -0,0 +1,85 @@
---
title: How-to Photobooth lädt keine Fotos
---
Dieses Howto beschreibt, wie du vorgehst, wenn ein Tenant meldet, dass von der Photobooth keine Fotos im Event ankommen.
## 1. Problem eingrenzen
Fragen an den Tenant:
- Welcher Event ist betroffen? (EventID oder Titel).
- Wird im TenantAdmin unter „Fotobox-Uploads“ angezeigt, dass die Photobooth aktiviert ist?
- Sieht der PhotoboothOperator offensichtliche Fehler am Gerät (z.B. FTPFehler, Timeout)?
- Seit wann kommt nichts mehr an? (Zeitfenster)
Diese Infos helfen dir, zwischen Konfigurations, FTP oder IngestProblem zu unterscheiden.
## 2. Konfiguration im Admin prüfen
1. Im Tenant-Admin:
- Den betroffenen Event öffnen.
- Prüfen, ob die PhotoboothFunktion für diesen Event aktiviert ist.
2. Wenn Photobooth deaktiviert ist:
- Tenant bitten, sie im UI zu aktivieren (dies triggert die Provisionierung und Credentials).
- Danach erneut testen, ob Uploads ankommen.
## 3. FTP-/Control-Service überprüfen
Siehe auch `docs/ops/photobooth/control_service.md` und `docs/ops/photobooth/ops_playbook.md`.
1. **FTP-Erreichbarkeit**
- Host/Port aus den PhotoboothEinstellungen entnehmen.
- Testverbindung (z.B. über lokales FTPTool oder `nc`/`telnet`) herstellen:
- Port (z.B. 2121) erreichbar?
2. **Credentials validieren**
- Prüfen, ob Username/Passwort im TenantAdmin zu den ControlServiceDaten passen.
- Bei Verdacht auf Fehler:
- Im Admin die Zugangsdaten neu generieren lassen.
- Tenant/PhotoboothTeam informieren, dass sie die neuen Credentials konfigurieren müssen.
## 4. Ingest-Service & Scheduler prüfen
Die Photobooth legt Dateien zunächst in einem ImportPfad ab, der dann vom IngestService verarbeitet wird.
1. **Import-Verzeichnis prüfen**
- Pfad: üblicherweise `storage/app/photobooth/{tenant}/{event}` (siehe `docs/ops/photobooth/README.md`).
- In den Logs kontrollieren, ob neue Dateien dort landen.
2. **Ingest-Command**
- Sicherstellen, dass `photobooth:ingest` regelmäßig läuft (Scheduler/Cron):
```bash
php artisan photobooth:ingest --max-files=100
```
- Optional: für einen konkreten Event:
```bash
php artisan photobooth:ingest --event=EVENT_ID --max-files=50 -vv
```
- Logs auf Hinweise prüfen:
- Fehler beim Lesen der FTPDateien.
- Probleme beim Schreiben in den HotStorage.
3. **Queues**
- Verifizieren, dass relevante Queues laufen (falls Ingest Jobs dispatcht).
## 5. Typische Fehlerbilder & Lösungen
- **FTP erreicht, aber Import-Verzeichnis bleibt leer**
- PhotoboothSoftware schreibt nicht an den erwarteten Pfad → Pfad in der PhotoboothKonfiguration mit den Angaben aus `PHOTOBOOTH_IMPORT_ROOT` abgleichen.
- Evtl. Berechtigungsproblem im FTPContainer (Perms/Ownership).
- **Import-Verzeichnis gefüllt, aber nichts im Event**
- `photobooth:ingest` läuft nicht oder bricht ab:
- Scheduler prüfen (`scheduler`Service in Docker/Dokploy).
- Kommando manuell ausführen und Fehler analysieren.
- **Fotos tauchen mit großer Verzögerung auf**
- Ingest läuft zu selten (Cron/Intervalle zu groß).
- Events haben hohe Medienlast → `--max-files` erhöhen oder Ingest häufiger anstoßen.
## 6. Kommunikation mit dem Tenant
- Sobald Ursache und Fix klar sind:
- Tenant informieren, ob es ein Konfig, Netzwerk oder IngestProblem war.
- Falls nötig, dem PhotoboothTeam neue Credentials/Anweisungen zukommen lassen.
- Falls einige Dateien irreversibel verloren gegangen sind:
- Transparent kommunizieren und ggf. Kulanzlösungen (z.B. Gutschrift) über Finance/Success abstimmen.
Nutze für tiefere Analysen die ausführlicheren Playbooks in `docs/ops/photobooth/ops_playbook.md`.

View File

@@ -0,0 +1,6 @@
---
title: Photobooth
type: group
order: 40
icon: heroicon-o-camera
---

View File

@@ -0,0 +1,147 @@
# Photobooth FTP Ingestion
This guide explains how to operate the Photobooth FTP workflow endtoend: provisioning FTP users for tenants, running the ingest pipeline, and exposing photobooth photos inside the Guest PWA.
## Architecture Overview
1. **vsftpd container** (port `2121`) accepts uploads into a shared volume (default `/var/www/storage/app/photobooth`). Each event receives isolated credentials and a dedicated directory.
2. **Control Service** (REST) provisions FTP accounts. Laravel calls it during enable/rotate/disable actions.
3. **Photobooth settings** (Filament SuperAdmin) define global port, rate limit, expiry grace, and Control Service connection.
4. **Ingest command** copies uploaded files into the events storage disk, generates thumbnails, records `photos.ingest_source = photobooth`, and respects package quotas.
5. **Guest PWA filter** consumes `/api/v1/events/{token}/photos?filter=photobooth` to render the “Fotobox” tab. Sparkbooth uploads reuse this filter via `ingest_source = sparkbooth`.
```
Photobooth -> FTP (vsftpd) -> photobooth disk
photobooth:ingest (queue/scheduler)
-> Event media storage (public disk/S3)
-> packages_usage, thumbnails, security scan
Sparkbooth -> HTTP upload endpoint -> ingest (direct, no staging disk)
```
## Environment Variables
Add the following to `.env` (already scaffolded in `.env.example`):
```env
PHOTOBOOTH_CONTROL_BASE_URL=https://control.internal/api
PHOTOBOOTH_CONTROL_TOKEN=your-control-token
PHOTOBOOTH_CONTROL_TIMEOUT=5
PHOTOBOOTH_FTP_HOST=ftp.internal
PHOTOBOOTH_FTP_PORT=2121
PHOTOBOOTH_USERNAME_PREFIX=pb
PHOTOBOOTH_USERNAME_LENGTH=8
PHOTOBOOTH_PASSWORD_LENGTH=8
PHOTOBOOTH_RATE_LIMIT_PER_MINUTE=20
PHOTOBOOTH_EXPIRY_GRACE_DAYS=1
PHOTOBOOTH_IMPORT_DISK=photobooth
PHOTOBOOTH_IMPORT_ROOT=/var/www/storage/app/photobooth
PHOTOBOOTH_IMPORT_MAX_FILES=50
PHOTOBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp
# Sparkbooth defaults (optional overrides)
SPARKBOOTH_ALLOWED_EXTENSIONS=jpg,jpeg,png,webp
SPARKBOOTH_MAX_SIZE_KB=8192
SPARKBOOTH_RATE_LIMIT_PER_MINUTE=20
SPARKBOOTH_RESPONSE_FORMAT=json
```
### Filesystem Disk
`config/filesystems.php` registers a `photobooth` disk that must point to the shared volume where vsftpd writes files. Mount the same directory inside both the FTP container and the Laravel app container.
## Control Service Contract
Laravel expects the Control Service to expose:
```
POST /users { username, password, path, rate_limit_per_minute, expires_at, ftp_port }
POST /users/{username}/rotate { password, rate_limit_per_minute, expires_at }
DELETE /users/{username}
POST /config { ftp_port, rate_limit_per_minute, expiry_grace_days }
```
Authentication is provided via `PHOTOBOOTH_CONTROL_TOKEN` (Bearer token).
## Scheduler & Commands
| Command | Purpose | Default schedule |
|---------|---------|------------------|
| `photobooth:ingest [--event=ID] [--max-files=N]` | Pulls files from the Photobooth disk and imports them into the event storage. | every 5 minutes |
| `photobooth:cleanup-expired` | De-provisions FTP accounts after their expiry. | hourly |
You can run the ingest job manually for a specific event:
```bash
php artisan photobooth:ingest --event=123 --max-files=20
```
## Sparkbooth HTTP Uploads (Custom Upload)
Use this when Sparkbooth runs in “Custom Upload” mode instead of FTP.
- Endpoint: `POST /api/v1/photobooth/upload`
- Auth: per-event username/password (set in Event Admin → Fotobox-Uploads; switch mode to “Sparkbooth”).
- Body (multipart/form-data): `media` (file or base64), `username`, `password`, optionally `name`, `email`, `message`.
- Response:
- JSON success: `{"status":true,"error":null,"url":null}`
- JSON failure: `{"status":false,"error":"Invalid credentials"}`
- XML (if `format=xml` or event preference is XML):
- Success: `<rsp status="ok" url="..."/>`
- Failure: `<rsp status="fail"><err msg="Invalid credentials" /></rsp>`
- Limits: allowed extensions reuse photobooth defaults; max size `SPARKBOOTH_MAX_SIZE_KB` (default 8 MB); per-event rate limit `SPARKBOOTH_RATE_LIMIT_PER_MINUTE` (fallback to photobooth rate limit).
- Ingest: writes straight to the events hot storage, applies thumbnail/watermark/security scan, sets `photos.ingest_source = sparkbooth`.
Example cURL (JSON response):
```bash
curl -X POST https://app.example.com/api/v1/photobooth/upload \
-F "media=@/path/to/photo.jpg" \
-F "username=PB123" \
-F "password=SECRET" \
-F "message=Wedding booth"
```
Example cURL (request XML response):
```bash
curl -X POST https://app.example.com/api/v1/photobooth/upload \
-F "media=@/path/to/photo.jpg" \
-F "username=PB123" \
-F "password=SECRET" \
-F "format=xml"
```
## Tenant Admin UX
Inside the Event Admin PWA, go to **Event → Fotobox-Uploads** to:
1. Enable/disable the Photobooth link.
2. Rotate credentials (max 10-char usernames, 8-char passwords).
3. Switch mode (FTP or Sparkbooth), view rate limit + expiry info, copy ftp:// or POST URL + creds.
## Guest PWA Filter
The Guest gallery now exposes a “Fotobox” tab (both preview card and full gallery). API usage:
```
GET /api/v1/events/{token}/photos?filter=photobooth
Headers: X-Device-Id (optional)
```
Response items contain `ingest_source`, allowing the frontend to toggle photobooth-only views.
## Operational Checklist
1. **Set env vars** from above and restart the app.
2. **Ensure vsftpd + Control Service** are deployed; verify port 2121 and REST endpoint connectivity.
3. **Mount shared volume** to `/var/www/storage/app/photobooth` (or update `PHOTOBOOTH_IMPORT_ROOT` + `filesystems.disks.photobooth.root`).
4. **Run migrations** (`php artisan migrate`) to create settings/event columns.
5. **Seed default storage target** (e.g., `MediaStorageTarget::create([... 'key' => 'public', ...])`) in non-test environments if not present.
6. **Verify scheduler** (Horizon or cron) is running commands `photobooth:ingest` and `photobooth:cleanup-expired`.
7. **Test end-to-end**: enable Photobooth on a staging event, upload a file via FTP, wait for ingest, and confirm it appears under the Fotobox filter in the PWA.
8. **Test Sparkbooth**: switch event mode to Sparkbooth, copy Upload URL/user/pass, send a sample POST (or real Sparkbooth upload), verify it appears under the Fotobox filter.

View File

@@ -0,0 +1,100 @@
# Photobooth Control Service API
The control service is a lightweight sidecar responsible for provisioning vsftpd accounts. Laravel talks to it via REST whenever an Event Admin enables, rotates, or disables the Photobooth feature.
## Authentication
- **Scheme:** Bearer token.
- **Header:** `Authorization: Bearer ${PHOTOBOOTH_CONTROL_TOKEN}`.
- **Timeout:** Configurable via `PHOTOBOOTH_CONTROL_TIMEOUT` (default 5s).
- **Token generation:** `openssl rand -hex 32` (or `php -r "echo bin2hex(random_bytes(32)), "`); store in `.env`/Dokploy secrets as `PHOTOBOOTH_CONTROL_TOKEN`.
## Endpoints
| Method & Path | Description |
|---------------|-------------|
| `POST /users` | Create a new FTP account for an event. |
| `POST /users/{username}/rotate` | Rotate credentials / extend expiry for an existing user. |
| `DELETE /users/{username}` | Remove an FTP account (called when an event disables Photobooth or expires). |
| `POST /config` | Optionally push global config changes (port, rate-limit, expiry grace) to the control service. |
### `POST /users`
```json
{
"username": "pbA12345",
"password": "F4P9K2QX",
"path": "tenant-slug/123",
"rate_limit_per_minute": 20,
"expires_at": "2025-06-15T22:59:59Z",
"ftp_port": 2121,
"allowed_ip_ranges": ["1.2.3.4/32"],
"metadata": {
"event_id": 123,
"tenant_id": 5
}
}
```
**Response:** `201 Created` with `{ "ok": true }`. On failure return 4xx/5xx JSON with `error.code` + `message`.
Implementation tips:
- Ensure the system user or virtual users home directory is set to the provided `path` (prefixed with the shared Photobooth root).
- Apply the rate limit token-bucket before writing to disk (or integrate with HAProxy).
- Store `expires_at` and automatically disable the account when reached (in addition to Laravels scheduled cleanup).
Reference implementation (current stack): `photobooth-ftp` builds from `docker/photobooth-control`, starts pure-ftpd and a Node-based control API on port 8080. Healthcheck: `GET /health` on 8080 (wired in docker-compose.dokploy.yml). Rate-limit/expiry enforcement inside the FTP tier is still to be implemented.
### `POST /users/{username}/rotate`
```json
{
"password": "K9M4T6QZ",
"rate_limit_per_minute": 20,
"expires_at": "2025-06-16T22:59:59Z"
}
```
- Rotate the password atomically and respond with `{ "ok": true }`.
- If username does not exist return `404` with a descriptive message so Laravel can re-provision.
### `DELETE /users/{username}`
No request body. Delete or disable the FTP account, removing access to the assigned directory.
### `POST /config`
Optional hook used when SuperAdmins change defaults:
```json
{
"ftp_port": 2121,
"rate_limit_per_minute": 20,
"expiry_grace_days": 1
}
```
Use this to reload vsftpd or adjust proxy rules without redeploying the control service.
## Error Contract
Return JSON structured as:
```json
{
"error": {
"code": "user_exists",
"message": "Username already provisioned",
"context": { "username": "pbA12345" }
}
}
```
Laravel treats any non-2xx as fatal and logs the payload (sans password). Prefer descriptive `code` values: `user_exists`, `user_not_found`, `rate_limit_violation`, `invalid_payload`, etc.
## Observability
- Emit structured logs for every create/rotate/delete with event + tenant IDs.
- Expose `/health` so Laravel (or uptime monitors) can verify connectivity.
- Consider metrics (e.g., Prometheus) for active accounts, rotations, and failures.

View File

@@ -0,0 +1,53 @@
# Photobooth Operations Playbook
Use this checklist when bringing Photobooth FTP online for a tenant or debugging ingest issues.
## 1. Provisioning Flow
1. **SuperAdmin config** set defaults in Filament → Platform Management → Photobooth Settings.
2. **Tenant enablement** Event Admin opens the event → Fotobox-Uploads → “Photobooth aktivieren”.
3. Laravel generates credentials and calls the control service (`POST /users`).
4. vsftpd accepts uploads at `ftp://username:password@HOST:PORT/`.
5. `photobooth:ingest` copies files into the hot storage disk and applies moderation/security pipelines.
## 2. Troubleshooting
| Symptom | Action |
|---------|--------|
| Tenants Photobooth page shows “Deaktiviert” immediately | Check `storage/logs/laravel.log` for control-service errors; re-run `photobooth:ingest --event=ID -vv`. |
| Files remain under `/storage/app/photobooth/<tenant>/<event>` | Ensure scheduler (Horizon/cron) runs `photobooth:ingest`; run manual command to force ingestion. |
| Photos missing from guest “Fotobox” tab | Confirm `photos.ingest_source = photobooth` and that `/api/v1/events/{token}/photos?filter=photobooth` returns data. |
| Rate-limit complaints | Inspect control service logs; adjust `PHOTOBOOTH_RATE_LIMIT_PER_MINUTE` and re-save settings (fires `/config`). |
| Credentials leaked/compromised | Click “Zugang neu generieren” in Event Admin; optional `php artisan photobooth:cleanup-expired --event=ID` to force deletion before expiry. |
## 3. Command Reference
```bash
# Manually ingest pending files for a single event
php artisan photobooth:ingest --event=123 --max-files=100
# Check ingest for all active events (dry run)
php artisan photobooth:ingest --max-files=10
# Remove expired accounts (safe to run ad hoc)
php artisan photobooth:cleanup-expired
```
## 4. Pre-flight Checklist for New Deployments
1. `php artisan migrate`
2. Configure `.env` Photobooth variables.
3. Mount shared Photobooth volume in all containers (FTP + Laravel).
4. Verify `MediaStorageTarget` records exist (hot target pointing at the hot disk).
5. Seed baseline emotions (Photobooth ingest assigns `emotion_id` from existing rows).
6. Confirm scheduler runs (Horizon supervisor or system cron).
## 5. Incident Response
1. **Identify scope** which events/tenants are affected? Check ingestion logs for specific usernames/path.
2. **Quarantine** disable the Photobooth toggle for impacted events via Admin UI.
3. **Remediate** fix FTP/control issues, rotate credentials, run `photobooth:ingest`.
4. **Audit** review `photobooth_metadata` on events and `photos.ingest_source`.
5. **Communicate** notify tenant admins via in-app message or email template referencing incident ID.
Keep this playbook updated whenever infra/process changes. PRs to `/docs/ops/photobooth` welcome.

View File

@@ -0,0 +1,6 @@
---
title: Billing
type: group
order: 50
icon: heroicon-o-credit-card
---

View File

@@ -0,0 +1,205 @@
---
title: Billing & Zahlungs-Operationen
---
Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Lemon Squeezy/RevenueCatWebhooks und PaketInkonsistenzen operativ umzugehen ist.
## 1. Komponentenüberblick
- **Lemon Squeezy**
- Abwicklung von WebCheckout, Paketen und Subscriptions.
- Webhooks für Bestellungen, Verlängerungen, Stornos.
- **Fotospiel Backend**
- Modelle wie `Tenant`, `Packages`, `tenant_packages`, `event_packages`.
- Services/Jobs zur PaketZuweisung, LimitBerechnung und Nutzungstracking.
> Details zur Architektur findest du in den PRPKapiteln (Billing/Freemium) sowie in den bd-Issues zur Lemon SqueezyMigration und zum KatalogSync.
## 2. Typische Problemszenarien
- **Webhook kommt nicht an / schlägt fehl**
- Symptom: Lemon Squeezy zeigt Zahlung „completed/paid“, TenantPaket im Backend bleibt unverändert.
- Checkliste:
- Logs der WebhookRoutes prüfen (Statuscodes, Exceptions).
- Endpoint: `POST /lemonsqueezy/webhook``LemonSqueezyWebhookController::handle()`.
- Controller ruft `CheckoutWebhookService::handleLemonSqueezyEvent()` auf.
- WebhookReplay über das Lemon Squeezy Dashboard auslösen (für einzelne Events).
- QueueStatus prüfen:
- Falls Webhooks über Queues verarbeitet werden, auf `default`/`billing`Queues achten (je nach Konfiguration).
- **Doppelte oder fehlende Abbuchungen**
- Abgleich von ZahlungsproviderDaten (Lemon Squeezy/RevenueCat) mit internem Ledger.
- Bei doppelten Buchungen: Prozess definieren (Refund via Lemon Squeezy, Anpassung im Ledger).
- Bei fehlenden Buchungen: ggf. manuelle Paketzuweisung nach erfolgter Zahlung.
- **Pakete/Limits passen nicht zur Realität**
- Tenant meldet: „Paket falsch“, „Galerie schon abgelaufen“ o.Ä.
- Prüfen:
- Aktives Paket (`tenant_packages`, `event_packages`).
- LimitZähler (`used_photos`, `used_events`) und aktuelle Nutzung.
- Letzte relevante Webhooks/Jobs (z.B. vor kurzem migriert?).
## 3. Operative Schritte bei Payment Incidents
1. **Event/Tenant identifizieren**
- IDs und relevante PaketInfos aus DB/Admin UI holen.
2. **Provider-Status prüfen**
- Lemon SqueezyDashboard: ist die Zahlung dort korrekt verbucht? (Order/SubscriptionAnsicht).
3. **Backend-Status prüfen**
- Paketzuweisung und Limits in der DB (Readonly!) inspizieren:
- `checkout_sessions` wurde die Session korrekt auf `completed` gesetzt? (`provider = lemonsqueezy`, `lemonsqueezy_order_id` gefüllt?)
- `package_purchases` existiert ein Eintrag für Tenant/Package mit erwarteter ProviderReferenz?
- `tenant_packages` stimmt der `active`Status und `expires_at` mit dem erwarteten Abostatus überein?
4. **Entscheidung**
- Automatische Nachverarbeitung via WebhookReplay/JobRetry:
- Lemon SqueezyEvents erneut senden lassen, ggf. `tests/api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy` (in TestUmgebungen) nutzen.
- Notfall: manuelle PaketAnpassung (nur mit klar dokumentierter Begründung):
- Paket in `tenant_packages` aktivieren/verlängern und `package_purchases` sauber nachziehen.
5. **Dokumentation**
- Vorgang im Ticket oder als bd-Issue festhalten, falls wiederkehrend.
> TODO: Ergänze konkrete Tabellen-/Modellnamen und die relevanten Jobs/Artisan Commands, sobald Lemon Squeezy/RevenueCat Migration finalisiert ist.
## 4. Zusammenarbeit mit Finance/Support
- Klar definieren, wer Rückerstattungen freigibt und durchführt.
- Playbook für Support:
- Welche Informationen sie sammeln sollen, bevor sie an Ops eskalieren (TenantID, EventID, PaymentProviderReferenz, Zeitstempel).
- Welche Standardantworten es gibt (z.B. „Zahlung in Prüfung, Paket kurzfristig manuell freigeschaltet“).
## 5. Hinweise zur Implementierung
- **Konfiguration**
- Lemon SqueezyKeys, WebhookSecrets und FeatureFlags sollten ausschließlich in `.env`/ConfigDateien liegen und niemals im Code/Logs landen.
- Sandbox vs. LiveKeys klar trennen; Ops sollte wissen, welche Umgebung gerade aktiv ist.
- **Sicherheit**
- WebhookSignaturen und Timestamps prüfen; bei verdächtigen Mustern (z.B. ReplayAngriffe) SecurityRunbooks konsultieren.
- Keine sensiblen PaymentDetails in ApplikationsLogs ausgeben.
Diese Sektion ist bewusst generisch gehalten, damit sie auch nach Implementation der finalen BillingArchitektur noch passt. Details zu Tabellen/Jobs sollten ergänzt werden, sobald die Lemon SqueezyMigration abgeschlossen ist.
## 6. Konkrete Lemon Squeezy-Flows im System
### 6.1 Checkout-Erstellung
- Marketing-Checkout / API:
- `MarketingController` und `PackageController` nutzen `LemonSqueezyCheckoutService::createCheckout()` (`App\Services\LemonSqueezy\LemonSqueezyCheckoutService`).
- Der Service:
- Baut `custom_data` (`tenant_id`, `package_id`, optional `checkout_session_id`) für spätere Zuordnung.
- Ruft `POST /checkouts` im Lemon SqueezyAPI auf und erhält eine `checkout_url`.
- Ops-Sicht:
- Wenn `lemonsqueezy_variant_id` bei einem Package fehlt, wird kein Checkout erzeugt MarketingUI zeigt entsprechende Fehlertexte (siehe `resources/lang/*/marketing.php`).
- Bei wiederkehrenden „checkout failed“Fehlern die Logs (`LemonSqueezyCheckoutService`, Controller) und PackageKonfiguration prüfen.
### 6.2 Webhook-Verarbeitung & Idempotenz
- Endpoint: `POST /lemonsqueezy/webhook``LemonSqueezyWebhookController::handle()`.
- Service: `CheckoutWebhookService` (`App\Services\Checkout\CheckoutWebhookService`):
- Unterscheidet zwischen **OrderEvents** (`order_*`) und **SubscriptionEvents** (`subscription_*`).
- Idempotenz:
- Nutzt ein CacheLock (`checkout:webhook:lemonsqueezy:{order_id|session_id}`), um parallele Verarbeitung desselben Events zu verhindern.
- Schreibt Metadaten (`lemonsqueezy_last_event`, `lemonsqueezy_status`, `lemonsqueezy_checkout_id`, `lemonsqueezy_order_id`) in `checkout_sessions.provider_metadata`.
- Ergebnis:
- Bei `order_created`/`order_updated` mit Status `paid`:
- `CheckoutSession` wird als `processing` markiert.
- `CheckoutAssignmentService::finalise()` weist Paket/Tenant zu.
- Session wird auf `completed` gesetzt.
- Bei `order_payment_failed` / `order_refunded`:
- Session wird auf `failed` gesetzt, Coupons werden als fehlgeschlagen markiert.
### 6.2.1 SandboxWebhook registrieren
- Command (TestMode aktiv):
- `php artisan lemonsqueezy:webhooks:register --test-mode`
- Optional mit URL-Override:
- `php artisan lemonsqueezy:webhooks:register --url=https://staging.example.com/lemonsqueezy/webhook --test-mode`
- EventListe kommt aus `config/lemonsqueezy.php` (`webhook_events`). Override möglich:
- `php artisan lemonsqueezy:webhooks:register --events=order_created --events=subscription_created`
### 6.2.2 Verarbeitete Lemon Squeezy-Events
Die WebhookHandler erwarten mindestens diese Events:
- `order_created`
- `order_updated`
- `order_payment_failed`
- `order_refunded`
- `subscription_created`
- `subscription_updated`
- `subscription_cancelled`
- `subscription_expired`
- `subscription_paused`
### 6.3 Subscriptions & TenantPackages
- SubscriptionEvents (`subscription_*`) werden ebenfalls von `CheckoutWebhookService` behandelt:
- Tenant wird aus `metadata.tenant_id` oder `lemonsqueezy_customer_id` ermittelt.
- Package wird über `metadata.package_id` oder `lemonsqueezy_variant_id` aufgelöst.
- `TenantPackage` wird erstellt/aktualisiert (`lemonsqueezy_subscription_id`, `expires_at`, `active`).
- `Tenant.subscription_status` und `subscription_expires_at` werden gesteuert.
- Ops-Sicht:
- Bei abweichenden Abostatus (z.B. Lemon Squeezy zeigt „active“, Tenant nicht):
- SubscriptionEvents im Lemon SqueezyDashboard prüfen.
- Letzte `subscription_*`Events in den Logs, `TenantPackage` und `Tenant`Felder gegenprüfen.
### 6.4 Paket- & Coupon-Synchronisation
- Pakete:
- ArtisanCommand `lemonsqueezy:sync-packages` (`App\Console\Commands\LemonSqueezySyncPackages`) stößt für ausgewählte oder alle Pakete `SyncPackageToLemonSqueezy`/`PullPackageFromLemonSqueezy` Jobs an.
- SyncJobs nutzen `LemonSqueezyCatalogService`, um Produkte/Preise in Lemon Squeezy zu erstellen/aktualisieren und `lemonsqueezy_product_id`/`lemonsqueezy_variant_id` lokal zu pflegen.
- Coupons:
- `SyncCouponToLemonSqueezy`Job spiegelt interne CouponKonfiguration in Lemon Squeezy Discounts (`LemonSqueezyDiscountService`).
- Ops-Sicht:
- Bei KatalogAbweichungen `lemonsqueezy:sync-packages --dry-run` verwenden, um Snapshots zu prüfen, bevor tatsächliche Änderungen gesendet werden.
- Fehlgeschlagene Syncs in den Logs (`Lemon Squeezy package sync failed`, `Lemon Squeezy addon sync failed`, `Failed syncing coupon to Lemon Squeezy`) beobachten.
### 6.5 Recovery-Playbook: KatalogSync fehlgeschlagen
Wenn der KatalogSync fehlschlägt oder Pakete nicht mehr korrekt verknüpft sind:
1. **Status im Admin prüfen**
- `PackageResource` → Felder `lemonsqueezy_sync_status`, `lemonsqueezy_synced_at` und `Letzter Fehler`.
- Fehlermeldung stammt aus `lemonsqueezy_snapshot.error.message`.
2. **Trockenlauf ausführen**
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --dry-run`
- Prüfe die erzeugten PayloadSnapshots (in `lemonsqueezy_snapshot`) auf falsche IDs/Preise.
3. **Mapping prüfen**
- Falls `lemonsqueezy_product_id` oder `lemonsqueezy_variant_id` fehlt oder falsch ist: im PaketAdmin korrigieren.
- BulkSync blockiert unmapped Pakete; gezielte Korrektur vor dem nächsten Lauf ist Pflicht.
4. **Sync erneut anstoßen**
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --queue`
- Bei Bedarf: `--allow-unmapped` nur bewusst verwenden (z.B. initiales Mapping).
5. **Pull für Abgleich**
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --pull` zum Abgleich mit Lemon Squeezy.
6. **Logs prüfen**
- Erwartete Logeinträge: `Lemon Squeezy package sync failed`, `Lemon Squeezy addon sync failed`, `Failed syncing coupon to Lemon Squeezy`.
- Achte auf wiederkehrende Fehler (z.B. invalid product/price IDs).
Diese Untersektion soll dir als Operator helfen zu verstehen, wie Lemon SqueezyAktionen im System abgebildet sind und an welchen Stellen du im Fehlerfall ansetzen kannst.
## 7. Production Cutover: Lemon Squeezy Migration
Diese Checkliste beschreibt den kontrollierten Wechsel auf Lemon Squeezy in Produktion.
1. **Vorbereitung (T1 Woche)**
- Confirm: `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_WEBHOOK_SECRET`, `LEMONSQUEEZY_TEST_MODE=false`.
- Package IDs validieren: alle aktiven Packages haben `lemonsqueezy_product_id` und `lemonsqueezy_variant_id`.
- `lemonsqueezy:sync-packages --dry-run` auf eine Stichprobe anwenden.
- EventListe prüfen: `config/lemonsqueezy.php` (`webhook_events`).
2. **Staging Smoke (T2 Tage)**
- `lemonsqueezy:webhooks:register --test-mode` auf Staging ausführen.
- Testkauf via Lemon Squeezy Test Mode und Webhook Replay verifizieren.
3. **Cutover Window (T0)**
- MarketingCheckout kurz einfrieren (kein Checkout waehrend der Umschaltung).
- Production Webhook registrieren:
- `lemonsqueezy:webhooks:register --url=https://<prod-domain>/lemonsqueezy/webhook`
- Queue worker laufen lassen (Queue: `webhooks`/`billing` sofern konfiguriert).
4. **Activation**
- Erstes ProduktionsCheckout ausfuehren.
- Verify: `checkout_sessions.provider_metadata` wird mit `lemonsqueezy_*` Feldern befuellt.
- Verify: `TenantPackage` aktiv und `subscription_status` korrekt.
5. **Rollback (falls notwendig)**
- Checkout wieder deaktivieren (MarketingCheckout ausblenden).
- Lemon Squeezy Webhook Destination im Lemon Squeezy Dashboard deaktivieren.
- Status und Logs sichern (Webhooks, `lemonsqueezy-sync` Log).
6. **PostCutover (T+1)**
- Stichproben auf neue Tenants/Packages.
- Monitoring: Fehlgeschlagene Webhooks, SyncFehler, Support Tickets.

View File

@@ -0,0 +1,92 @@
---
title: How-to Zahlung erfolgreich, Paket nicht aktiv
---
Dieses Howto beschreibt, wie du vorgehst, wenn ein Tenant meldet: „Zahlung war erfolgreich, aber mein Paket ist nicht aktiv / Galerie bleibt limitiert.“
## 1. Informationen vom Tenant einsammeln
Bevor du nachschaust:
- TenantID oder TenantSlug.
- Betroffenes Paket (Name oder Beschreibung, z.B. „ProPaket 79 €“).
- Zeitpunkt der Zahlung (Datum/Uhrzeit, ggf. Screenshot).
- Ggf. Auszug aus der Lemon SqueezyBestätigung (ohne vollständige Kartendaten!).
Diese Infos erlauben dir, die korrekte Transaktion sowohl in Lemon Squeezy als auch im Backend zu finden.
## 2. Lemon Squeezy-Status prüfen
1. Im Lemon SqueezyDashboard:
- Suche nach EMail, TenantName oder der vom Tenant genannten OrderID.
- Stelle sicher, dass die Zahlung dort als „paid“/„completed“ markiert ist.
2. Notiere:
- Lemon SqueezyOrderID und ggf. CheckoutID.
- Status (paid/processing/failed/cancelled).
Wenn Lemon Squeezy die Zahlung nicht als erfolgreich zeigt, ist dies primär ein Finance/CustomerTopic ggf. mit Customer Support klären, ob eine neue Zahlung oder Klärung mit dem Kunden notwendig ist.
## 3. Backend-Status prüfen
Mit bestätigter Zahlung in Lemon Squeezy:
1. `checkout_sessions`:
- Suche nach Sessions des Tenants (`tenant_id`) mit dem betroffenen `package_id`:
- Achte auf `status` (erwartet `completed`) und `provider = lemonsqueezy`.
- Prüfe `provider_metadata` auf `lemonsqueezy_last_event`, `lemonsqueezy_status`, `lemonsqueezy_checkout_id`, `lemonsqueezy_order_id`.
- Wenn du die Session über Lemon SqueezyMetadaten finden möchtest:
- `lemonsqueezy_checkout_id` oder `lemonsqueezy_order_id` verwenden.
2. `package_purchases`:
- Prüfe, ob ein Eintrag für `(tenant_id, package_id)` mit passender ProviderReferenz existiert:
- z.B. `provider = 'lemonsqueezy'`, `provider_id` = OrderID.
3. `tenant_packages`:
- Prüfe, ob es einen aktiven Eintrag für `(tenant_id, package_id)` gibt:
- `active = 1`, `expires_at` in der Zukunft.
## 4. Webhook-/Verarbeitungsstatus untersuchen
Wenn `checkout_sessions` noch nicht auf `completed` steht oder `tenant_packages` nicht aktualisiert wurden:
1. Logs prüfen:
- `storage/logs/laravel.log` und ggf. `billing`Channel.
- Suche nach Einträgen von `LemonSqueezyWebhookController` / `CheckoutWebhookService` rund um den Zahlungszeitpunkt.
2. Typische Ursachen:
- Webhook nicht zugestellt (Netzwerk/SSL).
- Webhook konnte die Session nicht auflösen (`[CheckoutWebhook] Lemon Squeezy session not resolved`).
- IdempotenzLock (`Lemon Squeezy lock busy`) hat dazu geführt, dass Event nur teilweise verarbeitet wurde.
## 5. Korrektur-Schritte
### 5.1 Automatischer Replay (empfohlen)
1. Im Lemon SqueezyDashboard:
- Den betreffenden `order_*`Event finden.
- WebhookReplay auslösen.
2. In den Logs beobachten:
- Ob `CheckoutWebhookService::handleLemonSqueezyEvent()` diesmal die Session findet und `CheckoutAssignmentService::finalise()` ausführt.
3. Nochmal `checkout_sessions` und `tenant_packages` prüfen:
- Session sollte auf `completed` stehen, Paket aktiv sein.
### 5.2 Manuelle Korrektur (Notfall)
Nur anwenden, wenn klare Freigabe vorliegt und Lemon Squeezy die Zahlung eindeutig als erfolgreich listet.
1. `tenant_packages` aktualisieren:
- Entweder neuen Eintrag anlegen oder bestehenden für `(tenant_id, package_id)` so setzen, dass:
- `active = 1`,
- `purchased_at` und `expires_at` zu Lemon SqueezyDaten passen.
2. `package_purchases` ergänzen:
- Sicherstellen, dass die Zahlung als Zeile mit `provider = 'lemonsqueezy'`, `provider_id = OrderID` und passender `price` existiert (für spätere Audits).
3. Konsistenz prüfen:
- Admin UI für Tenant öffnen und prüfen, ob Limits/Paketstatus jetzt korrekt angezeigt werden.
4. Dokumentation:
- Den Vorgang im Ticket oder als bd-Issue (falls wiederkehrend) dokumentieren.
## 6. Kommunikation mit dem Tenant
- Sobald der BackendStatus korrigiert ist:
- Kurz bestätigen, dass das Paket aktiv ist und welche Auswirkungen das hat (z.B. neue Limits, verlängerte Galerie).
- Falls Lemon Squeezy die Zahlung nicht als erfolgreich führt:
- Ehrlich kommunizieren, dass laut Zahlungsprovider noch keine endgültige Zahlung vorliegt und welche Optionen es gibt (z.B. neue Zahlung, Klärung mit Bank/Kreditkarte).
Dieses Howto sollte dem Support/OnCall helfen, den gängigsten BillingFehlerfall strukturiert abzuarbeiten. Für tiefere Ursachenanalysen siehe `docs/ops/billing-ops.md`.

View File

@@ -0,0 +1,6 @@
---
title: AI Operations
type: group
order: 60
icon: heroicon-o-sparkles
---

View File

@@ -0,0 +1,236 @@
---
title: AI Magic Edits (Ops Runbook)
---
Dieses Runbook beschreibt den operativen Betrieb von AI Magic Edits (AI Styling) inklusive Entitlements, Provider-Betrieb, Monitoring, Recovery und Incident-Handling.
## 1. Scope und aktueller Stand
- Feature-Name im System: `ai_styling` (User-facing: "AI Magic Edits"/"AI-Styling").
- Architektur ist provider-agnostisch umgesetzt (`AiImageProvider` Interface), aktuell ist `runware.ai` als erster Provider aktiv.
- Das Guest-PWA-Rollout ist derzeit clientseitig hinter einem Flag deaktiviert:
- `resources/js/guest-v2/lib/featureFlags.ts``GUEST_AI_MAGIC_EDITS_ENABLED = false`.
- Im Event Admin Control Room wird der AI-Bereich nur angezeigt, wenn das Event das Capability `ai_styling` hat.
## 2. Entitlements und Freischaltung
### 2.1 Paket-/Addon-Logik
- Erforderliches Paket-Feature: `ai_styling` (`config/ai-editing.php`).
- Standard-Addon-Key: `ai_styling_unlock` (`config/ai-editing.php`, `config/package-addons.php`).
- Entitlement wird event-spezifisch aufgelöst über:
- aktives Event-Paket (`eventPackage.package.features`, Tabelle `event_packages`)
- aktive, abgeschlossene Addons (`eventPackage.addons`, Tabelle `event_package_addons`, `status=completed`, optionales Ablaufdatum in `metadata`).
- Quelle der Berechtigung:
- `granted_by=package` (Feature im Paket enthalten, z.B. Premium)
- `granted_by=addon` (Addon für Event aktiv)
- sonst gesperrt (`feature_locked`).
### 2.2 Superadmin-Konfiguration
- Paket-Feature-Verwaltung erfolgt über die bestehende Package-Resource (Feature `ai_styling` setzen).
- Addon-Katalog:
- Key `ai_styling_unlock` in `config/package-addons.php`
- kaufbar nur, wenn im Checkout-Katalog ein valider Preis hinterlegt ist.
## 3. Runtime-Steuerung (Superadmin)
Die zentrale Laufzeitsteuerung liegt in `AI Editing Settings` (Rare Admin Cluster):
- `is_enabled`: Globaler Kill-Switch.
- `status_message`: Text bei globaler Deaktivierung (`feature_disabled`).
- `default_provider`: aktuell `runware`.
- `fallback_provider`: reserviert für späteres Failover.
- `runware_mode`: `live` oder `fake`.
- `queue_auto_dispatch`: automatische Queue-Dispatches direkt nach Request-Erstellung.
- `queue_name`: Ziel-Queue für AI Jobs.
- `queue_max_polls`: maximale Poll-Versuche für Provider-Status.
- `blocked_terms`: globale Prompt-Blockliste.
Wichtiger Betriebsaspekt:
- Standard ist `queue_auto_dispatch=false`. In diesem Modus werden neue Requests zwar erstellt, aber nicht automatisch verarbeitet.
- Für Live-Betrieb muss entweder `queue_auto_dispatch=true` gesetzt oder ein separater Dispatch-Prozess etabliert sein.
## 4. Styles und Event-Policy
### 4.1 Globale Styles
- Verwaltung über `AI Styles` Resource.
- Styles definieren u.a.:
- `provider`/`provider_model`
- `prompt_template`/`negative_prompt_template`
- `is_active`, `is_premium`
- optionales Entitlement-Metadatenprofil (`metadata.entitlements.*`).
### 4.2 Event-spezifische Policy
Event-Settings unter `settings.ai_editing` unterstützen:
- `enabled` (Feature pro Event an/aus)
- `allow_custom_prompt`
- `allowed_style_keys` (leer = alle erlaubten Styles)
- `policy_message` (Rückmeldung bei Blockierung/Deaktivierung)
## 5. API/Queue-Verarbeitung
### 5.1 Endpunkte
- Guest:
- `POST /api/v1/events/{token}/photos/{photo}/ai-edits`
- `GET /api/v1/events/{token}/ai-edits/{requestId}`
- `GET /api/v1/events/{token}/ai-styles`
- Tenant:
- `GET /api/v1/tenant/events/{eventSlug}/ai-edits`
- `GET /api/v1/tenant/events/{eventSlug}/ai-edits/summary`
- `POST /api/v1/tenant/events/{eventSlug}/ai-edits`
- `GET /api/v1/tenant/events/{eventSlug}/ai-edits/{aiEditRequest}`
- `GET /api/v1/tenant/events/{eventSlug}/ai-styles`
### 5.2 Schutzschichten pro Request
Reihenfolge der Kernprüfungen:
1. Global aktiviert (`is_enabled`)
2. Entitlement vorhanden (Paket/Addon)
3. Event-Policy erlaubt Request
4. Budget erlaubt Request (Soft/Hard Cap)
5. Style-Zulässigkeit
6. Prompt-Safety (blocked terms)
7. Queue/Provider-Verarbeitung
### 5.3 Queue-Jobs
- `ProcessAiEditRequest`: Provider-Submit und ggf. Übergang in Polling.
- `PollAiEditRequest`: Provider-Statusabfrage bis `queue_max_polls`.
- Terminalstatus:
- `succeeded`
- `failed`
- `blocked`
### 5.4 Output-Speicherung und Wasserzeichen
- Bei erfolgreichem Provider-Result versucht das Backend, den Output lokal zu persistieren:
- Download von `provider_url`
- Speicherung unter Event-Pfad (`events/{eventSlug}/ai-edits/...`)
- Watermark-Anwendung über dieselbe Logik wie normale Uploads (`WatermarkConfigResolver` + `ImageHelper`).
- Primäre Auslieferung erfolgt über lokale `storage_path`/`url`; `provider_url` bleibt als Fallback erhalten.
- Backfill für Alt-Daten ohne lokale Pfade:
- `php artisan ai-edits:backfill-storage`
## 6. Provider-Betrieb (Runware)
### 6.1 Erforderliche ENV/Config
- `RUNWARE_API_KEY`
- `RUNWARE_BASE_URL` (Default `https://api.runware.ai/v1`)
- `RUNWARE_TIMEOUT` (Sekunden)
- `AI_EDITING_DEFAULT_PROVIDER` (Default `runware`)
- `AI_EDITING_RUNWARE_MODE` (`live` oder `fake`)
### 6.2 Fake-Mode
- `runware_mode=fake` liefert synthetische Antworten ohne externen API-Call.
- Geeignet für interne Validierung von Flow/UI/Queue-Logik.
- Nicht für Kosten- oder Qualitätsaussagen nutzen.
## 7. Monitoring und Alerts
### 7.1 Wichtige Kennzahlen
- Request-Statusverteilung je Event (`queued`, `processing`, `succeeded`, `failed`, `blocked`)
- Failure-Rate
- Moderation-Hit-Rate
- Provider-Failure-Rate
- durchschnittliche Provider-Latenz
- Monatsausgaben (`ai_usage_ledgers`)
Das Tenant-Summary-Endpoint liefert diese Daten inkl. Alert-Flags:
- `observability.alerts.failure_rate_threshold_reached`
- `observability.alerts.latency_threshold_reached`
### 7.2 Schwellenwerte aus Config
- `ai-editing.observability.failure_rate_alert_threshold` (Default `0.35`)
- `ai-editing.observability.failure_rate_min_samples` (Default `10`)
- `ai-editing.observability.latency_warning_ms` (Default `15000`)
- Budget-Alert-Cooldown: `ai-editing.billing.budget.alert_cooldown_minutes` (Default `30`)
- Abuse-Eskalation: `ai-editing.abuse.escalation_threshold_per_hour` (Default `25`)
### 7.3 Relevante Log-Signale
- `AI provider latency warning`
- `AI failure-rate alert threshold reached`
- `AI budget threshold reached`
- `AI abuse escalation threshold reached`
## 8. Betriebs-Kommandos
### 8.1 Stuck Requests analysieren/recovern
- Dry-run:
- `php artisan ai-edits:recover-stuck --minutes=30`
- Requeue:
- `php artisan ai-edits:recover-stuck --minutes=30 --requeue`
- Hart auf failed setzen:
- `php artisan ai-edits:recover-stuck --minutes=30 --fail`
### 8.2 Retention-Pruning
- Dry-run:
- `php artisan ai-edits:prune --pretend`
- Ausführen (mit optionalen Overrides):
- `php artisan ai-edits:prune --request-days=90 --ledger-days=365`
Retention Defaults:
- Requests: `AI_EDITING_REQUEST_RETENTION_DAYS` (90)
- Ledger: `AI_EDITING_USAGE_LEDGER_RETENTION_DAYS` (365)
Hinweis:
- `ai-edits:prune` läuft täglich per Scheduler (`02:30`), kann aber bei Bedarf manuell ausgeführt werden.
### 8.3 Backfill lokaler AI-Outputs
- Dry-run:
- `php artisan ai-edits:backfill-storage --pretend`
- Ausführen:
- `php artisan ai-edits:backfill-storage --limit=200`
- Für einzelne Request-ID:
- `php artisan ai-edits:backfill-storage --request-id=123`
## 9. Incident-Playbooks
### 9.1 Provider-Ausfall / hohe Failure-Rate
1. `is_enabled=false` als Kill-Switch setzen (optional mit `status_message`).
2. `runware_mode=fake` nur für interne Funktionstests aktivieren.
3. Queue-Backlog prüfen, ggf. `ai-edits:recover-stuck --requeue` nach Stabilisierung.
4. Fehlerraten im Summary pro Event kontrollieren.
### 9.2 Kostenanstieg / Budget-Hard-Cap
1. Budget-Alerts (`ai_budget_soft_cap`/`ai_budget_hard_cap`) prüfen.
2. Tenant-Budget in `tenant.settings.ai_editing.budget.*` validieren.
3. Bei Bedarf zeitweises Override `override_until` setzen.
4. Bei Missbrauchsspitzen zusätzlich Abuse-Signale prüfen.
### 9.3 Safety-/Abuse-Spike
1. `blocked_terms` nachschärfen.
2. Event-Policy enger setzen (`allow_custom_prompt=false`, `allowed_style_keys` einschränken).
3. Warn-Logs mit `scope_hash`, `event_id`, `tenant_id` korrelieren.
## 10. Datenschutz und Datenhaltung
- Keine Secrets in Logs/Docs schreiben (`RUNWARE_API_KEY`, `.env`).
- Prompt-/Negativprompt-Inhalte liegen in `ai_edit_requests`; mit PII vorsichtig umgehen.
- Nur notwendige Aufbewahrungsdauer halten; Pruning regelmäßig durchführen.
- Keine Erweiterung der Retention ohne abgestimmte Privacy-Änderung.
## 11. Go-Live-Checkliste
1. Paket-/Addon-Freischaltung verifiziert (`ai_styling`, `ai_styling_unlock`).
2. `RUNWARE_API_KEY` gesetzt und Provider-Erreichbarkeit geprüft.
3. `queue_auto_dispatch=true` und Worker auf korrekter Queue aktiv.
4. Budget-Limits und Alerting pro Tenant geprüft.
5. Safety-Baseline (`blocked_terms`) gesetzt.
6. Recovery- und Prune-Commands in Ops-Routine aufgenommen.

View File

@@ -0,0 +1,6 @@
---
title: DSGVO & Compliance
type: group
order: 70
icon: heroicon-o-shield-check
---

View File

@@ -0,0 +1,89 @@
---
title: DSGVO & Compliance-Operationen
---
Dieses Runbook beschreibt praktische Abläufe für DatenschutzAnfragen, Datenlöschung und Aufbewahrungsfristen.
## 1. Grundprinzipien
- Gäste benötigen kein Konto; die meisten Daten sind Event und Fotobezogen.
- Tenant Admins sind in der Regel Verantwortliche im Sinne der DSGVO, Fotospiel fungiert als Auftragsverarbeiter (je nach Vertragsmodell).
- Rechtsgrundlagen, Impressum und Datenschutzerklärungen werden über die LegalPages im Admin verwaltet dieses Dokument fokussiert sich auf **Betriebsprozesse**.
## 2. Typische Anfragen & Aktionen
- **Auskunftsanfragen**
- Gast möchte wissen, ob Fotos von ihm/ihr gespeichert sind.
- Typischer Ablauf:
- Gast an Tenant verweisen (wenn Veranstalter Ansprechpartner ist).
- Falls Fotospiel direkt handeln muss: Event/FotoIDs anhand bereitgestellter Infos identifizieren (z.B. Link, Screenshot, Zeitfenster).
- Relevante Datensätze in DB (Fotos, Likes, Meldungen) lokalisieren und dokumentieren.
- **Löschanfragen**
- Ein Gast/Tenant bittet um Löschung spezifischer Fotos oder eines ganzen Events.
- Vorgehen:
1. Identität und Berechtigung prüfen (z.B. Tenant Admin, verifizierte Anfrage).
2. Fotos/Ereignisse über Admin UI oder interne Tools löschen/archivieren.
- Sicherstellen, dass `event_media_assets` und ArchivSpeicher ebenfalls bereinigt werden.
3. Prüfen, ob Logs/Audits pseudonymisiert bleiben können, ohne personenbezogene Inhalte.
4. Anfrage und Zeitpunkt dokumentieren (Ticket, internes Log).
- **Datenexport**
- Tenant will einen Export seiner Daten (Events, Medien, Statistiken).
- Nutzung der vorhandenen ExportFunktionen (z.B. CSV/ZIP im Admin) bevorzugen.
- Falls diese nicht ausreichen, manuelle Exporte via Skript/DB mit Datenschutz im Blick (keine unnötigen Felder).
- Beispiel: nur die Felder exportieren, die für den Zweck der Anfrage wirklich notwendig sind (kein DebugLog, keine internen IDs, sofern nicht sinnvoll).
### 2.1 Konkrete Tools & Endpoints
- **Profil-Datenexport (User-Ebene)**
- Controller: `App\Http\Controllers\ProfileDataExportController`.
- UI: Profilbereich der Tenant Admin PWA; Nutzer kann dort einen Export anstoßen.
- Ablauf:
1. Nutzer triggert Export → `ProfileDataExportController::store()` legt einen `DataExport`Eintrag an (`status = pending`) und dispatcht `GenerateDataExport`.
2. Der Job `GenerateDataExport` erstellt ein ZIP mit relevanten Daten und setzt `status = ready`, `path`, ggf. `expires_at`.
3. Nutzer kann die Datei über `ProfileDataExportController::download()` abrufen, solange `isReady()` und nicht `hasExpired()`.
- Ops-Sicht:
- Wenn Exporte „hängen bleiben“ (lange `pending`/`processing`), Queue/Horizon und Logs prüfen, ggf. Job neu anstoßen.
- Für DSGVOExports bevorzugt diesen Pfad nutzen, statt adhoc DBAbfragen.
- **Account-Anonymisierung (User/Tenant-Ebene)**
- Service: `App\Services\Compliance\AccountAnonymizer`.
- Job: `App\Jobs\AnonymizeAccount` nutzt diesen Service typischerweise im Hintergrund.
- Verhalten:
- Löscht/entfernt Medien (`EventMediaAsset`, `Photo`) für einen Tenant.
- Anonymisiert Tenant und UserDaten (setzt neutrale Namen, entfernt Kontaktinfos, sperrt Accounts).
- Ops-Sicht:
- Nur nach klarer Freigabe einsetzen, da Anonymisierung irreversibel ist.
- Vor Einsatz prüfen, ob für den betreffenden Tenant alle vertraglichen Zusagen (z.B. Datenexport) erfüllt sind.
## 3. Retention & automatisierte Löschung
- **Event-bezogene Aufbewahrung**
- StandardRetentionsfristen für Events/Fotos (z.B. X Tage nach Eventende/Archivierung) laut ProduktSpezifikation.
- Jobs/Kommandos, die nach Ablauf Medien archivieren oder löschen (siehe `docs/ops/media-storage-spec.md`).
- **Logs**
- Aufbewahrungsdauer von ApplikationsLogs (z.B. 3090 Tage), Rotation/Anonymisierung.
- **Konfiguration pro Tenant**
- Wenn Tenants eigene Retention wünschen, prüfen ob das UI/ConfigModel dies unterstützt (nicht adhoc in SQL ändern).
## 4. Operative Checkliste bei DSGVO-Fällen
1. Anfrage klassifizieren (Auskunft, Löschung, Export, Sonstiges).
2. Verantwortlichkeit klären (Tenant vs. Fotospiel).
3. Technische Schritte definieren (welche Events/Fotos/Accounts betroffen).
4. Durchführung:
- In Admin UI oder via internen Tools.
- Medien/Metadaten konsistent behandeln (keine „hängenden“ Records).
5. Dokumentation:
- Ticket/E-MailThread mit Datum, Betreff, Maßnahmen.
6. FollowUp:
- Prüfen, ob Runbooks/Automatisierungen angepasst werden sollten (z.B. besserer SelfService für Tenants).
## 5. Verbindung zu Security-Hardening
Das SecurityHardeningEpic wird in bd-Issues gepflegt und enthält mehrere DSGVOrelevante Workstreams:
- Signierte AssetURLs statt direkter StorageLinks.
- Verbesserte Token/AuthFlows.
- StorageHealth und ChecksummenVerifizierung.
Wenn dort neue Features produktiv gehen, sollten die Auswirkungen auf DSGVOProzesse in diesem Runbook ergänzt werden.

View File

@@ -0,0 +1,77 @@
---
title: How-to DSGVO-Löschung eines Fotos
---
Dieses Howto beschreibt den operativen Ablauf, wenn ein Gast verlangt, dass ein konkretes Foto DSGVOkonform gelöscht wird.
## 1. Anfrage & Berechtigung prüfen
Bevor du etwas löschst:
- Handelt der Gast über den Veranstalter (Tenant) oder direkt bei Fotospiel?
- Kann das Foto eindeutig identifiziert werden?
- Am besten via Link zur Galerie / FotoDetailseite.
- Alternativ via Screenshot + EventName + Zeitfenster.
- Ist der Tenant (Veranstalter) einverstanden?
- In der Regel sollte die Entscheidung, ob ein Foto gelöscht wird, beim Tenant liegen, sofern der Vertrag dies vorsieht.
Alle relevanten Informationen und Entscheidungen sollten im Ticket erfasst werden.
## 2. Foto im System identifizieren
1. Über die AdminUI:
- In der Tenant Admin PWA den betroffenen Event öffnen.
- Foto über Moderations/Galerieansicht suchen.
- ID des Fotos notieren (sofern sichtbar) oder den direkten AdminLink verwenden.
2. Falls nötig, über DB/Logs:
- Anhand von Dateinamen/URLs aus Logs (`event_media_assets.path`, `photos.thumbnail_path`) das Foto lokalisieren.
## 3. Löschung über Admin-UI (präferiert)
Wenn die AdminOberfläche eine Delete/HideFunktion bietet:
1. Tenant Admin das Foto über das Moderationsinterface löschen lassen.
2. Sicherstellen, dass:
- Foto nicht mehr in Galerie/Moderationslisten erscheint.
- ShareLinks oder öffentliche Galerien das Foto nicht mehr anzeigen.
3. Falls es trotzdem noch angezeigt wird:
- Caches prüfen (Browser, CDN, ggf. ThumbnailCaches).
## 4. Technischer Löschpfad (Backend)
Falls eine UILöschung nicht ausreicht oder du nachkontrollieren willst:
1. **Datenbank**
- `photos`:
- Prüfen, dass der Eintrag für das Foto gelöscht oder hinreichend anonymisiert wurde.
- `event_media_assets`:
- Alle Einträge, die auf dieses Foto (`photo_id`) zeigen, identifizieren.
- Pfade (`disk`, `path`) notieren.
2. **Storage**
- Für alle relevanten `EventMediaAsset`Einträge:
- Dateien im Hot/ArchiveStorage löschen (Original + Derivate/Thumbnails).
3. **Verknüpfungen**
- Sicherstellen, dass keine weiteren Verweise existieren:
- Likes/Statistiken für dieses Foto (z.B. `photo_likes`) optional mit entfernen, sofern vorhanden.
> Hinweis: Wenn ihr `AccountAnonymizer` auf Tenant/UserEbene verwendet, löscht dieser im Regelfall großflächig Medien. Für Einzelfälle (ein Foto) ist der oben skizzierte Weg geeigneter.
## 5. Dokumentation & Bestätigung
- Im Ticket festhalten:
- Welches Foto (Event, ID/URL).
- Wer die Löschung veranlasst und genehmigt hat.
- Welche Schritte tatsächlich durchgeführt wurden (UI, DB, Storage).
- Tenant/Gast informieren:
- Bestätigung, dass das Foto aus Galerie und Speicher entfernt wurde.
- Hinweis, dass ggf. Browser/CDNCaches eine kurze Zeit nachlaufen können, aber keine neuen Zugriffe mehr möglich sind.
## 6. Präventive Verbesserungen
Wenn dieser Vorgang häufig vorkommt:
- Prüfen, ob die AdminUI einen klareren, selbstbedienbaren Weg zur FotoLöschung bietet.
- Sicherstellen, dass die Dokumentation für Tenant Admins (siehe HelpCenter) erklärt, wie sie Fotos eigenständig löschen und wie sich das auf Gäste auswirkt.
Dieses Howto ergänzt `docs/ops/compliance-dsgvo-ops.md` um einen konkreten Einzelfall. Für komplexere AnonymisierungsSzenarien siehe den Abschnitt zum `AccountAnonymizer`.

View File

@@ -0,0 +1,50 @@
---
title: Howto TenantKomplettExport
---
Dieses Howto beschreibt, wie du für einen Tenant kurz vor Vertragsende einen möglichst vollständigen DatenExport erstellst.
## 1. Anfrage prüfen
- Schriftliche Anfrage des Tenants (EMail/Ticket).
- Klarer Scope:
- Nur Medien?
- Medien + Metadaten (Events, Gäste, Likes)?
- BillingNachweise (Rechnungen)?
## 2. MedienExport
- Für jeden relevanten Event:
- Prüfen, ob alle UploadJobs durch sind (`event_media_assets` ohne `pending`/`failed`).
- ArchivExport nutzen (sofern vorhanden) oder:
- MedienOrdner pro Event aus dem Storage exportieren.
- Thumbnails optional, Originale Pflicht.
## 3. MetadatenExport
- Events, Gäste, Likes, Kommentare nach Bedarf exportieren:
- Entweder über bestehende ExportFunktion (CSV/JSON).
- Oder über einen einmaligen, internen Report (z.B. `php artisan make:report`ähnlicher Flow, falls vorhanden).
- Output als ZIP mit klarer Ordnerstruktur:
- `media/`
- `metadata/events.csv`
- `metadata/guests.csv`
## 4. Billing-Unterlagen
- Rechnungen / Zahlungsbelege:
- Lemon SqueezyBelege (Links oder PDFs).
- Interne RechnungsPDFs (falls generiert).
## 5. Nach dem Export
- Export dem Tenant sicher zur Verfügung stellen (z.B. DownloadLink mit Ablaufdatum).
- Dokumentieren:
- Datum des Exports.
- Umfang (welche Tabellen/Events enthalten).
- Speicherort und Aufbewahrungsdauer des ExportBundles.
Siehe auch:
- `docs/ops/compliance-dsgvo-ops.md`
- `docs/ops/backup-restore.md`

View File

@@ -0,0 +1,6 @@
---
title: Deployment
type: group
order: 80
icon: heroicon-o-server-stack
---

View File

@@ -0,0 +1,126 @@
# Docker Deployment Guide
This guide describes the recommended, repeatable way to run the Fotospiel platform in Docker for production or high-fidelity staging environments. It pairs a multi-stage build (PHP-FPM + asset pipeline) with a Compose stack that includes Nginx, worker processes, Redis, and MySQL.
> **Dokploy users:** see `docs/ops/deployment/dokploy.md` for service definitions, secrets, and how to wire the same containers (web, queue, scheduler, vsftpd) inside Dokploy. That document builds on the base Docker instructions below.
## 1. Prerequisites
- Docker Engine 24+ and Docker Compose v2.
- A `.env` file for the application (see step 4).
- Optional: an external MySQL/Redis if you do not want to run the bundled containers.
## 2. Build the application image
```bash
docker compose build app
```
The build performs the following steps:
1. Installs Node dependencies and runs `npm run build` to produce production assets.
2. Installs PHP dependencies with Composer (`--no-dev --no-scripts`).
3. Creates a PHP 8.3 FPM image with required extensions (GD, intl, Redis, etc.).
4. Stores the compiled application under `/opt/app`; the runtime entrypoint syncs it into the shared volume when a container starts.
## 3. Configure environment
Copy the sample Docker environment file and edit the secrets:
```bash
cp docker/.env.docker docker/.env.docker.local
```
Set (at minimum):
- `APP_KEY` — generate with `docker compose run --rm app php artisan key:generate --show`.
- Database credentials (`DB_*`). The provided MySQL service defaults to `fotospiel/secret`.
- `STORAGE_ALERT_EMAIL` — recipient for upload failure alerts (optional).
Point `docker-compose.yml` to the file you created by either renaming it to `docker/.env.docker` or adjusting the `env_file` entries.
## 4. Boot the stack
```bash
docker compose up -d
```
Services started:
- `app`: PHP-FPM container serving the Laravel application.
- `web`: Nginx proxy forwarding requests to PHP-FPM on port `8080` by default (`APP_HTTP_PORT`).
- `queue` & `media-storage-worker`: queue consumers (default + media archival).
- `scheduler`: runs `php artisan schedule:work`.
- `horizon` (optional, disabled unless `--profile horizon` is supplied).
- `redis` & `mysql`.
### Migrations & seeds
Run once after the first boot or when deploying new schema changes:
```bash
docker compose exec app php artisan migrate --force
docker compose exec app php artisan db:seed --class=MediaStorageTargetSeeder --force
```
If you already have data, skip the seeder or seed only new records.
## 5. Queue & Horizon management
Worker entrypoints live in `/scripts/` inside the container (copied from the repositorys `scripts/` folder). The Compose services mount the same application volume so code stays in sync. Adjust concurrency by scaling services:
```bash
docker compose up -d --scale queue=2 --scale media-storage-worker=2
```
To enable Horizon (dashboard, smart balancing):
```bash
docker compose --profile horizon up -d horizon
```
## 6. Scheduler & cron jobs
The compose stack ships a `scheduler` service that runs `php artisan schedule:work`, so all scheduled commands defined in `App\Console\Kernel` stay active. For upload health monitoring, keep the helper script from `cron/upload_queue_health.sh` on the host (or inside a management container) and add a cron entry:
```
*/5 * * * * /var/www/html/cron/upload_queue_health.sh
```
This wrapper logs to `storage/logs/cron-upload-queue-health.log` and executes `php artisan storage:check-upload-queues`, which in turn issues guest-facing upload alerts when queues stall or fail repeatedly. In containerised environments mount the repository so the script can reuse the same PHP binary as the app, or call the artisan command directly via `docker compose exec app php artisan storage:check-upload-queues`.
The dashboard becomes available at `/horizon` and is protected by the Filament super-admin auth guard.
## 6. Persistent data & volumes
- `app-code` — contains the synced application, including the `storage` directory and generated assets.
- `mysql-data` — MySQL data files.
- `redis-data` — Redis persistence (disabled by default; change the Redis command if you want AOF snapshots).
Back up the volumes before upgrades to maintain tenant media and database state.
## 7. Updating the stack
1. `git pull` the repository (or deploy your release branch).
2. `docker compose build app`.
3. `docker compose up -d`.
4. Run migrations + seeders if required.
5. Check logs: `docker compose logs -f app queue media-storage-worker`.
Because the app image keeps the authoritative copy of the code, each container restart rsyncs fresh sources into the shared volume ensuring reliable updates without lingering artefacts.
## 8. Production hardening
- Terminate TLS with a dedicated reverse proxy (Traefik, Caddy, AWS ALB, etc.) in front of the `web` container.
- Point `APP_URL` to your public domain and enable trusted proxies.
- Externalize MySQL/Redis to managed services for better resilience.
- Configure backups for the `storage` directories and database dumps.
- Hook into your observability stack (e.g., ship container logs to Loki or ELK).
## 9. Internal docs access
- Internal operations/admin documentation is now delivered in Filament through the Guava Knowledge Base plugin.
- Access path: `/super-admin/docs` (same auth guard as SuperAdmin).
- No separate static docs build/container is required.
With the provided configuration you can bootstrap a consistent Docker-based deployment across environments while keeping queue workers, migrations, and asset builds manageable. Adjust service definitions as needed for staging vs. production.

View File

@@ -0,0 +1,128 @@
# Dokploy Deployment Guide
Dokploy is our self-hosted PaaS for orchestrating the Fotospiel stack (Laravel app, scheduler, queue workers, Horizon, and the Photobooth FTP pipeline). This guide explains how to provision the services in Dokploy and how to wire the SuperAdmin observability widgets that now talk to the Dokploy API.
## 1. Services to provision
| Service | Notes |
|---------|-------|
| **Laravel App** | Build from this repository. Expose port 8080 (or Dokploy HTTP service). Attach the production `.env`. Health check `/up`. |
| **Scheduler** | Clone the app container; command `php artisan schedule:work`. |
| **Queue workers** | Use the `/scripts/queue-worker.sh` entrypoints (default, media-storage, media-security). Deploy each as a dedicated Dokploy application or Docker service. |
| **Horizon (optional)** | Run `/scripts/horizon.sh` for dashboard + metrics. |
| **Redis / Database** | Use managed offerings or self-host in Dokploy. Configure network access for the app + workers. |
| **vsftpd container** | Expose port 2121 and mount the shared Photobooth volume. |
| **Photobooth Control Service** | Lightweight API (Go/Node/Laravel Octane) that can be redeployed together with vsftpd for ingest controls. |
### Volumes
Create persistent volumes inside Dokploy and mount them across the services:
- `storage-app` Laravel `storage`, uploads, compiled views.
- `photobooth` shared by vsftpd, the control service, and Laravel for ingest.
- Database / Redis volumes if you self-manage those containers.
## 2. Environment & secrets
Every Dokploy application should include the regular Laravel secrets (see `.env.example`). Important blocks:
- `APP_KEY`, `APP_URL`, `DB_*`, `CACHE_DRIVER`, `QUEUE_CONNECTION`, `MAIL_*`.
- Photobooth integration (`PHOTOBOOTH_CONTROL_*`, `PHOTOBOOTH_FTP_*`, `PHOTOBOOTH_IMPORT_*`).
- AWS / S3 credentials if the tenant media is stored remotely.
### Dokploy integration variables
Add the infrastructure observability variables to the Laravel app environment:
```
DOKPLOY_API_BASE_URL=https://dokploy.example.com/api
DOKPLOY_API_KEY=pat_xxxxxxxxxxxxxxxxx
DOKPLOY_WEB_URL=https://dokploy.example.com
DOKPLOY_COMPOSE_IDS={"stack":"cmp_main","ftp":"cmp_ftp"}
DOKPLOY_API_TIMEOUT=10
```
- `DOKPLOY_COMPOSE_IDS` ist eine JSON-Map Label → `composeId` (siehe Compose-Detailseite in Dokploy). Diese IDs steuern Widget & Buttons.
- Optional kannst du weiterhin `DOKPLOY_APPLICATION_IDS` pflegen, falls du später einzelne Apps statt Compose-Stacks integrieren möchtest.
- Die API benötigt Rechte für `compose.one`, `compose.loadServices`, `compose.redeploy`, `compose.stop` etc.
## 3. Project & server setup
1. **Register the Docker host** in Dokploy (`Servers → Add Server`). Install the Dokploy agent on the target VM.
2. **Create a Project** (e.g., `fotospiel-prod`) to group all services.
3. **Attach repositories** using Dokploy Git providers (GitHub / Gitea / GitLab / Bitbucket) or Docker images. Fotospiel uses the source build (Dockerfile at repo root).
4. **Networking** keep all services on the same internal network so they can talk to Redis/DB. Expose the public HTTP service only for the Laravel app (behind Traefik/Lets Encrypt).
## 4. Deploy applications
Follow these steps for each component:
1. **Laravel HTTP app**
- Build from the repo.
- `Dockerfile` already exposes port `8080`.
- Set branch (e.g. `main`) for automatic deployments.
- Add health check `/up`.
- Mount `storage-app` and `photobooth` volumes.
2. **Scheduler**
- Duplicate the image.
- Override command: `php artisan schedule:work`.
- Disable HTTP exposure.
3. **Queue workers**
- Duplicate the image.
- Commands:
- `/var/www/html/scripts/queue-worker.sh default`
- `/var/www/html/scripts/queue-worker.sh media-storage`
- `/var/www/html/scripts/queue-worker.sh media-security`
- Optionally create a dedicated container for Horizon using `/var/www/html/scripts/horizon.sh`.
4. **vsftpd + Photobooth control**
- Nutze deinen bestehenden Docker-Compose-Stack (z.B. `docker-compose.dokploy.yml`) oder dedizierte Compose-Applikationen.
- Mount `photobooth` volume read-write.
5. **Database/Redis**
- Dokploy can provision standard MySQL/Postgres/Redis apps. Configure credentials to match `.env`.
6. **Apply migrations**
- Use Dokploy one-off command to run `php artisan migrate --force` on first deploy.
- Seed storage targets if required: `php artisan db:seed --class=MediaStorageTargetSeeder --force`.
## 5. SuperAdmin observability (Dokploy API)
Das SuperAdmin-Dashboard nutzt jetzt ausschließlich Compose-Endpunkte:
1. **Config file** `config/dokploy.php` liest `DOKPLOY_COMPOSE_IDS`.
2. **Client** `App\Services\Dokploy\DokployClient` kapselt:
- `GET /compose.one?composeId=...` für Meta- und Statusinfos (deploying/error/done).
- `GET /compose.loadServices?composeId=...` für die einzelnen Services innerhalb des Stacks.
- `GET /deployment.allByCompose?composeId=...` für die Deploy-Historie.
- `POST /compose.redeploy`, `POST /compose.deploy`, `POST /compose.stop` (Buttons im UI).
3. **Widgets / Pages** `DokployPlatformHealth` zeigt jeden Compose-Stack inkl. Services; die `DokployDeployments`-Seite bietet Redeploy/Stop + Audit-Log (`InfrastructureActionLog`).
4. **Auditing** jede Aktion wird mit User, Payload, Response & HTTP-Code in `infrastructure_action_logs` festgehalten.
Only SuperAdmins should have access to these widgets. If you rotate the API key, update the `.env` and deploy the app to refresh the cache.
## 6. Monitoring & alerts
- Dokploy already produces container metrics and deployment logs. Surface the most important ones (CPU, memory, last deployment) through the widget using the monitoring endpoint.
- Configure Dokploy webhooks (Deploy succeeded/failed, health alerts) to call a Laravel route that records incidents in `photobooth_metadata` or sends notifications.
- Use Dokploys Slack/email integrations for infrastructure-level alerts. Application-specific alerts (e.g., ingest failures) still live inside Laravel notifications.
## 7. Production readiness checklist
1. Alle Compose-Stacks in Dokploy laufen mit Health Checks & Volumes.
2. `photobooth` volume mounted for Laravel + vsftpd + control service.
3. Database/Redis backups scheduled (Dokploy snapshot or external tooling).
4. `.env` enthält die Dokploy-API-Credentials und `DOKPLOY_COMPOSE_IDS`.
5. Scheduler, Worker, Horizon werden im Compose-Stack überwacht.
6. SuperAdmin-Widget zeigt die Compose-Stacks und erlaubt Redeploy/Stop.
7. Webhooks/alerts configured for failed deployments or unhealthy containers.
With this setup the Fotospiel team can manage deployments, restarts, and metrics centrally through Dokploy while Laravels scheduler and workers continue to run within the same infrastructure.
## 8. Internal docs in Dokploy
- Internal operations/admin documentation is served from the SuperAdmin panel itself via the Guava Knowledge Base.
- Access path: `/super-admin/docs` with normal SuperAdmin authentication (`super_admin` guard).
- No dedicated documentation build container or static docs artifact is required in Dokploy.

View File

@@ -0,0 +1,12 @@
Services & URLs
| Service | URL / Port | Notes |
|----------------|--------------------------------|-------|
| Laravel app | http://localhost:8000 | Default web UI; Horizon dashboard at /horizon if laravel/horizon is installed. |
| Vite dev server| http://localhost:5173 | Hot module reload for the marketing/guest frontend. |
| Mailpit UI | http://localhost:8025 | No auth; SMTP listening on port 1025. |
| Grafana | http://localhost:3000 | Anonymous admin already enabled; dashboards will show Loki logs once you add Loki as a data source (URL http://loki:3100). |
| Loki API | http://localhost:3100 | Used by Grafana/Promtail; direct browsing usually not needed. |
| Portainer | https://localhost:9443 | First visit prompts you to set an admin password; point it to /var/run/docker.sock (already mounted from the `PODMAN_SOCKET` path). |
| Redis | Bound to localhost:6379 | Matches QUEUE_CONNECTION=redis. |
| Promtail | Internal only (port 9080) | Tails storage/logs and pushes to Loki. |

View File

@@ -0,0 +1,132 @@
# Public API Incident Response Playbook (SEC-API-02)
Scope: Guest-facing API endpoints that rely on join tokens and power the guest PWA plus the public gallery. This includes:
- `/api/v1/events/{token}/*` (stats, tasks, uploads, photos)
- `/api/v1/gallery/{token}/*`
- Signed download/asset routes generated via `EventPublicController`
The playbook focuses on abuse, availability loss, and leaked content.
---
## 1. Detection & Alerting
| Signal | Where to Watch | Notes |
| --- | --- | --- |
| 4xx/5xx spikes | Application logs (`storage/logs/laravel.log`), centralized logging | Look for repeated `Join token access denied` / `token_rate_limited` or unexpected 5xx. |
| Rate-limit triggers | Laravel log lines emitted from `EventPublicController::handleTokenFailure` | Contains IP + truncated token preview. |
| CDN/WAF alerts | Reverse proxy (if enabled) | Ensure 429/403 anomalies are forwarded to incident channel. |
| Synthetic monitors | Uptime Kuma (SEC-API-03) | Public uptime + guest API + support API health checks. |
Manual check commands:
```bash
php artisan log:tail --lines=200 | grep "Join token"
php artisan log:tail --lines=200 | grep "gallery"
```
## 1.1 Synthetic Monitors (Uptime Kuma)
Primary uptime:
- `GET /` (base domain) — HTTP 200-399.
Guest API (stable synthetic event tokens required):
- `GET /api/v1/events/{token}` — expect JSON containing `"slug"` or `"engagement_mode"`.
- `GET /api/v1/gallery/{token}` — expect JSON containing `"event"` or `"branding"`.
- `GET /api/v1/gallery/{token}/photos` — expect JSON containing `"data"`.
- `GET /api/v1/events/{token}/photos` — expect JSON containing `"data"`.
Support API health metrics (read-only token stored in Kuma):
- `GET /api/v1/support/tenants?per_page=1`
- `GET /api/v1/support/events?per_page=1`
- `GET /api/v1/support/photos?per_page=1`
Defaults:
- Interval: 60s
- Timeout: 10s
- Retries before alert: 2
Notes:
- Do not store bearer tokens in the repo; configure them directly in Kuma.
- If synthetic tokens rotate, guest monitors will flap. Keep a dedicated synthetic event/token.
- Import template: `docs/ops/deployment/uptime-kuma-import.template.json` (replace placeholders before import).
## 2. Severity Classification
| Level | Criteria | Examples |
| --- | --- | --- |
| SEV-1 | Wide outage (>50% error rate), confirmed data leak or malicious mass-download | Gallery downloads serving wrong event, join-token table compromised. |
| SEV-2 | Localised outage (single tenant/event) or targeted brute force attempting to enumerate tokens | Single event returning 500, repeated `invalid_token` from single IP range. |
| SEV-3 | Minor functional regression or cosmetic issue | Rate limit misconfiguration causing occasional 429 for legitimate users. |
Escalate SEV-1/2 immediately to on-call via Slack `#incident-response` and open PagerDuty incident (if configured).
## 3. Immediate Response Checklist
1. **Confirm availability**
- `curl -I https://app.test/api/v1/gallery/{known_good_token}`
- Use tenant-provided test token to validate `/events/{token}` flow.
2. **Snapshot logs**
- Export last 15 minutes from log aggregator or `storage/logs`. Attach to incident ticket.
3. **Assess scope**
- Identify affected tenant/event IDs via log context.
- Note IP addresses triggering rate limits.
4. **Decide mitigation**
- Brute force? → throttle/bock offending IPs.
- Compromised token? → revoke token via Filament or `php artisan tenant:join-tokens:revoke {id}` (once command exists).
- Endpoint regression? → begin rolling fix or feature flag toggle.
## 4. Mitigation Tactics
### 4.1 Abuse / Brute force
- Increase rate-limiter strictness temporarily by editing `config/limiting.php` (if available) or applying runtime block in the load balancer.
- Use fail2ban/WAF rules to block offending IPs. For quick local action:
```bash
sudo ufw deny from <ip_address>
```
- Consider temporarily disabling gallery download by setting `PUBLIC_GALLERY_ENABLED=false` (feature flag planned) and clearing cache.
### 4.2 Token Compromise
- Revoke specific token via Filament “Join Tokens” modal (Event → Join Tokens → revoke).
- Notify tenant with replacement token instructions.
- Audit join-token logs for additional suspicious use and consider rotating all tokens for the event.
### 4.3 Internal Failure (500s)
- Tail logs for stack traces.
- If due to downstream storage, fail closed: return 503 with maintenance banner while running `php artisan storage:diagnostics`.
- Roll back recent deployment or disable new feature flag if traced to release.
## 5. Communication
| Audience | Channel | Cadence |
| --- | --- | --- |
| Internal on-call | Slack `#incident-response`, PagerDuty | Initial alert, hourly updates. |
| Customer Support | Slack `#support` with summary | Once per significant change (mitigation applied, issue resolved). |
| Tenants | Email template “Public gallery disruption” (see `resources/lang/*/emails.php`) | Only for SEV-1 or impactful SEV-2 after mitigation. |
Document timeline, impact, and mitigation in the incident ticket.
## 6. Verification & Recovery
After applying mitigation:
1. Re-run test requests for affected endpoints.
2. Validate join-token creation/revocation via Filament.
3. Confirm error rates return to baseline in monitoring/dashboard.
4. Remove temporary firewall blocks once threat subsides.
## 7. Post-Incident Actions
- File RCA within 48 hours including: root cause, detection gaps, follow-up tasks (e.g., enabling synthetic monitors, adding audit fields).
- Update documentation if new procedures are required (`docs/prp/11-public-gallery.md`, `docs/prp/03-api.md`).
- Schedule backlog items for long-term fixes (e.g., better anomaly alerting, token analytics dashboards).
## 8. References & Tools
- Log aggregation: `storage/logs/laravel.log` (local), Stackdriver/Splunk (staging/prod).
- Rate limit config: `App\Providers\AppServiceProvider``RateLimiter::for('tenant-api')` and `EventPublicController::handleTokenFailure`.
- Token management UI: Filament → Events → Join Tokens.
- Signed URL generation: `app/Http/Controllers/Api/EventPublicController` (for tracing download issues).
Keep this document alongside the other deployment runbooks and review quarterly.

View File

@@ -0,0 +1,6 @@
---
title: Releases & Tests
type: group
order: 90
icon: heroicon-o-rocket-launch
---

View File

@@ -0,0 +1,47 @@
---
title: Releases & Deployments
---
Dieses Dokument beschreibt, wie Releases vorbereitet und durchgeführt werden und welche Tests aus OpsSicht Pflicht sind.
## 1. Vor dem Release
- Changelog grob durchsehen (Feature/BugfixUmfang verstehen).
- DatenbankMigrations prüfen:
- Sind `up`/`down` sauber und idempotent?
- Gibt es lange laufende Migrations (IndexBuilds)?
- KonfigÄnderungen:
- Neue ENVVariablen in `dokploy`/Compose hinterlegt?
- Secrets über SecretStore / DokployUI konfiguriert?
## 2. PflichtTests vor ProdDeploy
- **PHPUnit**:
- `php artisan test` oder mindestens relevante Suites (z.B. „Checkout“, „Storage“).
- **FrontendBuild**:
- `npm run build` (bzw. CIJob).
- **E2ESmokeTests** (siehe `docs/testing/e2e.md`):
- GuestFlow: Event beitreten, Foto hochladen, Anzeige prüfen.
- TenantFlow: Login, Event anlegen, Medienübersicht öffnen.
## 3. DeploymentAblauf (Beispiel Dokploy)
- Neues Image wird gebaut und getaggt (z.B. `fotospiel-app:2025-11-20`).
- DokployStack aktualisieren:
- AppContainer.
- Queue/HorizonContainer.
- DocsContainer (falls betroffen).
- Nach dem Deploy:
- `php artisan migrate --force`.
- Queues prüfen (`horizon:status`, `queue:failed`).
- Schnelle SmokeTests in Prod (nur lesende Aktionen oder TestTenant).
## 4. RollbackStrategie
- Vor dem Deploy aktuellen DatenbankSnapshot sicherstellen.
- Vorheriges ImageTag notieren.
- Rollback:
- Dokploy/Compose auf vorheriges Image zurückdrehen.
- Falls Migrations rückwärtskompatibel: ggf. `migrate:rollback`.
- IncidentEintrag mit Ursache und Lessons Learned ergänzen.

View File

@@ -0,0 +1,163 @@
# UI Test Suites (Playwright)
This document tracks the UI/E2E automation efforts. The suites now live under `tests/ui` and are organized by product surface (Purchase, Auth, Admin, Guest PWA).
## Prerequisites
- Node 18+
- `npm install`
- Laravel app running at `http://localhost:8000`
- Seeded tenant admin account (see below)
- Lemon Squeezy sandbox credentials/config applied to the local `.env`
## Deterministic Data
### Tenant Admin
Use the existing seeder to provision a reusable tenant account for Admin suite flows:
```bash
php artisan db:seed --class=E2ETenantSeeder
```
Override defaults when necessary:
```bash
E2E_TENANT_EMAIL="tenant@example.com" \
E2E_TENANT_PASSWORD="super-secret" \
php artisan db:seed --class=E2ETenantSeeder
```
Expose the same credentials to Playwright:
```bash
export E2E_TENANT_EMAIL="tenant@example.com"
export E2E_TENANT_PASSWORD="super-secret"
```
### Coupon Presets & Mailbox API
The backend exposes `/api/_testing/...` endpoints (local/testing env only):
| Endpoint | Description |
| --- | --- |
| `POST /api/_testing/coupons/seed` | Seeds default coupons (`PERCENT10`, `FLAT50`, `EXPIRED25`) or accepts a custom payload. |
| `GET /api/_testing/mailbox` | Returns every captured email (see `App\Testing\Mailbox`). |
| `DELETE /api/_testing/mailbox` | Flushes the captured emails. |
| `GET /api/_testing/checkout/sessions/latest` | Fetches the newest checkout session for a given email/tenant filter. |
| `POST /api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy` | Triggers the Lemon Squeezy webhook handler for the given session with a mock payload. |
| `GET /api/_testing/events/join-token` | Resolves (and optionally regenerates) a join token + QR for a given event ID or slug. |
| `POST /api/_testing/guest-events` | Provisions a deterministic guest/tenant event with sample tasks and returns its slug + join token. |
### Guest Demo Event
- Call `POST /api/_testing/guest-events` (optionally pass a custom `slug` or `name`) to ensure there is an event with ready-to-use tasks and join token.
- Export the slug so the guest suite knows which event to target:
```bash
export E2E_GUEST_EVENT_SLUG="pwa-demo-event"
export E2E_GUEST_BASE_URL="http://localhost:8000"
```
- The response includes `join_token` if you need to debug locally, but the UI tests grab a fresh token through `fetchJoinToken`.
Playwright fixtures (`tests/ui/helpers/test-fixtures.ts`) provide helpers that wrap these endpoints.
## Security Review (Dynamic Tests)
This section provides a staged, repeatable checklist for dynamic security reviews across product surfaces. It complements the UI suites above and is intended for staging/test environments.
### Environment Assumptions (Required)
- **Run in staging/test only** — never against production data.
- **Dedicated test tenants/users** — use seeded accounts (see above) and avoid real customer data.
- **Sandbox billing** — Lemon Squeezy sandbox and mock webhook endpoints only.
- **Testing token enabled** — set `E2E_TESTING_TOKEN` and ensure the backend accepts it for `/api/_testing/*`.
- **Stable base URL** — set `E2E_BASE_URL` to the target environment (`http://localhost:8000` or staging).
- **Email sink** — use `/api/_testing/mailbox` instead of real email delivery.
- **Rate limits** — keep request volume low; avoid concurrent fuzzing unless explicitly safe.
### Checklist: Marketing + Public API (Dynamic)
1) **Public routes**: `/de`, `/en`, `/de/packages`, `/de/blog`, `/de/kontakt` render with expected locale and canonical/hreflang tags.
2) **Redirect hygiene**: nonprefixed routes redirect to locale (`/contact``/de/kontakt` or `/en/contact`).
3) **Contact form**: validation errors for missing fields; honeypot rejects bot payload; throttle returns 429 on excessive posts.
4) **Public API**: `GET /api/v1/events/{token}` and `/photos` reject invalid/expired tokens with 404/410 (no sensitive info).
5) **Abuse controls**: upload endpoints return 429 when ratelimited; no 500s on malformed payloads.
6) **CORS**: public API does not allow wildcard origins for authenticated endpoints.
### Checklist: Guest PWA (Dynamic)
1) **Join token**: valid token joins, invalid/expired token shows safe error (no leakage).
2) **Permissions**: guest cannot access tenant/admin endpoints; 401/403 as expected.
3) **Uploads**: file type + size limits enforced; invalid uploads fail gracefully.
4) **Offline mode**: queued uploads dont leak data; resync uses same join token.
5) **Likes/tasks**: actions scoped to event; crossevent access denied.
### Checklist: Event Admin (Dynamic)
1) **Login flow**: correct error on invalid creds; throttling kicks in after repeated attempts.
2) **Tenant isolation**: admin cannot access other tenants events/photos (403/404).
3) **Join token lifecycle**: regenerate/disable token invalidates old links immediately.
4) **Moderation controls**: only admin can approve/hide; guest cannot mutate.
5) **Exports**: adminonly endpoints require auth; signed URLs expire as expected.
### Checklist: Webhooks/Billing (Dynamic)
1) **Signature validation**: invalid signature is rejected (401/403) and logged.
2) **Freshness**: stale timestamps are rejected; replayed webhook payloads are idempotent.
3) **Lemon Squeezy sandbox flow**: use `/api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy` to simulate success/failure; verify ledger updates.
4) **Webhook retries**: transient failures produce retrysafe behavior (no duplicate ledger entries).
5) **Error handling**: malformed payload returns 4xx (not 500), with minimal error detail.
## Suite Layout & Goals
| Suite | Location | Primary Coverage |
| --- | --- | --- |
| Purchase | `tests/ui/purchase` | Marketing site package selection, checkout flow, coupon handling, Lemon Squeezy sandbox hand-off, post-purchase dashboard verification. |
| Auth | `tests/ui/auth` | Registration/login fuzzing, password reset, Social/OAuth hooks, email delivery assertions, throttling/error UX. |
| Admin | `tests/ui/admin` | Tenant onboarding wizard, dashboard widgets, event creation (incl. wedding preset), task assignment, join-token + QR verification, Lemon Squeezy billing history. |
| Guest | `tests/ui/guest` | Guest PWA onboarding, join-token entry, offline sync, uploads/likes/tasks for ≥15 guests, achievement + notification UX. |
Each suite should be executable independently to keep CI fast and to allow targeted debugging.
## Commands
- `npm run test:ui` — run all suites serially.
- `npm run test:ui:purchase` — Purchase-only regressions.
- `npm run test:ui:auth` — Authentication fuzzing.
- `npm run test:ui:admin` — Admin panel journeys.
- `npm run test:ui:guest` — Guest PWA scenarios.
Traces are recorded on first retry (`playwright.config.ts`); open via `npx playwright show-trace path/to/trace.zip`.
## Implementation Roadmap
1. **Purchase suite**
- Seed coupons via helper.
- Cover `/de/packages` Standard selection, coupon states (valid/invalid/expired), Lemon Squeezy inline + hosted checkout using sandbox card `4000 0566 5566 5557 / CVV 100`.
- Simulate webhook success (helper endpoint TBD) so dashboard reflects the purchase.
- Assert confirmation emails captured via mailbox API.
2. **Auth suite**
- Expand current scaffold to fully automate registration, login, password reset, MFA prompts, and throttling.
- Fuzz invalid inputs; assert inline validation + error banners.
- Use mailbox helper to fetch verification/reset emails and follow links.
3. **Admin suite**
- After purchase, log into `/event-admin`, confirm latest package appears, create a wedding event, assign predefined tasks, fetch join token + QR (helper should expose raw token/URL).
- Cover task management UX (assign, reorder, complete).
- Verify billing history shows the recent Lemon Squeezy transaction.
4. **Guest suite**
- Use join token from Admin suite (or seed via helper) to onboard 15 simulated guests in parallel contexts.
- Exercise uploads (with quota edge cases), likes, task completion, achievements, push subscription, offline queue + resync.
- Validate guest-facing error states (expired token, upload failure, network loss).
5. **Shared helpers (backend + Playwright)**
- Webhook trigger endpoint for Lemon Squeezy sandbox.
- Join token + QR extraction endpoint for tests.
- Task template seeding helper.
- Optional guest factory endpoint to mint attendees quickly.
Track implementation progress in this document to keep future contributors aligned.
### Guest Suite
`tests/ui/guest/guest-pwa-journey.test.ts` simulates 15 independent guests joining the event, naming themselves, visiting the task list, opening the upload screen (via gallery picker), toggling offline mode for the final wave, and visiting the gallery to like a photo when one exists.
#### Requirements
1. **Existing event** create a tenant admin event with active join token and set `E2E_GUEST_EVENT_SLUG` (e.g., `export E2E_GUEST_EVENT_SLUG=wedding-showcase`).
2. **Guest base URL** defaults to `http://localhost:8000`. Override with `E2E_GUEST_BASE_URL` if the PWA runs elsewhere (Capacitor/TWA build).
3. **Media fixture** the test auto-generates `tests/ui/guest/fixtures/sample-upload.png` for the gallery upload flow.
Run with `npm run test:ui:guest`. The test creates Playwright contexts sequentially to keep memory usage predictable.

View File

@@ -0,0 +1,6 @@
---
title: Monitoring & Diagramme
type: group
order: 100
icon: heroicon-o-chart-bar
---

View File

@@ -0,0 +1,145 @@
---
title: Monitoring & Observability
---
Dieses Dokument sammelt die wichtigsten MonitoringPunkte der Plattform und soll helfen, die richtigen Dashboards und Alerts aufzubauen.
## 1. Was sollte überwacht werden?
- **Verfügbarkeit**
- HTTPChecks auf zentrale Endpunkte (Landing, JoinTokenFlows, Guest Upload, Tenant Admin Login).
- PublicAPIChecks (`/api/v1/events/{token}`, Galerie, UploadEndpoints).
- **Queues**
- Länge und Durchsatz der Queues `default`, `media-storage`, `media-security`, `notifications`.
- Age/TimeinQueue, Anzahl der Failed Jobs.
- **Storage**
- Füllstand der HotStorageVolumes/Buckets.
- Anzahlen/Status in `event_media_assets` (z.B. viele `pending` oder `failed`).
- **Fehler-Raten**
- HTTP 5xx/4xx spitzenweise, gruppiert nach Route/Service.
- ApplikationsLogs mit Error/WarningLevel.
- **Billing & Webhooks**
- Fehlgeschlagene Lemon Squeezy/RevenueCatWebhooks.
- Differenz zwischen erwarteten und verarbeiteten Zahlungen (optional).
## 2. Werkzeuge & Quellen
- **Horizon**
- LiveÜberblick über LaravelQueues.
- Alerts, wenn eine Queue zu lange Backlog aufbaut.
- **Docker/Dokploy**
- ContainerHealth (RestartLoops, Ressourcennutzung).
- ServiceHealthchecks.
- **Logs**
- LaravelLogs (`storage/logs/*.log`), ggf. via Promtail/Loki oder ELK zentralisiert.
- Spezifische Channels (z.B. `storage-jobs`, `notifications`, `billing`).
- **Metriken**
- Falls vorhanden: Prometheus/GrafanaDashboards für App/DB/RedisMetriken.
> TODO: Wenn konkrete Dashboards in Grafana o.Ä. existieren, füge hier Screenshots/Links und eine kurze Erklärung der Panels ein.
## 3. Alarmierung & Schwellenwerte
Konkrete Schwellenwerte hängen von Traffic und Infrastruktur ab, aber folgende Muster haben sich bewährt:
- **Queues**
- `default`:
- Warnung: > 100 Jobs oder ältester Job > 5 Minuten.
- Kritisch: > 300 Jobs oder ältester Job > 15 Minuten.
- `media-storage`:
- Warnung: > 200 Jobs oder ältester Job > 10 Minuten.
- Kritisch: > 500 Jobs oder ältester Job > 30 Minuten.
- `media-security`:
- Warnung: > 50 Jobs oder ältester Job > 5 Minuten.
- Kritisch: > 150 Jobs oder ältester Job > 15 Minuten.
- `notifications`:
- Warnung: > 100 Jobs dauerhaft im Backlog.
- Kritisch: wenn die Queue dauerhaft wächst, während Events live laufen.
- **Uploads**
- Fehlerquote bei UploadEndpoints:
- Warnung: 25 % Fehler (HTTP 4xx/5xx) über 5minütiges Fenster.
- Kritisch: > 510 % Fehler in 5 Minuten.
- Anzahl „hängender“ Uploads:
- Warnung, wenn `storage:check-upload-queues` für denselben Event wiederholt Alarm schlägt (z.B. mehr als 5 benachrichtigte Events in 10 Minuten).
- **Public API**
- Latenz für `GET /api/v1/events/{token}` und `/photos`:
- Warnung: P95 > 500 ms über 5 Minuten.
- Kritisch: P95 > 12 s über 5 Minuten.
- Fehlerraten für diese Endpoints:
- Warnung: > 2 % 5xx über 5 Minuten.
- Kritisch: > 5 % 5xx über 5 Minuten.
- **Billing**
- Failed Webhooks:
- Warnung: mehr als N (z.B. 510) fehlgeschlagene Webhooks pro 10 Minuten.
- Kritisch: schneller Anstieg oder > 20 % Fehleranteil.
- Differenz zwischen erwarteten und verarbeiteten Zahlungen:
- Regelmäßige Reports (z.B. täglich) statt harter Alerts, aber auffällige Abweichungen sollten ein Incident auslösen.
## 4. Betriebliche Nutzung
- **Daily Checks**
- Horizon QueueDashboard kurz prüfen.
- Logs auf neue Fehler/Warnmuster scannen.
- **Bei Incidents**
- MonitoringDaten helfen, Ursache und zeitlichen Verlauf zu rekonstruieren (siehe `docs/ops/incidents-major.md`).
## 5. Zusammenspiel mit bestehenden Kommandos
Einige ArtisanKommandos sind explizit für Monitoring/Health gedacht. Sie sollten in Cron/Scheduler oder externe Checks integriert werden:
- `storage:monitor` (falls vorhanden)
- Aggregiert StorageAuslastung und QueueHealth basierend auf `config/storage-monitor.php`.
- Kann Alerts per Mail/Log triggern, wenn Schwellwerte überschritten werden.
- `storage:check-upload-queues`
- Überprüft, ob Uploadbezogene Queues im erwarteten Rahmen liegen und triggert GastAlerts bei Problemen (siehe `docs/ops/guest-notification-ops.md`).
- `storage:archive-pending`
- Kein klassisches MonitoringKommando, aber relevant, um zu prüfen, ob Archivierungsjobs hinterherhinken (z.B. viele alte `hot`Assets).
Diese Kommandos sind kein Ersatz für echtes Monitoring, liefern aber wertvolle Signale, die in Dashboards und Alerts einfließen können.
## 6. Beispiele für Metrik- und Alert-Definitionen
Nachfolgend beispielhafte Formulierungen, wie Alerts unabhängig vom verwendeten MonitoringSystem aussehen könnten.
### 6.1 Queue-Backlog Alert (Pseudocode)
**Ziel**: Meldung, wenn `media-storage` zu lange Backlog aufbaut.
- Bedingung:
- `queue_length("media-storage") > 500` **OR**
- `oldest_job_age("media-storage") > 30min`
- Dauer:
- 2 aufeinanderfolgende Messintervalle (z.B. 2×5 Minuten).
- Aktion:
- Alarm an OnCall + Hinweis auf `docs/ops/media-storage-spec.md` und `docs/ops/dr-storage-issues.md`.
### 6.2 Upload-Error-Rate Alert
**Ziel**: UploadProbleme für Gäste früh erkennen.
- Bedingung:
- Anteil `5xx` oder „applikationsspezifische Fehlercodes“ bei `POST /api/v1/events/*/upload` > 5 % in 5 Minuten.
- Aktion:
- Alarm an OnCall, Link zum PublicAPIIncidentPlaybook und StorageRunbook.
### 6.3 Public-API-Latenz Alert
**Ziel**: Langsame Galerien / TokenAufrufe frühzeitig sehen.
- Bedingung:
- `P95(latency(GET /api/v1/events/*)) > 1000ms` über 5 Minuten.
- Aktion:
- Alarm an OnCall, eventuell automatische Skalierung oder Untersuchung (DB/RedisLast).
### 6.4 Billing-Webhook Alert
**Ziel**: Fehler bei Lemon Squeezy/RevenueCatWebhookVerarbeitung erkennen.
- Bedingung:
- Mehr als 10 fehlgeschlagene WebhookVerarbeitungen innerhalb von 10 Minuten, oder Verhältnis `failed/success` > 0,2.
- Aktion:
- Alarm an OnCall + Finance/BillingVerantwortliche, Verweis auf `docs/ops/billing-ops.md`.
Diese Beispiele sollen helfen, konkrete Regeln in eurem MonitoringTool zu definieren. Die genauen Zahlen sollten anhand realer TrafficMuster feinjustiert werden.
Dieses Dokument ist bewusst technologieagnostisch formuliert die konkrete Implementierung (Prometheus, Grafana, Loki, ELK, SaaSMonitoring) sollte hier nachgezogen und mit Beispielen ergänzt werden.

View File

@@ -0,0 +1,104 @@
---
title: GlitchTip (Error Monitoring)
---
GlitchTip 5.2 is our Sentry-compatible error and performance monitoring stack. It lives at **https://logsder.fotospiel.app** and accepts the standard Sentry SDKs (Laravel + React). This page explains how to wire the DSNs, roll it out in Docker/Dokploy, and how to sanity-check events. Keep PII out of breadcrumbs and context; log only what is needed to debug.
## 1) Environment variables
Set these in `.env` (never commit) and ensure they are passed through Docker (`docker-compose.dokploy.yml` already forwards them):
```
SENTRY_LARAVEL_DSN=https://<key>@logsder.fotospiel.app/<project-id>
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=0.05 # adjust per env
SENTRY_PROFILES_SAMPLE_RATE=0.02 # optional profiling
SENTRY_RELEASE=$(git rev-parse --short HEAD)
VITE_SENTRY_DSN=https://<key>@logsder.fotospiel.app/<project-id>
VITE_SENTRY_ENV=production
VITE_SENTRY_RELEASE=$(git rev-parse --short HEAD)
```
Notes:
- DSN is Sentry-format; projects are created in GlitchTip UI. Use per-environment DSNs when possible.
- Keep sampling conservative in production to avoid noise and cost.
- If you do **not** want PII, set `SENTRY_SEND_DEFAULT_PII=false` in Laravel config (or handle in `config/sentry.php`).
- For source map uploads via the Vite plugin, also set: `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_URL=https://logsder.fotospiel.app`.
## 2) Backend (Laravel 12, PHP 8.3)
1. Install SDK: `composer require sentry/sentry-laravel`.
2. (Optional) Publish config: `php artisan vendor:publish --provider="Sentry\\Laravel\\ServiceProvider" --tag="config"` → tweak `config/sentry.php` (environment, sample rates, PII).
3. Context enrichment (recommended):
- In `app/Exceptions/Handler.php`, set user (`id`, `email`) and tenant/event IDs on the Sentry scope.
- Drop noisy 4xx exceptions via `shouldReport` or by ignoring `HttpException` with status < 500.
4. Health check: run `php artisan sentry:test` (sends a test event).
5. Queue/CLI: the DSN is forwarded to workers via the shared env anchor, so job failures also report.
## 3) Frontend (React 19 / Vite 7 PWAs)
1. Install SDKs: `npm i @sentry/react @sentry/vite-plugin @sentry/tracing`.
2. Initialize in each app entry (`resources/js/guest/...` and `resources/js/admin/...`):
```ts
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENV,
release: import.meta.env.VITE_SENTRY_RELEASE,
tracesSampleRate: 0.05,
});
```
Wrap the root in `<Sentry.ErrorBoundary>` with a user-friendly fallback.
3. (Optional) Vite plugin for source maps:
```ts
import { sentryVitePlugin } from '@sentry/vite-plugin';
export default defineConfig({
plugins: [
react(),
sentryVitePlugin({
org: 'glitchtip',
project: 'fotospiel-web',
authToken: process.env.SENTRY_AUTH_TOKEN,
url: 'https://logsder.fotospiel.app', // GlitchTip base
}),
],
});
```
If you skip source map upload, events still work; stack traces will be minified.
## 4) Docker/Dokploy wiring
- `docker-compose.dokploy.yml` now forwards `SENTRY_*` and `VITE_SENTRY_*` to `app`, workers, scheduler, and build-time asset steps.
- Ensure the build container that runs `npm run build` sees `VITE_SENTRY_*` (set in Dokploy env UI or `.env` loaded by Dokploy).
- Redeploy stack after setting envs; no other container changes needed.
## 5) Rollout / validation checklist
1. Set env vars for the target environment (`production`, `staging`).
2. Deploy containers.
3. Trigger test events:
- Backend: `php artisan sentry:test`.
- Frontend: throw a test error from console (`Sentry.captureException(new Error('GlitchTip smoke test')))`.
4. Verify events arrive in GlitchTip project at `https://logsder.fotospiel.app`.
5. Tune `tracesSampleRate`/`profilesSampleRate` after a day of traffic.
## 6) Hygiene & PII guardrails
- Do not attach raw request bodies or tokens. Scrub secrets in Sentry beforeSend hooks if added.
- Limit user context to non-sensitive identifiers (user id/email) and tenant/event IDs.
- Avoid logging photo metadata or guest names in breadcrumbs.
## 7) Troubleshooting
- No events: check DSN matches project and env vars are present in container (`printenv | grep SENTRY`).
- Minified stack traces: upload source maps via the Vite plugin or disable code splitting for critical bundles.
- High volume: drop sampling (`tracesSampleRate`, `profilesSampleRate`) and add ignore rules for expected 4xx.

View File

@@ -0,0 +1,40 @@
---
title: Architekturdiagramme
---
Diese Seite bündelt einfache Diagramme für zentrale PlattformFlows. Sie sind absichtlich highlevel gehalten und sollen neuen Operatoren einen schnellen Überblick geben.
## 1. MedienPipeline (Mermaid)
```mermaid
flowchart LR
Guest[Guest PWA] -->|Foto upload| API[Laravel API]
API -->|Validierung & DB| DB[(DB: events,\nevent_media_assets)]
API -->|Datei schreiben| HotStorage[(Hot Storage\n/var/www/storage)]
HotStorage --> QueueMedia[Queue: media-storage]
QueueMedia --> WorkerMedia[Worker: media-storage-worker]
WorkerMedia --> Archive[(Archive Storage\nz.B. S3/Wasabi)]
WorkerMedia --> Thumbs[Job: Thumbnails]
Thumbs --> HotStorage
DB --> PublicAPI[Public API]
PublicAPI --> Guest
```
## 2. Checkout & Billing (Mermaid)
```mermaid
flowchart LR
Tenant[Browser Tenant-Admin] -->|Paket wählen| App[Laravel App]
App -->|CheckoutSession anlegen| DB[(DB: checkout_sessions,\n tenant_packages)]
App -->|Redirect| LemonSqueezy[Lemon Squeezy Checkout]
LemonSqueezy -->|Zahlung erfolgreich| Webhook[Lemon Squeezy Webhook Endpoint]
Webhook -->|Event verarbeiten| BillingService[CheckoutWebhookService]
BillingService -->|TenantPackage aktualisieren| DB
DB --> App
App --> Tenant
```

View File

@@ -44,9 +44,6 @@ export default [
'public',
'bootstrap/ssr',
'tailwind.config.js',
'docs/site/**',
'docs/site/.docusaurus/**',
'docs/site/build/**',
'i18next-scanner.config.js',
],
},

62
package-lock.json generated
View File

@@ -31,7 +31,6 @@
"@sentry/tracing": "^7.120.4",
"@sentry/vite-plugin": "^4.6.2",
"@stripe/stripe-js": "^8.6.1",
"@tailwindcss/vite": "^4.1.18",
"@tamagui/animations-react-native": "^2.0.0-rc.0",
"@tamagui/button": "~2.0.0-rc.0",
"@tamagui/config": "~2.0.0-rc.0",
@@ -74,7 +73,6 @@
"react-router-dom": "^7.12.0",
"swiper": "^12.0.3",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.0.0",
"tailwindcss-animate": "^1.0.7",
"tamagui": "^2.0.0-rc.0",
"typescript": "^5.9.3",
@@ -84,6 +82,8 @@
"@eslint/js": "^9.19.0",
"@laravel/vite-plugin-wayfinder": "^0.1.7",
"@playwright/test": "^1.57.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@tamagui/cli": "^2.0.0-rc.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
@@ -101,6 +101,7 @@
"playwright": "^1.55.1",
"prettier": "^3.8.0",
"shadcn": "^3.7.0",
"tailwindcss": "^4.1.18",
"typescript-eslint": "^8.53.0",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^2.1.9"
@@ -7116,6 +7117,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@@ -7131,6 +7133,7 @@
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -7140,6 +7143,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
@@ -7166,6 +7170,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7182,6 +7187,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7198,6 +7204,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7214,6 +7221,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7230,6 +7238,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7246,6 +7255,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7262,6 +7272,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7294,6 +7305,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7318,6 +7330,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -7339,6 +7352,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7355,6 +7369,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7364,10 +7379,38 @@
"node": ">= 10"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
"integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.18",
@@ -12222,6 +12265,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -12448,6 +12492,7 @@
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -16079,6 +16124,7 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -16425,6 +16471,7 @@
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"devOptional": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -16457,6 +16504,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16477,6 +16525,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16497,6 +16546,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16517,6 +16567,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16537,6 +16588,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16557,6 +16609,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16577,6 +16630,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16617,6 +16671,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16637,6 +16692,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -16657,6 +16713,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -21436,6 +21493,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"

View File

@@ -22,6 +22,8 @@
"@eslint/js": "^9.19.0",
"@laravel/vite-plugin-wayfinder": "^0.1.7",
"@playwright/test": "^1.57.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@tamagui/cli": "^2.0.0-rc.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
@@ -39,6 +41,7 @@
"playwright": "^1.55.1",
"prettier": "^3.8.0",
"shadcn": "^3.7.0",
"tailwindcss": "^4.1.18",
"typescript-eslint": "^8.53.0",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^2.1.9"
@@ -70,7 +73,6 @@
"@sentry/tracing": "^7.120.4",
"@sentry/vite-plugin": "^4.6.2",
"@stripe/stripe-js": "^8.6.1",
"@tailwindcss/vite": "^4.1.18",
"@tamagui/animations-react-native": "^2.0.0-rc.0",
"@tamagui/button": "~2.0.0-rc.0",
"@tamagui/config": "~2.0.0-rc.0",
@@ -113,7 +115,6 @@
"react-router-dom": "^7.12.0",
"swiper": "^12.0.3",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.0.0",
"tailwindcss-animate": "^1.0.7",
"tamagui": "^2.0.0-rc.0",
"typescript": "^5.9.3",

View File

@@ -0,0 +1,7 @@
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@plugin "@tailwindcss/typography";
@source '../../../../app/Filament/SuperAdminKb/**/*';
@source '../../../../resources/views/filament/superadmin-kb/**/*';
@source '../../../../vendor/guava/filament-knowledge-base/src/**/*';
@source '../../../../vendor/guava/filament-knowledge-base/resources/views/**/*';

View File

@@ -0,0 +1,7 @@
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@plugin "@tailwindcss/typography";
@source '../../../../app/Filament/SuperAdmin/**/*';
@source '../../../../resources/views/filament/super-admin/**/*';
@source '../../../../vendor/guava/filament-knowledge-base/src/**/*';
@source '../../../../vendor/guava/filament-knowledge-base/resources/views/**/*';

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SITE_DIR="${ROOT_DIR}/docs/site"
echo "[docs] Installing dependencies in ${SITE_DIR}"
cd "${SITE_DIR}"
npm ci
echo "[docs] Building static site"
npm run build
echo "[docs] Build complete. Output: ${SITE_DIR}/build"

View File

@@ -74,4 +74,27 @@ class UserRoleAccessTest extends TestCase
$response2 = $this->actingAs($user)->get('/event-admin/dashboard');
$response2->assertStatus(200);
}
public function test_super_admin_can_access_super_admin_docs_panel(): void
{
$user = User::factory()->create(['role' => 'super_admin']);
$response = $this->actingAs($user, 'super_admin')->get('/super-admin/docs');
$response->assertRedirect();
$redirectPath = parse_url((string) $response->headers->get('Location'), PHP_URL_PATH);
$this->assertNotSame('/super-admin/docs/login', $redirectPath);
$this->assertStringStartsWith('/super-admin/docs', (string) $redirectPath);
}
public function test_non_super_admin_cannot_access_super_admin_docs_panel(): void
{
$user = User::factory()->create(['role' => 'tenant_admin']);
$response = $this->actingAs($user, 'super_admin')->get('/super-admin/docs');
$response->assertForbidden();
}
}

View File

@@ -96,7 +96,26 @@ class FilamentPanelNavigationTest extends TestCase
$pages = $resource::getPages();
$this->assertNotEmpty($pages, $resource);
$registration = $pages['index'] ?? reset($pages);
$registration = $pages['index'] ?? null;
if (! $registration) {
foreach ($pages as $pageRegistration) {
$candidate = $pageRegistration instanceof PageRegistration ? $pageRegistration->getPage() : $pageRegistration;
if ($this->pageRequiresMountArguments($candidate)) {
continue;
}
$registration = $pageRegistration;
break;
}
}
if (! $registration) {
continue;
}
$pageClass = $registration instanceof PageRegistration ? $registration->getPage() : $registration;
Livewire::test($pageClass)
@@ -107,4 +126,23 @@ class FilamentPanelNavigationTest extends TestCase
Filament::setTenant(null, true);
}
}
private function pageRequiresMountArguments(string $pageClass): bool
{
if (! method_exists($pageClass, 'mount')) {
return false;
}
$reflection = new \ReflectionMethod($pageClass, 'mount');
foreach ($reflection->getParameters() as $parameter) {
if ($parameter->isOptional() || $parameter->isDefaultValueAvailable() || $parameter->allowsNull()) {
continue;
}
return true;
}
return false;
}
}

View File

@@ -25,6 +25,8 @@ const plugins: PluginOption[] = [
laravel({
input: [
'resources/css/app.css',
'resources/css/filament/superadmin/theme.css',
'resources/css/filament/superadmin-kb/theme.css',
'resources/js/app.js',
'resources/js/app.tsx',
'resources/js/guest-v2/main.tsx',