Kommagetrennte Liste von Regex-Mustern für auszuschließende Routen\n";
+ echo " --all Alle GET-Routen generieren (ignoriert #[StaticPage] Attribut)\n";
+ echo " --help Diese Hilfe anzeigen\n";
+ exit(0);
+}
+
+// Konfiguration
+$config = [
+ 'debug' => false,
+ 'async_discovery' => true,
+ // weitere Konfigurationsoptionen...
+];
+
+// Anwendung initialisieren
+$basePath = dirname(__DIR__);
+$bootstrapper = new AppBootstrapper($basePath, $config);
+$app = $bootstrapper->bootstrap();
+
+// Verzeichnis für statische Seiten
+$outputDir = $options['output'] ?? $basePath . '/public/static';
+
+// Routen zum Generieren
+$routesToGenerate = [];
+
+// Ausschlussmuster verarbeiten
+$excludePatterns = [];
+if (isset($options['exclude'])) {
+ $patterns = explode(',', $options['exclude']);
+ foreach ($patterns as $pattern) {
+ $excludePatterns[] = '/' . trim($pattern) . '/';
+ }
+}
+
+// Prüfen, ob eine manuelle Routendatei angegeben wurde
+if (isset($options['manual-routes']) && file_exists($options['manual-routes'])) {
+ $routesToGenerate = json_decode(file_get_contents($options['manual-routes']), true) ?? [];
+ echo "Routen aus Datei geladen: " . count($routesToGenerate) . " Routen gefunden.\n";
+} elseif (isset($options['all'])) {
+ // Alle GET-Routen sammeln (ignoriert StaticPage-Attribut)
+ $router = $app->getContainer()->get(HttpRouter::class);
+ $staticPageCollector = new StaticPageCollector($router);
+ $allRoutes = $staticPageCollector->collectAllGetRoutes();
+ $routesToGenerate = $staticPageCollector->filterRoutes($allRoutes, $excludePatterns);
+ echo "Alle GET-Routen gesammelt: " . count($routesToGenerate) . " Routen gefunden.\n";
+} else {
+ // Routen mit StaticPage-Attribut sammeln
+ $router = $app->getContainer()->get(HttpRouter::class);
+ $staticPageCollector = new StaticPageCollector($router);
+ $staticRoutes = $staticPageCollector->collectStaticPages();
+ $routesToGenerate = $staticPageCollector->filterRoutes($staticRoutes, $excludePatterns);
+ echo "Routen mit #[StaticPage] Attribut gesammelt: " . count($routesToGenerate) . " Routen gefunden.\n";
+}
+
+if (empty($routesToGenerate)) {
+ echo "Keine Routen zum Generieren gefunden.\n";
+ echo "Füge das #[StaticPage] Attribut zu Controller-Methoden hinzu oder verwende --all, um alle Routen zu generieren.\n";
+ exit(1);
+}
+
+// Static Site Generator initialisieren und ausführen
+$generator = new StaticSiteGenerator($app, $routesToGenerate, $outputDir);
+
+// Statische Seiten generieren
+echo "Beginne mit der Generierung von statischen Seiten...\n";
+$generator->generate();
+
+echo "\nDie statischen Seiten wurden erfolgreich generiert im Verzeichnis: {$outputDir}\n";
diff --git a/bin/setup b/bin/setup
index 29c4ae0a..0707018f 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,3 +1,86 @@
-#!/bin/sh
-# Führt das Ansible-Deploy-Playbook aus
-/home/michael/.local/bin/ansible-playbook -i ansible/inventory.ini ansible/playbooks/setup.yml
+#!/bin/bash
+# Server-Setup-Skript für verschiedene Umgebungen
+#!/bin/bash
+
+# Server-Setup-Skript für verschiedene Umgebungen
+
+# Konfiguration
+ANSIBLE_INVENTORY="ansible/inventory/hosts.ini"
+SETUP_PLAYBOOK="ansible/setup.yml"
+
+# Farbdefinitionen
+GREEN="\033[0;32m"
+YELLOW="\033[1;33m"
+RED="\033[0;31m"
+NC="\033[0m" # No Color
+
+# Funktion zum Anzeigen von Nachrichten
+echo_msg() {
+ echo -e "${GREEN}[SETUP]${NC} $1"
+}
+
+echo_warn() {
+ echo -e "${YELLOW}[WARNUNG]${NC} $1"
+}
+
+echo_error() {
+ echo -e "${RED}[FEHLER]${NC} $1"
+}
+
+# Parameter auswerten
+ENVIRONMENT="$1"
+TAGS="$2"
+
+if [ -z "$ENVIRONMENT" ]; then
+ echo_warn "Keine Umgebung angegeben. Verfügbare Optionen:"
+ echo " ./bin/setup staging - Staging-Server einrichten"
+ echo " ./bin/setup prod - Produktionsserver einrichten"
+ echo " ./bin/setup all - Alle Server einrichten"
+ exit 1
+fi
+
+# Tags zusammenbauen (falls angegeben)
+TAGS_OPTION=""
+if [ -n "$TAGS" ]; then
+ TAGS_OPTION="--tags=$TAGS"
+ echo_msg "Verwende Tags: $TAGS"
+fi
+
+# Limit für die Server-Auswahl
+LIMIT_OPTION=""
+case "$ENVIRONMENT" in
+ staging|stage)
+ LIMIT_OPTION="--limit=staging"
+ echo_msg "Richte Staging-Server ein..."
+ ;;
+ prod|production)
+ LIMIT_OPTION="--limit=production"
+ echo_msg "Richte Produktionsserver ein..."
+ read -p "Sind Sie sicher, dass Sie den Produktionsserver einrichten möchten? (j/N) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Jj]$ ]]; then
+ echo_warn "Einrichtung des Produktionsservers abgebrochen."
+ exit 1
+ fi
+ ;;
+ all)
+ echo_msg "Richte alle Server ein..."
+ ;;
+ *)
+ echo_error "Unbekannte Umgebung: $ENVIRONMENT"
+ exit 1
+ ;;
+esac
+
+# Setup-Playbook ausführen
+echo_msg "Führe Ansible-Playbook aus..."
+ansible-playbook -i "$ANSIBLE_INVENTORY" "$SETUP_PLAYBOOK" $LIMIT_OPTION $TAGS_OPTION
+
+# Setup-Status prüfen
+if [ $? -eq 0 ]; then
+ echo_msg "Server-Setup erfolgreich abgeschlossen."
+ exit 0
+else
+ echo_error "Server-Setup fehlgeschlagen! Bitte überprüfen Sie die Logs."
+ exit 1
+fi
diff --git a/build.sh b/build.sh
new file mode 100755
index 00000000..b12e3ee3
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+set -e
+
+echo "🚀 Starting Docker build with network resilience..."
+
+# Prüfe Netzwerk-Konnektivität
+echo "🔍 Checking network connectivity..."
+if ! curl -s --connect-timeout 5 https://registry-1.docker.io/v2/ > /dev/null; then
+ echo "⚠️ Docker Hub nicht erreichbar - verwende lokale Images"
+ export DOCKER_BUILDKIT=0
+ export COMPOSE_DOCKER_CLI_BUILD=0
+fi
+
+# DNS-Cache leeren
+echo "🔄 Flushing DNS cache..."
+sudo systemctl flush-dns 2>/dev/null || sudo systemd-resolve --flush-caches 2>/dev/null || true
+
+# Docker daemon neu starten falls nötig
+if ! docker info > /dev/null 2>&1; then
+ echo "🔄 Restarting Docker daemon..."
+ sudo systemctl restart docker
+ sleep 5
+fi
+
+# Versuche erst mit Pull
+echo "📥 Attempting to pull base images..."
+if timeout 60 docker-compose pull --ignore-pull-failures; then
+ echo "✅ Images pulled successfully"
+else
+ echo "⚠️ Pull failed - building with local images only"
+fi
+
+# Build mit verschiedenen Strategien
+echo "🏗️ Building containers..."
+
+# Strategie 1: Normaler Build
+if timeout 300 docker-compose build --parallel; then
+ echo "✅ Build completed successfully!"
+ exit 0
+fi
+
+echo "⚠️ Normal build failed - trying fallback strategies..."
+
+# Strategie 2: Ohne Cache und Pull
+if timeout 300 docker-compose build --no-cache --pull=false; then
+ echo "✅ Build completed with fallback strategy!"
+ exit 0
+fi
+
+# Strategie 3: Sequenzieller Build
+echo "🔄 Trying sequential build..."
+for service in web php db redis queue-worker; do
+ echo "Building $service..."
+ if timeout 300 docker-compose build --no-cache --pull=false "$service"; then
+ echo "✅ $service built successfully"
+ else
+ echo "❌ Failed to build $service"
+ fi
+done
+
+echo "🏁 Build process completed"
diff --git a/composer.json b/composer.json
index cc1208cc..c0d03787 100644
--- a/composer.json
+++ b/composer.json
@@ -25,10 +25,16 @@
}
},
"require": {
- "predis/predis": "^3.0",
+ "predis/predis": "^3.0",
"ext-dom": "*",
"ext-libxml": "*",
- "ext-curl": "*"
+ "ext-curl": "*",
+ "ext-pcntl": "*",
+ "ext-fileinfo": "*",
+ "ext-zlib": "*",
+ "ext-gd": "*",
+ "ext-pdo": "*",
+ "ext-apcu": "*"
},
"scripts": {
diff --git a/config/app.php b/config/app.php
index b3d9bbc7..de6bfcb8 100644
--- a/config/app.php
+++ b/config/app.php
@@ -1 +1,40 @@
true,
+
+ // Discovery-Konfiguration
+ 'async_discovery' => true,
+
+ // Standard-Middleware-Stack
+ 'middleware' => [
+ // Globale Middleware für alle Requests
+ 'global' => [
+ \App\Framework\Http\Middlewares\ControllerRequestMiddleware::class,
+ \App\Framework\Validation\ValidationErrorMiddleware::class,
+ \App\Framework\Http\Middlewares\RoutingMiddleware::class
+ ],
+
+ // Gruppen-spezifische Middleware
+ 'api' => [
+ // Hier können Sie API-spezifische Middleware hinzufügen
+ ],
+
+ 'web' => [
+ // Hier können Sie Web-spezifische Middleware hinzufügen
+ ]
+ ],
+
+ // Cache-Einstellungen
+ 'cache' => [
+ 'enabled' => true,
+ 'ttl' => 3600, // Zeit in Sekunden
+ ],
+
+ // Logging-Einstellungen
+ 'logging' => [
+ 'level' => 'debug', // Optionen: debug, info, notice, warning, error, critical, alert, emergency
+ 'path' => 'logs/app.log',
+ ],
+];
diff --git a/config/middleware.php b/config/middleware.php
new file mode 100644
index 00000000..5d7e5176
--- /dev/null
+++ b/config/middleware.php
@@ -0,0 +1,11 @@
+ [
+ ServeStaticFilesMiddleware::class,
+ // Weitere globale Middleware hier hinzufügen
+ ]
+];
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 00000000..7f620e81
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,62 @@
+has(RedisClient::class)) {
+ $container->bind(RedisClient::class, function () {
+ return new RedisClient([
+ 'scheme' => $_ENV['REDIS_SCHEME'] ?? 'tcp',
+ 'host' => $_ENV['REDIS_HOST'] ?? 'redis',
+ 'port' => (int)($_ENV['REDIS_PORT'] ?? 6379),
+ ]);
+ });
+ }
+
+ // Rate Limiter Konfiguration
+ $container->bind(RateLimiterConfig::class, function () {
+ $config = new RateLimiterConfig(
+ defaultLimit: (int)($_ENV['RATE_LIMIT_DEFAULT'] ?? 60),
+ windowSeconds: (int)($_ENV['RATE_LIMIT_WINDOW'] ?? 60)
+ );
+
+ // Strengere Limits für sensible Bereiche
+ return $config
+ ->withAuthLimit((int)($_ENV['RATE_LIMIT_AUTH'] ?? 10), (int)($_ENV['RATE_LIMIT_AUTH_WINDOW'] ?? 300))
+ ->withApiLimit((int)($_ENV['RATE_LIMIT_API'] ?? 30), (int)($_ENV['RATE_LIMIT_API_WINDOW'] ?? 60))
+ // Weitere Limits hier hinzufügen...
+ ;
+ });
+
+ // Rate Limiter Service
+ $container->bind(RedisRateLimiter::class, function ($container) {
+ return new RedisRateLimiter(
+ $container->get(RedisClient::class)
+ );
+ });
+
+ // Rate Limiting Middleware
+ $container->bind(RateLimitingMiddleware::class, function ($container) {
+ $config = $container->get(RateLimiterConfig::class);
+ return new RateLimitingMiddleware(
+ $container->get(RedisClient::class),
+ $config->getDefaultLimit(),
+ $config->getWindowSeconds(),
+ $config->getPathLimits()
+ );
+ });
+
+ // Weitere Dienste hier registrieren...
+};
diff --git a/config/static-routes.json b/config/static-routes.json
new file mode 100644
index 00000000..2412e0fe
--- /dev/null
+++ b/config/static-routes.json
@@ -0,0 +1,10 @@
+[
+ "/",
+ "/about",
+ "/contact",
+ "/blog",
+ "/products",
+ "/services",
+ "/imprint",
+ "/privacy"
+]
diff --git a/console.php b/console.php
new file mode 100755
index 00000000..a7761463
--- /dev/null
+++ b/console.php
@@ -0,0 +1,27 @@
+#!/usr/bin/env php
+bootstrapConsole()->run($argv);
+ exit($exitCode);
+
+} catch (Throwable $e) {
+ echo "\033[31mFehler: " . $e->getMessage() . "\033[0m" . PHP_EOL;
+ exit(1);
+}
+
+/*
+ * docker exec php php console.php
+ */
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 00000000..ec87eeca
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# Konfiguration
+SERVER_USER="deploy"
+SERVER_IP="94.16.110.151"
+REMOTE_PATH="/var/www/michaelschiemer"
+LOCAL_PATH="."
+EXCLUDES="--exclude=.git --exclude=node_modules --exclude=vendor --exclude=.env"
+SSH_OPTS="-o StrictHostKeyChecking=no"
+
+# Farben für Ausgabe
+GREEN="\e[32m"
+YELLOW="\e[33m"
+RED="\e[31m"
+RESET="\e[0m"
+
+echo -e "${YELLOW}Deployment auf ${SERVER_IP} starten...${RESET}"
+
+# 1. Temporäres Verzeichnis erstellen
+TMP_DIR=$(mktemp -d)
+echo -e "${YELLOW}Temporäres Verzeichnis erstellt: ${TMP_DIR}${RESET}"
+
+# 2. Dateien in temporäres Verzeichnis kopieren (ohne Ausschlüsse)
+echo -e "${YELLOW}Kopiere Projektdateien...${RESET}"
+rsync -a $EXCLUDES "$LOCAL_PATH/" "$TMP_DIR/"
+
+# 3. Archiv erstellen
+echo -e "${YELLOW}Erstelle Archiv...${RESET}"
+TMP_ARCHIVE="/tmp/project-$(date +%Y%m%d-%H%M%S).tar.gz"
+tar -czf "$TMP_ARCHIVE" -C "$TMP_DIR" .
+
+# 4. Verzeichnisse auf dem Server erstellen
+echo -e "${YELLOW}Erstelle Verzeichnisse auf dem Server...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "mkdir -p $REMOTE_PATH"
+
+# 5. Archiv auf den Server übertragen
+echo -e "${YELLOW}Übertrage Dateien auf den Server...${RESET}"
+scp $SSH_OPTS "$TMP_ARCHIVE" "$SERVER_USER@$SERVER_IP:/tmp/project.tar.gz"
+
+# 6. Archiv auf dem Server entpacken
+echo -e "${YELLOW}Entpacke Dateien auf dem Server...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "tar -xzf /tmp/project.tar.gz -C $REMOTE_PATH"
+
+# 7. Umgebungsdatei erstellen, falls nicht vorhanden
+echo -e "${YELLOW}Überprüfe/erstelle .env Datei...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "[ -f $REMOTE_PATH/.env ] || cp $REMOTE_PATH/.env.example $REMOTE_PATH/.env"
+
+# 8. Berechtigungen setzen
+echo -e "${YELLOW}Setze Berechtigungen...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "chmod -R 775 $REMOTE_PATH/storage $REMOTE_PATH/cache"
+
+# 9. Abhängigkeiten installieren und Build ausführen
+echo -e "${YELLOW}Installiere Abhängigkeiten und baue Assets...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "cd $REMOTE_PATH && composer install --no-dev && npm ci && npm run build"
+
+# 10. Docker Container neu starten
+echo -e "${YELLOW}Starte Docker Container neu...${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "cd $REMOTE_PATH && docker compose up -d"
+
+# 11. Aufräumen
+echo -e "${YELLOW}Räume auf...${RESET}"
+rm -rf "$TMP_DIR" "$TMP_ARCHIVE"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "rm /tmp/project.tar.gz"
+
+echo -e "${GREEN}Deployment abgeschlossen!${RESET}"
+echo -e "${YELLOW}Docker Container Status:${RESET}"
+ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "cd $REMOTE_PATH && docker compose ps"
+
+exit 0
diff --git a/docker-compose.yml b/docker-compose.yml
index f7b42311..14ca363f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,99 +1,166 @@
-x-docker-settings: &docker-settings
- build:
- context: .
- args:
- - BUILDKIT_INLINE_CACHE=1
-
-services:
- web:
- build:
- context: ./docker/nginx
- dockerfile: Dockerfile
- ports:
- - "${APP_PORT:-8000}:80"
- - "127.0.0.1:8080:80"
- - "${APP_SSL_PORT:-443}:443"
- environment:
- - APP_ENV=${APP_ENV:-development}
- volumes:
- - ./:/var/www/html:cached
- - ./ssl:/etc/nginx/ssl:ro # SSL-Zertifikate mounten
- depends_on:
- php:
- condition: service_started
- restart: unless-stopped
- networks:
- - frontend
- - backend
- env_file:
- - .env
-
- php:
- container_name: php
- build:
- context: .
- dockerfile: docker/php/Dockerfile
- args:
- - ENV=${APP_ENV:-dev}
- - COMPOSER_INSTALL_FLAGS=${COMPOSER_INSTALL_FLAGS:---no-scripts --no-autoloader}
-
- volumes:
- # Shared Volume für Composer-Cache über Container-Neustarts hinweg
- - composer-cache:/root/.composer/cache
- # Bindet das Projektverzeichnis für Live-Änderungen ein
- - ./:/var/www/html:cached
- # Verhindert Überschreiben der Vendor-Verzeichnisse
- #- /var/www/html/vendor
-
- #- cache-volume:/var/www/html/cache:rw
- environment:
- PHP_IDE_CONFIG: "serverName=docker"
- APP_ENV: ${APP_ENV:-development}
-
- healthcheck:
- test: [ "CMD", "php", "-v" ]
- interval: 10s
- timeout: 5s
- retries: 5
-
- restart: unless-stopped
- networks:
- - backend
- #- cache
-# backend:
-# aliases:
-# - php
-# cache:
- env_file:
- - .env
-
- redis:
- image: redis:8-alpine
- volumes:
- - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
- - redis_data:/data
- command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 30s
- timeout: 5s
- retries: 3
-
- restart: unless-stopped
- networks:
- - cache
- env_file:
- - .env
-
-networks:
- frontend:
- driver: bridge
- backend:
- driver: bridge
- cache:
- driver: bridge
-
-volumes:
- redis_data:
- composer-cache:
- #cache-volume:
+services:
+ web:
+ container_name: web
+ build:
+ context: docker/nginx
+ dockerfile: Dockerfile
+ ports:
+ - "${APP_PORT:-8000}:80"
+ - "127.0.0.1:8080:80"
+ - "${APP_SSL_PORT:-443}:443/tcp"
+ - "443:443/udp"
+ environment:
+ - APP_ENV=${APP_ENV:-development}
+ healthcheck:
+ test: ["CMD", "curl", "-f", "https://localhost/"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
+ volumes:
+ - ./:/var/www/html:cached
+ #- ./ssl:/etc/nginx/ssl:ro # SSL-Zertifikate mounten
+ - ./ssl:/var/www/ssl:ro
+ depends_on:
+ php:
+ condition: service_started
+ restart: unless-stopped
+ networks:
+ - frontend
+ - backend
+ env_file:
+ - .env
+
+ php:
+ container_name: php
+ build:
+ context: .
+ dockerfile: docker/php/Dockerfile
+ args:
+ - ENV=${APP_ENV:-dev}
+ - COMPOSER_INSTALL_FLAGS=${COMPOSER_INSTALL_FLAGS:---no-scripts --no-autoloader}
+ user: "1000:1000"
+ logging:
+ driver: "local"
+ options:
+ max-size: "5m"
+ max-file: "2"
+ volumes:
+ # Shared Volume für Composer-Cache über Container-Neustarts hinweg
+ - composer-cache:/root/.composer/cache
+ # Bindet das Projektverzeichnis für Produktivbetrieb ein
+ #- project-data:/var/www/html:cached
+ # Variante mit mounting:
+ - ./:/var/www/html:cached
+ # Verhindert Überschreiben der Vendor-Verzeichnisse
+ #- /var/www/html/vendor
+ #- cache-volume:/var/www/html/cache:rw
+ environment:
+ PHP_IDE_CONFIG: "serverName=docker"
+ APP_ENV: ${APP_ENV:-development}
+ healthcheck:
+ test: [ "CMD", "php", "-v" ]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ restart: unless-stopped
+ networks:
+ - backend
+ - cache
+ env_file:
+ - .env
+
+ db:
+ container_name: db
+ image: mariadb:latest
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: qwee65132ertert # ändere das bitte!
+ MYSQL_DATABASE: database # optionale Initial-Datenbank
+ MYSQL_USER: mdb-user # optionaler zusätzlicher User
+ MYSQL_PASSWORD: dfghreh5465fghfgh # Passwort für den zusätzlichen User
+ ports:
+ - "33060:3306"
+ volumes:
+ - db_data:/var/lib/mysql
+ healthcheck:
+ test: [ "CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-u", "root", "-pqwee65132ertert" ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 30s
+ networks:
+ - backend
+
+ redis:
+ container_name: redis
+ image: redis:8-alpine
+ volumes:
+ - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
+ - redis_data:/data
+ command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ restart: unless-stopped
+ networks:
+ - cache
+ env_file:
+ - .env
+
+ queue-worker:
+ container_name: queue-worker
+ build:
+ context: .
+ dockerfile: docker/worker/Dockerfile
+ # user: "1000:1000" # Same user ID as PHP container
+ depends_on:
+ php:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ db:
+ condition: service_healthy
+ volumes:
+ - ./:/var/www/html:cached
+ - ./storage/logs:/var/www/html/storage/logs:rw
+ - ./src/Framework/CommandBus/storage:/var/www/html/src/Framework/CommandBus/storage:rw
+ environment:
+ - APP_ENV=${APP_ENV:-development}
+ - WORKER_DEBUG=${WORKER_DEBUG:-false}
+ - WORKER_SLEEP_TIME=${WORKER_SLEEP_TIME:-100000}
+ - WORKER_MAX_JOBS=${WORKER_MAX_JOBS:-1000}
+ restart: unless-stopped
+ networks:
+ - backend
+ - cache
+ env_file:
+ - .env
+ # Graceful shutdown timeout
+ stop_grace_period: 30s
+ # Resource limits for the worker
+ deploy:
+ resources:
+ limits:
+ memory: 512M
+ reservations:
+ memory: 256M
+
+networks:
+ frontend:
+ driver: bridge
+ backend:
+ driver: bridge
+ cache:
+ driver: bridge
+
+volumes:
+ redis_data:
+ composer-cache:
+ #cache-volume:
+ db_data:
+ project-data:
+ worker-logs:
+ worker-queue:
diff --git a/docker/DOCKER-TODO.md b/docker/DOCKER-TODO.md
new file mode 100644
index 00000000..ac49566d
--- /dev/null
+++ b/docker/DOCKER-TODO.md
@@ -0,0 +1,48 @@
+# Optimierungsvorschläge für die Docker-Compose Umgebung
+
+## ToDo-Liste
+
+- [ ] **Datenbank Passwörter & Secrets absichern**
+ - Datenbankpasswörter nicht im Klartext in der YAML speichern, sondern `secrets`-Mechanismus verwenden.
+ - `.env` Werte für Datenbanken statt statischer Angaben verwenden.
+ - Beispiel: `MYSQL_ROOT_PASSWORD_FILE` und `MYSQL_PASSWORD_FILE` setzen und Secrets einbinden.
+
+- [ ] **Performance & Caching verbessern**
+ - `cache_from` und `cache_to` im Build-Prozess (BuildKit) einrichten.
+ - Für PHP einen dedizierten Volume für den Composer-Cache nutzen.
+ - Nginx-Cache als eigenes Volume deklarieren.
+ - Die Vendor-Ordner aus Mounts ausschließen oder gesondert berücksichtigen, damit lokale Änderungen keine Build-Optimierungen verhindern.
+
+- [ ] **Netzwerk- und Bind-Mounts optimieren**
+ - Bei Nginx nur das Public-Verzeichnis (`public/`) einbinden, nicht das gesamte Projektverzeichnis.
+ - Nicht benötigte Verzeichnisse (wie z.B. `vendor/`) explizit ausschließen.
+ - Healthchecks und Startbedingungen konsistent definieren.
+
+- [ ] **Image-Versionen festlegen**
+ - Keine `latest`-Images nutzen, sondern möglichst immer eine feste Version angeben (z.B. `mariadb:11.3` statt `mariadb:latest`).
+ - Gilt auch für Redis, PHP und weitere Services.
+
+- [ ] **Ressourcenlimits setzen**
+ - `deploy.resources` für Speicher und CPU bei allen Services, nicht nur beim Worker.
+
+- [ ] **Security-Best-Practices**
+ - Nicht produktive Ports (z.B. bei Entwicklung) durch `.env` variabel und gezielt auf localhost begrenzen.
+ - Feste Netzwerkbereiche und eigene Netzwerke für sensible Kommunikation (z.B. Backend, Cache).
+
+- [ ] **Multi-Stage Builds in Dockerfiles nutzen**
+ - Die Images im PHP- und Worker-Bereich sollten über Multi-Stage-Builds möglichst klein gehalten werden (z.B. `FROM php:X-cli AS base`, dann Production-Image).
+
+- [ ] **Environment-Konfiguration für Dev/Prod trennen**
+ - Eine `docker-compose.override.yml` für Entwicklung mit vollem Source-Mount und Debug-Konfiguration anlegen.
+ - Für Produktion keine Source-Mounts, keine Debug-Variablen, optimierte Settings.
+
+- [ ] **Log-Rotation aktivieren**
+ - Logging-Driver auf `json-file` einstellen und Optionen für Größe/Rotation setzen.
+
+- [ ] **Monitoring & Healthchecks**
+ - Für alle Services sinnvolle Healthchecks ergänzen.
+ - (Optional) Monitoring und/oder Alerting ergänzen.
+
+---
+
+**Tipp:** Die oben stehenden Punkte können Schritt für Schritt umgesetzt und pro optimiertem Bereich abgehakt werden.
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index fb9af359..0705b114 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -1,33 +1,73 @@
-FROM nginx:alpine
+FROM macbre/nginx-http3
-# Standard-Konfiguration entfernen
-RUN rm /etc/nginx/conf.d/default.conf
+# Zurück zu root wechseln
+USER root
-# Verzeichnisse erstellen mit korrekten Berechtigungen
-RUN mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/template && \
- chmod -R 777 /var/cache/nginx /var/log/nginx
+# Entferne Default-Site
+RUN rm -f /etc/nginx/conf.d/default.conf || true
-# Kopiere die Template-Konfiguration
+# Verzeichnisse erstellen
+RUN mkdir -p /var/cache/nginx /var/log/nginx /var/www/ssl && \
+ chmod 755 /var/cache/nginx /var/log/nginx /var/www/ssl
+
+# Konfigurationen kopieren
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf
+COPY ./ssl/ /var/www/ssl/
-# Kopiere die SSL-Zertifikate
-COPY ./ssl/ /etc/nginx/ssl/
+# Entry-Script kopieren
+COPY ./docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
+RUN chmod +x /usr/local/bin/docker-entrypoint.sh
-# Startup-Skript zum Ersetzen der Variablen
-COPY ./docker-entrypoint.sh /
-RUN chmod +x /docker-entrypoint.sh
+# su-exec und netcat installieren
+RUN apk add --no-cache su-exec netcat-openbsd
-#Install Netcat
-RUN apk add --no-cache netcat-openbsd
+# Berechtigungen für stdout/stderr anpassen
+RUN chmod a+rw /dev/stdout /dev/stderr
-
-# Als user www-data laufen lassen
-RUN addgroup -g 1000 www && adduser -D -G www -u 1000 www-data \
- && chown -R www-data:www /var/cache/nginx /var/log/nginx /etc/nginx
-USER www-data
+# Ordner-Berechtigungen für den nginx-User setzen
+RUN chown -R nginx:nginx /var/cache/nginx /var/log/nginx /var/www/ssl
EXPOSE 80 443
-ENTRYPOINT ["/docker-entrypoint.sh"]
-CMD ["nginx", "-g", "daemon off;"]
+ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+#CMD ["nginx", "-g", "daemon off;"]
+
+
+
+
+
+## Standard-Konfiguration entfernen
+#RUN rm -f /etc/nginx/conf.d/default.conf
+#
+## Verzeichnisse erstellen mit korrekten Berechtigungen
+#RUN mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/template && \
+# chmod -R 777 /var/cache/nginx /var/log/nginx
+#
+## Kopiere die Template-Konfiguration
+#COPY ./nginx.conf /etc/nginx/nginx.conf
+#COPY ./default.conf /etc/nginx/conf.d/default.conf
+#
+## Kopiert config Include
+#COPY ./vite-proxy.inc.dev /etc/nginx/vite-proxy.inc
+#
+## Kopiere die SSL-Zertifikate
+#COPY ./ssl/ /etc/nginx/ssl/
+#
+## Startup-Skript zum Ersetzen der Variablen
+#COPY ./docker-entrypoint.sh /
+#RUN chmod +x /docker-entrypoint.sh
+#
+##Install Netcat
+#RUN apk add --no-cache netcat-openbsd
+#
+#
+## Als user www-data laufen lassen
+#RUN addgroup -g 1000 www && adduser -D -G www -u 1000 www-data \
+# && chown -R www-data:www /var/cache/nginx /var/log/nginx /etc/nginx
+#USER www-data
+#
+#EXPOSE 80 443
+#
+#ENTRYPOINT ["/docker-entrypoint.sh"]
+#CMD ["nginx", "-g", "daemon off;"]
diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf
index 7b7b379c..966289a7 100644
--- a/docker/nginx/default.conf
+++ b/docker/nginx/default.conf
@@ -18,8 +18,8 @@ map $env_mode $should_skip_cache {
# Skip-Cache für Sessions und basierend auf Umgebung
map $http_cookie$should_skip_cache $skip_cache {
- "~PHPSESSID" 1; # Sessions nie cachen
- "1" 1; # Cache überspringen, wenn should_skip_cache = 1
+ "~ms_context" 1; # Sessions nie cachen
+ "~1$" 1; # Cache überspringen, wenn should_skip_cache = 1
default 0; # Ansonsten cachen
}
@@ -41,23 +41,38 @@ server {
server {
# Korrigierte HTTP/2 Syntax
listen 443 ssl;
+ listen [::]:443 ssl;
+ listen 443 quic; # QUIC für HTTP/3 // Removed "reuseport"
+ listen [::]:443 quic;
+
http2 on; # Neue Syntax für HTTP/2
+ http3 on;
+
+ add_header Alt-Svc 'h3=":443"; ma=86400; persist=1, h2=":443"; ma=86400';
+
server_name localhost;
#ssl_certificate /etc/nginx/ssl/localhost+2.pem;
#ssl_certificate_key /etc/nginx/ssl/localhost+2-key.pem;
- ssl_certificate /etc/nginx/ssl/fullchain.pem;
- ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+ #ssl_certificate /etc/nginx/ssl/fullchain.pem;
+ #ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+ ssl_certificate /var/www/ssl/fullchain.pem;
+ ssl_certificate_key /var/www/ssl/privkey.pem;
- ssl_protocols TLSv1.2 TLSv1.3;
+ # add_header Alt-Svc 'h3=":443"'; # Für HTTP/3 Unterstützung
+ #add_header QUIC-Status $quic;
+
+
+ ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Verbesserte SSL-Konfiguration
- ssl_session_timeout 1d;
- ssl_session_cache shared:SSL:10m;
- ssl_session_tickets off;
+ #ssl_session_timeout 1d;
+ #ssl_session_cache shared:SSL:10m;
+ #ssl_session_tickets off;
# OCSP Stapling (auskommentiert, wenn Zertifikate fehlen)
# ssl_stapling on;
@@ -68,6 +83,11 @@ server {
root /var/www/html/public;
index index.php index.html;
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ autoindex off;
+ }
+
# Debug-Header für die Entwicklung
add_header X-Environment $env_mode always;
@@ -88,6 +108,34 @@ server {
client_max_body_size 10m;
large_client_header_buffers 2 1k;
+ brotli on;
+ brotli_comp_level 6;
+ brotli_types
+ application/atom+xml
+ application/javascript
+ application/json
+ application/ld+json
+ application/manifest+json
+ application/rss+xml
+ application/vnd.geo+json
+ application/vnd.ms-fontobject
+ application/x-font-ttf
+ application/x-web-app-manifest+json
+ application/xhtml+xml
+ application/xml
+ font/opentype
+ image/bmp
+ image/svg+xml
+ image/x-icon
+ text/cache-manifest
+ text/css
+ text/plain
+ text/vcard
+ text/vnd.rim.location.xloc
+ text/vtt
+ text/x-componentFs
+ text/x-cross-domain-policy;
+
# Verbesserte Gzip-Kompression
gzip on;
gzip_vary on;
@@ -119,33 +167,43 @@ server {
text/vcard
text/vnd.rim.location.xloc
text/vtt
- text/x-component
+ text/x-componentFs
text/x-cross-domain-policy;
# Logs
- access_log /var/log/nginx/access.log combined;
- error_log /var/log/nginx/error.log error;
+ #access_log /var/log/nginx/access.log combined;
+ #error_log /var/log/nginx/error.log error;
+ access_log /dev/stdout;
+ error_log /dev/stderr warn;
- location / {
- try_files $uri $uri/ /index.php?$query_string;
- autoindex off;
+ # läuft aktuell oben über dynamischen include!
+ #location / {
+ # try_files $uri $uri/ /index.php?$query_string;
+ # autoindex off;
+ #}
+
+ # Service-Worker explizit erlauben (auch im Production-Server ungefährlich!)
+ location = /sw.js {
+ # je nach Build-Ordner anpassen!
+ alias /var/www/html/public/sw.js;
+ add_header Cache-Control "no-cache, must-revalidate";
}
# Caching Header für statische Dateien
- location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
- expires 1y;
- add_header Cache-Control "public, immutable, max-age=31536000";
- }
+ #location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ # expires 1y;
+ # add_header Cache-Control "public, immutable, max-age=31536000";
+ #}
location ~* \.(css|js)$ {
expires 1w;
add_header Cache-Control "public, max-age=604800";
}
- location ~* \.(json|xml)$ {
- expires 1d;
- add_header Cache-Control "public, max-age=86400";
- }
+# location ~* \.(json|xml)$ {
+# expires 1d;
+# add_header Cache-Control "public, max-age=86400";
+# }
location ~ \.php$ {
try_files $uri =404;
@@ -182,6 +240,8 @@ server {
# Für bessere Performance
fastcgi_keep_conn on;
+
+ fastcgi_hide_header X-Powered-By;
}
# Sicherheitseinstellungen
diff --git a/docker/nginx/docker-entrypoint.sh b/docker/nginx/docker-entrypoint.sh
index 2ec3a5f4..d7641c97 100644
--- a/docker/nginx/docker-entrypoint.sh
+++ b/docker/nginx/docker-entrypoint.sh
@@ -1,20 +1,58 @@
#!/bin/sh
set -e
+#!/bin/sh
+set -e
+# Umgebungsvariablen-Substitution in Nginx-Konfiguration
+if [ -n "$APP_ENV" ]; then
+ echo "Setting APP_ENV to: $APP_ENV"
+ sed -i "s/\${APP_ENV}/$APP_ENV/g" /etc/nginx/conf.d/default.conf
+fi
+
+# Warte auf PHP-FPM Container
+echo "Waiting for PHP-FPM to be ready..."
+while ! nc -z php 9000; do
+ sleep 1
+done
+echo "PHP-FPM is ready!"
+
+# SSL-Zertifikate prüfen
+if [ ! -f "/var/www/ssl/fullchain.pem" ] || [ ! -f "/var/www/ssl/privkey.pem" ]; then
+ echo "Warning: SSL certificates not found. HTTPS may not work properly."
+fi
+
+# Nginx-Konfiguration testen
+echo "Testing Nginx configuration..."
+nginx -t
+
+# Nginx starten
+echo "Starting Nginx..."
+exec "$@"
until nc -z -w 2 php 9000; do
echo "Warte auf PHP-FPM..."
sleep 1
done
-
# Optional: eigene Umgebungsvariable mit Default setzen
export APP_ENV="${APP_ENV:-production}"
echo "Starte Nginx mit APP_ENV=$APP_ENV"
-# Ersetze Umgebungsvariablen wie ${APP_ENV} in der Nginx-Config per envsubst
-envsubst '${APP_ENV}' < /etc/nginx/conf.d/default.conf > /etc/nginx/conf.d/default.conf.tmp
-mv /etc/nginx/conf.d/default.conf.tmp /etc/nginx/conf.d/default.conf
+# Ersetze Platzhalter in temporäre Datei
+envsubst '${APP_ENV}' < /etc/nginx/conf.d/default.conf > /tmp/default.conf
-# Starte Nginx (Foreground)
+# Ersetzte Originalkonfiguration
+cp /tmp/default.conf /etc/nginx/conf.d/default.conf
+
+# WICHTIG: Rechte für stdout/stderr anpassen
+chmod a+rw /dev/stdout /dev/stderr
+
+# Nginx-Ordner Rechte anpassen
+mkdir -p /var/cache/nginx /var/log/nginx
+chown -R nginx:nginx /var/cache/nginx /var/log/nginx
+
+# Stelle sicher, dass das SSL-Verzeichnis existiert
+mkdir -p /var/www/ssl
+
+# Jetzt kann nginx sicher starten
exec nginx -g 'daemon off;'
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index 1a675e65..3415cddb 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -1,37 +1,78 @@
+# Standard Nginx User
+user nginx;
worker_processes auto;
-pid /tmp/nginx.pid;
+error_log /var/log/nginx/error.log notice;
+pid /var/run/nginx.pid;
+# Worker-Prozess-Einstellungen
events {
worker_connections 1024;
+ use epoll;
+ multi_accept on;
}
http {
+ # MIME-Types
include /etc/nginx/mime.types;
default_type application/octet-stream;
+ # Server-Tokens für Sicherheit ausblenden
server_tokens off;
- # Rate-Limiting für besseren DDoS-Schutz
+ # Rate-Limiting
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
- # Logging-Einstellungen
+ # Logging-Format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
+ '"$http_user_agent" "$http_x_forwarded_for" '
+ 'rt=$request_time uct="$upstream_connect_time" '
+ 'uht="$upstream_header_time" urt="$upstream_response_time"';
- access_log /var/log/nginx/access.log main;
- error_log /var/log/nginx/error.log warn;
+ # Container-optimierte Logs
+ access_log /dev/stdout main;
+ error_log /dev/stderr warn;
+ # Performance-Optimierungen
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
+ client_max_body_size 64M;
- # TLS-Einstellungen
- ssl_session_cache shared:SSL:10m;
- ssl_session_timeout 10m;
+ # Gzip-Kompression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/json
+ application/javascript
+ application/xml+rss
+ application/atom+xml
+ image/svg+xml
+ application/rss+xml
+ application/vnd.ms-fontobject
+ application/x-font-ttf
+ font/opentype;
- # Include server configs
+ # Basis-Sicherheits-Header
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer-when-downgrade" always;
+
+ # SSL-Session-Cache
+ # ssl_session_cache shared:SSL:10m;
+ # ssl_session_timeout 10m;
+ # ssl_session_tickets off;
+
+ # Server-Konfigurationen einbinden
include /etc/nginx/conf.d/*.conf;
}
diff --git a/docker/nginx/vite-proxy.inc.dev b/docker/nginx/vite-proxy.inc.dev
new file mode 100644
index 00000000..991db4b7
--- /dev/null
+++ b/docker/nginx/vite-proxy.inc.dev
@@ -0,0 +1,4 @@
+location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ autoindex off;
+}
diff --git a/docker/nginx/vite-proxy.inc.prod b/docker/nginx/vite-proxy.inc.prod
new file mode 100644
index 00000000..991db4b7
--- /dev/null
+++ b/docker/nginx/vite-proxy.inc.prod
@@ -0,0 +1,4 @@
+location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ autoindex off;
+}
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
index 18949481..d5a5c343 100644
--- a/docker/php/Dockerfile
+++ b/docker/php/Dockerfile
@@ -7,10 +7,34 @@ RUN apt-get update && apt-get install -y \
unzip \
libzip-dev \
zip \
- && docker-php-ext-install zip pdo pdo_mysql \
- && docker-php-ext-install opcache \
+ libpng-dev \
+ libjpeg-dev \
+ libfreetype6-dev \
+ libwebp-dev \
+ libavif-dev \
+ libxpm-dev \
&& apt-get clean \
- && rm -rf /var/lib/apt/lists/*
+ && rm -rf /var/lib/apt/lists/*
+
+RUN docker-php-ext-configure gd \
+ --with-freetype \
+ --with-jpeg \
+ --with-webp \
+ --with-avif \
+ --with-xpm \
+ && docker-php-ext-install -j$(nproc) gd
+
+RUN docker-php-ext-install -j$(nproc) \
+ zip \
+ pdo \
+ pdo_mysql \
+ opcache \
+ pcntl \
+ posix \
+ shmop
+
+RUN pecl install apcu \
+ && docker-php-ext-enable apcu
# Composer installieren
RUN curl -sS https://getcomposer.org/installer | php \
@@ -25,14 +49,21 @@ RUN if [ "$ENV" = "dev" ]; then \
WORKDIR /var/www/html
-# Kopiere zuerst nur composer.json/lock für besseres Layer-Caching
-COPY composer.json composer.lock ./
+# Kopiere composer.json
+COPY composer.json ./
-# Installiere Abhängigkeiten - variiert je nach Umgebung
-RUN if [ "$ENV" = "prod" ]; then \
- composer install --no-dev --no-scripts --no-autoloader --optimize-autoloader; \
+# Kopiere composer.lock falls vorhanden (robuste Lösung)
+COPY composer.loc[k] ./
+
+# Falls keine composer.lock existiert, erstelle eine leere um Layer-Caching zu ermöglichen
+RUN [ ! -f composer.lock ] && touch composer.lock || true
+
+# Remove potentially corrupted composer.lock and install dependencies
+RUN rm -f composer.lock && \
+ if [ "$ENV" = "prod" ]; then \
+ composer install --no-dev --no-scripts --no-autoloader --optimize-autoloader; \
else \
- composer install --no-scripts --no-autoloader; \
+ composer install --no-scripts --no-autoloader; \
fi
# Kopiere PHP-Konfigurationen
diff --git a/docker/php/php.common.ini b/docker/php/php.common.ini
index 1198d636..55565d63 100644
--- a/docker/php/php.common.ini
+++ b/docker/php/php.common.ini
@@ -6,3 +6,5 @@ session.cookie_samesite = Lax
date.timezone = Europe/Berlin
+
+opcache.preload=/var/www/michaelschiemer/src/preload.php
diff --git a/docker/php/php.development.ini b/docker/php/php.development.ini
index 921652d4..e8831c9f 100644
--- a/docker/php/php.development.ini
+++ b/docker/php/php.development.ini
@@ -4,7 +4,7 @@ include = php.common.ini
[opcache]
opcache.enable=1
-opcache.enable_cli=0
+opcache.enable_cli=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
; Häufigere Validierung im Dev-Modus
diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile
new file mode 100644
index 00000000..aedf6c5b
--- /dev/null
+++ b/docker/worker/Dockerfile
@@ -0,0 +1,115 @@
+FROM php:8.4.8-cli
+
+# Install system dependencies including libraries for GD and other extensions
+RUN apt-get update && apt-get install -y \
+ git \
+ curl \
+ libpng-dev \
+ libjpeg62-turbo-dev \
+ libfreetype6-dev \
+ libonig-dev \
+ libxml2-dev \
+ libzip-dev \
+ libicu-dev \
+ zip \
+ unzip \
+ procps \
+ && rm -rf /var/lib/apt/lists/* \
+ && apt-get clean
+
+# Configure GD extension with JPEG and FreeType support
+RUN docker-php-ext-configure gd --with-freetype --with-jpeg
+
+# Install PHP extensions for worker functionality and web features
+RUN docker-php-ext-install -j$(nproc) \
+ pdo_mysql \
+ mbstring \
+ exif \
+ pcntl \
+ posix \
+ sockets \
+ gd \
+ zip \
+ intl \
+ opcache
+
+# Install Composer
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+
+# Set working directory
+WORKDIR /var/www/html
+
+# Copy application files
+COPY . .
+
+# Install dependencies (composer.lock wird automatisch erstellt falls nicht vorhanden)
+# Check if composer.json exists, if not create a minimal one
+RUN if [ ! -f composer.json ]; then \
+ echo "Creating minimal composer.json..."; \
+ echo '{\
+ "name": "worker/app",\
+ "description": "Worker application",\
+ "type": "project",\
+ "require": {\
+ "php": ">=8.4"\
+ },\
+ "autoload": {\
+ "psr-4": {\
+ "App\\\\": "src/"\
+ }\
+ },\
+ "minimum-stability": "stable",\
+ "prefer-stable": true\
+}' > composer.json; \
+ fi && \
+ composer install \
+ --no-dev \
+ --optimize-autoloader \
+ --no-interaction || echo "Composer install skipped or failed - continuing without dependencies"
+
+# Create startup script for permission fixing
+RUN echo '#!/bin/bash\n\
+set -e\n\
+\n\
+echo "🔧 Fixing permissions..."\n\
+\n\
+# Create directories if they do not exist\n\
+mkdir -p /var/www/html/src/Framework/CommandBus/storage/queue\n\
+mkdir -p /var/www/html/storage/logs\n\
+mkdir -p /var/www/html/storage/cache\n\
+\n\
+# Fix permissions on mounted volumes\n\
+chown -R www-data:www-data /var/www/html/storage || true\n\
+chown -R www-data:www-data /var/www/html/src/Framework/CommandBus/storage || true\n\
+chmod -R 775 /var/www/html/storage || true\n\
+chmod -R 775 /var/www/html/src/Framework/CommandBus/storage || true\n\
+\n\
+echo "✅ Permissions fixed"\n\
+echo "🚀 Starting worker..."\n\
+\n\
+# Switch to www-data user and run the worker\n\
+exec gosu www-data php /var/www/html/worker.php\n' > /usr/local/bin/start-worker.sh \
+ && chmod +x /usr/local/bin/start-worker.sh
+
+# Install gosu for better user switching (alternative to su-exec for Debian)
+RUN apt-get update && apt-get install -y gosu && rm -rf /var/lib/apt/lists/*
+
+# Create necessary directories and set permissions
+RUN mkdir -p \
+ /var/www/html/src/Framework/CommandBus/storage/queue \
+ /var/www/html/storage/logs \
+ /var/www/html/storage/cache \
+ && chown -R www-data:www-data /var/www/html/storage \
+ && chmod -R 775 /var/www/html/storage
+
+# Create queue storage directory with proper permissions
+RUN mkdir -p /var/www/html/src/Framework/CommandBus/storage \
+ && chown -R www-data:www-data /var/www/html/src/Framework/CommandBus/storage \
+ && chmod -R 775 /var/www/html/src/Framework/CommandBus/storage
+
+# Health check for the worker
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+ CMD ps aux | grep -v grep | grep "worker.php" || exit 1
+
+# Use startup script instead of direct PHP command
+CMD ["/usr/local/bin/start-worker.sh"]
diff --git a/docs/MARKDOWN.md b/docs/MARKDOWN.md
new file mode 100644
index 00000000..cc718c24
--- /dev/null
+++ b/docs/MARKDOWN.md
@@ -0,0 +1,61 @@
+# Heading 1
+## Heading 2
+### Heading 3
+#### Heading 4
+##### Heading 5
+###### Heading 6
+
+*italics*
+
+**bold**
+
+***italics & bold***
+
+~~crossed off~~
+
+highlight
+
+Test
+
+`monospace`
+
+```html
+
+
+
+
+
+
+
+
+
+```
+
+[This is a link](https://localhost)
+
+
+
+> This is a blockquote
+>> and it can be nested
+
+***
+
+---
+
+___
+
+1. Item 1
+2. Item 2
+3. Item 3
+
+* Test
+* Test
+ * Subitem
+ * Subitem
+
+| Column1 | Column 2 |
+| --- |-----|
+| Test | Test2 |
+
+- [ ] Test
+- [x] Test 2
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..cc6159b0
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,77 @@
+# Projekt-Dokumentation
+
+## Übersicht
+
+Willkommen zur Dokumentation des Projekts. Diese Dokumentation dient als zentrale Informationsquelle für Entwickler, die am Projekt arbeiten.
+
+## Inhaltsverzeichnis
+
+### Standards und Guidelines
+
+- [Coding Guidelines](/docs/standards/CODING-GUIDELINES.md) - Allgemeine Coding-Standards für das Projekt
+- [Sicherheitsrichtlinien](/docs/standards/SICHERHEITS-GUIDELINES.md) - Standards für sichere Softwareentwicklung
+
+### Entwicklungsrichtlinien
+
+- [Performance Guidelines](/docs/guidelines/PERFORMANCE-GUIDELINES.md) - Richtlinien zur Optimierung der Anwendungsleistung
+- [Testing Guidelines](/docs/guidelines/TESTING-GUIDELINES.md) - Standards und Best Practices für Tests
+
+### KI-Assistent Konfiguration
+
+- [Guidelines für KI-Assistenten](/docs/ai/GUIDELINES-FÜR-AI-ASSISTANT.md) - Spezifische Richtlinien für den KI-Assistenten
+- [PhpStorm Einrichtung](/docs/ai/EINRICHTUNG-PHPSTORM.md) - Anleitung zur Einrichtung des KI-Assistenten in PhpStorm
+
+### Architektur und Struktur
+
+- [Projektstruktur](/docs/architecture/STRUKTUR-DOKUMENTATION.md) - Überblick über die Struktur des Projekts
+
+### Framework-Entwicklung
+
+- [Modul-Checkliste](/docs/framework/MODUL-CHECKLISTE.md) - Leitfaden für die Erstellung neuer Module
+- [Erweiterungsmuster](/docs/framework/ERWEITERUNGSPATTERN.md) - Muster zur Erweiterung des Frameworks
+
+### Framework-Module
+
+- [Analytics-Modul](/docs/framework/analytics/README.md) - Tracking und Analyse von Anwendungsdaten
+- [Core-Modul](/docs/framework/core/README.md) - Kernkomponenten und Event-System
+- [DI-Modul](/docs/framework/di/README.md) - Dependency-Injection-Container
+- [HTTP-Modul](/docs/framework/http/README.md) - HTTP-Request und -Response-Handling
+
+## Mitwirken
+
+### Neue Module entwickeln
+
+1. Folge der [Framework-Modul Checkliste](/docs/framework/MODUL-CHECKLISTE.md) für neue Module
+2. Stelle sicher, dass dein Code den [Coding Guidelines](/docs/standards/CODING-GUIDELINES.md) entspricht
+3. Schreibe Tests gemäß den [Testing Guidelines](/docs/guidelines/TESTING-GUIDELINES.md)
+4. Erstelle eine ausführliche Dokumentation für dein Modul
+
+### Dokumentation verbessern
+
+Wir begrüßen Beiträge zur Verbesserung der Dokumentation. Wenn du Fehler findest oder Vorschläge zur Verbesserung hast, erstelle bitte einen Pull Request mit deinen Änderungen.
+
+## Erste Schritte
+
+Neue Entwickler sollten mit folgenden Schritten beginnen:
+
+1. Projekt lokal einrichten (siehe [Installation](#installation))
+2. Die [Projektstruktur](/docs/architecture/STRUKTUR-DOKUMENTATION.md) verstehen
+3. Die [Coding Guidelines](/docs/standards/CODING-GUIDELINES.md) lesen
+4. PhpStorm mit dem [KI-Assistenten einrichten](/docs/ai/EINRICHTUNG-PHPSTORM.md)
+
+## Installation
+
+```bash
+# Repository klonen
+git clone [repository-url]
+
+# Abhängigkeiten installieren
+composer install
+
+# Entwicklungsserver starten
+php -S localhost:8000 -t public/
+```
+
+## Updates und Änderungen
+
+Diese Dokumentation wird kontinuierlich aktualisiert. Prüfe regelmäßig auf Aktualisierungen, um über die neuesten Best Practices und Standards informiert zu bleiben.
diff --git a/docs/WORKER.md b/docs/WORKER.md
new file mode 100644
index 00000000..ba08b44a
--- /dev/null
+++ b/docs/WORKER.md
@@ -0,0 +1,16 @@
+# Queue Worker - Docker Management
+
+Dieses Dokument beschreibt alle Docker-spezifischen Befehle für die Verwaltung des Queue Workers.
+
+## 📋 Inhaltsverzeichnis
+
+- [Schnellstart](#schnellstart)
+- [Worker Management](#worker-management)
+- [Monitoring & Debugging](#monitoring--debugging)
+- [Konfiguration](#konfiguration)
+- [Troubleshooting](#troubleshooting)
+- [Wartung](#wartung)
+
+## 🚀 Schnellstart
+
+### Worker starten
diff --git a/docs/ai/EINRICHTUNG-PHPSTORM.md b/docs/ai/EINRICHTUNG-PHPSTORM.md
new file mode 100644
index 00000000..93ff5832
--- /dev/null
+++ b/docs/ai/EINRICHTUNG-PHPSTORM.md
@@ -0,0 +1,53 @@
+# Einrichtung des KI-Assistenten in PhpStorm
+
+## Übersicht
+
+Diese Anleitung beschreibt, wie der KI-Assistent in PhpStorm eingerichtet wird, um automatisch die Projekt-Guidelines zu verwenden.
+
+## Methode 1: Über die PhpStorm-Einstellungen
+
+1. Öffne PhpStorm und gehe zu **Settings/Preferences**
+ - Windows/Linux: File → Settings
+ - macOS: PhpStorm → Preferences
+
+2. Navigiere zu **Tools** → **AI Assistant** → **Custom Instructions**
+
+3. Aktiviere die Option **Use custom instructions**
+
+4. Füge in das Textfeld den Inhalt aus der Datei `/docs/ai/GUIDELINES-FÜR-AI-ASSISTANT.md` ein
+ - Alternativ kannst du auf einen relativen Pfad verweisen
+
+5. Aktiviere die Option **Apply project-specific instructions**, damit diese Einstellungen nur für dieses Projekt gelten
+
+6. Klicke auf **Apply** und dann auf **OK**
+
+## Methode 2: Über die Projektkonfiguration (empfohlen)
+
+Die `.idea/aiAssistant.xml`-Datei ist bereits im Projekt enthalten und konfiguriert den KI-Assistenten automatisch mit den richtigen Einstellungen. Wenn du das Projekt öffnest, sollte der KI-Assistent bereits korrekt eingerichtet sein.
+
+Um zu überprüfen, ob die Einstellungen korrekt übernommen wurden:
+
+1. Öffne die PhpStorm-Einstellungen wie oben beschrieben
+2. Navigiere zu **Tools** → **AI Assistant** → **Custom Instructions**
+3. Überprüfe, ob **Use custom instructions** aktiviert ist und die Guidelines angezeigt werden
+
+## Testen der Einrichtung
+
+Um zu testen, ob der KI-Assistent die Guidelines korrekt anwendet:
+
+1. Öffne eine PHP-Datei im Projekt
+2. Drücke `Alt+A` (Windows/Linux) oder `Option+A` (macOS) um den KI-Assistenten zu öffnen
+3. Bitte den Assistenten, eine neue Klasse zu erstellen
+4. Überprüfe, ob die generierte Klasse den Guidelines entspricht:
+ - Sie sollte als `final` und wenn möglich `readonly` deklariert sein
+ - Constructor Property Promotion sollte verwendet werden
+ - Es sollten keine externen Abhängigkeiten importiert werden
+
+## Fehlerbehebung
+
+Falls die Guidelines nicht korrekt angewendet werden:
+
+1. Stelle sicher, dass du die neueste Version von PhpStorm verwendest
+2. Überprüfe, ob die AI Assistant-Funktion aktiviert ist
+3. Versuche, das Projekt neu zu öffnen
+4. Führe einen Cache-Clear in PhpStorm durch: File → Invalidate Caches
diff --git a/docs/ai/GUIDELINES-FÜR-AI-ASSISTANT.md b/docs/ai/GUIDELINES-FÜR-AI-ASSISTANT.md
new file mode 100644
index 00000000..41f8c10a
--- /dev/null
+++ b/docs/ai/GUIDELINES-FÜR-AI-ASSISTANT.md
@@ -0,0 +1,174 @@
+# Guidelines für KI-Assistenten
+
+## Übersicht
+
+Diese Guidelines helfen dir, dem KI-Assistenten, konsistenten, modernen und qualitativ hochwertigen PHP-Code zu generieren, der den Projektstandards entspricht.
+
+## Verwendung in PhpStorm
+
+Diese Guidelines können in PhpStorm so eingerichtet werden, dass sie automatisch vom KI-Assistenten verwendet werden:
+
+1. Gehe zu **Settings/Preferences** → **Tools** → **AI Assistant** → **Custom Instructions**
+2. Aktiviere **Use custom instructions**
+3. Füge im Textfeld den Inhalt dieser Datei ein oder verwende einen relativen Pfad zu dieser Datei
+4. Optional: Aktiviere **Apply project-specific instructions** für projektspezifische Einstellungen
+
+Alternativ kann die Datei `.idea/aiAssistant.xml` angepasst werden, um diese Guidelines als Standardeinstellung für das Projekt zu verwenden.
+
+## Kernprinzipien
+
+### Abhängigkeitsvermeidung
+
+- **Keine externen Abhängigkeiten** außer den explizit freigegebenen
+- Eigene Implementierungen gegenüber externen Bibliotheken bevorzugen
+- Bei Bedarf nach externen Funktionen zuerst prüfen, ob eine eigene Implementierung möglich ist
+- Erlaubte Abhängigkeiten sind auf die vorhandenen Composer-Pakete beschränkt
+
+## Architekturprinzipien
+
+Bei der Codeanalyse und -generierung sind folgende Architekturprinzipien zu beachten:
+
+1. **Modularer Aufbau**: Das Projekt ist in Module unter `/src/Framework/` organisiert
+2. **Service-orientierte Architektur**: Funktionalitäten als unabhängige Services implementieren
+3. **Dependency Injection**: Abhängigkeiten werden per Constructor Injection bereitgestellt
+4. **Event-basierte Kommunikation**: Module kommunizieren über den EventDispatcher
+5. **Selbstständigkeit**: Module sollten möglichst unabhängig von externen Bibliotheken sein
+
+## Coding-Standards
+
+### Klassen
+
+- **IMMER `final` verwenden**, außer bei zwingenden Gründen für Vererbung
+- **KEINE abstrakten Klassen** verwenden - stattdessen Interfaces und Kompositionen
+- Klassen und Properties wenn möglich als `readonly` deklarieren
+- Bevorzuge Interfaces für Vertragsgestaltung zwischen Komponenten
+
+```php
+// RICHTIG
+final readonly class AnalyticsService implements AnalyticsInterface
+{
+ // ...
+}
+
+// FALSCH
+abstract class BaseAnalytics
+{
+ // ...
+}
+```
+
+### Properties und Methoden
+
+- Private `readonly` Properties mit Typisierung
+- Return-Types immer angeben
+- Parameter-Types immer angeben
+- Union Types und Nullable Types nutzen
+
+```php
+// RICHTIG
+private readonly LoggerInterface $logger;
+
+public function process(?int $id): Result|null
+{
+ // ...
+}
+
+// FALSCH
+public $logger;
+
+public function process($id)
+{
+ // ...
+}
+```
+
+### Moderne PHP-Features
+
+Nutze aktiv die neuesten PHP-Features:
+
+- Constructor Property Promotion
+- Match Expressions statt Switch
+- Named Arguments
+- Enums statt Konstanten
+- Nullsafe Operator (`?->`) wo sinnvoll
+- Typed Properties
+
+## Klassenaufbau
+
+Folgende Reihenfolge für Klassenelemente:
+
+1. Konstanten
+2. Properties
+3. Constructor
+4. Öffentliche Methoden
+5. Private/Protected Methoden
+
+## Service-Initialisierung
+
+Services werden durch Initializer-Klassen registriert:
+
+```php
+#[Initializer]
+final readonly class ServiceInitializer
+{
+ public function __construct(
+ private Configuration $config,
+ private DependencyInterface $dependency
+ ) {}
+
+ public function __invoke(Container $container): ServiceInterface
+ {
+ return new Service(
+ $this->config->get('service'),
+ $this->dependency
+ );
+ }
+}
+```
+
+## Fehlerbehandlung
+
+- Spezifische Exception-Klassen werfen
+- Early Return Pattern bevorzugen
+- Defensive Programmierung mit Validierung
+
+## Testing
+
+Bei Test-Vorschlägen Pest-Framework nutzen:
+
+```php
+test('method does something correctly', function () {
+ // Arrangement
+ $service = new Service($dependency);
+
+ // Action
+ $result = $service->method();
+
+ // Assertion
+ expect($result)->toBe('expected');
+});
+```
+
+## Dokumentation
+
+- PHPDoc für alle öffentlichen Methoden
+- Kurze, präzise Beschreibungen
+- Parameter und Return-Types in PHPDoc
+
+## Zu vermeidende Praktiken
+
+- Globale Zustände und statische Methoden
+- Tiefe Vererbungshierarchien
+- Lange, komplexe Methoden
+- Magische Methoden (`__call`, etc.) ohne triftigen Grund
+- Unnötige Abstraktionen
+
+## Bei Codeanalyse und -vorschlägen
+
+1. **Aktuellen Stil beibehalten**: Bei Vorschlägen den vorhandenen Codierungsstil beibehalten
+2. **Standards berücksichtigen**: Auf Einhaltung der hier definierten Guidelines achten
+3. **Modernisierung vorschlagen**: Auf Möglichkeiten zur Modernisierung hinweisen
+4. **Begründen**: Bei Empfehlungen die Gründe erläutern
+5. **Vollständigkeit**: Vollständige Lösungen anbieten, nicht nur Fragmente
+
+Diese Guidelines sind als lebendiges Dokument zu betrachten, das mit der Evolution des Projekts und von PHP weiterentwickelt wird.
diff --git a/docs/ai/index.md b/docs/ai/index.md
new file mode 100644
index 00000000..6b962f71
--- /dev/null
+++ b/docs/ai/index.md
@@ -0,0 +1,54 @@
+# KI-Assistent Dokumentation
+
+## Übersicht
+
+Diese Dokumentation beschreibt die Einrichtung und Verwendung des KI-Assistenten im Projekt. Der KI-Assistent hilft bei der Entwicklung durch Code-Generierung, Refactoring-Vorschläge und mehr, während er die Projektstandards einhält.
+
+## Inhalte
+
+- [Guidelines für KI-Assistenten](/ai/GUIDELINES-FÜR-AI-ASSISTANT.md) - Richtlinien für den KI-Assistenten
+- [PhpStorm Einrichtung](/ai/EINRICHTUNG-PHPSTORM.md) - Anleitung zur Einrichtung in PhpStorm
+
+## KI-Assistent Guidelines
+
+Die [Guidelines für KI-Assistenten](/ai/GUIDELINES-FÜR-AI-ASSISTANT.md) stellen sicher, dass der KI-generierte Code den Projektstandards entspricht:
+
+- Einhaltung der Coding-Standards
+- Vermeidung externer Abhängigkeiten
+- Verwendung moderner PHP-Features
+- Konsistente Klassenstruktur
+- Korrekte Fehlerbehandlung
+
+## Einrichtung in PhpStorm
+
+Die [PhpStorm Einrichtungsanleitung](/ai/EINRICHTUNG-PHPSTORM.md) führt Sie durch den Prozess der Integration des KI-Assistenten in Ihre IDE:
+
+- Konfiguration der Custom Instructions
+- Verwendung der projektspezifischen Einstellungen
+- Testen der korrekten Einrichtung
+- Fehlerbehebung bei Problemen
+
+## Effektive Nutzung des KI-Assistenten
+
+### Best Practices
+
+1. **Klare Anfragen stellen**: Je präziser die Anfrage, desto besser das Ergebnis
+2. **Kontext bereitstellen**: Dem Assistenten relevanten Kontext geben
+3. **Ergebnisse überprüfen**: Generierte Code immer prüfen und verstehen
+4. **Iterativ arbeiten**: Bei komplexen Aufgaben schrittweise vorgehen
+
+### Häufige Anwendungsfälle
+
+- Erstellung neuer Klassen und Interfaces
+- Implementierung von Tests
+- Refactoring bestehenden Codes
+- Dokumentation generieren
+- Code-Optimierung
+
+## Datenschutz und Sicherheit
+
+Beim Umgang mit dem KI-Assistenten sollten Sie folgende Punkte beachten:
+
+- Keine sensiblen Daten oder Geschäftsgeheimnisse teilen
+- Keine Passwörter, API-Schlüssel oder Zugangsdaten teilen
+- Bei Unsicherheit den KI-Assistenten nicht verwenden
diff --git a/docs/architecture/STRUKTUR-DOKUMENTATION.md b/docs/architecture/STRUKTUR-DOKUMENTATION.md
new file mode 100644
index 00000000..68f2361f
--- /dev/null
+++ b/docs/architecture/STRUKTUR-DOKUMENTATION.md
@@ -0,0 +1,149 @@
+# Projektstruktur-Dokumentation
+
+## Übersicht
+
+Diese Dokumentation bietet einen Überblick über die Architektur und Struktur des Projekts. Die Anwendung folgt einer modularen, serviceorientierten Architektur mit klarer Trennung von Verantwortlichkeiten.
+
+## Hauptverzeichnisse
+
+### `/src`
+
+Das Hauptverzeichnis für den Anwendungscode, unterteilt in mehrere Unterverzeichnisse:
+
+#### `/src/Framework`
+
+Enthält das Framework mit grundlegenden Infrastrukturkomponenten:
+
+- **Analytics**: System zur Erfassung und Analyse von Anwendungsdaten
+- **Attributes**: Attribute/Annotations für Metadaten
+- **Cache**: Caching-Mechanismen
+- **CommandBus**: Command-Handling-Komponenten
+- **Config**: Konfigurationsverwaltung
+- **Console**: Konsolenanwendung und -befehle
+- **Core**: Kernkomponenten und Events
+- **DI**: Dependency-Injection-Container
+- **ErrorHandling**: Fehlerbehandlungsmechanismen
+- **EventBus**: Event-Handling-System
+- **Exception**: Framework-Exceptions
+- **Filesystem**: Dateisystemoperationen
+- **Http**: HTTP-Request/Response-Handling
+- **HttpClient**: HTTP-Client für externe API-Aufrufe
+- **Logging**: Logging-Infrastruktur
+- **Performance**: Performance-Monitoring
+- **Queue**: Nachrichtenwarteschlangen
+- **Redis**: Redis-Integration
+- **Router**: URL-Routing
+- **StaticSite**: Statische Site-Generation
+- **Validation**: Datenvalidierung
+- **View**: Template-Rendering
+
+#### `/src/Application`
+
+Anwendungsspezifische Komponenten, die das Framework nutzen.
+
+#### `/src/Domain`
+
+Domain-Modelle, Entities und Business-Logik.
+
+#### `/src/Infrastructure`
+
+Infrastrukturkomponenten, die externe Systeme integrieren.
+
+#### `/src/Config`
+
+Konfigurationsdateien für verschiedene Module.
+
+## Framework-Architektur
+
+### Dependency Injection
+
+Das System nutzt einen leistungsfähigen DI-Container zur Verwaltung von Services:
+
+```php
+#[Initializer]
+class ServiceInitializer
+{
+ public function __invoke(Container $container): Service
+ {
+ // Service erstellen und zurückgeben
+ }
+}
+```
+
+### Event-System
+
+Ein Event-System ermöglicht lose Kopplung zwischen Komponenten:
+
+```php
+$eventDispatcher->addHandler(EventClass::class, function($event) {
+ // Event verarbeiten
+});
+```
+
+### HTTP-Pipeline
+
+HTTP-Requests durchlaufen eine Middleware-Pipeline:
+
+```php
+class CustomMiddleware implements Middleware
+{
+ public function process(Request $request, callable $next): Response
+ {
+ // Request verarbeiten
+ $response = $next($request);
+ // Response verarbeiten
+ return $response;
+ }
+}
+```
+
+## Module und ihre Interaktionen
+
+### Analytics-Modul
+
+Das Analytics-Modul erfasst und analysiert Anwendungsdaten:
+
+- **Events tracken**: `$analytics->track('event_name', $properties)`
+- **HTTP-Tracking**: Automatisch durch `AnalyticsMiddleware`
+- **Error-Tracking**: Integration mit dem Error-Handling-System
+- **Dashboard**: Admin-Interface zur Datenvisualisierung
+
+### Konfigurationssystem
+
+Konfigurationen werden zentral verwaltet und injiziert:
+
+```php
+class Service
+{
+ public function __construct(private Configuration $config)
+ {
+ $settings = $this->config->get('module_name', $defaults);
+ }
+}
+```
+
+## Erweiterbarkeit
+
+### Neue Module hinzufügen
+
+1. Erstellen Sie ein neues Verzeichnis unter `/src/Framework`
+2. Implementieren Sie eine Initializer-Klasse mit dem `#[Initializer]`-Attribut
+3. Erstellen Sie eine entsprechende Konfigurationsdatei unter `/src/Config`
+
+### Middleware hinzufügen
+
+```php
+// In Ihrer Anwendungsklasse
+public function bootstrap(): void
+{
+ $this->addMiddleware(YourMiddleware::class);
+}
+```
+
+## Best Practices
+
+- **Dependency Injection**: Verwenden Sie Constructor-Injection für Abhängigkeiten
+- **Interfaces**: Definieren Sie Interfaces für alle Services
+- **Events**: Nutzen Sie Events für lose Kopplung zwischen Modulen
+- **Konfiguration**: Externalisieren Sie Konfigurationen in dedizierte Dateien
+- **Typsicherheit**: Nutzen Sie strenge Typisierung und readonly-Properties
diff --git a/docs/architecture/index.md b/docs/architecture/index.md
new file mode 100644
index 00000000..5723f40c
--- /dev/null
+++ b/docs/architecture/index.md
@@ -0,0 +1,70 @@
+# Architektur-Dokumentation
+
+## Übersicht
+
+Diese Dokumentation beschreibt die Architektur und Struktur des Projekts. Sie bietet einen Überblick über die wichtigsten Komponenten, deren Beziehungen und die zugrundeliegenden Architekturprinzipien.
+
+## Inhalte
+
+- [Projektstruktur](/architecture/STRUKTUR-DOKUMENTATION.md) - Überblick über die Struktur des Projekts
+
+## Architekturprinzipien
+
+Das Projekt folgt diesen grundlegenden Architekturprinzipien:
+
+1. **Modulare Architektur**: Klare Trennung von Verantwortlichkeiten in Modulen
+2. **Service-orientiertes Design**: Funktionalitäten als unabhängige Services
+3. **Dependency Injection**: Abhängigkeiten werden explizit injiziert
+4. **Event-basierte Kommunikation**: Lose Kopplung durch Events
+5. **Schichtenarchitektur**: Trennung von Präsentation, Anwendungslogik und Daten
+
+## Hauptkomponenten
+
+### Framework-Kern
+
+Der Framework-Kern stellt grundlegende Infrastrukturkomponenten bereit:
+
+- **DI-Container**: Verwaltung von Service-Abhängigkeiten
+- **Event-System**: Event-basierte Kommunikation
+- **HTTP-Komponenten**: Request/Response-Handling
+- **Routing**: URL-zu-Controller-Mapping
+
+### Anwendungsschicht
+
+Die Anwendungsschicht implementiert die Geschäftslogik:
+
+- **Services**: Implementierung von Anwendungsfunktionen
+- **Commands/Queries**: Command-Query-Separation-Prinzip
+- **Controllers**: HTTP-Request-Handling
+
+### Domainschicht
+
+Die Domainschicht enthält die Kerngeschäftslogik:
+
+- **Entities**: Geschäftsobjekte mit Identität
+- **Value Objects**: Unveränderliche Wertobjekte
+- **Domain Services**: Domänenspezifische Logik
+
+### Infrastrukturschicht
+
+Die Infrastrukturschicht bietet technische Funktionen:
+
+- **Persistenz**: Datenbankzugriff und -verwaltung
+- **Messaging**: Externe Kommunikation
+- **Integration**: Anbindung an externe Systeme
+
+## Datenfluss
+
+Ein typischer Datenfluss im System:
+
+1. HTTP-Request wird vom Router empfangen
+2. Middleware-Pipeline verarbeitet den Request
+3. Controller erhält den Request und delegiert an Services
+4. Services implementieren die Geschäftslogik
+5. Domain-Objekte repräsentieren den Geschäftszustand
+6. Repositories speichern/laden Daten
+7. Response wird erstellt und zurückgegeben
+
+## Weitere Informationen
+
+Für detailliertere Informationen zur Architektur siehe die [Projektstruktur-Dokumentation](/architecture/STRUKTUR-DOKUMENTATION.md).
diff --git a/docs/framework/ANALYTICS.md b/docs/framework/ANALYTICS.md
new file mode 100644
index 00000000..5f61a82e
--- /dev/null
+++ b/docs/framework/ANALYTICS.md
@@ -0,0 +1,14 @@
+# Analytics-Framework
+
+> **Dokumentationshinweis:** Die vollständige Dokumentation für das Analytics-Framework finden Sie im Ordner [/docs/framework/analytics](/docs/framework/analytics/).
+
+## Schnellzugriff
+
+- [Übersicht und Einstieg](/docs/framework/analytics/index.md)
+- [Detaillierte Architektur](/docs/framework/analytics/architecture.md)
+- [Anwendungsbeispiele](/docs/framework/analytics/usage.md)
+- [Migrationsleitfaden](/docs/framework/analytics/migration.md)
+
+## Implementierungsdetails
+
+Die Code-Dokumentation befindet sich in der [README-Datei des Frameworks](/Framework/Analytics1/README.md).
diff --git a/docs/framework/ERWEITERUNGSPATTERN.md b/docs/framework/ERWEITERUNGSPATTERN.md
new file mode 100644
index 00000000..565dd261
--- /dev/null
+++ b/docs/framework/ERWEITERUNGSPATTERN.md
@@ -0,0 +1,200 @@
+# Erweiterungsmuster im Framework
+
+## Übersicht
+
+Dieses Dokument beschreibt die verschiedenen Muster und Techniken, um das Framework zu erweitern oder anzupassen, ohne den Kern-Code zu verändern.
+
+## Event-basierte Erweiterungen
+
+### Event-Listener
+
+Die primäre Methode zur Erweiterung des Frameworks ist das Lauschen auf System-Events:
+
+```php
+// Event-Listener registrieren
+public function __construct(private readonly EventDispatcher $eventDispatcher)
+{
+ $this->eventDispatcher->addHandler(
+ 'App\Framework\Core\Events\ApplicationBooted',
+ [$this, 'onApplicationBooted']
+ );
+}
+
+// Event-Handler-Methode
+public function onApplicationBooted(ApplicationBooted $event): void
+{
+ // Erweiterungslogik implementieren
+}
+```
+
+### Eigene Events
+
+Benutzerdefinierte Events erstellen:
+
+```php
+final readonly class UserRegistered
+{
+ public function __construct(
+ public string $userId,
+ public string $email,
+ public \DateTimeImmutable $timestamp
+ ) {}
+}
+
+// Event auslösen
+$this->eventDispatcher->dispatch(new UserRegistered(
+ $user->getId(),
+ $user->getEmail(),
+ new \DateTimeImmutable()
+));
+```
+
+## Middleware
+
+HTTP-Anfragen können durch Middleware-Klassen erweitert werden:
+
+```php
+final readonly class CustomMiddleware implements Middleware
+{
+ public function process(Request $request, callable $next): Response
+ {
+ // Vor der Anfrageverarbeitung
+ $modifiedRequest = $this->modifyRequest($request);
+
+ // Anfrage weiterleiten
+ $response = $next($modifiedRequest);
+
+ // Nach der Anfrageverarbeitung
+ return $this->modifyResponse($response);
+ }
+}
+
+// Middleware registrieren
+$app->addMiddleware(CustomMiddleware::class);
+```
+
+## Service-Erweiterungen
+
+### Service-Ersetzen
+
+Standardimplementierungen durch eigene ersetzen:
+
+```php
+#[Initializer]
+final readonly class CustomStorageInitializer
+{
+ public function __invoke(Container $container): StorageInterface
+ {
+ return new CustomStorage();
+ }
+}
+```
+
+### Service-Decorator
+
+Bestehende Services erweitern ohne Änderung der Original-Implementierung:
+
+```php
+#[Initializer]
+final readonly class LoggingAnalyticsInitializer
+{
+ public function __construct(
+ private readonly Configuration $config,
+ private readonly LoggerInterface $logger
+ ) {}
+
+ public function __invoke(Container $container): Analytics
+ {
+ // Original-Analytics-Service erstellen
+ $originalAnalytics = new Analytics(
+ new AnalyticsManager($this->config->get('analytics'), new FileStorage($path)),
+ $container->get(EventDispatcher::class)
+ );
+
+ // Mit Logging-Decorator umhüllen
+ return new LoggingAnalyticsDecorator($originalAnalytics, $this->logger);
+ }
+}
+
+// Decorator-Implementierung
+final readonly class LoggingAnalyticsDecorator implements AnalyticsInterface
+{
+ public function __construct(
+ private Analytics $analytics,
+ private LoggerInterface $logger
+ ) {}
+
+ public function track(string $event, array $properties = [], ?string $userId = null): void
+ {
+ $this->logger->debug("Tracking event: {$event}", [
+ 'properties' => $properties,
+ 'user_id' => $userId
+ ]);
+
+ $this->analytics->track($event, $properties, $userId);
+ }
+
+ // Andere Methoden implementieren
+}
+```
+
+## Plugin-System
+
+### Plugin-Interface
+
+```php
+interface PluginInterface
+{
+ public function register(Application $app): void;
+ public function boot(Application $app): void;
+}
+
+// Plugin-Implementierung
+final readonly class CustomPlugin implements PluginInterface
+{
+ public function register(Application $app): void
+ {
+ // Services registrieren
+ }
+
+ public function boot(Application $app): void
+ {
+ // Nach Initialisierung der Anwendung
+ }
+}
+
+// Plugin registrieren
+$app->registerPlugin(new CustomPlugin());
+```
+
+## Konfigurationserweiterungen
+
+### Konfigurationsquellen
+
+Benutzerdefinierte Konfigurationsquellen implementieren:
+
+```php
+final readonly class EnvironmentConfigSource implements ConfigSourceInterface
+{
+ public function load(string $key, mixed $default = null): mixed
+ {
+ $envKey = strtoupper(str_replace('.', '_', $key));
+ return $_ENV[$envKey] ?? $default;
+ }
+}
+
+// Konfigurationsquelle registrieren
+$config->addSource(new EnvironmentConfigSource());
+```
+
+## Zusammenfassung
+
+Die bevorzugten Erweiterungsmuster sind:
+
+1. **Event-Listener** für reaktive Erweiterungen
+2. **Middleware** für HTTP-Anfrageverarbeitung
+3. **Service-Initializer** zum Ersetzen oder Dekorieren von Services
+4. **Plugins** für umfassendere Funktionalitätserweiterungen
+5. **Konfigurationsquellen** für benutzerdefinierte Konfigurationen
+
+Diese Muster ermöglichen es, das Framework zu erweitern, ohne den Kern zu modifizieren, was zu einer besseren Wartbarkeit und einfacheren Updates führt.
diff --git a/docs/framework/MODUL-CHECKLISTE.md b/docs/framework/MODUL-CHECKLISTE.md
new file mode 100644
index 00000000..a074da28
--- /dev/null
+++ b/docs/framework/MODUL-CHECKLISTE.md
@@ -0,0 +1,73 @@
+# Framework-Modul Checkliste
+
+## Übersicht
+
+Diese Checkliste dient als Leitfaden für die Erstellung neuer Module im Framework. Sie hilft sicherzustellen, dass alle Module den Projektstandards entsprechen und konsistent implementiert werden.
+
+## Strukturelle Anforderungen
+
+- [ ] Modul in eigenem Verzeichnis unter `/src/Framework/`
+- [ ] Konsistente Namensgebung im PascalCase (z.B. `CacheManager` statt `Cache_manager`)
+- [ ] README.md mit Modul-Dokumentation
+- [ ] Interface(s) für öffentliche API
+- [ ] Implementierungsklassen als `final readonly`
+- [ ] Initializer-Klasse mit `#[Initializer]`-Attribut
+
+## Abhängigkeiten
+
+- [ ] Minimale externe Abhängigkeiten (idealerweise keine)
+- [ ] Klar definierte Abhängigkeiten zu anderen Framework-Modulen
+- [ ] Keine zirkulären Abhängigkeiten
+- [ ] Verwendung des DI-Containers für Abhängigkeiten
+
+## Konfiguration
+
+- [ ] Konfigurationsdatei unter `/src/Config/{modul-name}.php`
+- [ ] Standardkonfiguration in der Initializer-Klasse
+- [ ] Dokumentierte Konfigurationsoptionen
+
+## Code-Qualität
+
+- [ ] Vollständige Typisierung (Parameter, Rückgabewerte, Properties)
+- [ ] PHPDoc für öffentliche Methoden und Klassen
+- [ ] Keine abstrakten Klassen oder Vererbung (außer Interfaces)
+- [ ] Immutable Objekte wo möglich
+- [ ] Spezifische Exceptions für Fehlerbehandlung
+
+## Tests
+
+- [ ] Unit-Tests mit Pest für alle öffentlichen Methoden
+- [ ] Integrationstests für Modul-Interaktionen
+- [ ] Testabdeckung für Fehlerfälle
+
+## Integration
+
+- [ ] Event-Listener für relevante System-Events
+- [ ] Erweiterungspunkte für andere Module
+- [ ] Keine direkten Abhängigkeiten zu Domain-Klassen
+
+## Dokumentation
+
+- [ ] Beispiele für Verwendung
+- [ ] Architektur-Beschreibung
+- [ ] API-Dokumentation
+- [ ] Konfigurationsreferenz
+
+## Performance
+
+- [ ] Lazy-Loading für ressourcenintensive Operationen
+- [ ] Caching-Strategie (falls relevant)
+- [ ] Performancekritische Teile identifiziert und optimiert
+
+## Sicherheit
+
+- [ ] Validierung aller externen Eingaben
+- [ ] Keine sensiblen Daten in Logs
+- [ ] Schutz vor bekannten Sicherheitslücken
+
+## Wartbarkeit
+
+- [ ] Kleine, fokussierte Klassen
+- [ ] Klare Trennung von Verantwortlichkeiten
+- [ ] Konsistente Fehlerbehandlung
+- [ ] Logging an strategischen Stellen
diff --git a/docs/framework/analytics/README.md b/docs/framework/analytics/README.md
new file mode 100644
index 00000000..b4a7e85a
--- /dev/null
+++ b/docs/framework/analytics/README.md
@@ -0,0 +1,158 @@
+# Analytics-Modul Dokumentation
+
+## Übersicht
+
+Das Analytics-Modul ist ein internes Tracking- und Analysesystem, das ohne externe Abhängigkeiten Benutzeraktivitäten, Systemereignisse und Leistungsdaten erfasst und analysiert.
+
+## Kernkomponenten
+
+### Analytics (Analytics.php)
+
+Die Hauptklasse, die als zentraler Einstiegspunkt für das Tracking von Events dient.
+
+**Hauptfunktionen:**
+- `track()`: Zeichnet ein generisches Event auf
+- `page()`: Spezifisch für Seitenaufrufe
+- `user()`: Verfolgt Benutzeridentifikation
+- `performance()`: Erfasst Leistungsmetriken
+- `error()`: Protokolliert Fehler und Ausnahmen
+
+### AnalyticsManager (AnalyticsManager.php)
+
+Verwaltet die Verarbeitung und Speicherung von Events.
+
+**Hauptfunktionen:**
+- `track()`: Verarbeitet Events und wendet Middleware an
+- `addMiddleware()`: Fügt Verarbeitungsfunktionen hinzu
+- `flush()`: Schreibt gepufferte Events in den Speicher
+
+### StorageInterface und FileStorage
+
+Das Interface definiert die Speichermethoden, FileStorage implementiert die Speicherung in Dateien.
+
+**Hauptoperationen:**
+- `store()`: Speichert Events
+- `retrieve()`: Ruft Events mit Filtern ab
+- `clear()`: Löscht alle gespeicherten Daten
+
+### AnalyticsInitializer (AnalyticsInitializer.php)
+
+Konfiguriert und initialisiert den Analytics-Service beim Anwendungsstart.
+
+**Konfigurationsoptionen:**
+- Aktivierung/Deaktivierung
+- Auto-Flush und Batch-Größe
+- Storage-Typ und Pfad
+
+### AnalyticsMiddleware (AnalyticsMiddleware.php)
+
+HTTP-Middleware zum automatischen Tracking von Requests und Responses.
+
+### AnalyticsDashboard (AnalyticsDashboard.php)
+
+Stellt Methoden für Datenanalyse und -aggregation bereit:
+- `getEventStats()`: Ereignisstatistiken
+- `getTopPages()`: Meistbesuchte Seiten
+- `getUserStats()`: Benutzerstatistiken
+- `getErrorStats()`: Fehlerstatistiken
+- `getPerformanceStats()`: Leistungsmetriken
+
+### Events
+
+Vordefinierte Event-Typen:
+- `AnalyticsEvent`: Basis-Event-Klasse
+- `PageViewEvent`: Speziell für Seitenaufrufe
+
+### Controllers
+
+`AdminAnalyticsController`: Stellt das Admin-Dashboard bereit mit:
+- Übersichtsseite
+- Seitenstatistiken
+- Fehlerstatistiken
+- Leistungsstatistiken
+
+### Console
+
+`AnalyticsClearCommand`: Konsolenbefehl zum Löschen von Analytics-Daten.
+
+## Konfiguration
+
+Die Konfiguration erfolgt in `src/Config/analytics.php` mit folgenden Optionen:
+
+```php
+return [
+ 'enabled' => true, // Aktiviert/deaktiviert das Tracking
+ 'auto_flush' => true, // Automatisches Speichern nach Batch-Größe
+ 'batch_size' => 50, // Anzahl Events pro Batch
+ 'storage' => 'file', // Storage-Backend
+ 'storage_path' => '...', // Speicherpfad
+ 'anonymize_ip' => true, // IP-Anonymisierung
+ 'track_events' => [...] // Zu trackende Events
+];
+```
+
+## Verwendung
+
+### Basis-Tracking
+
+```php
+// Event tracken
+$analytics->track('button_click', ['button' => 'signup']);
+
+// Seitenaufruf tracken
+$analytics->page('/dashboard', ['section' => 'analytics']);
+
+// Benutzer identifizieren
+$analytics->user('user123', ['plan' => 'premium']);
+```
+
+### Middleware einrichten
+
+```php
+$this->addMiddleware(App\Framework\Analytics\AnalyticsMiddleware::class);
+```
+
+### Eigene Middleware hinzufügen
+
+```php
+$analyticsManager->addMiddleware(function(array $event) {
+ // Daten verarbeiten oder filtern
+ return $event;
+});
+```
+
+## Datenschutz
+
+Das System bietet integrierte Funktionen zur Anonymisierung personenbezogener Daten:
+- IP-Adressen-Anonymisierung (letztes Oktett wird entfernt)
+- Konfigurierbare Filterung sensibler Daten durch Middleware
+
+## Erweiterbarkeit
+
+### Eigene Storage-Provider
+
+Implementieren Sie das `StorageInterface` für benutzerdefinierte Speicherlösungen:
+
+```php
+class CustomStorage implements StorageInterface
+{
+ public function store(array $events): void { /* ... */ }
+ public function retrieve(array $filters = []): array { /* ... */ }
+ public function clear(): void { /* ... */ }
+}
+```
+
+### Event-Typen erweitern
+
+Erstellen Sie benutzerdefinierte Event-Klassen, die von `AnalyticsEvent` erben:
+
+```php
+class CustomEvent extends AnalyticsEvent
+{
+ public function __construct(string $customData, array $additionalProps = [])
+ {
+ parent::__construct('custom_event',
+ array_merge(['custom_data' => $customData], $additionalProps));
+ }
+}
+```
diff --git a/docs/framework/analytics/architecture.md b/docs/framework/analytics/architecture.md
new file mode 100644
index 00000000..f49ec68f
--- /dev/null
+++ b/docs/framework/analytics/architecture.md
@@ -0,0 +1,102 @@
+# Analytics-Framework: Architektur
+
+## Überblick
+
+Das Analytics-Framework verwendet eine Schichtenarchitektur mit klaren Verantwortlichkeiten:
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Öffentliche API │
+│ (Analytics) │
+└───────────────────────┬─────────────────────────────┘
+ │
+┌───────────────────────▼─────────────────────────────┐
+│ Event-Verarbeitung │
+│ (AnalyticsManager) │
+└───────────────────────┬─────────────────────────────┘
+ │
+ ┌──────────────┴──────────────┐
+ │ │
+┌────────▼─────────┐ ┌─────────▼────────┐
+│ Middleware │ │ Storage-Layer │
+│ (Pipeline) │ │ (StorageInterface)│
+└──────────────────┘ └──────────────────┘
+```
+
+## Komponenten im Detail
+
+### Analytics (Frontend)
+
+Bietet eine benutzerfreundliche API für Tracking-Operationen. Diese Klasse ist der primäre Einstiegspunkt für Anwendungscode und abstrahiert die Komplexität der Eventverarbeitung.
+
+Verantwortlichkeiten:
+- Bereitstellung der öffentlichen API (`track`, `page`, `user`, `error`)
+- Integration mit dem Event-Dispatcher des Frameworks
+- Typ-Konvertierung zwischen benutzerdefinierten Ereignissen und dem internen Datenformat
+
+### AnalyticsManager (Verarbeitung)
+
+Der Manager ist das Herzstück des Systems und verantwortlich für:
+- Anwendung von Middleware-Transformationen auf Events
+- Eventpufferung für effiziente Speicherung
+- Verwaltung der Konfiguration
+- Steuerung des Storage-Layers
+
+Der Manager implementiert einen Event-Buffer, der Daten sammelt und bei Erreichen einer konfigurierbaren Größe automatisch speichert.
+
+### Middleware-System
+
+Eine Kette von Verarbeitungsfunktionen, die auf jedes Event angewendet werden:
+- Datenfilterung und -transformation
+- Anonymisierung persönlicher Daten
+- Validierung und Anreicherung von Events
+
+Jede Middleware kann Events modifizieren oder komplett verwerfen.
+
+### Storage-Layer
+
+Abstraktion für verschiedene Speichermethoden durch das `StorageInterface`:
+
+```php
+interface StorageInterface
+{
+ public function store(array $events): void;
+ public function retrieve(array $filters = []): array;
+ public function clear(): void;
+}
+```
+
+Implementierungen:
+- `FileStorage`: Speichert Events in JSON-Dateien mit täglicher Rotation
+- Erweiterbar für andere Backends (Datenbank, Redis, etc.)
+
+### HTTP-Integration
+
+`AnalyticsMiddleware` integriert Analytics in den HTTP-Request-Lifecycle:
+- Tracking von eingehenden Requests
+- Erfassung von Antwortzeiten und Statuscodes
+- Fehlererfassung bei Exceptions
+
+### Admin-Dashboard
+
+`AnalyticsDashboard` und `AdminAnalyticsController` bieten:
+- Datenaggregation und -analyse
+- Visualisierung von Metriken
+- Filterung nach Zeiträumen
+- Verschiedene spezialisierte Ansichten (Seiten, Fehler, Performance)
+
+## Datenfluss
+
+1. Event wird durch `Analytics::track()` oder ähnliche Methoden erstellt
+2. `AnalyticsManager` wendet Middleware-Pipeline an
+3. Event wird zum Buffer hinzugefügt
+4. Bei Buffer-Füllung oder explizitem `flush()` werden Events an Storage übergeben
+5. Storage speichert Events im konfigurierten Backend
+6. `AnalyticsDashboard` ruft Daten bei Bedarf vom Storage ab und aggregiert sie
+
+## Erweiterungspunkte
+
+- **Storage-Provider**: Neue Implementierungen von `StorageInterface`
+- **Middleware**: Funktionen für Filterung/Transformation
+- **Event-Typen**: Spezialisierte Event-Klassen für typsicheres Tracking
+- **Dashboard-Views**: Zusätzliche Visualisierungen und Berichte
diff --git a/docs/framework/analytics/index.md b/docs/framework/analytics/index.md
new file mode 100644
index 00000000..887c99ee
--- /dev/null
+++ b/docs/framework/analytics/index.md
@@ -0,0 +1,114 @@
+# Analytics-Framework
+
+> **Dokumentationshinweis:** Diese Dokumentation ist vollständig aktualisiert und stellt die aktuelle Implementierung des Analytics-Frameworks korrekt dar.
+
+## Übersicht
+
+Das Analytics-Framework ist ein eigenständiges Tracking- und Analysesystem, das vollständig in die Anwendung integriert ist und ohne externe Dienste auskommt. Es ermöglicht die Erfassung, Speicherung und Analyse von:
+
+- Benutzeraktivitäten (Seitenaufrufe, Interaktionen)
+- Systemereignissen (Anwendungsstart, Fehler)
+- Leistungsdaten (Speicherverbrauch, Ausführungszeit, Datenbankabfragen)
+
+## Hauptkomponenten
+
+### Analytics-Klasse
+
+Die zentrale API für Tracking-Operationen:
+
+```php
+// Klasse initialisieren
+$analytics = new Analytics($analyticsManager, $eventDispatcher);
+
+// Event tracken
+$analytics->track('event_name', ['property' => 'value']);
+
+// Seite tracken
+$analytics->page('/pfad/zur/seite', ['eigenschaft' => 'wert']);
+
+// Benutzer identifizieren
+$analytics->user('user_id', ['eigenschaft' => 'wert']);
+
+// Fehler tracken
+$analytics->error($exception);
+```
+
+### AnalyticsManager
+
+Verarbeitet und speichert Events:
+
+- Wendet Middleware auf Events an
+- Puffert Events für effiziente Speicherung
+- Verwaltet die Konfiguration
+- Steuert die Speicherung in verschiedenen Backends
+
+### Storage-System
+
+Basierend auf dem `StorageInterface` mit verschiedenen Implementierungen:
+
+- `FileStorage`: Speichert Events in Dateien
+- Erweiterbar für Datenbank, Redis oder andere Backends
+
+### Middleware-System
+
+```php
+$analyticsManager->addMiddleware(function(array $event) {
+ // Event verarbeiten oder filtern
+ return $event; // oder null um zu verwerfen
+});
+```
+
+### HTTP-Middleware
+
+Automatisches Tracking von HTTP-Requests und -Responses:
+
+```php
+// In Bootstrap oder Application-Klasse
+$app->addMiddleware(AnalyticsMiddleware::class);
+```
+
+### Dashboard und Berichterstattung
+
+Das `AnalyticsDashboard` bietet Methoden zur Datenanalyse:
+
+- `getEventStats()`: Statistiken zu Events
+- `getTopPages()`: Meistbesuchte Seiten
+- `getUserStats()`: Benutzerstatistiken
+- `getErrorStats()`: Fehlerstatistiken
+- `getPerformanceStats()`: Leistungsmetriken
+
+Das Admin-Dashboard ist unter `/admin/analytics` verfügbar.
+
+### Konsolen-Befehle
+
+```bash
+# Analytics-Daten löschen
+php console analytics:clear [--force]
+```
+
+## Integration
+
+### Service-Container
+
+Der Analytics-Service wird automatisch registriert und kann per Dependency Injection verwendet werden:
+
+```php
+public function __construct(private readonly Analytics $analytics) {}
+```
+
+### Event-Integration
+
+Standardmäßig werden folgende Anwendungsereignisse getrackt:
+
+- Anwendungsstart (`application_booted`)
+- Fehlerbehandlung (`error_occurred`)
+- Request-Verarbeitung (`request_started`, `request_completed`)
+
+## Konfiguration
+
+Detaillierte Konfigurationsoptionen finden Sie in der [Framework-README](/Framework/Analytics1/README.md).
+
+## Weitere Informationen
+
+- [Framework-Erweiterungsmuster](/docs/framework/ERWEITERUNGSPATTERN.md)
+- [Modul-Checkliste](/docs/framework/MODUL-CHECKLISTE.md)
diff --git a/docs/framework/analytics/migration.md b/docs/framework/analytics/migration.md
new file mode 100644
index 00000000..b4b6e5f0
--- /dev/null
+++ b/docs/framework/analytics/migration.md
@@ -0,0 +1,160 @@
+# Analytics-Framework: Migrationsleitfaden
+
+## Von Version 1.x zu 2.x
+
+### Überblick der Änderungen
+
+Die Version 2.x des Analytics-Frameworks führt mehrere wichtige Verbesserungen und Änderungen ein:
+
+- **Typsicherheit**: Neue Event-Klassen statt Arrays
+- **Dependency Injection**: Verbesserte Integration mit dem Container
+- **Storage-Abstraktion**: Flexiblere Speichermethoden
+- **Fehlerbehandlung**: Robustere Exception-Handling
+- **Middleware-System**: Standardisierte Middleware-Pipeline
+
+### Notwendige Migrationsschritte
+
+#### 1. Konfiguration aktualisieren
+
+**Alt (1.x):**
+```php
+return [
+ 'enabled' => true,
+ 'auto_flush' => true,
+ 'batch_size' => 50,
+ 'storage' => 'file',
+ 'storage_path' => '/pfad/zum/speicher',
+];
+```
+
+**Neu (2.x):**
+```php
+return [
+ 'enabled' => true,
+ 'auto_flush' => true,
+ 'batch_size' => 50,
+ 'storage_driver' => 'file', // Umbenannt
+ 'storage_config' => [ // Neue Struktur
+ 'path' => '/pfad/zum/speicher',
+ ],
+ 'excluded_paths' => [], // Neue Option
+ 'excluded_user_agents' => [], // Neue Option
+ 'max_file_size' => 10 * 1024 * 1024, // Neue Option
+];
+```
+
+#### 2. Event-Objekte (falls genutzt)
+
+**Alt (1.x):**
+```php
+$analytics->track('custom_event', ['property' => 'value']);
+```
+
+**Neu (2.x) - Option 1 (abwärtskompatibel):**
+```php
+// Weiterhin unterstützt
+$analytics->track('custom_event', ['property' => 'value']);
+```
+
+**Neu (2.x) - Option 2 (typsicher):**
+```php
+use App\Framework\Analytics\Events\CustomEvent;
+
+$event = new CustomEvent('wert', ['weitere' => 'daten']);
+$analytics->trackEvent($event);
+```
+
+#### 3. Eigene Storage-Provider
+
+**Alt (1.x):**
+```php
+class CustomStorage implements StorageInterface
+{
+ public function store(array $events): void
+ {
+ // Implementation
+ }
+
+ public function retrieve(array $filters = []): array
+ {
+ // Implementation
+ }
+
+ public function clear(): void
+ {
+ // Implementation
+ }
+}
+```
+
+**Neu (2.x):**
+```php
+use Psr\Log\LoggerInterface;
+
+class CustomStorage implements StorageInterface
+{
+ public function __construct(
+ private readonly array $config,
+ private readonly LoggerInterface $logger
+ ) {}
+
+ public function store(array $events): void
+ {
+ try {
+ // Implementation mit Fehlerbehandlung
+ } catch (\Exception $e) {
+ $this->logger->error('Storage error', ['exception' => $e]);
+ throw new StorageException('Failed to store events', 0, $e);
+ }
+ }
+
+ // Andere Methoden analog
+}
+```
+
+#### 4. Middleware anpassen
+
+**Alt (1.x):**
+```php
+$analyticsManager->addMiddleware(function(array $event) {
+ // Verarbeitung
+ return $event;
+});
+```
+
+**Neu (2.x) - Option 1 (abwärtskompatibel):**
+```php
+// Weiterhin unterstützt
+$analyticsManager->addMiddleware(function(array $event) {
+ // Verarbeitung
+ return $event;
+});
+```
+
+**Neu (2.x) - Option 2 (typsicher):**
+```php
+use App\Framework\Analytics\Middleware\AnalyticsMiddleware;
+use App\Framework\Analytics\Events\AnalyticsEvent;
+
+class CustomMiddleware implements AnalyticsMiddleware
+{
+ public function process(AnalyticsEvent $event): ?AnalyticsEvent
+ {
+ // Verarbeitung
+ return $event;
+ }
+}
+
+// Registrierung
+$analyticsManager->addMiddleware(new CustomMiddleware());
+```
+
+### Automatisierte Tests
+
+Führen Sie die folgenden Tests durch, um sicherzustellen, dass die Migration erfolgreich war:
+
+```bash
+php console test:run --group=analytics
+```
+
+Weitere Informationen zur Migration finden Sie in den Änderungsprotokollen im Quellcode-Repository.
diff --git a/docs/framework/analytics/usage.md b/docs/framework/analytics/usage.md
new file mode 100644
index 00000000..d5363cb2
--- /dev/null
+++ b/docs/framework/analytics/usage.md
@@ -0,0 +1,269 @@
+# Analytics-Framework: Anwendungsbeispiele
+
+## Grundlegende Verwendung
+
+### Events tracken
+
+```php
+// Über Dependency Injection
+public function __construct(private readonly Analytics $analytics) {}
+
+// Einfaches Event tracken
+$this->analytics->track('login_attempt', [
+ 'success' => true,
+ 'user_type' => 'admin',
+ 'method' => 'password'
+]);
+
+// Seitenaufruf tracken
+$this->analytics->page('/produkte/kategorie/elektronik', [
+ 'referrer' => 'homepage',
+ 'search_query' => 'smartphones'
+]);
+
+// Benutzer identifizieren
+$this->analytics->user($user->getId(), [
+ 'email' => $user->getEmail(),
+ 'plan' => $user->getSubscriptionPlan(),
+ 'registered_since' => $user->getCreatedAt()->format('Y-m-d')
+]);
+```
+
+### Fehler tracken
+
+```php
+try {
+ // Fehleranfälliger Code
+ $result = $this->riskyOperation();
+ return $result;
+} catch (\Exception $e) {
+ // Fehler tracken
+ $this->analytics->error($e);
+
+ // Fehler behandeln
+ $this->logger->error($e->getMessage());
+ return $this->fallbackOperation();
+}
+```
+
+### Performance tracken
+
+```php
+// Manuelles Performance-Tracking
+$startTime = microtime(true);
+$startMemory = memory_get_usage();
+
+// Operation durchführen
+$result = $this->heavyOperation();
+
+// Performance-Metriken tracken
+$this->analytics->performance([
+ 'operation' => 'heavy_operation',
+ 'execution_time' => microtime(true) - $startTime,
+ 'memory_used' => memory_get_usage() - $startMemory,
+ 'result_size' => is_countable($result) ? count($result) : 0
+]);
+```
+
+## Erweiterte Anwendungsfälle
+
+### Benutzerdefinierte Middleware
+
+```php
+// In einem Service Provider oder Initializer
+public function initialize(AnalyticsManager $manager): void
+{
+ // DSGVO-Middleware zur Anonymisierung personenbezogener Daten
+ $manager->addMiddleware(function(array $event) {
+ // E-Mail-Adressen anonymisieren
+ if (isset($event['properties']['email'])) {
+ $parts = explode('@', $event['properties']['email']);
+ if (count($parts) === 2) {
+ $event['properties']['email'] = substr($parts[0], 0, 1) .
+ '***@' . $parts[1];
+ }
+ }
+
+ // Passwörter und Tokens entfernen
+ foreach (['password', 'token', 'api_key', 'secret'] as $key) {
+ if (isset($event['properties'][$key])) {
+ $event['properties'][$key] = '[redacted]';
+ }
+ }
+
+ return $event;
+ });
+
+ // Spam-Filter
+ $manager->addMiddleware(function(array $event) {
+ // Zu viele Events von einem Benutzer filtern
+ static $userCounts = [];
+ $userId = $event['user_id'] ?? $event['session_id'] ?? null;
+
+ if ($userId) {
+ $userCounts[$userId] = ($userCounts[$userId] ?? 0) + 1;
+
+ // Mehr als 100 Events pro Session ist verdächtig
+ if ($userCounts[$userId] > 100) {
+ return null; // Event verwerfen
+ }
+ }
+
+ return $event;
+ });
+}
+```
+
+### Integration mit dem Domain-Layer
+
+```php
+// In einem Domain-Service
+namespace App\Domain\Shop\Services;
+
+use App\Framework\Analytics\Analytics;
+
+class ProductService
+{
+ public function __construct(private readonly Analytics $analytics) {}
+
+ public function viewProduct(string $productId, ?string $userId): Product
+ {
+ $product = $this->productRepository->find($productId);
+
+ if (!$product) {
+ throw new ProductNotFoundException($productId);
+ }
+
+ // Produktansicht tracken
+ $this->analytics->track('product_view', [
+ 'product_id' => $product->getId(),
+ 'product_name' => $product->getName(),
+ 'product_price' => $product->getPrice(),
+ 'product_category' => $product->getCategory()->getName(),
+ 'in_stock' => $product->isInStock()
+ ], $userId);
+
+ return $product;
+ }
+
+ public function addToCart(string $productId, int $quantity, ?string $userId): Cart
+ {
+ // Implementation...
+
+ // Event tracken
+ $this->analytics->track('add_to_cart', [
+ 'product_id' => $productId,
+ 'quantity' => $quantity,
+ 'cart_value' => $cart->getTotalValue()
+ ], $userId);
+
+ return $cart;
+ }
+}
+```
+
+### Verwendung im Controller
+
+```php
+namespace App\Application\Controllers;
+
+use App\Framework\Analytics\Analytics;
+use App\Framework\Http\Request;
+use App\Framework\Http\Response;
+use App\Framework\Attributes\Route;
+
+class CheckoutController
+{
+ public function __construct(
+ private readonly Analytics $analytics,
+ private readonly CheckoutService $checkoutService
+ ) {}
+
+ #[Route('/checkout/complete', method: 'POST')]
+ public function completeCheckout(Request $request): Response
+ {
+ $userId = $request->getSession()->get('user_id');
+ $cartId = $request->getSession()->get('cart_id');
+
+ try {
+ $order = $this->checkoutService->completeCheckout($cartId, $userId);
+
+ // Erfolgreichen Checkout tracken
+ $this->analytics->track('checkout_complete', [
+ 'order_id' => $order->getId(),
+ 'order_value' => $order->getTotalValue(),
+ 'items_count' => count($order->getItems()),
+ 'payment_method' => $order->getPaymentMethod(),
+ 'shipping_method' => $order->getShippingMethod()
+ ], $userId);
+
+ return new Response([
+ 'success' => true,
+ 'order_id' => $order->getId()
+ ], 200);
+ } catch (\Exception $e) {
+ // Fehler beim Checkout tracken
+ $this->analytics->track('checkout_error', [
+ 'error' => $e->getMessage(),
+ 'cart_id' => $cartId
+ ], $userId);
+
+ // Auch den Exception-Stack tracken
+ $this->analytics->error($e);
+
+ return new Response([
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ], 400);
+ }
+ }
+}
+```
+
+## Analyse der Daten
+
+### Dashboard-Controller
+
+```php
+namespace App\Application\Controllers;
+
+use App\Framework\Analytics\AnalyticsDashboard;
+use App\Framework\Http\Request;
+use App\Framework\Http\Response;
+use App\Framework\View\ViewRenderer;
+use App\Framework\Attributes\Route;
+
+class AnalyticsDashboardController
+{
+ public function __construct(
+ private readonly AnalyticsDashboard $dashboard,
+ private readonly ViewRenderer $viewRenderer
+ ) {}
+
+ #[Route('/admin/analytics/conversion')]
+ public function conversionReport(Request $request): Response
+ {
+ $from = $request->getQueryParam('from');
+ $to = $request->getQueryParam('to');
+
+ // Benutzerdefinierte Analyse für Conversion-Funnel
+ $pageViews = $this->dashboard->getEventCountByType('page_view', $from, $to);
+ $productViews = $this->dashboard->getEventCountByType('product_view', $from, $to);
+ $addToCarts = $this->dashboard->getEventCountByType('add_to_cart', $from, $to);
+ $checkouts = $this->dashboard->getEventCountByType('checkout_complete', $from, $to);
+
+ // Conversion-Raten berechnen
+ $data = [
+ 'total_visitors' => $pageViews,
+ 'product_view_rate' => $pageViews > 0 ? $productViews / $pageViews : 0,
+ 'add_to_cart_rate' => $productViews > 0 ? $addToCarts / $productViews : 0,
+ 'checkout_rate' => $addToCarts > 0 ? $checkouts / $addToCarts : 0,
+ 'overall_conversion' => $pageViews > 0 ? $checkouts / $pageViews : 0,
+ 'from_date' => $from,
+ 'to_date' => $to
+ ];
+
+ return $this->viewRenderer->render('admin/analytics/conversion', $data);
+ }
+}
+```
diff --git a/docs/framework/core/README.md b/docs/framework/core/README.md
new file mode 100644
index 00000000..4435e80a
--- /dev/null
+++ b/docs/framework/core/README.md
@@ -0,0 +1,45 @@
+# Core-Modul Dokumentation
+
+## Übersicht
+
+Das Core-Modul bildet das Herzstück des Frameworks und stellt grundlegende Funktionalitäten bereit, die von anderen Modulen genutzt werden.
+
+## Hauptkomponenten
+
+### Events und EventDispatcher
+
+Das Event-System ermöglicht die Kommunikation zwischen Komponenten über einen zentralen Event-Bus.
+
+**Kernklassen:**
+- `EventDispatcher`: Zentraler Service zum Registrieren und Auslösen von Events
+- Bekannte Events:
+ - `ApplicationBooted`
+ - `ErrorOccurred`
+ - `BeforeHandleRequest`
+ - `AfterHandleRequest`
+
+**Beispielverwendung:**
+```php
+// Event-Handler registrieren
+$eventDispatcher->addHandler('App\Framework\Core\Events\ApplicationBooted', function($event) {
+ // Event verarbeiten
+});
+```
+
+### PathProvider
+
+Stellt Pfadinformationen für verschiedene Bereiche der Anwendung bereit.
+
+**Hauptfunktionen:**
+- `getDataPath()`: Liefert Pfade zu Datenverzeichnissen
+
+## Integration mit anderen Modulen
+
+Das Core-Modul wird von vielen anderen Modulen verwendet, wie z.B.:
+
+- **Analytics-Modul**: Nutzt den EventDispatcher zum Tracking von Systemereignissen
+- **DI-Container**: Nutzt Core-Komponenten für die Initialisierung von Services
+
+## Architektur
+
+Das Core-Modul folgt einer ereignisgesteuerten Architektur, bei der Komponenten über Events miteinander kommunizieren können, anstatt direkte Abhängigkeiten zu haben.
diff --git a/docs/framework/di/README.md b/docs/framework/di/README.md
new file mode 100644
index 00000000..e961c8c6
--- /dev/null
+++ b/docs/framework/di/README.md
@@ -0,0 +1,89 @@
+# Dependency Injection Modul
+
+## Übersicht
+
+Das DI-Modul implementiert ein Dependency-Injection-Container-System, das die automatische Auflösung und Verwaltung von Abhängigkeiten ermöglicht.
+
+## Hauptkomponenten
+
+### Container
+
+Der zentrale Service-Container, der Instanzen erstellt und verwaltet.
+
+**Hauptfunktionen:**
+- Service-Erstellung und -Auflösung
+- Singleton-Verwaltung
+- Rekursive Abhängigkeitsauflösung
+
+### Initializer-Attribut
+
+Das `#[Initializer]`-Attribut kennzeichnet Klassen, die Services im Container registrieren können.
+
+**Beispiel:**
+```php
+#[Initializer]
+readonly class AnalyticsInitializer
+{
+ public function __invoke(Container $container): Analytics
+ {
+ // Service erstellen und konfigurieren
+ return $analytics;
+ }
+}
+```
+
+## Verwendung
+
+### Service definieren
+
+```php
+// Service-Interface
+interface MyServiceInterface
+{
+ public function doSomething(): void;
+}
+
+// Konkrete Implementierung
+class MyService implements MyServiceInterface
+{
+ public function doSomething(): void
+ {
+ // Implementierung
+ }
+}
+```
+
+### Service registrieren
+
+```php
+#[Initializer]
+class MyServiceInitializer
+{
+ public function __invoke(Container $container): MyServiceInterface
+ {
+ return new MyService();
+ }
+}
+```
+
+### Service verwenden
+
+```php
+class MyController
+{
+ public function __construct(
+ private readonly MyServiceInterface $myService
+ ) {}
+
+ public function action(): void
+ {
+ $this->myService->doSomething();
+ }
+}
+```
+
+## Prinzipien
+
+- **Automatische Auflösung**: Abhängigkeiten werden anhand von Typ-Hints automatisch aufgelöst
+- **Lazy Loading**: Services werden erst erstellt, wenn sie benötigt werden
+- **Singleton-Modus**: Standardmäßig werden Services als Singletons verwaltet
diff --git a/docs/framework/http/README.md b/docs/framework/http/README.md
new file mode 100644
index 00000000..4e8e3f53
--- /dev/null
+++ b/docs/framework/http/README.md
@@ -0,0 +1,92 @@
+# HTTP-Modul Dokumentation
+
+## Übersicht
+
+Das HTTP-Modul stellt Komponenten für die Verarbeitung von HTTP-Anfragen und -Antworten bereit.
+
+## Hauptkomponenten
+
+### Request
+
+Repräsentiert eine HTTP-Anfrage mit Methoden zum Zugriff auf:
+- HTTP-Methode (`getMethod()`)
+- Pfad (`getPath()`)
+- Query-Parameter (`getQueryParams()`)
+- Request-Body
+- Headers
+
+### Response
+
+Repräsentiert eine HTTP-Antwort mit:
+- Status-Code (`getStatusCode()`)
+- Headers
+- Body
+
+### Middleware
+
+Ein Interface für HTTP-Middleware-Komponenten, die die Request-Verarbeitung verändern können:
+
+```php
+interface Middleware
+{
+ public function process(Request $request, callable $next): Response;
+}
+```
+
+**Beispiel-Middleware:**
+```php
+class AnalyticsMiddleware implements Middleware
+{
+ public function __construct(
+ private readonly Analytics $analytics
+ ) {}
+
+ public function process(Request $request, callable $next): Response
+ {
+ // Request tracken
+ $this->analytics->track('http_request', [
+ 'method' => $request->getMethod(),
+ 'path' => $request->getPath(),
+ ]);
+
+ // Request weiterleiten
+ $response = $next($request);
+
+ // Response tracken
+ $this->analytics->track('http_response', [
+ 'status_code' => $response->getStatusCode(),
+ ]);
+
+ return $response;
+ }
+}
+```
+
+## Integration mit anderen Modulen
+
+- **Router**: Routing von HTTP-Requests zu Controllers
+- **Analytics**: Tracking von HTTP-Requests und -Responses
+- **Validation**: Validierung von Request-Daten
+
+## Middlewares registrieren
+
+In der Anwendungsklasse:
+
+```php
+$this->addMiddleware(LoggingMiddleware::class);
+$this->addMiddleware(AnalyticsMiddleware::class);
+$this->addMiddleware(AuthenticationMiddleware::class);
+```
+
+## Responses erzeugen
+
+```php
+// JSON-Response
+return new JsonResponse(['success' => true]);
+
+// HTML-Response
+return new HtmlResponse('Hello World');
+
+// Redirect
+return new RedirectResponse('/dashboard');
+```
diff --git a/docs/framework/index.md b/docs/framework/index.md
new file mode 100644
index 00000000..58a1a271
--- /dev/null
+++ b/docs/framework/index.md
@@ -0,0 +1,42 @@
+# Framework-Dokumentation
+
+## Übersicht
+
+Diese Dokumentation beschreibt die Architektur, Komponenten und Verwendung des Framework-Kerns des Projekts.
+
+## Hauptmodule
+
+- [Analytics](/framework/analytics/README.md) - System zur Erfassung und Analyse von Anwendungsdaten
+- [Core](/framework/core/README.md) - Kernkomponenten und Event-System
+- [DI (Dependency Injection)](/framework/di/README.md) - Container und Service-Management
+- [HTTP](/framework/http/README.md) - HTTP-Request/Response-Handling
+
+## Richtlinien und Muster
+
+- [Modul-Checkliste](/framework/MODUL-CHECKLISTE.md) - Leitfaden für die Erstellung neuer Module
+- [Erweiterungsmuster](/framework/ERWEITERUNGSPATTERN.md) - Muster zur Erweiterung des Frameworks
+
+## Modulare Architektur
+
+Das Framework ist modular aufgebaut, mit klaren Verantwortlichkeiten für jedes Modul. Module kommunizieren über klar definierte Interfaces und den Event-Dispatcher.
+
+### Neues Modul erstellen
+
+Um ein neues Modul zu erstellen, folgen Sie der [Modul-Checkliste](/framework/MODUL-CHECKLISTE.md) und beachten Sie die folgenden Kernprinzipien:
+
+1. Klare Verantwortlichkeiten definieren
+2. Dependency Injection verwenden
+3. Interface-basiertes Design umsetzen
+4. Event-basierte Kommunikation nutzen
+5. Externe Abhängigkeiten minimieren
+
+### Framework erweitern
+
+Es gibt verschiedene Möglichkeiten, das Framework zu erweitern:
+
+1. **Middleware**: HTTP-Request-Pipeline erweitern
+2. **Event-Listener**: Auf System-Events reagieren
+3. **Service-Provider**: Eigene Services registrieren
+4. **Plugin-System**: Umfangreichere Erweiterungen implementieren
+
+Weitere Details finden Sie im Dokument [Erweiterungsmuster](/framework/ERWEITERUNGSPATTERN.md).
diff --git a/docs/guidelines/PERFORMANCE-GUIDELINES.md b/docs/guidelines/PERFORMANCE-GUIDELINES.md
new file mode 100644
index 00000000..21dbf67f
--- /dev/null
+++ b/docs/guidelines/PERFORMANCE-GUIDELINES.md
@@ -0,0 +1,169 @@
+# Performance-Richtlinien
+
+## Übersicht
+
+Diese Richtlinien beschreiben Best Practices für die Optimierung der Anwendungsleistung im Framework.
+
+## Grundprinzipien
+
+### 1. Frühe Optimierung vermeiden
+
+- Code zuerst korrekt und lesbar schreiben
+- Optimierungen auf Basis von Messungen durchführen
+- Performance-Engpässe mit dem Profiler identifizieren
+
+### 2. Caching strategisch einsetzen
+
+- Ergebnisse teurer Operationen cachen
+- Geeignete Cache-Invalidierungsstrategien implementieren
+- Cache-Hierarchie nutzen (Memory → Filesystem → Datenbank)
+
+```php
+// Beispiel: Strategisches Caching
+public function getExpensiveData(string $key): array
+{
+ $cacheKey = "data_{$key}";
+
+ // Prüfen, ob Daten im Cache sind
+ if ($this->cache->has($cacheKey)) {
+ return $this->cache->get($cacheKey);
+ }
+
+ // Teure Operation durchführen
+ $result = $this->performExpensiveOperation($key);
+
+ // Ergebnis cachen mit angemessener TTL
+ $this->cache->set($cacheKey, $result, 3600);
+
+ return $result;
+}
+```
+
+### 3. Lazy Loading
+
+- Ressourcen erst bei Bedarf laden
+- Schwere Abhängigkeiten verzögert initialisieren
+
+```php
+// Beispiel: Lazy Loading
+private ?ExpensiveService $expensiveService = null;
+
+private function getExpensiveService(): ExpensiveService
+{
+ if ($this->expensiveService === null) {
+ $this->expensiveService = new ExpensiveService($this->config);
+ }
+
+ return $this->expensiveService;
+}
+```
+
+### 4. Datenstrukturen optimieren
+
+- Passende Datenstrukturen für den Anwendungsfall wählen
+- Große Arrays vermeiden, wenn möglich Iteratoren oder Generatoren nutzen
+- Bei großen Datenmengen paginieren
+
+```php
+// Beispiel: Generator statt Array
+public function processLargeDataset(): Generator
+{
+ $handle = fopen($this->largeFile, 'r');
+
+ while (($line = fgets($handle)) !== false) {
+ yield $this->processLine($line);
+ }
+
+ fclose($handle);
+}
+```
+
+## Spezifische Optimierungen
+
+### Datenbankzugriffe
+
+- Anzahl der Datenbankabfragen minimieren
+- Indizes für häufige Abfragen
+- N+1 Problem vermeiden durch Eager Loading
+- Datenbankverbindungen poolen
+
+### HTTP-Anfragen
+
+- Statische Assets komprimieren und cachen
+- HTTP/2 nutzen, wenn möglich
+- Content-Kompression aktivieren
+- Browser-Caching durch geeignete Header
+
+### Speichernutzung
+
+- Große Objekte nach Gebrauch freigeben (`unset()`)
+- Zirkuläre Referenzen vermeiden
+- Bei Dateiberarbeitung auf Streaming-Ansätze setzen
+
+### Autoloading
+
+- Composer-Autoloader in Produktionsumgebung optimieren
+- Preloading für häufig verwendete Klassen nutzen
+
+```php
+// preload.php Beispiel
+performOperation();
+
+$duration = microtime(true) - $startTime;
+$this->logger->debug("Operation ausgeführt in {$duration}s");
+```
+
+### Automatisiertes Monitoring
+
+- Performance-Regressions automatisch erkennen
+- Regelmäßige Lasttests durchführen
+- Kritische Pfade überwachen
+
+## Umgebungsspezifische Optimierungen
+
+### Entwicklung
+
+- Debug-Tools aktivieren
+- Performance-Optimierungen deaktivieren, die Debugging erschweren
+
+### Produktion
+
+- Opcache aktivieren und optimieren
+- Fehlerbehandlung optimieren (keine Stack Traces)
+- Logging auf nötige Informationen beschränken
+
+```php
+// Umgebungsspezifische Einstellungen
+if ($environment === 'production') {
+ ini_set('opcache.validate_timestamps', 0);
+ ini_set('display_errors', 0);
+ error_reporting(E_ERROR | E_WARNING | E_PARSE);
+} else {
+ ini_set('opcache.validate_timestamps', 1);
+ ini_set('display_errors', 1);
+ error_reporting(E_ALL);
+}
+```
diff --git a/docs/guidelines/TESTING-GUIDELINES.md b/docs/guidelines/TESTING-GUIDELINES.md
new file mode 100644
index 00000000..9875f5fe
--- /dev/null
+++ b/docs/guidelines/TESTING-GUIDELINES.md
@@ -0,0 +1,181 @@
+# Testing-Richtlinien
+
+## Übersicht
+
+Diese Richtlinien beschreiben die Standards und Best Practices für Tests im Framework.
+
+## Grundprinzipien
+
+### 1. Testarten
+
+- **Unit Tests**: Testen einzelner Komponenten isoliert
+- **Integrationstests**: Testen des Zusammenspiels mehrerer Komponenten
+- **Funktionstests**: Testen ganzer Funktionalitäten aus Benutzersicht
+
+### 2. Pest-Framework
+
+Alle Tests werden mit dem Pest-Framework geschrieben:
+
+```php
+test('Analytics::track zeichnet Events korrekt auf', function () {
+ // Arrange
+ $manager = new AnalyticsManager(['enabled' => true], new InMemoryStorage());
+ $analytics = new Analytics($manager, new EventDispatcher());
+
+ // Act
+ $analytics->track('test_event', ['key' => 'value']);
+
+ // Assert
+ expect($manager->getStorage()->retrieve())->toHaveCount(1);
+ expect($manager->getStorage()->retrieve()[0]['event'])->toBe('test_event');
+});
+```
+
+### 3. Teststruktur
+
+- Jeder Test folgt dem AAA-Prinzip: Arrange, Act, Assert
+- Tests sollten unabhängig voneinander sein
+- Tests sollten deterministisch sein (keine zufälligen Ergebnisse)
+
+## Best Practices
+
+### 1. Testabdeckung
+
+- Jede öffentliche Methode sollte getestet werden
+- Edge Cases und Fehlersituationen explizit testen
+- Fokus auf Business-Logik und kritische Pfade
+
+### 2. Mocking
+
+- Externe Abhängigkeiten mocken
+- Eigene Test-Implementierungen für Interfaces erstellen
+- Mock-Objekte nur für die spezifischen Testfälle konfigurieren
+
+```php
+// Beispiel: Test mit Mock
+test('Fehlerbehandlung bei Storage-Ausfall', function () {
+ // Arrange
+ $storage = new class implements StorageInterface {
+ public function store(array $events): void
+ {
+ throw new \RuntimeException('Storage-Fehler');
+ }
+
+ public function retrieve(array $filters = []): array
+ {
+ return [];
+ }
+
+ public function clear(): void
+ {
+ }
+ };
+
+ $manager = new AnalyticsManager(['enabled' => true], $storage);
+
+ // Act & Assert
+ expect(fn() => $manager->flush())->toThrow(\RuntimeException::class);
+});
+```
+
+### 3. Fixtures und Factories
+
+- Testdaten mit Factories erstellen
+- Komplexe Objekte über Helpers aufbauen
+- Wiederverwendbare Fixtures für ähnliche Tests
+
+```php
+// Beispiel: Test-Factory
+function createAnalyticsManager(array $config = []): AnalyticsManager
+{
+ $defaultConfig = ['enabled' => true, 'auto_flush' => false];
+ return new AnalyticsManager(
+ array_merge($defaultConfig, $config),
+ new InMemoryStorage()
+ );
+}
+
+test('Auto-Flush funktioniert korrekt', function () {
+ // Arrange
+ $manager = createAnalyticsManager(['auto_flush' => true, 'batch_size' => 2]);
+
+ // Act
+ $manager->track('event1', []);
+ $manager->track('event2', []);
+
+ // Assert
+ expect($manager->getStorage()->retrieve())->toHaveCount(2);
+});
+```
+
+### 4. Datenbankzugriffe
+
+- In Unit-Tests Datenbankzugriffe vermeiden oder mocken
+- In Integrationstests separate Testdatenbank verwenden
+- Testdatenbank nach Tests zurücksetzen
+
+### 5. Asynchrone Tests
+
+- Timeouts für asynchrone Tests setzen
+- Auf Async-Events warten, statt feste Wartezeiten
+
+## Testorganisation
+
+### 1. Dateistruktur
+
+- Tests in `/tests`-Verzeichnis mit gleicher Struktur wie `/src`
+- Dateinamen mit `Test`-Suffix
+
+### 2. Testgruppen
+
+- Tests mit Attributen in Gruppen einteilen
+- Langsame Tests markieren
+
+```php
+#[Group('analytics')]
+#[Group('slow')]
+test('große Datenmenge verarbeiten', function () {
+ // Test mit vielen Daten
+});
+```
+
+## CI/CD-Integration
+
+- Tests in CI-Pipeline automatisiert ausführen
+- Testabdeckung messen und überwachen
+- Pull Requests nur mit erfolgreichen Tests mergen
+
+## Fehlersuche
+
+### 1. Debugging-Techniken
+
+- Pest-Debugging aktivieren mit `->dump()`
+- PHPUnit-Assertions für detaillierte Fehlermeldungen
+
+```php
+test('komplexes Objekt validieren', function () {
+ $result = processData();
+
+ // Bei Fehlern Kontext ausgeben
+ if (!expect($result->isValid())->toBeTrue()->isSuccess()) {
+ dump($result->getErrors());
+ }
+});
+```
+
+### 2. Problematische Tests
+
+- Flaky Tests identifizieren und beheben
+- Tests isolieren mit `only` oder `skip`
+
+```php
+test('problematischer Test', function () {
+ // Test-Code
+})->skip('Wird untersucht');
+```
+
+## Zusammenfassung
+
+- Tests sind ein integraler Bestandteil der Codebase
+- Qualität und Wartbarkeit der Tests sind genauso wichtig wie die des Produktionscodes
+- Tests dienen als lebende Dokumentation der Funktionalität
diff --git a/docs/guidelines/index.md b/docs/guidelines/index.md
new file mode 100644
index 00000000..605610b9
--- /dev/null
+++ b/docs/guidelines/index.md
@@ -0,0 +1,43 @@
+# Entwicklungsrichtlinien
+
+## Übersicht
+
+Diese Richtlinien bieten Best Practices und Standards für die Entwicklung innerhalb des Projekts. Sie helfen, qualitativ hochwertigen, wartbaren und leistungsfähigen Code zu schreiben.
+
+## Verfügbare Richtlinien
+
+- [Performance-Richtlinien](/guidelines/PERFORMANCE-GUIDELINES.md) - Optimierung der Anwendungsleistung
+- [Testing-Richtlinien](/guidelines/TESTING-GUIDELINES.md) - Standards und Best Practices für Tests
+
+## Performance-Optimierung
+
+Leistungsoptimierung ist ein wichtiger Aspekt der Anwendungsentwicklung. Die [Performance-Richtlinien](/guidelines/PERFORMANCE-GUIDELINES.md) bieten Einblicke in:
+
+- Strategisches Caching
+- Lazy Loading
+- Optimierung von Datenbankabfragen
+- Effiziente Datenstrukturen
+- Speichernutzung
+
+## Teststrategien
+
+Testen ist ein integraler Bestandteil des Entwicklungsprozesses. Die [Testing-Richtlinien](/guidelines/TESTING-GUIDELINES.md) decken folgende Themen ab:
+
+- Unit Tests mit Pest-Framework
+- Integrationstests
+- Test-Fixtures und Factories
+- Mocking-Strategien
+- Testorganisation und -struktur
+
+## Anwendung der Richtlinien
+
+Diese Richtlinien sollten in allen Phasen der Entwicklung berücksichtigt werden:
+
+1. **Planungsphase**: Frühzeitige Berücksichtigung von Performance und Testbarkeit
+2. **Implementierungsphase**: Anwendung der Best Practices während der Entwicklung
+3. **Review-Phase**: Überprüfung des Codes anhand der Richtlinien
+4. **Refactoring**: Verbesserung bestehenden Codes gemäß den Richtlinien
+
+## Continuous Improvement
+
+Diese Richtlinien werden kontinuierlich verbessert und aktualisiert. Wenn Sie Vorschläge zur Verbesserung haben, zögern Sie nicht, diese einzubringen.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..ee0b4e55
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,35 @@
+# Projekt-Dokumentation
+
+## Willkommen zur Projektdokumentation
+
+Diese Dokumentation bietet umfassende Informationen zu Coding-Standards, Architektur, Best Practices und Framework-Komponenten des Projekts.
+
+## Schnellzugriff
+
+- [Coding-Guidelines](/standards/CODING-GUIDELINES.md)
+- [Sicherheitsrichtlinien](/standards/SICHERHEITS-GUIDELINES.md)
+- [Projektstruktur](/architecture/STRUKTUR-DOKUMENTATION.md)
+- [Performance-Optimierung](/guidelines/PERFORMANCE-GUIDELINES.md)
+- [KI-Assistent Einrichtung](/ai/EINRICHTUNG-PHPSTORM.md)
+
+## Dokumentationsstruktur
+
+Die Dokumentation ist in verschiedene Bereiche unterteilt:
+
+- **Standards und Guidelines**: Grundlegende Prinzipien und Richtlinien für die Entwicklung
+- **Framework-Module**: Dokumentation zu den einzelnen Framework-Komponenten
+- **Entwicklungsrichtlinien**: Detaillierte Anleitungen zu Performance, Testing und mehr
+- **Architektur**: Übersicht und Details zur Systemarchitektur
+- **KI-Assistent-Integration**: Anleitung zur Verwendung des KI-Assistenten
+
+## Für neue Entwickler
+
+Wenn Sie neu im Projekt sind, empfehlen wir Ihnen, mit folgenden Dokumenten zu beginnen:
+
+1. [Projektstruktur](/architecture/STRUKTUR-DOKUMENTATION.md) - Um einen Überblick zu bekommen
+2. [Coding-Guidelines](/standards/CODING-GUIDELINES.md) - Um die Coding-Standards zu verstehen
+3. [Modul-Checkliste](/framework/MODUL-CHECKLISTE.md) - Um die Modularisierung zu verstehen
+
+## Beitragen zur Dokumentation
+
+Die Dokumentation ist ein lebendiges Dokument. Wenn Sie Fehler finden oder Verbesserungen vorschlagen möchten, erstellen Sie bitte einen Pull Request mit Ihren Änderungen.
diff --git a/docs/standards/CODING-GUIDELINES.md b/docs/standards/CODING-GUIDELINES.md
new file mode 100644
index 00000000..ead459c7
--- /dev/null
+++ b/docs/standards/CODING-GUIDELINES.md
@@ -0,0 +1,234 @@
+# Coding Guidelines
+
+## Allgemeine Prinzipien
+
+Diese Guidelines definieren die Standards für die Entwicklung und Wartung des Projekts. Sie sorgen für Konsistenz, Lesbarkeit und Wartbarkeit des Codes.
+
+## PHP-Version
+
+- **PHP 8.4**: Die Codebase nutzt stets die neueste stabile PHP-Version (aktuell PHP 8.4)
+- Alle neuen PHP-Sprachfeatures sollten, wo sinnvoll, genutzt werden
+
+## Abhängigkeiten
+
+- **Externe Abhängigkeiten vermeiden**: Das Projekt soll möglichst ohne externe Bibliotheken auskommen
+- **Eigene Implementierungen bevorzugen**: Anstatt externe Pakete einzubinden, sollten eigene Lösungen entwickelt werden
+- **Erlaubte Abhängigkeiten**: Nur die bereits in composer.json definierten Pakete dürfen verwendet werden
+- **Neue Abhängigkeiten**: Müssen explizit genehmigt werden und sollten nur in Ausnahmefällen hinzugefügt werden
+
+## Klassenstruktur
+
+### Klassen
+
+- **Alle Klassen müssen `final` sein**, es sei denn, es gibt einen zwingenden Grund für Vererbung
+- **Keine abstrakten Klassen** - bevorzuge Interfaces und Kompositionen
+- **Klassen sollten `readonly` sein**, wann immer möglich
+- Interfaces verwenden, um Verträge zwischen Komponenten zu definieren
+
+```php
+// Gut
+final readonly class AnalyticsManager
+{
+ // ...
+}
+
+// Vermeiden
+abstract class BaseManager
+{
+ // ...
+}
+```
+
+### Properties
+
+- **Properties sollten `readonly` sein**, wann immer möglich
+- Private Visibility für alle Properties, es sei denn, es gibt einen zwingenden Grund
+- Typisierung ist obligatorisch für alle Properties
+
+```php
+// Gut
+private readonly StorageInterface $storage;
+
+// Vermeiden
+public array $config;
+```
+
+## Methoden und Funktionen
+
+- Typisierung ist obligatorisch für alle Parameter und Rückgabewerte
+- Methoden sollten klein und fokussiert sein (Single Responsibility)
+- Verwende named arguments für bessere Lesbarkeit
+
+```php
+// Gut
+public function track(string $event, array $properties = [], ?string $userId = null): void
+{
+ // ...
+}
+
+// Vermeiden
+public function process($data)
+{
+ // ...
+}
+```
+
+## Dependency Injection
+
+- Constructor Injection für alle Abhängigkeiten
+- Keine Service Locator oder globale Zustände
+- Verwende das `#[Initializer]`-Attribut für Service-Registrierung
+
+```php
+// Gut
+public function __construct(
+ private readonly Configuration $config,
+ private readonly StorageInterface $storage
+) {}
+
+// Vermeiden
+public function __construct()
+{
+ $this->config = Container::get(Configuration::class);
+}
+```
+
+## Moderne PHP-Features
+
+### Nullable Types und Union Types
+
+```php
+public function findUser(?int $id): ?User
+public function process(int|string $identifier): void
+```
+
+### Match Expression statt Switch
+
+```php
+// Gut
+return match($storageType) {
+ 'file' => new FileStorage($path),
+ 'redis' => new RedisStorage($connection),
+ default => throw new \InvalidArgumentException("Nicht unterstützter Storage-Typ: {$storageType}")
+};
+
+// Vermeiden
+switch ($storageType) {
+ case 'file':
+ return new FileStorage($path);
+ // ...
+}
+```
+
+### Named Arguments
+
+```php
+$this->createUser(
+ email: 'user@example.com',
+ role: 'admin',
+ sendWelcomeEmail: true
+);
+```
+
+### Constructor Property Promotion
+
+```php
+public function __construct(
+ private readonly Configuration $config,
+ private readonly PathProvider $pathProvider
+) {}
+```
+
+## Fehlerbehandlung
+
+- Spezifische Exceptions werfen
+- Typed Exceptions verwenden
+- Early Return Pattern bevorzugen
+
+```php
+// Gut
+if (!$this->isValid()) {
+ throw new ValidationException('Ungültige Daten');
+}
+
+// Vermeiden
+if ($this->isValid()) {
+ // Lange Verarbeitung...
+} else {
+ throw new Exception('Fehler');
+}
+```
+
+## Testing
+
+- Tests mit Pest-Framework schreiben
+- Für jede öffentliche Methode sollte mindestens ein Test existieren
+- Mocking nur für externe Abhängigkeiten verwenden
+
+```php
+test('track method records events correctly', function () {
+ $analytics = new Analytics($manager, $dispatcher);
+ $analytics->track('test_event', ['key' => 'value']);
+
+ expect($manager->getEvents())->toHaveCount(1);
+});
+```
+
+## Dokumentation
+
+- PHPDoc für alle öffentlichen Methoden und Klassen
+- Typen in PHPDoc sollten den tatsächlichen Typen entsprechen
+- Klare, beschreibende Kommentare für komplexe Logik
+
+```php
+/**
+ * Zeichnet ein Event auf und wendet Middleware an
+ *
+ * @param string $event Event-Name
+ * @param array $properties Event-Eigenschaften
+ * @param string|null $userId Benutzer-ID (optional)
+ */
+public function track(string $event, array $properties = [], ?string $userId = null): void
+```
+
+## Namenskonventionen
+
+- **Klassen**: PascalCase (`AnalyticsManager`)
+- **Methoden/Funktionen**: camelCase (`getEventStats()`)
+- **Variablen**: camelCase (`$eventData`)
+- **Konstanten**: SNAKE_CASE (`MAX_BATCH_SIZE`)
+- **Dateien**: Klassenname.php (`AnalyticsManager.php`)
+
+## Architektur
+
+- Dependency Inversion Principle befolgen
+- Interfaces für alle externen Abhängigkeiten
+- Vermeide zirkuläre Abhängigkeiten zwischen Modulen
+- Services sollten eine klare, fokussierte Verantwortung haben
+
+## Performance
+
+- Lazy Loading für ressourcenintensive Operationen
+- Caching wo sinnvoll
+- Datenstrukturen sorgfältig auswählen
+
+## Security
+
+- Alle Benutzereingaben validieren und bereinigen
+- Prepared Statements für Datenbankabfragen
+- Vermeidung von `eval()` und ähnlichen unsicheren Funktionen
+- Sensible Daten niemals in Logs schreiben
+
+## Best Practices
+
+- **Immutability**: Bevorzuge unveränderliche Objekte
+- **Pure Functions**: Bevorzuge reine Funktionen ohne Seiteneffekte
+- **Enums**: Verwende Enums statt String-Konstanten für feste Optionssätze
+- **DTOs**: Verwende Data Transfer Objects für Datentransport
+- **Value Objects**: Verwende Value Objects für semantisch reiche Datentypen
+
+## Refactoring
+
+- Code, der diese Guidelines nicht erfüllt, sollte beim Bearbeiten aktualisiert werden
+- Tests müssen vor und nach dem Refactoring bestehen
+- Große Refactorings sollten in kleinen, separaten Commits erfolgen
diff --git a/docs/standards/SICHERHEITS-GUIDELINES.md b/docs/standards/SICHERHEITS-GUIDELINES.md
new file mode 100644
index 00000000..e9f3378b
--- /dev/null
+++ b/docs/standards/SICHERHEITS-GUIDELINES.md
@@ -0,0 +1,187 @@
+# Sicherheitsrichtlinien
+
+## Übersicht
+
+Diese Richtlinien definieren Standards und Best Practices für sichere Softwareentwicklung im Projekt.
+
+## Grundprinzipien
+
+### 1. Defense in Depth
+
+- Mehrere Sicherheitsschichten implementieren
+- Nicht auf eine einzelne Sicherheitsmaßnahme vertrauen
+- Fail-Safe-Mechanismen einbauen
+
+### 2. Least Privilege
+
+- Minimale Berechtigungen für Funktionen und Benutzer
+- Ressourcenzugriff nur bei Bedarf gewähren
+- Temporäre Berechtigungen nach Gebrauch entziehen
+
+### 3. Input-Validierung
+
+- Alle Benutzereingaben validieren und bereinigen
+- Whitelist-Ansatz bevorzugen (erlaubte Eingaben definieren)
+- Typprüfung und Formatvalidierung durchführen
+
+```php
+// Beispiel: Sichere Input-Validierung
+public function processUserInput(string $input): string
+{
+ // Länge prüfen
+ if (strlen($input) > 100) {
+ throw new ValidationException('Input zu lang (max 100 Zeichen)');
+ }
+
+ // Inhalt validieren (Whitelist-Ansatz)
+ if (!preg_match('/^[a-zA-Z0-9\s\-_]+$/', $input)) {
+ throw new ValidationException('Input enthält ungültige Zeichen');
+ }
+
+ // Bereinigung
+ return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
+}
+```
+
+## Spezifische Sicherheitsmaßnahmen
+
+### 1. SQL-Injection-Prävention
+
+- Prepared Statements für alle Datenbankabfragen
+- Keine dynamischen SQL-Queries
+- ORM-Framework bevorzugen
+
+```php
+// Sicher
+$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
+$stmt->execute([$email]);
+
+// Unsicher - NIEMALS SO MACHEN
+$query = "SELECT * FROM users WHERE email = '{$email}'";
+```
+
+### 2. Cross-Site Scripting (XSS) Prävention
+
+- Output-Escaping für alle benutzergenerierten Inhalte
+- Content-Security-Policy (CSP) implementieren
+- HttpOnly und Secure Flags für Cookies
+
+```php
+// Ausgabe in HTML
+echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
+
+// Ausgabe in JavaScript
+echo json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
+```
+
+### 3. Cross-Site Request Forgery (CSRF) Schutz
+
+- CSRF-Token für alle ändernden Anfragen
+- SameSite-Attribut für Cookies
+
+```php
+// CSRF-Token generieren
+public function generateCsrfToken(): string
+{
+ $token = bin2hex(random_bytes(32));
+ $_SESSION['csrf_token'] = $token;
+ return $token;
+}
+
+// CSRF-Token validieren
+public function validateCsrfToken(string $token): bool
+{
+ return hash_equals($_SESSION['csrf_token'] ?? '', $token);
+}
+```
+
+### 4. Sichere Authentifizierung
+
+- Passwörter mit starken Algorithmen hashen (Argon2id)
+- Multi-Faktor-Authentifizierung anbieten
+- Ratelimiting für Login-Versuche
+
+```php
+// Passwort hashen
+public function hashPassword(string $password): string
+{
+ return password_hash($password, PASSWORD_ARGON2ID);
+}
+
+// Passwort verifizieren
+public function verifyPassword(string $password, string $hash): bool
+{
+ return password_verify($password, $hash);
+}
+```
+
+### 5. Sichere Datenspeicherung
+
+- Sensible Daten verschlüsseln
+- Separate Schlüssel für unterschiedliche Daten
+- Schlüsselrotation implementieren
+
+```php
+// Daten verschlüsseln
+public function encrypt(string $data, string $purpose): string
+{
+ $key = $this->getEncryptionKey($purpose);
+ $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
+
+ $cipher = sodium_crypto_secretbox(
+ $data,
+ $nonce,
+ $key
+ );
+
+ return base64_encode($nonce . $cipher);
+}
+
+// Daten entschlüsseln
+public function decrypt(string $encrypted, string $purpose): string
+{
+ $key = $this->getEncryptionKey($purpose);
+ $decoded = base64_decode($encrypted);
+
+ $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
+ $cipher = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
+
+ return sodium_crypto_secretbox_open(
+ $cipher,
+ $nonce,
+ $key
+ );
+}
+```
+
+## Sicherheitstests
+
+### 1. Automatisierte Sicherheitstests
+
+- Static Application Security Testing (SAST)
+- Dynamic Application Security Testing (DAST)
+- Dependency-Scanning für bekannte Schwachstellen
+
+### 2. Penetrationstests
+
+- Regelmäßige Sicherheitsaudits
+- Manuelle Penetrationstests
+- Bug-Bounty-Programme
+
+## Sicherheitskultur
+
+### 1. Entwicklerschulungen
+
+- Regelmäßige Sicherheitsschulungen
+- Code-Reviews mit Sicherheitsfokus
+- Sicherheits-Champions im Team
+
+### 2. Incident Response
+
+- Sicherheitsvorfälle dokumentieren
+- Prozess für Sicherheitsmeldungen
+- Notfallpläne für Sicherheitsvorfälle
+
+## Zusammenfassung
+
+Sicherheit ist ein kontinuierlicher Prozess, keine einmalige Aufgabe. Diese Richtlinien sollten regelmäßig überprüft und aktualisiert werden, um neuen Bedrohungen zu begegnen.
diff --git a/docs/standards/index.md b/docs/standards/index.md
new file mode 100644
index 00000000..43a8c588
--- /dev/null
+++ b/docs/standards/index.md
@@ -0,0 +1,50 @@
+# Coding-Standards und Richtlinien
+
+## Übersicht
+
+Diese Standards und Richtlinien definieren die grundlegenden Prinzipien für die Codierung und Sicherheit im Projekt. Sie gewährleisten Konsistenz, Wartbarkeit und Sicherheit des Codes.
+
+## Verfügbare Standards
+
+- [Coding Guidelines](/standards/CODING-GUIDELINES.md) - Allgemeine Coding-Standards für das Projekt
+- [Sicherheitsrichtlinien](/standards/SICHERHEITS-GUIDELINES.md) - Standards für sichere Softwareentwicklung
+
+## Coding Guidelines
+
+Die [Coding Guidelines](/standards/CODING-GUIDELINES.md) definieren die technischen Standards für die Codeentwicklung, einschließlich:
+
+- Klassenstruktur und -design
+- Property- und Methodendefinitionen
+- Nutzung moderner PHP-Features
+- Fehlerbehandlung
+- Dokumentation
+- Namenskonventionen
+
+## Sicherheitsrichtlinien
+
+Die [Sicherheitsrichtlinien](/standards/SICHERHEITS-GUIDELINES.md) bieten umfassende Anleitungen zur sicheren Softwareentwicklung:
+
+- Input-Validierung
+- SQL-Injection-Prävention
+- XSS-Schutz
+- CSRF-Schutz
+- Sichere Authentifizierung
+- Sichere Datenspeicherung
+- Session-Management
+
+## Anwendung der Standards
+
+Alle Teammitglieder sind verpflichtet, diese Standards in ihrer Arbeit anzuwenden. Sie dienen als Grundlage für Code-Reviews und Qualitätssicherung.
+
+## Abweichungen von Standards
+
+In Ausnahmefällen können Abweichungen von diesen Standards erforderlich sein. Solche Abweichungen sollten:
+
+1. Dokumentiert werden
+2. Begründet werden
+3. Im Team besprochen werden
+4. Auf ein Minimum beschränkt werden
+
+## Aktualisierungen
+
+Diese Standards werden regelmäßig überprüft und aktualisiert, um sicherzustellen, dass sie den neuesten Best Practices und Technologien entsprechen.
diff --git a/etc/docker/daemon.json b/etc/docker/daemon.json
new file mode 100644
index 00000000..a330256b
--- /dev/null
+++ b/etc/docker/daemon.json
@@ -0,0 +1,18 @@
+{
+ "log-driver": "json-file",
+ "log-opts": {
+ "max-size": "100m",
+ "max-file": "3"
+ },
+ "storage-driver": "overlay2",
+ "live-restore": true,
+ "userland-proxy": false,
+ "experimental": false,
+ "metrics-addr": "0.0.0.0:9323",
+ "default-address-pools": [
+ {
+ "base": "172.17.0.0/16",
+ "size": 24
+ }
+ ]
+}
diff --git a/examples/StaticPageExample.php b/examples/StaticPageExample.php
new file mode 100644
index 00000000..339ef5c1
--- /dev/null
+++ b/examples/StaticPageExample.php
@@ -0,0 +1,63 @@
+generateSvg('https://example.com');
+file_put_contents('qr_code.svg', $svgContent);
+echo "SVG-QR-Code erstellt: qr_code.svg\n";
+
+// Mit benutzerdefinierten Einstellungen
+$pngContent = $qrCodeService->generatePng(
+ 'Hallo Welt!',
+ ErrorCorrectionLevel::H,
+ moduleSize: 6,
+ margin: 8
+);
+file_put_contents('qr_code.png', $pngContent);
+echo "PNG-QR-Code erstellt: qr_code.png\n";
+
+// Matrix direkt verwenden
+$matrix = $qrCodeService->generateQrCode('Benutzerdefinierter QR-Code', ErrorCorrectionLevel::L);
+echo "QR-Code Größe: " . $matrix->getSize() . "x" . $matrix->getSize() . "\n";
+
+// Ausgabe der Matrix als Text (für Debugging)
+echo "ASCII-QR-Code:\n";
+echo $qrCodeService->generateAscii('https://example.com');
diff --git a/migrations/2024_01_01_create_meta_entries_table.sql b/migrations/2024_01_01_create_meta_entries_table.sql
new file mode 100644
index 00000000..e388c86d
--- /dev/null
+++ b/migrations/2024_01_01_create_meta_entries_table.sql
@@ -0,0 +1,65 @@
+-- Migration für Meta-Entries Tabelle
+-- Führe diese SQL-Datei aus um die Tabelle zu erstellen
+
+CREATE TABLE IF NOT EXISTS meta_entries (
+ id INT PRIMARY KEY AUTO_INCREMENT,
+
+ -- Route-basierte Meta-Daten
+ route_pattern VARCHAR(255) NULL,
+
+ -- Entity-basierte Meta-Daten
+ entity_type VARCHAR(100) NULL,
+ entity_id INT NULL,
+
+ -- Meta-Daten Felder
+ title VARCHAR(255) NULL,
+ description TEXT NULL,
+ keywords TEXT NULL,
+
+ -- Open Graph
+ og_title VARCHAR(255) NULL,
+ og_description TEXT NULL,
+ og_image VARCHAR(500) NULL,
+ og_type VARCHAR(50) NULL,
+
+ -- Twitter
+ twitter_card VARCHAR(50) NULL,
+ twitter_site VARCHAR(100) NULL,
+
+ -- Andere
+ canonical VARCHAR(500) NULL,
+ custom_meta JSON NULL,
+
+ -- System-Felder
+ priority INT DEFAULT 0,
+ active BOOLEAN DEFAULT TRUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ -- Indizes
+ INDEX idx_route_pattern (route_pattern),
+ INDEX idx_entity (entity_type, entity_id),
+ INDEX idx_active_priority (active, priority),
+
+ -- Unique Constraints
+ UNIQUE KEY unique_route_pattern (route_pattern),
+ UNIQUE KEY unique_entity (entity_type, entity_id),
+
+ -- Constraints
+ CONSTRAINT check_route_or_entity CHECK (
+ (route_pattern IS NOT NULL AND entity_type IS NULL AND entity_id IS NULL) OR
+ (route_pattern IS NULL AND entity_type IS NOT NULL AND entity_id IS NOT NULL)
+ )
+);
+
+-- Beispiel-Daten einfügen
+INSERT INTO meta_entries (route_pattern, title, description, og_title, og_type, priority, active) VALUES
+('/', 'Startseite - Meine Website', 'Willkommen auf meiner Website. Hier finden Sie alles über...', 'Startseite', 'website', 100, TRUE),
+('/blog', 'Blog - Meine Website', 'Alle Artikel und Neuigkeiten aus unserem Blog', 'Blog', 'website', 90, TRUE),
+('/blog/*', 'Blog: {title} - Meine Website', '{description}', '{title}', 'article', 80, TRUE),
+('/contact', 'Kontakt - Meine Website', 'Nehmen Sie Kontakt mit uns auf. Wir freuen uns auf Ihre Nachricht.', 'Kontakt', 'website', 70, TRUE);
+
+-- Template-Beispiele mit Platzhaltern
+INSERT INTO meta_entries (entity_type, entity_id, title, description, og_title, og_description, priority, active) VALUES
+('product', 1, '{product.name} - Jetzt kaufen | Mein Shop', '{product.name} für nur {product.price}€. {truncate:product.description}', '{product.name}', '{truncate:product.description}', 100, TRUE),
+('article', 1, '{article.title} | Mein Blog', '{truncate:article.content}', '{article.title}', '{truncate:article.content}', 100, TRUE);
diff --git a/package.json b/package.json
index a73ec020..2630d4f5 100644
--- a/package.json
+++ b/package.json
@@ -2,12 +2,14 @@
"devDependencies": {
"@types/jest": "^29.2.5",
"jest": "^29.3.1",
- "vite": "^6.3.5"
+ "vite": "^6.3.5",
+ "vite-plugin-pwa": "^1.0.0"
},
"scripts": {
"test": "jest",
"dev": "vite",
- "build": "vite build",
- "preview": "vite preview"
+ "build": "vite build && cp dist/sw.js public/sw.js && rm -rf public/assets && cp -R dist/assets public/ && cp dist/.vite/manifest.json public/.vite/",
+ "preview": "vite preview",
+ "deploy": "vite build && rm -rf public/assets && cp -R dist/assets public/ && cp dist/.vite/manifest.json public/.vite/"
}
}
diff --git a/public/assets/css-CKd28aW2.js b/public/assets/css-CKd28aW2.js
deleted file mode 100644
index 8b137891..00000000
--- a/public/assets/css-CKd28aW2.js
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/public/assets/css-CLfz37Tz.css b/public/assets/css-CLfz37Tz.css
deleted file mode 100644
index 2e6f9e35..00000000
--- a/public/assets/css-CLfz37Tz.css
+++ /dev/null
@@ -1 +0,0 @@
-p{color:red}html{background:#00f}
diff --git a/public/assets/css/styles.css b/public/assets/css/styles.css
deleted file mode 100644
index 5d1fbafc..00000000
--- a/public/assets/css/styles.css
+++ /dev/null
@@ -1,12 +0,0 @@
-* {
-
-}
-
-
-footer > nav {
- display: flex;
-}
-
-footer > nav > li {
- flex-direction: row;
-}
diff --git a/public/assets/images/hero.jpg b/public/assets/images/hero.jpg
new file mode 100644
index 00000000..ca4466d3
Binary files /dev/null and b/public/assets/images/hero.jpg differ
diff --git a/ansible/group_vars/web.yml b/public/assets/images/hero_also_small.jpg
similarity index 100%
rename from ansible/group_vars/web.yml
rename to public/assets/images/hero_also_small.jpg
diff --git a/ansible/roles/nginx/files/docker-entrypoint.sh b/public/assets/images/hero_small.jpg
similarity index 100%
rename from ansible/roles/nginx/files/docker-entrypoint.sh
rename to public/assets/images/hero_small.jpg
diff --git a/public/assets/js-DjO_n7Y6.js b/public/assets/js-DjO_n7Y6.js
deleted file mode 100644
index 29481374..00000000
--- a/public/assets/js-DjO_n7Y6.js
+++ /dev/null
@@ -1 +0,0 @@
-import"./css-CKd28aW2.js";
diff --git a/public/build-container.php b/public/build-container.php
new file mode 100644
index 00000000..2916fccd
--- /dev/null
+++ b/public/build-container.php
@@ -0,0 +1,90 @@
+bind(\App\Framework\Http\RequestFactory::class, \App\Framework\Http\RequestFactory::class);
+// Weitere Bindungen...
+
+// Liste der zu kompilierenden Services
+$services = [
+ \App\Framework\Core\Application::class,
+ \App\Framework\EventBus\DefaultEventBus::class,
+ \App\Framework\CommandBus\DefaultCommandBus::class,
+ \App\Framework\Router\HttpRouter::class,
+ \App\Framework\Http\RequestFactory::class,
+ // Weitere wichtige Services...
+];
+
+// Services aus Verzeichnissen automatisch erkennen
+$servicesFromDiscovery = discoverServicesFromDirectories([
+ __DIR__ . '/../src/Application',
+ __DIR__ . '/../src/Framework',
+]);
+$services = array_merge($services, $servicesFromDiscovery);
+
+// Container kompilieren
+$compiler = new ContainerCompiler();
+$compiler->compile(
+ $container,
+ $services,
+ __DIR__ . '/../cache/CompiledContainer.php'
+);
+
+echo "Container kompiliert!\n";
+
+// Hilfsfunktion zum Entdecken von Services
+function discoverServicesFromDirectories(array $directories): array
+{
+ $services = [];
+ foreach ($directories as $directory) {
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
+ foreach ($iterator as $file) {
+ if ($file->isFile() && $file->getExtension() === 'php') {
+ $className = getClassNameFromFile($file->getRealPath());
+ if ($className) {
+ $services[] = $className;
+ }
+ }
+ }
+ }
+ return $services;
+}
+
+function getClassNameFromFile(string $file): ?string
+{
+ $content = file_get_contents($file);
+ $tokens = token_get_all($content);
+ $namespace = '';
+ $class = '';
+ $namespaceFound = false;
+ $classFound = false;
+
+ foreach ($tokens as $token) {
+ if (is_array($token)) {
+ if ($token[0] === T_NAMESPACE) {
+ $namespaceFound = true;
+ } elseif ($namespaceFound && $token[0] === T_STRING) {
+ $namespace .= $token[1];
+ } elseif ($namespaceFound && $token[0] === T_NS_SEPARATOR) {
+ $namespace .= '\\';
+ } elseif ($token[0] === T_CLASS) {
+ $classFound = true;
+ } elseif ($classFound && $token[0] === T_STRING) {
+ $class = $token[1];
+ break;
+ }
+ } elseif ($namespaceFound && $token === ';') {
+ $namespaceFound = false;
+ }
+ }
+
+ return $namespace && $class ? $namespace . '\\' . $class : null;
+}
diff --git a/public/chat.html b/public/chat.html
new file mode 100644
index 00000000..ac1d6f44
--- /dev/null
+++ b/public/chat.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ WebSocket Chat Demo
+
+
+
+ WebSocket Chat Demo
+
+
+ Nicht verbunden
+
+
+
+
+
Willkommen im Chat! Verbindung wird hergestellt...
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/css/admin.css b/public/css/admin.css
new file mode 100644
index 00000000..bb68dd5d
--- /dev/null
+++ b/public/css/admin.css
@@ -0,0 +1,380 @@
+/* Admin Dashboard Styles */
+:root {
+ --primary-color: #4a6cf7;
+ --secondary-color: #0e2e50;
+ --success-color: #28a745;
+ --danger-color: #dc3545;
+ --warning-color: #ffc107;
+ --info-color: #17a2b8;
+ --light-color: #f8f9fa;
+ --dark-color: #343a40;
+ --border-color: #e5e7eb;
+ --background-color: #f9fafb;
+ --header-height: 64px;
+ --footer-height: 60px;
+ --sidebar-width: 250px;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body.admin-page {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ background-color: var(--background-color);
+ color: var(--dark-color);
+ line-height: 1.5;
+}
+
+.admin-header {
+ background-color: var(--secondary-color);
+ color: white;
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.admin-header h1 {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.admin-nav {
+ background-color: white;
+ display: flex;
+ padding: 0 1rem;
+ border-bottom: 1px solid var(--border-color);
+ overflow-x: auto;
+}
+
+.admin-nav a {
+ padding: 1rem 1.25rem;
+ color: var(--dark-color);
+ text-decoration: none;
+ border-bottom: 2px solid transparent;
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.admin-nav a:hover {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+.admin-nav a.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+ font-weight: 600;
+}
+
+.admin-content {
+ padding: 2rem;
+ max-width: 1600px;
+ margin: 0 auto;
+ min-height: calc(100vh - var(--header-height) - var(--footer-height) - 48px);
+}
+
+.admin-footer {
+ background-color: white;
+ text-align: center;
+ padding: 1rem;
+ border-top: 1px solid var(--border-color);
+ color: #666;
+ font-size: 0.875rem;
+}
+
+/* Dashboard Stats */
+.dashboard-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.stat-box {
+ background-color: white;
+ border-radius: 8px;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ border: 1px solid var(--border-color);
+}
+
+.stat-box h3 {
+ font-size: 0.875rem;
+ color: #6b7280;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+}
+
+.stat-value {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--dark-color);
+}
+
+.status-connected {
+ color: var(--success-color);
+}
+
+/* Extensions List */
+.admin-section {
+ background-color: white;
+ border-radius: 8px;
+ padding: 1.5rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ margin-bottom: 2rem;
+ border: 1px solid var(--border-color);
+}
+
+.admin-section h2 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ color: var(--secondary-color);
+ font-weight: 600;
+}
+
+.extensions-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.extension-badge {
+ background-color: #e9ecef;
+ padding: 0.25rem 0.75rem;
+ border-radius: 50px;
+ font-size: 0.875rem;
+ color: #495057;
+}
+
+/* Tables */
+.admin-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 2rem;
+}
+
+.admin-table th,
+.admin-table td {
+ padding: 0.75rem 1rem;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.admin-table th {
+ background-color: #f8f9fa;
+ font-weight: 600;
+ color: #495057;
+}
+
+.admin-table tbody tr:hover {
+ background-color: #f8f9fa;
+}
+
+/* Search and Filters */
+.admin-tools {
+ margin-bottom: 1.5rem;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.search-input {
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 0.875rem;
+ width: 300px;
+ max-width: 100%;
+}
+
+.services-count {
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+/* Service List */
+.service-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1rem;
+}
+
+.service-item {
+ background-color: white;
+ border-radius: 6px;
+ padding: 1rem;
+ border: 1px solid var(--border-color);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.service-name {
+ font-weight: 500;
+ margin-bottom: 0.5rem;
+ word-break: break-word;
+}
+
+.service-category {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.category-badge {
+ background-color: var(--primary-color);
+ color: white;
+ padding: 0.1rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+.subcategory-badge {
+ background-color: #e9ecef;
+ color: #495057;
+ padding: 0.1rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+/* Filter Tags */
+.filter-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.filter-tag {
+ background-color: #e9ecef;
+ border: none;
+ padding: 0.25rem 0.75rem;
+ border-radius: 50px;
+ font-size: 0.875rem;
+ color: #495057;
+ cursor: pointer;
+}
+
+.filter-tag:hover {
+ background-color: #dee2e6;
+}
+
+.filter-tag.active {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+/* Progress Bar */
+.progress-bar {
+ width: 100%;
+ height: 8px;
+ background-color: #e9ecef;
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 0.25rem;
+}
+
+.progress {
+ height: 100%;
+ background-color: var(--primary-color);
+ border-radius: 4px;
+}
+
+.progress-value {
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+/* File List */
+.file-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+}
+
+.file-item {
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 0.875rem;
+ font-family: monospace;
+}
+
+.file-item:last-child {
+ border-bottom: none;
+}
+
+/* Key List */
+.key-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+}
+
+.key-item {
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid var(--border-color);
+ font-size: 0.875rem;
+ font-family: monospace;
+}
+
+.key-item:last-child {
+ border-bottom: none;
+}
+
+.empty-message {
+ padding: 2rem;
+ text-align: center;
+ color: #6b7280;
+}
+
+/* Error Message */
+.error-message {
+ background-color: #fff5f5;
+ border: 1px solid #fed7d7;
+ padding: 2rem;
+ text-align: center;
+ border-radius: 8px;
+ color: var(--danger-color);
+}
+
+.error-message h2 {
+ margin-bottom: 1rem;
+ color: var(--danger-color);
+}
+
+/* Buttons */
+.btn {
+ padding: 0.5rem 1rem;
+ background-color: var(--primary-color);
+ color: white;
+ border-radius: 4px;
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 0.875rem;
+ border: none;
+ cursor: pointer;
+}
+
+.btn:hover {
+ background-color: #3a5ce5;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .dashboard-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-content {
+ padding: 1rem;
+ }
+
+ .service-list {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/public/css/chat.css b/public/css/chat.css
new file mode 100644
index 00000000..15b48289
--- /dev/null
+++ b/public/css/chat.css
@@ -0,0 +1,87 @@
+body {
+ font-family: Arial, sans-serif;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.chat-container {
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 15px;
+ background: #f9f9f9;
+}
+
+.message {
+ margin-bottom: 10px;
+ padding: 8px 12px;
+ border-radius: 18px;
+ max-width: 80%;
+ word-wrap: break-word;
+}
+
+.message.system {
+ background-color: #f0f0f0;
+ color: #666;
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+ font-style: italic;
+}
+
+.message.user {
+ background-color: #e3f2fd;
+ margin-right: auto;
+}
+
+.message.self {
+ background-color: #dcf8c6;
+ margin-left: auto;
+}
+
+.chat-input {
+ display: flex;
+ padding: 10px;
+ background: #fff;
+ border-top: 1px solid #eee;
+}
+
+#messageInput {
+ flex: 1;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 20px;
+ margin-right: 10px;
+}
+
+button {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 20px;
+ cursor: pointer;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+.connection-status {
+ text-align: center;
+ margin-bottom: 10px;
+ font-weight: bold;
+}
+
+.status-connecting { color: #f39c12; }
+.status-connected { color: #2ecc71; }
+.status-disconnected { color: #e74c3c; }
diff --git a/public/index.php b/public/index.php
index f30ff099..82c2360f 100644
--- a/public/index.php
+++ b/public/index.php
@@ -2,83 +2,40 @@
declare(strict_types=1);
-use App\Framework\Core\Discovery;
-use App\Framework\Core\DynamicRoute;
-use App\Framework\Core\PhpObjectExporter;
-use App\Framework\Core\RouteCache;
-use App\Framework\Core\RouteMapper;
-use App\Framework\Core\StaticRoute;
-use App\Framework\ErrorHandling\ErrorHandler;
-use App\Framework\Http\HttpMethod;
-use App\Framework\Router\RouteCollection;
+use App\Framework\Performance\PerformanceMeter;
+use App\Framework\Core\AppBootstrapper;
require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../src/Framework/Debug/helpers.php';
// Fehleranzeige für die Entwicklung aktivieren
-error_reporting(E_ALL);
ini_set('display_errors', 1);
+ini_set('display_startup_errors', 1);
+error_reporting(E_ALL);
-$rfl = new ReflectionClass(Discovery::class);;
-$ghost = $rfl->newLazyGhost(function (Discovery $object) {
- // Initialize object in-place
- $object->__construct();
+register_shutdown_function(function() {
+ $error = error_get_last();
+ if ($error !== null) {
+ echo "SHUTDOWN ERROR: " . print_r($error, true);
+ }
});
-/*$clientrequest = new \App\Framework\HttpClient\ClientRequest(HttpMethod::GET, 'https://jsonplaceholder.typicode.com/posts');
+$meter = new PerformanceMeter();
-$client = new \App\Framework\HttpClient\CurlHttpClient();
+// Konfiguration
+$config = [
+ 'debug' => true,
+ 'async_discovery' => true,
+ // weitere Konfigurationsoptionen...
+];
-var_dump($client->send($clientrequest));*/
+// Anwendung initialisieren und ausführen
+$basePath = dirname(__DIR__);
+$bootstrapper = new AppBootstrapper($basePath, $meter, $config);
+$app = $bootstrapper->bootstrapWeb();
-$emitter = new \App\Framework\Http\ResponseEmitter();
-ErrorHandler::register($emitter);
-
-#echo dirname(__DIR__) . '/cache/routes.cache.php';
-
-$discovery = new Discovery(new \App\Framework\Core\RouteMapper());
-
-$results = $discovery->discover(__DIR__ . '/../src/Application/');
-
-
-$rc = new \App\Framework\Core\RouteCompiler();
-
-$routes = $rc->compile($results[\App\Framework\Attributes\Route::class]);
-
-$cacheFile = dirname(__DIR__) . '/cache/routes.cache.php';
-
-$routeCache = new \App\Framework\Core\RouteCache($cacheFile);
-
-$routeCache->save($routes);
-
-$request = new \App\Framework\Http\HttpRequest(
- method: \App\Framework\Http\HttpMethod::tryFrom($_SERVER['REQUEST_METHOD'] ?? 'GET'),
- path:parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH),
-);
-
-#var_dump("", $routeCache->load());
-
-$router = new \App\Framework\Router\HttpRouter(new RouteCollection($routeCache->load()));
-
-$match = $router->match($request->method->value, $request->path);
-
-
-$dispatcher = new \App\Framework\Router\RouteDispatcher();
-$return = $dispatcher->dispatch($match);
-
-$responder = new \App\Framework\Router\RouteResponder();
-$response = $responder->respond($return);
-
-$emitter = new \App\Framework\Http\ResponseEmitter();
-$emitter->emit($response);
-
-/*$redis = new Predis\Client([
- 'scheme' => 'tcp',
- 'host' => 'redis', // Service-Name aus docker-compose
- 'port' => 6379,
-]);
-
-$redis->set('hello', 'world');
-echo $redis->get('hello'); // Gibt: world aus*/
+// Anwendung ausführen
+$app->run();
exit;
diff --git a/public/js/chat.js b/public/js/chat.js
new file mode 100644
index 00000000..ea997ccd
--- /dev/null
+++ b/public/js/chat.js
@@ -0,0 +1,116 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const statusElement = document.getElementById('status');
+ const chatMessages = document.getElementById('chatMessages');
+ const messageInput = document.getElementById('messageInput');
+ const sendButton = document.getElementById('sendButton');
+
+ let socket;
+ let userId = 'user_' + Math.floor(Math.random() * 10000);
+
+ function connect() {
+ updateStatus('connecting', 'Verbindung wird hergestellt...');
+
+ // WebSocket-Verbindung herstellen
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const host = window.location.host;
+ socket = new WebSocket(`${protocol}//${host}/chat/websocket`);
+
+ socket.onopen = function() {
+ updateStatus('connected', 'Verbunden');
+ enableChat();
+ };
+
+ socket.onmessage = function(event) {
+ const message = JSON.parse(event.data);
+ handleMessage(message);
+ };
+
+ socket.onclose = function() {
+ updateStatus('disconnected', 'Verbindung getrennt');
+ disableChat();
+
+ // Nach 5 Sekunden erneut verbinden
+ setTimeout(connect, 5000);
+ };
+
+ socket.onerror = function(error) {
+ console.error('WebSocket Error:', error);
+ addMessage('system', 'Fehler bei der Verbindung.');
+ };
+ }
+
+ function updateStatus(state, text) {
+ statusElement.className = 'connection-status status-' + state;
+ statusElement.textContent = text;
+ }
+
+ function enableChat() {
+ messageInput.disabled = false;
+ sendButton.disabled = false;
+ messageInput.focus();
+ }
+
+ function disableChat() {
+ messageInput.disabled = true;
+ sendButton.disabled = true;
+ }
+
+ function handleMessage(message) {
+ switch(message.type) {
+ case 'system':
+ addMessage('system', message.message);
+ break;
+
+ case 'user_joined':
+ case 'user_left':
+ addMessage('system', message.message);
+ break;
+
+ case 'chat_message':
+ const isOwnMessage = message.user_id === userId;
+ addMessage(isOwnMessage ? 'self' : 'user', message.message);
+ break;
+
+ case 'error':
+ addMessage('system', 'Fehler: ' + message.error);
+ break;
+ }
+ }
+
+ function addMessage(type, text) {
+ const messageElement = document.createElement('div');
+ messageElement.className = 'message ' + type;
+ messageElement.textContent = text;
+
+ chatMessages.appendChild(messageElement);
+
+ // Automatisch nach unten scrollen
+ chatMessages.scrollTop = chatMessages.scrollHeight;
+ }
+
+ function sendMessage() {
+ const text = messageInput.value.trim();
+ if (!text || !socket || socket.readyState !== WebSocket.OPEN) return;
+
+ const message = {
+ type: 'chat_message',
+ message: text
+ };
+
+ socket.send(JSON.stringify(message));
+ messageInput.value = '';
+ messageInput.focus();
+ }
+
+ // Event-Listener
+ sendButton.addEventListener('click', sendMessage);
+
+ messageInput.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ sendMessage();
+ }
+ });
+
+ // Initialisierung
+ connect();
+});
diff --git a/public/offline.html b/public/offline.html
new file mode 100644
index 00000000..6e3468fd
--- /dev/null
+++ b/public/offline.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Offline
+
+
+
+📴 Du bist offline
+Diese Seite wurde aus dem Cache geladen.
+
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 00000000..5152ab98
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow:
+Sitemap: https://michaelschiemer.de/sitemap.xml
diff --git a/public/test.php b/public/test.php
new file mode 100644
index 00000000..63eb0215
--- /dev/null
+++ b/public/test.php
@@ -0,0 +1,5 @@
+
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+php:
+ version: 8.4 #(Applied in CI/CD pipeline)
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
+linter: jetbrains/qodana-php:2025.2
diff --git a/resources/css/STRUCTURE.md b/resources/css/STRUCTURE.md
new file mode 100644
index 00000000..fde5d740
--- /dev/null
+++ b/resources/css/STRUCTURE.md
@@ -0,0 +1,42 @@
+# CSS-Framework Struktur
+
+## Layer-Hierarchie
+1. reset
+2. base
+3. layout
+4. components
+5. utilities
+6. overrides
+
+## Benennungskonventionen
+- Dateinamen: kebab-case
+- Klassen: BEM oder Utility-First? (optional notieren)
+
+
+css/
+├── styles.css # Haupt-Importdatei
+├── settings/
+│ ├── colors.css # Farbdefinitionen
+│ ├── typography.css # Schriftgrößen, Fonts
+│ ├── spacing.css # Abstände (margin, padding)
+│ └── variables.css # Dauer, Easing, Radius, Z-Index etc.
+├── base/
+│ ├── reset.css # Reset/Normalize
+│ ├── global.css # globale Stile für html, body, etc.
+│ └── typography.css # h1, p, etc.
+├── components/
+│ ├── header.css
+│ ├── nav.css
+│ ├── footer.css
+│ └── buttons.css
+├── layout/
+│ ├── container.css # .page-container, max-widths, etc.
+│ └── grid.css # evtl. eigenes Grid-System
+├── utilities/
+│ ├── animations.css # .fade-in, .shake, usw.
+│ ├── helpers.css # .skip-link, .hidden, .visually-hidden
+│ └── scroll.css # scroll-behavior, scrollbar-style
+├── forms/
+│ └── inputs.css
+└── themes/
+ └── dark.css # Farbanpassungen für Dark-Mode (optional)
diff --git a/resources/css/base/focus.css b/resources/css/base/focus.css
new file mode 100644
index 00000000..be2c765a
--- /dev/null
+++ b/resources/css/base/focus.css
@@ -0,0 +1,31 @@
+:focus-visible {
+ --outline-size: max(2px, 0.1em);
+
+ outline:
+ var(--outline-width, var(--outline-size))
+ var(--outline-style, solid)
+ var(--outline-color, currentColor);
+
+ outline-offset: var(--outline-offset, var(--outline-size));
+
+ border-radius: 0.25em;
+}
+
+:where(:not(:active):focus-visible) {
+ outline-offset: 5px;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ :where(:focus-visible) {
+ transition: outline-offset .2s ease;
+ }
+ :where(:not(:active):focus-visible) {
+ transition-duration: .25s;
+ }
+}
+
+/* Scroll margin allowance below focused elements
+ to ensure they are clearly in view */
+:focus {
+ scroll-padding-block-end: 8vh;
+}
diff --git a/resources/css/base/global.css b/resources/css/base/global.css
new file mode 100644
index 00000000..af019f53
--- /dev/null
+++ b/resources/css/base/global.css
@@ -0,0 +1,147 @@
+:root {
+ --container-width: min(90vw, 2000px);
+ --content-padding: clamp(1rem, 4vw, 3rem);
+ --header-height: 80px;
+
+}
+
+html {
+ background-color: var(--bg);
+}
+
+body {
+ color: var(--text);
+ min-height: 100vh;
+
+ display: flex;
+ flex-direction: column;
+
+ --outline-color: var(--accent);
+}
+
+body>header {
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(10px);
+ border-block-end: 1px solid var(--accent);
+
+ position: sticky;
+ top: 0;
+ z-index: 100;
+
+
+ height: var(--header-height);
+ display: flex;
+ align-items: center;
+
+ &>div{
+ width: var(--container-width);
+ margin: 0 auto;
+ padding: 0 var(--content-padding);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
+
+body>main {
+ flex: 1;
+ padding-top: 2rem;
+}
+
+body>footer {
+ margin-top: auto;
+}
+
+main>section {
+ width: var(--container-width);
+ margin: 0 auto;
+ padding: 4rem var(--content-padding);
+}
+
+section>.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+ margin: 0 auto;
+ /*margin-top: 3rem;*/
+}
+
+
+
+@media(prefers-reduced-motion: no-preference) {
+ html {
+ scroll-behavior: smooth;
+ }
+}
+
+
+/* somehow does not work! */
+@media(prefers-reduced-motion: no-preference) {
+ :has(:target) {
+ scroll-behavior: smooth;
+ }
+}
+
+a {
+ --selection-text-decoration: underline;
+ --link-color: var(--accent);
+
+ --_color: var(--link-color, rgba(255, 255, 255, 0.5));
+ --_underline-color: var(--underline-color, currentColor);
+ --_thickness: 2px;
+ --_offset: 0.05em;
+ --_hover-color: oklch(from var(--link-color) 80% c h);
+
+ color: var(--_color);
+
+ text-decoration-line: var(--selection-text-decoration);
+ text-decoration-color: var(--_underline-color);
+ text-decoration-thickness: var(--_thickness);
+ text-underline-offset: var(--_offset);
+
+ padding: max(0.25rem, 0.1em) 0;
+
+ display: inline-block;
+
+ @media (prefers-reduced-motion: no-preference) {
+ transition:
+ color 0.15s ease-in-out,
+ text-decoration 0.15s,
+ transform 0.1s ease-in-out;
+ }
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+ @media (forced-colors: active) {
+ forced-color-adjust: none;
+ color: LinkText;
+ text-decoration-color: LinkText;
+ }
+
+ &:hover,
+ &:focus-visible {
+ --_color: var(--_hover-color);
+
+ --_underline-color: rgba(255, 255, 255, 0.5);
+
+ /*text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+
+ transform: translateY(-1px);*/
+ }
+
+ &:active {
+ --_color: oklch(from var(--link-color) 75% c h);
+
+ /*transform: translateY(1px);*/
+ }
+}
+
+/* Inline Link */
+p > a {
+ text-decoration: underline;
+ border:none;
+
+ /*&::after {
+ content: ' ↗';
+ }*/
+}
diff --git a/resources/css/base/index.css b/resources/css/base/index.css
new file mode 100644
index 00000000..2a9e3597
--- /dev/null
+++ b/resources/css/base/index.css
@@ -0,0 +1,4 @@
+@import "focus.css";
+@import "global.css";
+@import "media.css";
+@import "typography.css";
diff --git a/resources/css/base/media.css b/resources/css/base/media.css
new file mode 100644
index 00000000..55441fc1
--- /dev/null
+++ b/resources/css/base/media.css
@@ -0,0 +1,5 @@
+:where(img, video) {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
diff --git a/resources/css/base/reset.css b/resources/css/base/reset.css
new file mode 100644
index 00000000..57942c27
--- /dev/null
+++ b/resources/css/base/reset.css
@@ -0,0 +1,34 @@
+/* alle Elemente inklusive Pseudoelemente mit border-box rechnen lassen */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+:where(html, body, h1, h2, h3, h4, h5, h6, p, blockquote, figure, dl, dd, ul, ol) {
+ margin: 0;
+ padding: 0;
+}
+
+:where(table) {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+:where(article, aside, footer, header, nav, section, main) {
+ display: block;
+}
+
+[popover] {
+ /* CSSWG Issue #10258 */
+ inset: auto;
+}
+
+/* @link: https://moderncss.dev/12-modern-css-one-line-upgrades/#scroll-margin-topbottom */
+:where([id]) {
+ scroll-margin-block-start: 2rem;
+}
+
+/* Vererbung für SVG-Icons */
+svg {
+ fill: currentColor;
+ stroke: none;
+}
diff --git a/resources/css/base/typography.css b/resources/css/base/typography.css
new file mode 100644
index 00000000..8f30bf1e
--- /dev/null
+++ b/resources/css/base/typography.css
@@ -0,0 +1,108 @@
+html {
+ font-size: 100%;
+
+ font-family: 'Roboto', sans-serif;
+
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ line-height: 1.5;
+ font-size: clamp(1.125rem, 4cqi, 1.5rem);
+}
+
+section {
+ container-type: inline-size;
+}
+
+/* trims of empty pixels above and below fonts */
+:is(h1, h2, h3, h4, h5, h6, p, button) {
+ text-box: trim-both cap alphabetic;
+}
+
+p,
+li,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ overflow-wrap: break-word;
+ padding-block: 1.5rem;
+
+ hyphens: auto;
+}
+
+
+h1, h2, h3, h4, h5, h6 {
+ text-wrap: balance;
+}
+
+h1 {
+ font-size: clamp(2rem, 6cqi, 6rem);
+ font-weight: 700;
+
+ line-height: 1.125;
+
+ max-inline-size: 25ch;
+
+ text-transform: uppercase;
+}
+
+h2 {
+ text-transform: uppercase;
+
+ font-size: 3.5rem;
+ font-weight: 700;
+ line-height: 1.65;
+ max-inline-size: 30ch;
+}
+
+h3 {
+ font-size: 2rem;
+ --selection-bg-color: rgba(0, 0, 255, 0.3);
+ --selection-text-color: #d8cc48;
+}
+
+h4 {
+ font-size: clamp(3rem, 4vw + 0.5rem, 4rem);
+ font-weight: 700;
+
+ max-inline-size: 25ch;
+
+}
+
+h5 {
+ font-size: 1.75rem;
+
+}
+
+h6 {
+ font-size: clamp(2rem, 3vw + 0.5rem, 3rem);
+}
+
+
+p {
+ text-wrap: pretty;
+
+ line-height: 1.2;
+
+ font-weight: 500;
+ letter-spacing: -0.03em;
+
+ /* max 75ch */
+ max-inline-size: 65ch;
+}
+
+blockquote {
+ text-transform: uppercase;
+
+ &::before {
+ content: '\201C';
+ }
+
+ &::after {
+ content: '\201D';
+ }
+}
diff --git a/resources/css/components/buttons.css b/resources/css/components/buttons.css
new file mode 100644
index 00000000..515e9d17
--- /dev/null
+++ b/resources/css/components/buttons.css
@@ -0,0 +1,99 @@
+:where(button, input):where(:not(:active)):focus-visible {
+ outline-offset: 5px;
+}
+
+/* All buttons */
+:where(
+ button,
+ input[type="button"],
+ input[type="submit"],
+ input[type="reset"],
+ input[type="file"]
+),
+/* ::file-selector-button does not work inside of :where() */
+:where(input[type="file"])::file-selector-button {
+ --_color: var(--color, var(--accent));
+
+ background-color: var(--_color);
+ color: black;
+
+ border: 2px solid var(--_color);
+
+ font: inherit;
+ letter-spacing: inherit;
+ line-height: 1.5;
+
+ border-radius: 1rem;
+ padding: 0.5em;
+
+ font-weight: 700;
+
+ object-fit: contain;
+
+ /*display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ gap: 1ch;*/
+
+ cursor: pointer;
+ transition: all 0.2s ease-out;
+
+ user-select: none;
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-touch-callout: none;
+
+ &:is(:hover, :focus-visible) {
+ --_hoverColor: oklch(from var(--color, var(--accent)) 40% c h);
+ --_color: var(--hoverColor, var(--_hoverColor));
+ }
+
+ &.secondary {
+ --color: lightgreen;
+ /*--hoverColor: lawngreen;*/
+ }
+
+ &.outline {
+ background: transparent;
+ color: var(--_color);
+ }
+}
+
+/* on hover but not active */
+:where(
+ button,
+ input[type="button"],
+ input[type="submit"],
+ input[type="reset"]):where(:not(:active):hover
+) {
+
+}
+
+:where([type="reset"]) {
+ color: red;
+}
+
+:where([type="reset"]:focus-visible) {
+ outline-color: currentColor;
+}
+
+
+:where(button,
+input[type="button"],
+input[type="submit"],
+input[type="reset"]
+)[disabled] {
+
+ cursor: not-allowed;
+}
+
+:where(input[type="file"]) {
+ inline-size: 100%;
+ max-inline-size: max-content;
+}
+
+:where(input[type="button"]),
+:where(input[type="file"])::file-selector-button {
+ appearance: none;
+}
diff --git a/resources/css/components/card.css b/resources/css/components/card.css
new file mode 100644
index 00000000..4b9ea535
--- /dev/null
+++ b/resources/css/components/card.css
@@ -0,0 +1,202 @@
+.card {
+ --_bg: var(--card-bg, var(--bg-alt));
+ --_border: var(--card-border, var(--border));
+ --_accent: var(--card-accent, var(--accent));
+
+ background-color: var(--_bg);
+ border: 1px solid var(--_border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+ transition: all 0.2s ease;
+ container-type: inline-size;
+}
+
+.card:hover {
+ transform: translateY(-2px);
+ border-color: var(--_accent);
+}
+
+/* Strukturelle Elemente mit Selektoren */
+.card > header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--space-md);
+ margin-bottom: var(--space-md);
+}
+
+.card > main {
+ flex: 1;
+ margin-bottom: var(--space-md);
+}
+
+.card > footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: var(--space-sm);
+ border-top: 1px solid var(--_border);
+}
+
+/* Typografie mit Selektoren */
+.card h1,
+.card h2,
+.card h3 {
+ margin: 0;
+ color: var(--_accent);
+}
+
+.card h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.card p {
+ margin: 0 0 var(--space-sm) 0;
+ color: var(--text);
+}
+
+.card small {
+ color: oklch(70% 0.01 var(--h-bg));
+}
+
+/* Button-Gruppen mit Selektoren */
+.card footer > div {
+ display: flex;
+ gap: var(--space-sm);
+}
+
+.card button {
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--_accent);
+ background: transparent;
+ color: var(--_accent);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.card button:first-child {
+ background-color: var(--_accent);
+ color: var(--bg);
+}
+
+.card button:hover {
+ background-color: var(--_accent);
+ color: var(--bg);
+}
+
+
+/*
+ ======================
+ VARIANTEN MIT KLASSEN
+ ======================
+ */
+
+/* Status-Varianten */
+.card--success {
+ --card-bg: var(--success-subtle);
+ --card-border: var(--success-border);
+ --card-accent: var(--success);
+}
+
+.card--error {
+ --card-bg: var(--error-subtle);
+ --card-border: var(--error-border);
+ --card-accent: var(--error);
+}
+
+/* Größen-Varianten */
+.card--compact {
+ padding: var(--space-md);
+}
+
+.card--compact > * {
+ margin-bottom: var(--space-sm);
+}
+
+.card--spacious {
+ padding: calc(var(--space-lg) * 1.5);
+}
+
+/* Layout-Varianten */
+.card--horizontal {
+ display: flex;
+ align-items: center;
+}
+
+.card--horizontal > header,
+.card--horizontal > main {
+ margin-bottom: 0;
+ margin-right: var(--space-lg);
+}
+
+/* Media-Variante */
+.card--media img {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ border-radius: var(--radius-md);
+ margin-bottom: var(--space-md);
+}
+
+
+/*
+======================
+RESPONSIVE MIT CONTAINER QUERIES
+======================
+*/
+
+/* Automatische Anpassung basierend auf Card-Größe */
+@container (max-width: 300px) {
+ .card > header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .card > footer {
+ flex-direction: column;
+ gap: var(--space-sm);
+ align-items: flex-start;
+ }
+}
+
+/* Demo-Styles */
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-lg);
+ margin-top: var(--space-lg);
+}
+
+.demo-grid--wide {
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+}
+
+h1 {
+ /*text-align: center;*/
+ background: linear-gradient(45deg, var(--accent), var(--success));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.section {
+ margin: 3rem 0;
+}
+
+.section h2 {
+ color: var(--accent);
+ margin-bottom: var(--space-lg);
+}
+
+/* Badge mit semantischem Selector */
+.card [role="status"] {
+ padding: 0.25rem 0.5rem;
+ border-radius: var(--radius-md);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ background-color: var(--_accent);
+ color: var(--bg);
+}
diff --git a/resources/css/components/footer.css b/resources/css/components/footer.css
new file mode 100644
index 00000000..8948b64a
--- /dev/null
+++ b/resources/css/components/footer.css
@@ -0,0 +1,36 @@
+footer {
+ text-align: center;
+ padding-block: 1.5rem;
+
+ a {
+ --link-color: var(--muted);
+ font-size: 1.25rem;
+ font-weight: 400;
+ font-family: 'Roboto', sans-serif;
+ }
+}
+
+footer > p {
+ display: block;
+ margin-inline: auto;
+
+ letter-spacing: -0.01em;
+ font-size: 1rem;
+ color: var(--muted);
+}
+
+.footer-nav {
+ display: flex;
+ justify-content: center;
+ gap: 2rem; /* Abstand zwischen Links */
+ list-style: none;
+ padding: 0;
+ margin: 0.5rem 0 0; /* leichter Abstand nach dem Text */
+}
+
+@media (max-width: 500px) {
+ .footer-nav {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+}
diff --git a/resources/css/components/header.css b/resources/css/components/header.css
new file mode 100644
index 00000000..e66bb2d5
--- /dev/null
+++ b/resources/css/components/header.css
@@ -0,0 +1,50 @@
+main > .header {
+ position: fixed;
+ width: 100%;
+ z-index: 4000;
+}
+
+main > .header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 3rem; /* Optional: Abstand innen */
+
+ padding-inline: clamp(1.5rem, 4vw + 1rem, 3rem);
+
+ user-select: none;
+
+ a {
+ text-decoration: none;
+ color: #eee;
+ font-size: 2rem;
+ font-weight: 700;
+ font-family: 'Roboto', sans-serif;
+
+ transition: all 0.2s ease-in;
+
+ &:hover {
+ color: #fff;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+ }
+
+ }
+
+ button {
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ appearance: none;
+ font-size: 2rem;
+ font-weight: 700;
+
+ z-index: 9999;
+
+
+ &:hover {
+ color: #fff;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+ }
+ }
+}
diff --git a/resources/css/components/nav.css b/resources/css/components/nav.css
new file mode 100644
index 00000000..bcdd34c2
--- /dev/null
+++ b/resources/css/components/nav.css
@@ -0,0 +1,3 @@
+:root{
+
+}
diff --git a/resources/css/components/sidebar.css b/resources/css/components/sidebar.css
new file mode 100644
index 00000000..c9f42f11
--- /dev/null
+++ b/resources/css/components/sidebar.css
@@ -0,0 +1,119 @@
+body:has(aside.show) {
+ overflow: hidden;
+ scrollbar-gutter: stable;
+}
+
+aside {
+ --sidebar-width: min(50ch, 100vw);
+
+ position: absolute;
+ z-index: 3000;
+
+ visibility: hidden;
+
+ background-color: #2c1c59;
+ color: #fff;
+ padding: 3rem;
+ border: none;
+
+ /*position: fixed;*/
+ /*position: sticky;*/
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: calc(100vw - var(--sidebar-width));
+ width: var(--sidebar-width);
+ height: 100vh;
+
+ transition: opacity 0.25s, translate 0.25s, overlay 0.25s allow-discrete, display 0.25s allow-discrete;
+
+ opacity: 0;
+ translate: 100% 0;
+
+ &.show {
+ visibility: visible;
+
+ opacity: 1;
+ translate: 0 0;
+ }
+
+ button {
+ place-content: end;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ appearance: none;
+ font-size: 2rem;
+ font-weight: 700;
+
+ margin-inline-end: 0.5rem;
+ float: right;
+
+
+ &:hover {
+ color: #fff;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
+ }
+ }
+
+}
+
+/* Backdrop */
+.backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ z-index: 1000;
+
+ /*cursor: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/9632/heart.svg), auto;*/
+}
+:is(aside.show) .backdrop {
+ opacity: 1;
+}
+body:has(aside.show) .backdrop {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+aside > nav > ul {
+
+ padding-block-start: 10rem;
+
+ display: flex;
+ flex-direction: column;
+
+ list-style: none;
+ gap: 3rem;
+
+ a {
+ text-decoration: none;
+ font-weight: 700;
+ text-transform: uppercase;
+ font-size: 1rem;
+ /*font-size: 1.5rem;*/
+ }
+
+ @media (hover) and (prefers-reduced-motion: no-preference) {
+ & > li {
+ transition: opacity .3s ease;
+ }
+
+ &:has(:hover) > li:not(:hover) {
+ opacity: .85;
+ }
+
+ /* for keyboard support */
+ &:is(:hover, :focus-within) > li:not(:hover, :focus-within) {
+ opacity: .85;
+ }
+ }
+}
+
+.backdrop:has( ~ #sidebar a:hover, ~ #sidebar a:focus)/*, .backdrop:has( ~ #sidebar a:focus)*/ {
+ background-color: rgba(0 0 0 / 0.1);
+}
diff --git a/resources/css/forms/inputs.css b/resources/css/forms/inputs.css
new file mode 100644
index 00000000..32328fe4
--- /dev/null
+++ b/resources/css/forms/inputs.css
@@ -0,0 +1,101 @@
+:where(button, input, optgroup, select, textarea) {
+ font: inherit;
+}
+
+textarea, select, input {
+ field-sizing: content;
+}
+
+/* defensive style - just demo values */
+textarea {
+ min-block-size: 3lh;
+ max-block-size: 80svh;
+ min-inline-size: 30ch;
+ max-inline-size: 80ch;
+ resize: vertical;
+}
+
+form {
+ display: grid;
+ gap: 1rem;
+
+ padding: 1em;
+
+ background: radial-gradient(var(--bg), var(--bg-alt)) 50%;
+
+ border-radius: 0.5em;
+ border: 1px solid var(--muted);
+}
+
+
+:where(input) {
+ font-size: inherit;
+
+ inline-size: fit-content;
+
+ min-inline-size: 25ch;
+
+ padding: 0.5rem;
+
+ border-radius: 0.25rem;
+ border: none;
+
+ color: var(--text);
+
+ &::placeholder {
+ font-style: italic;
+ }
+
+ &:required {
+
+ }
+
+ &:not(:placeholder-shown, :focus):user-invalid {
+ background-color: #300a0a;
+ color: #ffc8c8;
+ }
+}
+
+label:has(input:required)::before {
+ content: "* ";
+}
+
+label:has(input) {
+ background-color: red;
+ display: inline-flex;
+ flex-direction: column;
+ inline-size: fit-content;
+}
+
+select,
+::picker(select) {
+ appearance: base-select;
+}
+
+select {
+ padding: 0.25em;
+ inline-size: fit-content;
+
+ &::marker {
+ color: red;
+ }
+}
+
+select::picker-icon {
+ color: #999;
+ transition: 0.4s rotate;
+}
+
+select:open::picker-icon {
+ rotate: 180deg;
+}
+
+
+/* select items */
+::picker(select) {
+ appearance: base-select;
+}
+
+option:checked {
+ font-weight: bold;
+}
diff --git a/resources/css/layout/container.css b/resources/css/layout/container.css
new file mode 100644
index 00000000..8178cf9f
--- /dev/null
+++ b/resources/css/layout/container.css
@@ -0,0 +1,15 @@
+.page-container {
+ /*display: flex;
+ flex: 1;
+ flex-direction: column;*/
+
+
+ padding-inline: 0; /* horizontale Abstände */
+ max-width: 100%;
+ margin-inline: auto; /* zentriert bei größerem Viewport */
+ width: 100%;
+}
+
+.page-content * {
+ padding-inline: 10rem; /* horizontale Abstände */
+}
diff --git a/resources/css/layout/grid.css b/resources/css/layout/grid.css
new file mode 100644
index 00000000..f81a31f0
--- /dev/null
+++ b/resources/css/layout/grid.css
@@ -0,0 +1,3 @@
+* {
+
+}
diff --git a/resources/css/settings/colors.css b/resources/css/settings/colors.css
new file mode 100644
index 00000000..aad13a11
--- /dev/null
+++ b/resources/css/settings/colors.css
@@ -0,0 +1,101 @@
+:root {
+ /* Hue-System */
+ --h-primary: 295; /* Ihr Violett-Rosa */
+ --h-bg: 270; /* Ihr Hintergrund-Violett */
+ --h-success: 145; /* Grün */
+ --h-warning: 65; /* Orange */
+ --h-error: 25; /* Rot */
+ --h-info: 240; /* Blau */
+
+ /* Einheitliche Lightness-Abstufungen */
+ --l-subtle: 25%; /* Für dezente Hintergründe */
+ --l-hover: 50%; /* Für Hover-States */
+ --l-border: 70%; /* Für sichtbare Rahmen */
+ --l-text: 85%; /* Für gut lesbaren Text */
+
+ /* Chroma-System basierend auf Anwendungsfall */
+ --c-vivid: 0.18; /* Für Hauptfarben, Buttons */
+ --c-moderate: 0.12; /* Für UI-Elemente */
+ --c-subtle: 0.06; /* Für Hintergründe */
+ --c-muted: 0.03; /* Für sehr dezente Effekte */
+
+ /**
+ * 🖤 Hintergrundfarben – kein HDR nötig
+ * L: Lightness, C: Chroma, H: Hue
+ */
+ --bg: oklch(18% 0.01 270); /* sehr dunkles Violettblau */
+ --bg-alt: oklch(26% 0.015 270); /* leicht aufgehellt */
+
+ /**
+ * 🤍 Textfarben – hoher Kontrast
+ */
+ --text: oklch(95% 0.005 270); /* fast weiß */
+ --muted: oklch(70% 0.01 270); /* leichtes Grau mit Farbstich */
+
+ /**
+ * 🎨 Akzentfarbe – sichtbar, auch auf HDR
+ * oklch ist bevorzugt, da linear; P3 gibt extra Boost bei HDR-Displays
+ */
+ --accent: oklch(70% 0.2 295); /* sattes Violett-Rosa */
+ --accent-p3: color(display-p3 1 0.3 0.8); /* pink/lila – intensiver auf HDR */
+
+ /** Border */
+ --border: oklch(40% 0.02 var(--h-bg));
+
+ /**
+ * ✨ Glow-Farbe – über 100% L (= HDR-like)
+ */
+ --glow: oklch(115% 0.22 295); /* extrem helles pink, über weiß hinaus */
+
+
+ /**
+ * Semantische Farbpalette
+ */
+ --success-base: oklch(60% var(--c-vivid) var(--h-success));
+ --success: var(--success-base);
+ --success-subtle: oklch(var(--l-subtle) var(--c-muted) var(--h-success));
+ --success-hover: oklch(var(--l-hover) var(--c-vivid) var(--h-success));
+ --success-border: oklch(var(--l-border) var(--c-moderate) var(--h-success));
+ --success-text: oklch(var(--l-text) var(--c-subtle) var(--h-success));
+
+ --error-base: oklch(55% 0.18 25);
+
+
+
+
+ --warning-base: oklch(70% 0.12 65);
+
+ --info-base: oklch(60% 0.15 240);
+
+
+
+ /* Manuelle Varianten für bessere Browser-Unterstützung */
+
+ --error: var(--error-base);
+ --error-subtle: oklch(var(--l-subtle) 0.036 25);
+ --error-hover: oklch(var(--l-hover) 0.18 25);
+ --error-border: oklch(var(--l-border) 0.14 25);
+ --error-text: oklch(var(--l-text) 0.12 25);
+
+ --warning: var(--warning-base);
+ --warning-subtle: oklch(25% 0.024 65);
+ --warning-border: oklch(80% 0.096 65);
+
+ /**
+ * 🎯 Fallback für alte Browser: überschreiben P3 mit oklch automatisch
+ */
+ color-scheme: dark;
+
+ background-color: var(--bg);
+ color: var(--text);
+}
+
+/**
+ * 🖥️ HDR-Support – bei Geräten mit Dynamic Range Support (aktuell v. a. Safari)
+ */
+/* noinspection CssInvalidMediaFeature */
+@media (dynamic-range: high) {
+ :root {
+ --accent: var(--accent-p3);
+ }
+}
diff --git a/resources/css/settings/variables.css b/resources/css/settings/variables.css
new file mode 100644
index 00000000..9172fa35
--- /dev/null
+++ b/resources/css/settings/variables.css
@@ -0,0 +1,9 @@
+* {
+ --color-primary: #412785;
+
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 1.5rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 1rem;
+}
diff --git a/resources/css/styles.css b/resources/css/styles.css
index 20b20fbd..9b492feb 100644
--- a/resources/css/styles.css
+++ b/resources/css/styles.css
@@ -1,7 +1,266 @@
-p {
- color: red;
+@layer reset, base, layout, components, utilities, overrides;
+
+@import url('settings/colors.css') layer(settings);
+@import url('settings/variables.css') layer(settings);
+@import url('base/reset.css') layer(reset);
+@import url('base') layer(base);
+@import url('layout/container.css') layer(layout);
+@import url('components/header.css') layer(components);
+@import url('components/footer.css') layer(components);
+@import url('components/sidebar.css') layer(components);
+@import url('components/nav.css') layer(components);
+@import url('components/buttons.css') layer(components);
+@import url('components/card.css') layer(components);
+@import url('forms/inputs.css') layer(components);
+@import url('utilities/helpers.css') layer(utilities);
+@import url('utilities/animations.css') layer(utilities);
+@import url('utilities/noise.css') layer(utilities);
+@import url('utilities/scroll.css') layer(utilities);
+
+@layer overrides {
+ /* Benutzerdefinierte Styles */
+}
+
+:root {
+ overscroll-behavior: none;
+
+ accent-color: #412785;
+
+ /* thumb = brighter brand-color */
+ scrollbar-color: #5d37bc rgba(0 0 0 / 0); /* thumb + track */
+
+
+ color-scheme: dark light;
+
+ --duration-default: 0.2s;
+ --duration-medium: 0.35s;
+ --duration-slow: 0.5s;
+
+ --easing-default: cubic-bezier(0.22, 0.61, 0.36, 1);
+ --easing-bounce: linear(0 0%, 0 0.27%, 0.02 4,53%);
+}
+
+::selection {
+ --_selection-bg-color: var(--selection-bg-color, #ff4081);
+ --_selection-text-color: var(--selection-text-color, #fff);
+
+
+ background-color: var(--_selection-bg-color); /* Pink */
+ color: var(--_selection-text-color);
+
+ text-shadow: 0 0 0.25rem var(--_selection-text-color);
+
+ text-decoration: var(--selection-text-decoration, none);
+}
+
+::-webkit-scrollbar {
+ /*width: 0.5rem;*/
+}
+
+
+
+@media(prefers-reduced-motion: no-preference) {
+ :focus {
+ transition: outline-offset .25s ease;
+ }
+ :focus:not(:active) {
+ outline-offset: 5px;
+ }
}
html {
- background: blue;
+ /* evtl per Mediaquery auf thin umschalten */
+ scrollbar-width: auto;
+ /*scrollbar-color: #412785 #412785;*/
+
+ /* @link: https://moderncss.dev/12-modern-css-one-line-upgrades/#scrollbar-gutter */
+ scrollbar-gutter: stable /*both-edges*/;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+
+ overflow-x: clip;
+ overscroll-behavior: contain;
+
+ /* move body back in 3d:
+ border-radius: 10px;
+ overflow: hidden;
+ scale: 95%;
+ background-color: #2c1;
+
+ * {
+ visibility: hidden;
+ }
+ */
+}
+
+
+picture {
+ user-select: none;
+}
+
+/*section,
+.page-container > * {
+ padding-inline: clamp(2.5rem, 20%, 10rem);
+}*/
+
+section.hero {
+ display: flex;
+ min-height: 100vh;
+ /*height: 100%;
+ width: 100%;*/
+ /*background-color: #412785;*/
+ background-color: var(--bg);
+ color: var(--accent);
+ padding-block-start: 10rem;
+
+ /*background-image: url("https://localhost/assets/images/hero_small.jpg");
+ background-size: cover;
+ background-position: center;*/
+
+ :where(:not(picture, img, h1)) {
+ position: relative;
+ }
+
+ flex-direction: column;
+ gap: 2ch;
+}
+
+.hero picture, .hero img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ z-index: 0;
+}
+
+/* Lazy loading? */
+section {
+ content-visibility: auto;
+}
+
+
+
+h1 {
+ user-select: none;
+ padding-block-end: 3.33rem; /* front-size / 3 */
+ /*text-align: center;*/
+
+
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
+
+ background: linear-gradient(
+ 90deg,
+ oklch(70% 0.25 280),
+ oklch(80% 0.3 340),
+ oklch(70% 0.25 280)
+ );
+ background-size: 200% auto;
+
+ /* Für Firefox */
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+
+ background-clip: text;
+ color: transparent;
+
+ animation: shimmer 10s linear infinite;
+
+ /*animation: shimmer 4s alternate-reverse infinite;*/
+}
+
+
+
+/*@keyframes shimmer {
+ 0% { opacity: 0.5; }
+ 100% { opacity: 1; }
+}*/
+
+@keyframes shimmer {
+ 0% { background-position: 0 center; }
+ 100% { background-position: -200% center; }
+}
+
+::backdrop {
+ background-color: #412785;
+ /*opacity: 0.7;*/
+ background-color: rgba(0, 0, 0, 0.2);
+
+ filter: blur(5px);
+ backdrop-filter: blur(5px);
+}
+
+
+
+@starting-style {
+ #mypopover {
+ &:popover-open {
+ opacity: 0;
+ translate: 100% 0;
+ }
+ }
+}
+
+html:has(aside.show) {
+ /*scrollbar-width: none;*/
+}
+
+.fade-in-on-scroll {
+ opacity: 0;
+ transform: translateY(100px);
+ transition: opacity 0.6s ease, transform 0.6s ease;
+ will-change: opacity, transform;
+}
+
+.fade-in-on-scroll.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+
+/*body::before {
+ content: '';
+ position: fixed;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: red;
+ pointer-events: none;
+ z-index: 9999;
+}*/
+
+[aria-current="page"] {}
+
+form>div {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ border: 1px solid var(--muted);
+
+ &:has(input:user-invalid:not(:placeholder-shown, :focus)) {
+ [role="alert"] {
+ visibility: visible;
+ }
+ }
+}
+
+[role="alert"] {
+ visibility: hidden;
+ min-height: 2rem;
+ font-size: 1rem;
+
+ background-color: red;
+ border: darkred 2px solid;
+ border-radius: 0.5em;
+ padding: 0.25em;
+ color: black;
+}
+
+span[id^="hint"] {
+ color: var(--muted);
}
diff --git a/resources/css/utilities/animations.css b/resources/css/utilities/animations.css
new file mode 100644
index 00000000..c8e86f56
--- /dev/null
+++ b/resources/css/utilities/animations.css
@@ -0,0 +1,38 @@
+.fade {
+ opacity: 0;
+ transform: translateY(40px);
+ transition: opacity 0.6s ease, transform 0.6s ease;
+}
+
+.fade.entered {
+ color: #0af;
+}
+
+.fade-in { transition: opacity 0.4s ease-out; }
+.fade-out { transition: opacity 0.4s ease-in; }
+.zoom-in { transition: transform 0.5s ease-out; }
+
+/* Beispielanimation */
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-5px); }
+ 75% { transform: translateX(5px); }
+}
+.shake {
+ animation: shake 0.8s ease-in-out;
+}
+
+
+
+.fade-in-on-scroll,
+.zoom-in {
+ opacity: 0;
+ transform: translateY(40px);
+ transition: opacity 0.6s ease, transform 0.6s ease;
+ will-change: opacity, transform;
+}
+
+.visible {
+ opacity: 1;
+ transform: none;
+}
diff --git a/resources/css/utilities/helpers.css b/resources/css/utilities/helpers.css
new file mode 100644
index 00000000..f4cea3e9
--- /dev/null
+++ b/resources/css/utilities/helpers.css
@@ -0,0 +1,18 @@
+/* Set some base styles, so it is easy to see */
+.skip-link {
+ position: absolute;
+ display: inline-block;
+ padding: .375rem .75rem;
+ line-height: 1;
+ font-size: 1.25rem;
+ background-color: rebeccapurple;
+ color: white;
+ /* Ensure the Y position is set to zero and any movement on the transform property */
+ transform: translateY(0);
+ transition: transform 250ms ease-in;
+}
+
+/* When it is not focused, transform its Y position by its total height, using a negative value, so it hides above the viewport */
+.skip-link:not(:focus) {
+ transform: translateY(-2rem);
+}
diff --git a/resources/css/utilities/noise.css b/resources/css/utilities/noise.css
new file mode 100644
index 00000000..1fe56fa6
--- /dev/null
+++ b/resources/css/utilities/noise.css
@@ -0,0 +1,62 @@
+.noise-bg {
+ --noise-opacity: 0.15;
+
+ background-image:
+ repeating-radial-gradient(circle at 1px 1px,
+ rgba(255, 255, 255, var(--noise-opacity, 0.03)) 0,
+ rgba(0, 0, 0, var(--noise-opacity, 0.03)) 1px,
+ transparent 2px
+ );
+ background-size: 3px 3px;
+}
+
+.grain {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 9999;
+
+ background-color: #000;
+ mix-blend-mode: overlay;
+ opacity: 0.5;
+
+ animation: noise 1s steps(10) infinite;
+}
+
+@keyframes noise {
+ 0% { filter: hue-rotate(0deg); }
+ 100% { filter: hue-rotate(360deg); }
+}
+
+.noise-overlay {
+ --noise-intensity: 0.03;
+
+ view-transition-name: noise;
+
+
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+ pointer-events: none;
+
+ mix-blend-mode: overlay;
+
+ background-size: cover;
+ background: url("data:image/svg+xml;utf8,\
+") repeat, rgba(0, 0, 0, var(--noise-intensity, 0.03));
+}
+
+.noise-overlay {
+ opacity: var(--noise-opacity, 0.05);
+ transition: opacity 0.3s ease;
+ pointer-events: none;
+}
+
+.noise-overlay.hidden {
+ opacity: 0;
+}
diff --git a/resources/css/utilities/scroll.css b/resources/css/utilities/scroll.css
new file mode 100644
index 00000000..23d4335f
--- /dev/null
+++ b/resources/css/utilities/scroll.css
@@ -0,0 +1,200 @@
+body {
+ transition: background-color 0.5s ease, color 0.5s ease;
+}
+
+/* FOR REFERENCE::
+
+body[data-active-scroll-step="1"] header {
+ opacity: 1;
+}
+
+body[data-active-scroll-step="2"] header {
+ opacity: 0.5;
+}
+
+body[data-active-scroll-step="3"] header {
+ transform: translateY(-100%);
+ transition: transform 0.3s ease;
+}*/
+
+body[data-active-scroll-step="1"] {
+ background-color: #1a1a1a;
+ color: #eee;
+}
+
+body[data-active-scroll-step="2"] {
+ background-color: #000;
+ color: #f0f0f0;
+}
+
+body[data-active-scroll-step="3"] {
+ background-color: #2c1c59;
+ color: #fff;
+}
+
+body[data-active-scroll-step="1"] {
+ /* z.B. Schriftart, Hintergrund etc. */
+ cursor: s-resize;
+}
+
+body[data-active-scroll-step="2"] {
+ background-color: #1a1a1a;
+}
+
+[data-scroll-step] {
+ opacity: 0;
+ transform: translateY(2rem);
+ transition: opacity 0.6s ease, transform 0.6s ease;
+ will-change: opacity, transform;
+}
+
+body {
+ opacity: 1;
+}
+
+/* Aktivierte Scroll-Section */
+[data-scroll-step].active {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+
+[data-scroll-step].active {
+ border-left: 4px solid var(--accent-color, #6c4dff);
+ background-color: rgba(255, 255, 255, 0.02);
+}
+
+
+
+/* Basis-Stil für Sticky Steps */
+[data-sticky-step] {
+ position: sticky;
+ top: 20vh;
+ margin: 2rem 0;
+ padding: 2rem;
+ background: #1e1e1e;
+ border-radius: 0.5rem;
+ opacity: 0.4;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+[data-sticky-step].is-sticky-active {
+ opacity: 1;
+ transform: scale(1.02);
+ box-shadow: 0 0 1rem rgba(255, 255, 255, 0.05);
+}
+
+/* Beispielhafte visuelle Steuerung per container-Datensatz */
+[data-sticky-container][data-active-sticky-step="0"] [data-sticky-step]:nth-child(1),
+[data-sticky-container][data-active-sticky-step="1"] [data-sticky-step]:nth-child(2),
+[data-sticky-container][data-active-sticky-step="2"] [data-sticky-step]:nth-child(3) {
+ border-left: 4px solid var(--accent-color, #6c4dff);
+ background: linear-gradient(to right, rgba(108, 77, 255, 0.1), transparent);
+}
+
+/* Optional: sanfte Farbübergänge */
+[data-sticky-step] {
+ transition: opacity 0.3s ease, transform 0.3s ease, background 0.4s ease;
+}
+
+/* Sticky Container zur Sicherheit sichtbar machen */
+[data-sticky-container] {
+ position: relative;
+ z-index: 0;
+
+ height: 300vh;
+ padding: 2rem;
+}
+
+
+/*
+ Scroll LOOPS
+*/
+
+.loop-container {
+ overflow: hidden;
+ position: relative;
+ height: 200px;
+}
+
+.loop-content {
+ display: flex;
+ flex-direction: column;
+}
+
+/* Basisstil für scroll-loop-Elemente */
+[data-scroll-loop] {
+ will-change: transform;
+ display: inline-block;
+ backface-visibility: hidden;
+ transform-style: preserve-3d;
+}
+
+/* Empfohlene Containerstruktur */
+.scroll-loop-container {
+ overflow: hidden;
+ position: relative;
+ height: 200px; /* anpassen je nach Bedarf */
+}
+
+/* Optional für automatische Verdopplung */
+.scroll-loop-container > [data-scroll-loop] {
+ display: flex;
+ flex-direction: column;
+}
+
+/* Für Background-Loop */
+[data-scroll-type="background"] {
+ background-repeat: repeat;
+ background-size: auto 100%;
+}
+
+/* Pausieren bei Hover oder Aktivierung */
+[data-scroll-loop][data-loop-pause="true"]:hover,
+[data-scroll-loop][data-loop-pause="true"]:active {
+ animation-play-state: paused;
+}
+
+
+/*
+ SCROLL_FADE
+ */
+
+/* Optionaler Grundstil für sticky-fade Elemente */
+/* Basisstil für sticky-fade Elemente */
+[data-sticky-fade] {
+ opacity: 0;
+ transform: translateY(20px);
+ transition-property: opacity, transform;
+ transition-duration: 0.4s;
+ transition-timing-function: ease-out;
+ will-change: opacity, transform;
+ pointer-events: auto;
+}
+
+[data-sticky-fade][data-fade-distance="10"] {
+ transform: translateY(10px);
+}
+[data-sticky-fade][data-fade-distance="30"] {
+ transform: translateY(30px);
+}
+[data-sticky-fade][data-fade-distance="40"] {
+ transform: translateY(40px);
+}
+
+[data-sticky-fade][data-fade-duration="fast"] {
+ transition-duration: 0.2s;
+}
+
+[data-sticky-fade][data-fade-duration="slow"] {
+ transition-duration: 0.8s;
+}
+
+[data-sticky-fade].visible {
+ opacity: 1;
+ transform: translateY(0);
+ box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.1);
+ filter: brightness(1.05);
+ transition-property: opacity, transform, box-shadow;
+ transition-timing-function: ease;
+}
diff --git a/resources/css/utilities/transitions.css b/resources/css/utilities/transitions.css
new file mode 100644
index 00000000..e3b78d65
--- /dev/null
+++ b/resources/css/utilities/transitions.css
@@ -0,0 +1,45 @@
+@view-transition {
+ navigation: auto;
+}
+
+@keyframes fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slide-down {
+ from { transform: translateY(-2rem); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes slide-up {
+ from { transform: translateY(2rem); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes blur-in {
+ from { filter: blur(10px); opacity: 0; }
+ to { filter: blur(0); opacity: 1; }
+}
+
+/* 🎯 Transitions für benannte Bereiche */
+
+::view-transition-old(main-content),
+::view-transition-new(main-content) {
+ animation: fade 0.4s ease;
+}
+
+::view-transition-old(site-header),
+::view-transition-new(site-header) {
+ animation: slide-down 0.4s ease;
+}
+
+::view-transition-old(site-footer),
+::view-transition-new(site-footer) {
+ animation: slide-up 0.4s ease;
+}
+
+::view-transition-old(sidebar),
+::view-transition-new(sidebar) {
+ animation: blur-in 0.3s ease;
+}
diff --git a/resources/js/core/ClickManager.js b/resources/js/core/ClickManager.js
new file mode 100644
index 00000000..70308573
--- /dev/null
+++ b/resources/js/core/ClickManager.js
@@ -0,0 +1,136 @@
+// modules/core/click-manager.js
+import { Logger } from './logger.js';
+import { useEvent } from './useEvent.js';
+import {navigateTo} from "./navigateTo";
+import {SimpleCache} from "../utils/cache";
+
+let callback = null;
+let unsubscribes = [];
+let cleanupInterval = null;
+const prefetchCache = new SimpleCache(20, 60000); //new Map();
+const maxCacheSize = 20; // max. Anzahl gecachter Seiten
+const cacheTTL = 60000; // Lebensdauer in ms (60s)
+
+function isInternal(link) {
+ return link.origin === location.origin;
+}
+
+function handleClick(e) {
+ const link = e.target.closest('a');
+ if (!link || e.defaultPrevented) return;
+
+ const href = link.getAttribute('href');
+ if (!href || href.startsWith('#')) return;
+
+ // Skip conditions
+ if (
+ link.target === '_blank' ||
+ link.hasAttribute('download') ||
+ link.getAttribute('rel')?.includes('external') ||
+ link.hasAttribute('data-skip')
+ ) {
+ Logger.info(`[click-manager] skipped: ${href}`);
+ return;
+ }
+
+ if (isInternal(link)) {
+ e.preventDefault();
+
+ const cached = prefetchCache.get(href);
+ const valid = cached && Date.now() - cached.timestamp < cacheTTL;
+
+ const options = {
+ viewTransition: link.hasAttribute('data-view-transition'),
+ replace: link.hasAttribute('data-replace'),
+ modal: link.hasAttribute('data-modal'),
+ prefetched: valid,
+ data: valid ? cached.data : null,
+ };
+
+ Logger.info(`[click-manager] internal: ${href}`, options);
+
+ if(options.modal) {
+ callback?.(href, link, options);
+ } else {
+ navigateTo(href, options);
+ }
+ }
+}
+
+let prefetchTimeout;
+function handleMouseOver(e) {
+ clearTimeout(prefetchTimeout);
+ const link = e.target.closest('a[href]');
+ if (!link || !isInternal(link)) return;
+
+ const href = link.getAttribute('href');
+ if (!href || prefetchCache.has(href)) return;
+
+ // optional: Wait 150ms to reduce noise
+ prefetchTimeout = setTimeout(() => prefetch(href), 150);
+}
+
+function prefetch(href) {
+ Logger.info(`[click-manager] prefetching: ${href}`);
+
+ fetch(href)
+ .then(res => res.text())
+ .then(html => {
+ if (prefetchCache.cache.size >= maxCacheSize) {
+ const oldestKey = [...prefetchCache.cache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
+ prefetchCache.cache.delete(oldestKey);
+ }
+ prefetchCache.set(href, {
+ data: html,
+ timestamp: Date.now(),
+ });
+ })
+ .catch(err => {
+ Logger.warn(`[click-manager] prefetch failed for ${href}`, err);
+ });
+}
+
+function handlePopState() {
+ const href = location.pathname;
+ Logger.info(`[click-manager] popstate: ${href}`);
+ navigateTo(href, { replace: true });
+}
+
+export function getPrefetched(href) {
+ const cached = prefetchCache.get(href);
+ const valid = cached && Date.now() - cached.timestamp < cacheTTL;
+ return valid ? cached.data : null;
+}
+
+export function prefetchHref(href) {
+ if (!href || prefetchCache.has(href)) return;
+ prefetch(href);
+}
+
+export function init(onNavigate) {
+ callback = onNavigate;
+ unsubscribes = [
+ useEvent(document, 'click', handleClick),
+ useEvent(document, 'mouseover', handleMouseOver),
+ useEvent(window, 'popstate', handlePopState),
+ ]
+
+ cleanupInterval = setInterval(() => {
+ for (const [key, val] of prefetchCache.cache.entries()) {
+ if (Date.now() - val.timestamp > prefetchCache.ttl) {
+ prefetchCache.cache.delete(key);
+ }
+ }
+ }, 120000);
+
+
+ Logger.info('[click-manager] ready');
+}
+
+export function destroy() {
+ callback = null;
+ prefetchCache.clear();
+ unsubscribes.forEach(unsub => unsub());
+ unsubscribes = [];
+ Logger.info('[click-manager] destroyed');
+}
diff --git a/resources/js/core/EventManager.js b/resources/js/core/EventManager.js
new file mode 100644
index 00000000..1325df84
--- /dev/null
+++ b/resources/js/core/EventManager.js
@@ -0,0 +1,49 @@
+// modules/core/EventManager.js
+const registry = new Map();
+
+export const EventManager = {
+ /**
+ * Fügt einen EventListener hinzu und speichert ihn im Modul-Kontext
+ */
+ add(target, type, handler, { module = 'global', options = false } = {}) {
+ target.addEventListener(type, handler, options);
+ if (!registry.has(module)) registry.set(module, []);
+ registry.get(module).push([target, type, handler, options]);
+ },
+
+ /**
+ * Entfernt alle Listener, die für ein bestimmtes Modul registriert wurden
+ */
+ removeModule(module) {
+ const entries = registry.get(module);
+ if (!entries) return;
+
+ entries.forEach(([target, type, handler, options]) => {
+ target.removeEventListener(type, handler, options);
+ });
+
+ registry.delete(module);
+ },
+
+ /**
+ * Entfernt alle Event-Listener aus allen Modulen
+ */
+ clearAll() {
+ for (const [module, entries] of registry.entries()) {
+ entries.forEach(([target, type, handler, options]) => {
+ target.removeEventListener(type, handler, options);
+ });
+ }
+ registry.clear();
+ },
+
+ /**
+ * Debug: Gibt die aktuelle Registry in der Konsole aus
+ */
+ debug() {
+ console.table([...registry.entries()].map(([module, events]) => ({
+ module,
+ listeners: events.length
+ })));
+ }
+};
diff --git a/resources/js/core/PerformanceMonitor.js b/resources/js/core/PerformanceMonitor.js
new file mode 100644
index 00000000..de55a3c0
--- /dev/null
+++ b/resources/js/core/PerformanceMonitor.js
@@ -0,0 +1,73 @@
+// modules/core/PerformanceMonitor.js
+export class PerformanceMonitor {
+ constructor({ fps = true } = {}) {
+ this.fpsEnabled = fps;
+ this.fps = 0;
+ this.frameCount = 0;
+ this.lastTime = performance.now();
+ this.visible = false;
+ this.taskTimings = new Map();
+ this.logs = [];
+
+ this.container = document.createElement('div');
+ this.container.style.position = 'fixed';
+ this.container.style.bottom = '0';
+ this.container.style.left = '0';
+ this.container.style.font = '12px Consolas, monospace';
+ this.container.style.color = '#0f0';
+ this.container.style.background = 'rgba(0,0,0,0.75)';
+ this.container.style.padding = '0.5rem';
+ this.container.style.zIndex = '9999';
+ this.container.style.pointerEvents = 'none';
+ this.container.style.lineHeight = '1.4';
+ this.container.style.whiteSpace = 'pre';
+ this.container.style.display = 'none';
+
+ document.body.appendChild(this.container);
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key === '§') {
+ this.visible = !this.visible;
+ this.container.style.display = this.visible ? 'block' : 'none';
+ }
+ });
+ }
+
+ log(message) {
+ const timestamp = new Date().toLocaleTimeString();
+ this.logs.push(`[${timestamp}] ${message}`);
+ if (this.logs.length > 5) this.logs.shift();
+ }
+
+ update(taskMap = new Map()) {
+ this.frameCount++;
+ const now = performance.now();
+ if (now - this.lastTime >= 1000) {
+ this.fps = this.frameCount;
+ this.frameCount = 0;
+ this.lastTime = now;
+
+ const timings = [];
+ for (const [id, duration] of this.taskTimings.entries()) {
+ timings.push(`${id}: ${duration.toFixed(2)}ms`);
+ }
+
+ const barWidth = Math.min(this.fps * 2, 100);
+ const logOutput = this.logs.slice().reverse().join('\n');
+
+ this.container.innerHTML = `
+FPS: ${this.fps} | Tasks: ${taskMap.size}
+${timings.join('\n')}
+
+${logOutput ? '\nLogs:\n' + logOutput : ''}
+ `;
+ }
+ }
+
+ trackTask(id, callback) {
+ const start = performance.now();
+ callback();
+ const duration = performance.now() - start;
+ this.taskTimings.set(id, duration);
+ }
+}
diff --git a/resources/js/core/events.js b/resources/js/core/events.js
new file mode 100644
index 00000000..97bf1e2f
--- /dev/null
+++ b/resources/js/core/events.js
@@ -0,0 +1,46 @@
+// src/core/events.js
+
+// --- 1. Globaler EventBus ---
+
+const listeners = new Map();
+
+/**
+ * Abonniert ein benanntes Event
+ */
+export function on(eventName, callback) {
+ if (!listeners.has(eventName)) listeners.set(eventName, []);
+ listeners.get(eventName).push(callback);
+
+ // Unsubscribe
+ return () => {
+ const arr = listeners.get(eventName);
+ if (arr) listeners.set(eventName, arr.filter(fn => fn !== callback));
+ };
+}
+
+/**
+ * Sendet ein benanntes Event mit Payload
+ */
+export function emit(eventName, payload) {
+ const arr = listeners.get(eventName);
+ if (arr) arr.forEach(fn => fn(payload));
+}
+
+// --- 2. ActionDispatcher ---
+
+const actionListeners = new Set();
+
+/**
+ * Action registrieren (globaler Listener für alle Aktionen)
+ */
+export function registerActionListener(callback) {
+ actionListeners.add(callback);
+ return () => actionListeners.delete(callback);
+}
+
+/**
+ * Aktion ausführen
+ */
+export function dispatchAction(type, payload = {}) {
+ actionListeners.forEach(fn => fn({ type, payload }));
+}
diff --git a/resources/js/core/frameloop.js b/resources/js/core/frameloop.js
new file mode 100644
index 00000000..10ee1b28
--- /dev/null
+++ b/resources/js/core/frameloop.js
@@ -0,0 +1,83 @@
+// modules/core/frameloop.js
+import {Logger} from "./logger";
+
+const tasks = new Map();
+let running = false;
+let showDebug = false;
+
+let lastTime = performance.now();
+let frameCount = 0;
+let fps = 0;
+
+
+const debugOverlay = document.createElement('div');
+debugOverlay.style.position = 'fixed';
+debugOverlay.style.bottom = '0';
+debugOverlay.style.left = '0';
+debugOverlay.style.font = '12px monospace';
+debugOverlay.style.color = '#0f0';
+debugOverlay.style.background = 'rgba(0,0,0,0.75)';
+debugOverlay.style.padding = '0.25rem 0.5rem';
+debugOverlay.style.zIndex = '9999';
+debugOverlay.style.pointerEvents = 'none';
+debugOverlay.style.display = 'none';
+const barWidth = Math.min(fps * 2, 100);
+const bar = ``;
+debugOverlay.innerHTML += bar;
+debugOverlay.style.lineHeight = '1.4';
+document.body.appendChild(debugOverlay);
+
+import { PerformanceMonitor } from './PerformanceMonitor.js';
+export const monitor = new PerformanceMonitor();
+
+window.addEventListener('keydown', (e) => {
+ if (e.key === '§') {
+ showDebug = !showDebug;
+ debugOverlay.style.display = showDebug ? 'block' : 'none';
+ }
+});
+
+export function registerFrameTask(id, callback, options = {}) {
+ tasks.set(id, callback);
+ if (options.autoStart && !running) startFrameLoop();
+}
+
+export function unregisterFrameTask(id) {
+ tasks.delete(id);
+}
+
+export function clearFrameTasks() {
+ tasks.clear();
+}
+
+export function startFrameLoop() {
+ if (running) return;
+ running = true;
+
+ function loop() {
+ for (const [id, task] of tasks) {
+ try {
+ if (showDebug) {
+ monitor.trackTask(id, task);
+ } else {
+ task();
+ }
+ } catch (err) {
+ Logger.warn(`[Frameloop] Fehler in Task:`, err);
+ }
+ }
+
+ if (showDebug) {
+ monitor.update(tasks);
+ }
+
+ requestAnimationFrame(loop);
+ }
+
+ requestAnimationFrame(loop);
+}
+
+export function stopFrameLoop() {
+ running = false;
+ // Achtung: Loop läuft weiter, solange nicht aktiv gestoppt
+}
diff --git a/resources/js/core/index.js b/resources/js/core/index.js
new file mode 100644
index 00000000..5f8c489c
--- /dev/null
+++ b/resources/js/core/index.js
@@ -0,0 +1,2 @@
+export * from './logger.js';
+export * from './useEvent';
diff --git a/resources/js/core/init.js b/resources/js/core/init.js
new file mode 100644
index 00000000..3f0c62a6
--- /dev/null
+++ b/resources/js/core/init.js
@@ -0,0 +1,125 @@
+/*
+const menu = document.getElementById("sidebar-menu");
+const closeBtn = menu.querySelector(".close-btn");
+
+closeBtn.addEventListener("click", () => {
+ menu.hidePopover();
+});
+*/
+
+
+import {registerModules} from "../modules";
+
+import {useEvent} from "./useEvent";
+
+
+
+/*import { createTrigger, destroyTrigger, destroyAllTriggers } from './scrollfx/index.js';
+
+createTrigger({
+ element: 'section',
+ target: '.fade',
+ start: 'top 80%',
+ end: 'bottom 30%',
+ scrub: true,
+ onUpdate: (() => {
+ const progressMap = new WeakMap();
+
+ return (el, progress) => {
+ if (!el) return;
+
+ let current = progressMap.get(el) || 0;
+ current += (progress - current) * 0.1;
+ progressMap.set(el, current);
+
+ el.style.opacity = current;
+ el.style.transform = `translateY(${30 - 30 * current}px)`;
+ };
+ })(),
+
+ onEnter: el => el.classList.add('entered'),
+ onLeave: el => {
+ el.classList.remove('entered');
+ el.style.opacity = 0;
+ el.style.transform = 'translateY(30px)';
+ }
+});*/
+
+
+
+
+
+/*
+let lastScrollY = window.scrollY;
+
+const fadeElements = document.querySelectorAll('.fade-in-on-scroll');
+
+// Observer 1: Einblenden beim Runterscrollen
+const fadeInObserver = new IntersectionObserver((entries) => {
+ const scrollingDown = window.scrollY > lastScrollY;
+ lastScrollY = window.scrollY;
+
+ entries.forEach(entry => {
+ if (entry.isIntersecting && scrollingDown) {
+ entry.target.classList.add('visible');
+ }
+ });
+}, {
+ threshold: 0.4,
+ rootMargin: '0px 0px -10% 0px'
+});
+
+// Observer 2: Ausblenden beim Hochscrollen
+const fadeOutObserver = new IntersectionObserver((entries) => {
+ const scrollingUp = window.scrollY < lastScrollY;
+ lastScrollY = window.scrollY;
+
+ entries.forEach(entry => {
+ if (!entry.isIntersecting && scrollingUp) {
+ entry.target.classList.remove('visible');
+ }
+ });
+}, {
+ threshold: 0.5,
+ rootMargin: '0% 0px 50% 0px' // früher triggern beim Hochscrollen
+});
+
+// Alle Elemente mit beiden Observern beobachten
+fadeElements.forEach(el => {
+ fadeInObserver.observe(el);
+ fadeOutObserver.observe(el);
+});
+*/
+
+
+/*
+const newContent = 'Neue Seite
'; // dein AJAX-Inhalt z. B.
+const container = document.querySelector('main');
+
+document.startViewTransition(() => {
+ container.innerHTML = newContent;
+});*/
+
+import { fadeScrollTrigger, zoomScrollTrigger, fixedZoomScrollTrigger } from '../modules/scrollfx/Tween.js';
+import {autoLoadResponsiveVideos} from "../utils/autoLoadResponsiveVideo";
+
+
+export async function initApp() {
+
+ await registerModules();
+
+ autoLoadResponsiveVideos();
+
+ /*initNoiseToggle({
+ selector: '.noise-overlay',
+ toggleKey: 'g', // Taste zum Umschalten
+ className: 'grainy', // Klasse auf
+ enableTransition: true // Smooth fade
+ });
+
+
+ fadeScrollTrigger('.fade');
+ zoomScrollTrigger('.zoomed');
+
+ fixedZoomScrollTrigger('h1');*/
+}
diff --git a/resources/js/core/logger-next.js b/resources/js/core/logger-next.js
new file mode 100644
index 00000000..9a55d239
--- /dev/null
+++ b/resources/js/core/logger-next.js
@@ -0,0 +1,277 @@
+/**
+ * Erweiterter Logger mit Processor-Architektur, Request-ID-Unterstützung und Server-Kommunikation.
+ */
+export class Logger {
+ /**
+ * Konfiguration des Loggers
+ */
+ static config = {
+ enabled: true,
+ apiEndpoint: '/api/log',
+ consoleEnabled: true,
+ serverEnabled: true,
+ minLevel: 'debug',
+ };
+
+ /**
+ * Liste der registrierten Processors
+ */
+ static processors = [];
+
+ /**
+ * Registrierte Handler
+ */
+ static handlers = [];
+
+ /**
+ * Aktive RequestID
+ */
+ static requestId = null;
+
+ /**
+ * Logger initialisieren
+ */
+ static initialize(config = {}) {
+ // Konfiguration überschreiben
+ this.config = { ...this.config, ...config };
+
+ // Standard-Processors registrieren
+ this.registerProcessor(this.requestIdProcessor);
+ this.registerProcessor(this.timestampProcessor);
+
+ // Standard-Handler registrieren
+ if (this.config.consoleEnabled) {
+ this.registerHandler(this.consoleHandler);
+ }
+
+ if (this.config.serverEnabled) {
+ this.registerHandler(this.serverHandler);
+ }
+
+ // Request-ID aus dem Document laden, wenn vorhanden
+ if (typeof document !== 'undefined') {
+ this.initFromDocument();
+ }
+
+ // Unhandled Errors abfangen
+ this.setupErrorHandling();
+ }
+
+ /**
+ * Debug-Nachricht loggen
+ */
+ static debug(...args) {
+ this.log('debug', ...args);
+ }
+
+ /**
+ * Info-Nachricht loggen
+ */
+ static info(...args) {
+ this.log('info', ...args);
+ }
+
+ /**
+ * Warnungs-Nachricht loggen
+ */
+ static warn(...args) {
+ this.log('warn', ...args);
+ }
+
+ /**
+ * Fehler-Nachricht loggen
+ */
+ static error(...args) {
+ this.log('error', ...args);
+ }
+
+ /**
+ * Log-Nachricht mit beliebigem Level erstellen
+ */
+ static log(level, ...args) {
+ if (!this.config.enabled) return;
+
+ // Level-Validierung
+ const validLevels = ['debug', 'info', 'warn', 'error'];
+ if (!validLevels.includes(level)) {
+ level = 'info';
+ }
+
+ // Nachricht und Kontext extrahieren
+ const message = this.formatMessage(args);
+ const context = args.find(arg => typeof arg === 'object' && arg !== null && !(arg instanceof Error)) || {};
+
+ // Exception extrahieren, falls vorhanden
+ const error = args.find(arg => arg instanceof Error);
+ if (error) {
+ context.exception = error;
+ }
+
+ // Log-Record erstellen
+ let record = {
+ level,
+ message,
+ context,
+ timestamp: new Date(),
+ extra: {},
+ };
+
+ // Alle Processors durchlaufen
+ this.processors.forEach(processor => {
+ record = processor(record);
+ });
+
+ // Log-Level-Prüfung (nach Processors, da sie das Level ändern könnten)
+ const levelPriority = {
+ debug: 100,
+ info: 200,
+ warn: 300,
+ error: 400,
+ };
+
+ if (levelPriority[record.level] < levelPriority[this.config.minLevel]) {
+ return;
+ }
+
+ // Alle Handler durchlaufen
+ this.handlers.forEach(handler => {
+ handler(record);
+ });
+ }
+
+ /**
+ * Nachricht aus verschiedenen Argumenten formatieren
+ */
+ static formatMessage(args) {
+ return args
+ .filter(arg => !(arg instanceof Error) && (typeof arg !== 'object' || arg === null))
+ .map(arg => String(arg))
+ .join(' ');
+ }
+
+ /**
+ * Processor registrieren
+ */
+ static registerProcessor(processor) {
+ if (typeof processor !== 'function') return;
+ this.processors.push(processor);
+ }
+
+ /**
+ * Handler registrieren
+ */
+ static registerHandler(handler) {
+ if (typeof handler !== 'function') return;
+ this.handlers.push(handler);
+ }
+
+ /**
+ * Error-Handling-Setup
+ */
+ static setupErrorHandling() {
+ if (typeof window !== 'undefined') {
+ // Unbehandelte Fehler abfangen
+ window.addEventListener('error', (event) => {
+ this.error('Unbehandelter Fehler:', event.error || event.message);
+ });
+
+ // Unbehandelte Promise-Rejects abfangen
+ window.addEventListener('unhandledrejection', (event) => {
+ this.error('Unbehandelte Promise-Ablehnung:', event.reason);
+ });
+ }
+ }
+
+ /**
+ * Request-ID aus dem Document laden
+ */
+ static initFromDocument() {
+ const meta = document.querySelector('meta[name="request-id"]');
+ if (meta) {
+ const fullRequestId = meta.getAttribute('content');
+ // Nur den ID-Teil ohne Signatur verwenden
+ this.requestId = fullRequestId.split('.')[0] || null;
+ }
+ }
+
+ /*** STANDARD-PROCESSORS ***/
+
+ /**
+ * Processor für Request-ID
+ */
+ static requestIdProcessor(record) {
+ if (Logger.requestId) {
+ record.extra.request_id = Logger.requestId;
+ }
+ return record;
+ }
+
+ /**
+ * Processor für Timestamp-Formatierung
+ */
+ static timestampProcessor(record) {
+ record.formattedTimestamp = record.timestamp.toLocaleTimeString('de-DE');
+ return record;
+ }
+
+ /*** STANDARD-HANDLERS ***/
+
+ /**
+ * Handler für Console-Ausgabe
+ */
+ static consoleHandler(record) {
+ const levelColors = {
+ debug: 'color: gray',
+ info: 'color: green',
+ warn: 'color: orange',
+ error: 'color: red',
+ };
+
+ const color = levelColors[record.level] || 'color: black';
+ const requestIdStr = record.extra.request_id ? `[${record.extra.request_id}] ` : '';
+ const formattedMessage = `[${record.formattedTimestamp}] [${record.level.toUpperCase()}] ${requestIdStr}${record.message}`;
+
+ // Farbige Ausgabe in der Konsole
+ console[record.level](
+ `%c${formattedMessage}`,
+ color,
+ ...(record.context ? [record.context] : [])
+ );
+ }
+
+ /**
+ * Handler für Server-Kommunikation
+ */
+ static serverHandler(record) {
+ fetch(Logger.config.apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Request-ID': Logger.requestId || ''
+ },
+ body: JSON.stringify({
+ level: record.level,
+ message: record.message,
+ context: record.context || {}
+ })
+ })
+ .then(response => {
+ // Request-ID aus dem Header extrahieren
+ const requestId = response.headers.get('X-Request-ID');
+ if (requestId) {
+ // Nur den ID-Teil ohne Signatur speichern
+ const idPart = requestId.split('.')[0];
+ if (idPart) {
+ Logger.requestId = idPart;
+ }
+ }
+ return response.json();
+ })
+ .catch(() => {
+ // Fehler beim Senden des Logs ignorieren (keine rekursive Fehlerbehandlung)
+ });
+ }
+}
+
+// Standard-Initialisierung
+Logger.initialize();
diff --git a/resources/js/core/logger.js b/resources/js/core/logger.js
new file mode 100644
index 00000000..a92c8446
--- /dev/null
+++ b/resources/js/core/logger.js
@@ -0,0 +1,37 @@
+import {monitor} from "./frameloop";
+
+export class Logger {
+ static enabled = true //import.meta.env.MODE !== 'production';
+
+ static log(...args) {
+ this._write('log', '[LOG]', args);
+ }
+
+ static warn(...args) {
+ this._write('warn', '[WARN]', args)
+ }
+
+ static info(...args) {
+ this._write('info', '[INFO]', args);
+ }
+
+ static error(...args) {
+ this._write('error', '[ERROR]', args);
+ }
+
+ static _write(consoleMethod, prefix, args) {
+ if(!this.enabled) return;
+
+ const date = new Date();
+ const timestamp = date.toLocaleTimeString('de-DE');
+
+ const msg = `${prefix} [${timestamp}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
+
+ if(typeof console[consoleMethod] === 'function') {
+ console[consoleMethod](msg);
+ }
+
+ monitor?.log(msg)
+
+ }
+}
diff --git a/resources/js/core/navigateTo.js b/resources/js/core/navigateTo.js
new file mode 100644
index 00000000..ae9c84ca
--- /dev/null
+++ b/resources/js/core/navigateTo.js
@@ -0,0 +1,51 @@
+// modules/core/navigateTo.js
+import { getPrefetched } from './ClickManager.js';
+
+/**
+ * Funktion für SPA-Navigation mit optionaler ViewTransition
+ * Diese Funktion kann als generische Utility überall eingebunden werden!
+ *
+ * @param {string} href - Die Ziel-URL
+ * @param {object} options - Steueroptionen
+ * @param {boolean} [options.replace=false] - history.replaceState statt push
+ * @param {boolean} [options.viewTransition=false] - ViewTransition API verwenden
+ * @param {Function} [options.onUpdate] Wird nach Laden des HTML aufgerufen (html)
+ * @param {Function} [options.getPrefetched] Funktion zum Abrufen gecachter Daten (optional)
+ */
+export async function navigateTo(href, options = {}) {
+ const {
+ replace = false,
+ viewTransition = false,
+ onUpdate = html => {},
+ getPrefetched = null
+ /*onUpdate = (html) => {
+ const container = document.querySelector('main');
+ if (container) container.innerHTML = html;
+ }*/
+ } = options;
+
+ const fetchHtml = async () => {
+
+ let html = '';
+ if(getPrefetched) {
+ html = getPrefetched(href) || '';
+ }
+
+ if(!html) {
+ html = await fetch(href).then(r => r.text());
+ }
+
+ onUpdate(html);
+ if(replace) {
+ history.replaceState(null, '', href);
+ } else {
+ history.pushState(null, '', href);
+ }
+ };
+
+ if (viewTransition && document.startViewTransition) {
+ document.startViewTransition(fetchHtml);
+ } else {
+ await fetchHtml();
+ }
+}
diff --git a/resources/js/core/removeModules.js b/resources/js/core/removeModules.js
new file mode 100644
index 00000000..4419c70a
--- /dev/null
+++ b/resources/js/core/removeModules.js
@@ -0,0 +1,14 @@
+// modules/core/removeModules.js
+import { EventManager } from './EventManager.js';
+
+/**
+ * Entfernt alle bekannten Events zu einem Array von Modulen
+ * Optional: Logging oder Cleanup-Hooks ergänzbar
+ */
+export function removeModules(moduleList) {
+ if (!Array.isArray(moduleList)) return;
+
+ moduleList.forEach((module) => {
+ EventManager.removeModule(module);
+ });
+}
diff --git a/resources/js/core/router.js b/resources/js/core/router.js
new file mode 100644
index 00000000..efdba4df
--- /dev/null
+++ b/resources/js/core/router.js
@@ -0,0 +1,138 @@
+// modules/core/router.js
+import { init as initClickManager } from './ClickManager.js';
+import { navigateTo } from './navigateTo.js';
+
+const routes = new Map();
+const wildcards = [];
+const guards = new Map();
+
+let currentRoute = null;
+let layoutCallback = null;
+let metaCallback = null;
+
+/**
+ * Registriert eine neue Route mit optionalem Handler
+ * @param {string} path - Pfad der Route
+ * @param {Function} handler - Callback bei Treffer
+ */
+export function defineRoute(path, handler) {
+ if (path.includes('*')) {
+ wildcards.push({ pattern: new RegExp('^' + path.replace('*', '.*') + '$'), handler });
+ } else {
+ routes.set(path, handler);
+ }
+}
+
+/**
+ * Definiert einen Guard für eine Route
+ * @param {string} path - Pfad
+ * @param {Function} guard - Guard-Funktion (return false = block)
+ */
+export function guardRoute(path, guard) {
+ guards.set(path, guard);
+}
+
+/**
+ * Gibt die aktuell aktive Route zurück
+ */
+export function getRouteContext() {
+ return currentRoute;
+}
+
+/**
+ * Setzt eine Callback-Funktion für dynamische Layout-Switches
+ */
+export function onLayoutSwitch(fn) {
+ layoutCallback = fn;
+}
+
+/**
+ * Setzt eine Callback-Funktion für Meta-Daten (z. B. title, theme)
+ */
+export function onMetaUpdate(fn) {
+ metaCallback = fn;
+}
+
+function matchRoute(href) {
+ if (routes.has(href)) return routes.get(href);
+ for (const entry of wildcards) {
+ if (entry.pattern.test(href)) return entry.handler;
+ }
+ return null;
+}
+
+function runGuard(href) {
+ const guard = guards.get(href);
+ return guard ? guard(href) !== false : true;
+}
+
+function extractMetaFromHTML(html) {
+ const temp = document.createElement('div');
+ temp.innerHTML = html;
+ const metaTags = {};
+ temp.querySelectorAll('[data-meta]').forEach(el => {
+ for (const attr of el.attributes) {
+ if (attr.name.startsWith('data-meta-')) {
+ const key = attr.name.replace('data-meta-', '');
+ metaTags[key] = attr.value;
+ }
+ }
+ })
+ //const title = temp.querySelector('[data-meta-title]')?.getAttribute('data-meta-title');
+ //const theme = temp.querySelector('[data-meta-theme]')?.getAttribute('data-meta-theme');
+ return { metaTags };
+}
+
+function animateLayoutSwitch(type) {
+ document.body.dataset.layout = type;
+ document.body.classList.add('layout-transition');
+ setTimeout(() => document.body.classList.remove('layout-transition'), 300);
+}
+
+/**
+ * Startet den Router
+ */
+export function startRouter() {
+ initClickManager((href, link, options) => {
+ if (!runGuard(href)) return;
+
+ if (options.modal) {
+ const handler = matchRoute(href);
+ currentRoute = { href, modal: true, link, options };
+ handler?.(currentRoute);
+ layoutCallback?.(currentRoute);
+ metaCallback?.(currentRoute);
+ } else {
+ navigateTo(href, {
+ ...options,
+ onUpdate: (html) => {
+ const container = document.querySelector('main');
+ if (container) container.innerHTML = html;
+
+ const routeHandler = matchRoute(href);
+ currentRoute = { href, html, modal: false };
+
+ const meta = extractMetaFromHTML(html);
+ if (meta.title) document.title = meta.title;
+ if (meta.theme) document.documentElement.style.setProperty('--theme-color', meta.theme);
+
+ routeHandler?.(currentRoute);
+ layoutCallback?.(currentRoute);
+ metaCallback?.(currentRoute);
+ }
+ });
+ }
+ });
+
+ // Bei Seitenstart erste Route prüfen
+ window.addEventListener('DOMContentLoaded', () => {
+ const href = location.pathname;
+ const routeHandler = matchRoute(href);
+ currentRoute = { href, modal: false };
+ routeHandler?.(currentRoute);
+ layoutCallback?.(currentRoute);
+ metaCallback?.(currentRoute);
+ });
+}
+
+export { animateLayoutSwitch, extractMetaFromHTML };
diff --git a/resources/js/core/secureLogger.js b/resources/js/core/secureLogger.js
new file mode 100644
index 00000000..0cb97739
--- /dev/null
+++ b/resources/js/core/secureLogger.js
@@ -0,0 +1,99 @@
+/**
+ * Erweiterter Logger mit Request-ID-Unterstützung und Server-Kommunikation.
+ */
+export class SecureLogger {
+ static enabled = true;
+ static apiEndpoint = '/api/log';
+ static requestId = null;
+
+ static log(...args) {
+ this._write('log', args);
+ }
+
+ static warn(...args) {
+ this._write('warn', args)
+ }
+
+ static info(...args) {
+ this._write('info', args);
+ }
+
+ static error(...args) {
+ this._write('error', args);
+ }
+
+ static _write(level, args) {
+ if(!this.enabled) return;
+
+ const date = new Date();
+ const timestamp = date.toLocaleTimeString('de-DE');
+ const requestIdStr = this.requestId ? `[${this.requestId}] ` : '';
+
+ const message = args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ');
+ const formattedMessage = `[${timestamp}] ${requestIdStr}[${level.toUpperCase()}] ${message}`;
+
+ // Lokales Logging in der Konsole
+ console[level](formattedMessage);
+
+ // An den Server senden (wenn nicht in Produktion)
+ this._sendToServer(level, message, args.find(a => typeof a === 'object'));
+ }
+
+ static _sendToServer(level, message, context) {
+ fetch(this.apiEndpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Request-ID': this.requestId || ''
+ },
+ body: JSON.stringify({
+ level,
+ message,
+ context
+ })
+ })
+ .then(response => {
+ // Request-ID aus dem Header extrahieren
+ const requestId = response.headers.get('X-Request-ID');
+ if (requestId) {
+ // Nur den ID-Teil ohne Signatur speichern
+ const idPart = requestId.split('.')[0];
+ if (idPart) {
+ this.requestId = idPart;
+ }
+ }
+ return response.json();
+ })
+ .catch(err => {
+ console.error('Fehler beim Senden des Logs:', err);
+ });
+ }
+
+ /**
+ * Request-ID aus dem Document laden
+ */
+ static initFromDocument() {
+ // Versuche die Request-ID aus einem Meta-Tag zu lesen
+ const meta = document.querySelector('meta[name="request-id"]');
+ if (meta) {
+ const fullRequestId = meta.getAttribute('content');
+ // Nur den ID-Teil ohne Signatur verwenden
+ this.requestId = fullRequestId.split('.')[0] || null;
+ }
+ }
+}
+
+// Request-ID initialisieren, wenn das DOM geladen ist
+document.addEventListener('DOMContentLoaded', () => {
+ SecureLogger.initFromDocument();
+
+ // Abfangen aller unbehandelten Fehler und Logging
+ window.addEventListener('error', (event) => {
+ SecureLogger.error('Unbehandelter Fehler:', event.error || event.message);
+ });
+
+ // Abfangen aller unbehandelten Promise-Rejects
+ window.addEventListener('unhandledrejection', (event) => {
+ SecureLogger.error('Unbehandelte Promise-Ablehnung:', event.reason);
+ });
+});
diff --git a/resources/js/core/state.js b/resources/js/core/state.js
new file mode 100644
index 00000000..760178e7
--- /dev/null
+++ b/resources/js/core/state.js
@@ -0,0 +1,213 @@
+// src/core/state.js
+
+const globalState = new Map();
+
+/**
+ * Normale State-Erstellung (nicht persistent)
+ */
+export function createState(key, initialValue) {
+ if (!globalState.has(key)) globalState.set(key, initialValue);
+
+ return {
+ get() {
+ return globalState.get(key);
+ },
+ set(value) {
+ globalState.set(key, value);
+ dispatchEvent(new CustomEvent('statechange', {
+ detail: { key, value }
+ }));
+ },
+ subscribe(callback) {
+ const handler = (event) => {
+ if (event.detail.key === key) {
+ callback(event.detail.value);
+ }
+ };
+ addEventListener('statechange', handler);
+ return () => removeEventListener('statechange', handler);
+ }
+ };
+}
+
+/**
+ * Persistent State via localStorage
+ */
+export function createPersistentState(key, defaultValue) {
+ const stored = localStorage.getItem(`state:${key}`);
+ const initial = stored !== null ? JSON.parse(stored) : defaultValue;
+
+ const state = createState(key, initial);
+
+ state.subscribe((val) => {
+ localStorage.setItem(`state:${key}`, JSON.stringify(val));
+ });
+
+ return state;
+}
+
+/**
+ * Zugriff auf alle States (intern)
+ */
+export function getRawState() {
+ return globalState;
+}
+
+/**
+ * Elementbindung: Text, Attribut, Klasse oder Custom-Callback
+ */
+export function bindStateToElement({ state, element, property = 'text', attributeName = null }) {
+ const apply = (value) => {
+ if (property === 'text') {
+ element.textContent = value;
+ } else if (property === 'attr' && attributeName) {
+ element.setAttribute(attributeName, value);
+ } else if (property === 'class') {
+ element.className = value;
+ } else if (typeof property === 'function') {
+ property(element, value);
+ }
+ };
+
+ apply(state.get());
+ return state.subscribe(apply);
+}
+
+/**
+ * Mehrere Elemente gleichzeitig binden
+ */
+export function bindStateToElements({ state, elements, ...rest }) {
+ return elements.map(el => bindStateToElement({ state, element: el, ...rest }));
+}
+
+/**
+ * Declarative Auto-Bindings via data-bind Attribute
+ */
+export function initStateBindings() {
+ document.querySelectorAll('[data-bind]').forEach(el => {
+ const key = el.getAttribute('data-bind');
+ const state = createState(key, '');
+ bindStateToElement({ state, element: el, property: 'text' });
+ });
+
+ document.querySelectorAll('[data-bind-attr]').forEach(el => {
+ const key = el.getAttribute('data-bind-attr');
+ const attr = el.getAttribute('data-bind-attr-name') || 'value';
+ const state = createState(key, '');
+ bindStateToElement({ state, element: el, property: 'attr', attributeName: attr });
+ });
+
+ document.querySelectorAll('[data-bind-class]').forEach(el => {
+ const key = el.getAttribute('data-bind-class');
+ const state = createState(key, '');
+ bindStateToElement({ state, element: el, property: 'class' });
+ });
+
+ document.querySelectorAll('[data-bind-input]').forEach(el => {
+ const key = el.getAttribute('data-bind-input');
+ const persistent = el.hasAttribute('data-persistent');
+ const state = persistent ? createPersistentState(key, '') : createState(key, '');
+
+ bindInputToState(el, state);
+ });
+}
+
+/**
+ * Zwei-Wege-Bindung für Formulareingaben
+ */
+export function bindInputToState(inputEl, state) {
+ // Initialwert setzen
+ if (inputEl.type === 'checkbox') {
+ inputEl.checked = !!state.get();
+ } else {
+ inputEl.value = state.get();
+ }
+
+ // DOM → State
+ inputEl.addEventListener('input', () => {
+ if (inputEl.type === 'checkbox') {
+ state.set(inputEl.checked);
+ } else {
+ state.set(inputEl.value);
+ }
+ });
+
+ // State → DOM
+ return state.subscribe((val) => {
+ if (inputEl.type === 'checkbox') {
+ inputEl.checked = !!val;
+ } else {
+ inputEl.value = val;
+ }
+ });
+}
+
+
+/**
+ * Berechneter Zustand auf Basis anderer States
+ */
+export function createComputedState(dependencies, computeFn) {
+ let value = computeFn(...dependencies.map(d => d.get()));
+ const key = `computed:${Math.random().toString(36).slice(2)}`;
+ const state = createState(key, value);
+
+ dependencies.forEach(dep => {
+ dep.subscribe(() => {
+ const newValue = computeFn(...dependencies.map(d => d.get()));
+ state.set(newValue);
+ });
+ });
+
+ return state;
+}
+
+/**
+ * Aktiviert Undo/Redo für einen State
+ */
+export function enableUndoRedoForState(state) {
+ const history = [];
+ let index = -1;
+ let lock = false;
+
+ const record = (val) => {
+ if (lock) return;
+ history.splice(index + 1); // zukünftige States verwerfen
+ history.push(val);
+ index++;
+ };
+
+ record(state.get());
+
+ const unsubscribe = state.subscribe((val) => {
+ if (!lock) record(val);
+ });
+
+ return {
+ undo() {
+ if (index > 0) {
+ lock = true;
+ index--;
+ state.set(history[index]);
+ lock = false;
+ }
+ },
+ redo() {
+ if (index < history.length - 1) {
+ lock = true;
+ index++;
+ state.set(history[index]);
+ lock = false;
+ }
+ },
+ destroy: unsubscribe
+ };
+}
+
+/**
+ * Dev-Konsole zur Nachverfolgung von State-Änderungen
+ */
+export function enableStateLogging(state, label = 'state') {
+ state.subscribe((val) => {
+ console.debug(`[State Change: ${label}]`, val);
+ });
+}
diff --git a/resources/js/core/useEvent.js b/resources/js/core/useEvent.js
new file mode 100644
index 00000000..1090c2b8
--- /dev/null
+++ b/resources/js/core/useEvent.js
@@ -0,0 +1,23 @@
+// modules/core/useEvent.js
+import { EventManager } from './EventManager.js';
+
+/**
+ * Vereinfachte Kurzform zur Registrierung eines EventListeners
+ *
+ * @param {EventTarget} target - Das Element, auf das der Event gehört
+ * @param {string} type - Der Eventtyp (z.B. 'click')
+ * @param {Function} handler - Die Callback-Funktion
+ * @param {object} meta - Optionen oder automatisch aus `import.meta` (optional)
+ * @param {Object|boolean} options - EventListener-Optionen
+ */
+export function useEvent(target, type, handler, meta = import.meta, options = false) {
+ const module = typeof meta === 'string'
+ ? meta
+ : (meta.url?.split('/').slice(-2, -1)[0] || 'unknown');
+
+ EventManager.add(target, type, handler, { module, options });
+
+ return () => {
+ EventManager.removeModule(module);
+ }
+}
diff --git a/resources/js/docs/SCROLLER.md b/resources/js/docs/SCROLLER.md
new file mode 100644
index 00000000..20a943a8
--- /dev/null
+++ b/resources/js/docs/SCROLLER.md
@@ -0,0 +1,115 @@
+# 📦 Scroll-Module – Anwendung und Integration
+
+Dieses Projekt enthält mehrere leichtgewichtige JavaScript-Module zur Gestaltung interaktiver Scroll-Erlebnisse.
+
+## 🔧 Modulübersicht
+
+| Modul | Funktion |
+|------------------|-------------------------------------|
+| `scroll-timeline` | Zustandswechsel bei Scrollschritten |
+| `parallax` | Parallax-Scrolling-Effekte |
+| `sticky-steps` | Scrollbasierte Sticky-Kapitel |
+
+---
+
+## 1️⃣ scroll-timeline
+
+### 📄 Verwendung
+```html
+
+
+
+```
+
+### ⚙️ Konfiguration
+```js
+init({
+ attribute: 'data-scroll-step', // Attributname
+ triggerPoint: 0.4, // Auslösehöhe (in % der Viewporthöhe)
+ once: false // true = nur einmal triggern
+});
+```
+
+### 🎯 Callbacks (in steps.js)
+```js
+export const scrollSteps = {
+ onEnter(index, el) {
+ // Element wird aktiv
+ },
+ onLeave(index, el) {
+ // Element verlässt Fokus (wenn once = false)
+ }
+};
+```
+
+---
+
+## 2️⃣ parallax
+
+### 📄 Verwendung
+```html
+
+```
+
+### ⚙️ Konfiguration
+```js
+init({
+ selector: '[data-parallax]', // Ziel-Selektor
+ speedAttr: 'data-parallax-speed', // Attribut für Geschwindigkeit
+ defaultSpeed: 0.5 // Fallback-Geschwindigkeit
+});
+```
+
+### 💡 Hinweis
+Je niedriger die `speed`, desto "langsamer" scrollt das Element.
+
+---
+
+## 3️⃣ sticky-steps
+
+### 📄 Verwendung
+```html
+
+
Kapitel 1
+
Kapitel 2
+
+```
+
+### ⚙️ Konfiguration
+```js
+init({
+ containerSelector: '[data-sticky-container]',
+ stepSelector: '[data-sticky-step]',
+ activeClass: 'is-sticky-active'
+});
+```
+
+### 🎨 CSS-Vorschlag
+```css
+[data-sticky-step] {
+ position: sticky;
+ top: 20vh;
+ opacity: 0.3;
+ transition: opacity 0.3s ease;
+}
+
+.is-sticky-active {
+ opacity: 1;
+}
+```
+
+---
+
+## 🚀 Integration in dein Framework
+
+- Jedes Modul exportiert eine `init()`-Funktion
+- Wird automatisch über `modules/index.js` geladen
+- Konfiguration erfolgt über `modules/config.js`
+
+```js
+export const moduleConfig = {
+ 'scroll-timeline': { once: false },
+ 'parallax': { defaultSpeed: 0.3 },
+ 'sticky-steps': { activeClass: 'active' }
+};
+```
diff --git a/resources/js/docs/SCROLL_LOOP.md b/resources/js/docs/SCROLL_LOOP.md
new file mode 100644
index 00000000..fcde32b2
--- /dev/null
+++ b/resources/js/docs/SCROLL_LOOP.md
@@ -0,0 +1,9 @@
+| Attribut | Typ | Standardwert | Beschreibung |
+| ------------------- | ---------------------------------------------------------- | ------------- | ------------------------------------------------------------------------- |
+| `data-scroll-loop` | — | — | Aktiviert das Scroll-Loop-Modul auf dem Element |
+| `data-scroll-speed` | `float` | `0.2` | Faktor für Scrollgeschwindigkeit – positiv oder negativ |
+| `data-scroll-axis` | `"x"` \| `"y"` | `"y"` | Achse der Bewegung |
+| `data-scroll-type` | `"translate"` \| `"rotate"` \| `"background"` \| `"scale"` | `"translate"` | Art der Scrollanimation |
+| `data-loop-offset` | `float` | `0` | Start-Offset in Pixeln (nützlich für Desynchronisation mehrerer Elemente) |
+| `data-loop-limit` | `number` (Pixelwert) | — | Obergrenze für Scrollbereich – ab dieser Position stoppt die Animation |
+| `data-loop-pause` | `"true"` \| `"false"` | — | Bei `"true"` wird die Animation bei Hover oder aktivem Element pausiert |
diff --git a/resources/js/docs/prefetch.md b/resources/js/docs/prefetch.md
new file mode 100644
index 00000000..2b3545fb
--- /dev/null
+++ b/resources/js/docs/prefetch.md
@@ -0,0 +1,29 @@
+
+---
+
+## 🔧 Neue Funktionen:
+
+### `getPrefetched(href: string): string | null`
+
+* Gibt den HTML-Text zurück, **falls gültig gecached**
+* Sonst `null`
+
+### `prefetchHref(href: string): void`
+
+* Manuelles Prefetching von URLs
+* Wird nur ausgeführt, wenn nicht bereits gecached
+
+---
+
+## 🧪 Verwendung:
+
+```js
+import { getPrefetched, prefetchHref } from './core/click-manager.js';
+
+prefetchHref('/about.html'); // manuelles Prefetching
+
+const html = getPrefetched('/about.html');
+if (html) {
+ render(html);
+}
+```
diff --git a/resources/js/docs/router.md b/resources/js/docs/router.md
new file mode 100644
index 00000000..492a0fc5
--- /dev/null
+++ b/resources/js/docs/router.md
@@ -0,0 +1,34 @@
+✅ Dein Router-Modul wurde erweitert um:
+
+---
+
+## 🎭 Layout-Animation
+
+```js
+import { animateLayoutSwitch } from './router.js';
+
+onLayoutSwitch(ctx => {
+ const type = ctx.href.startsWith('/studio') ? 'studio' : 'default';
+ animateLayoutSwitch(type);
+});
+```
+
+→ Fügt `data-layout="…"`, animiert via `.layout-transition` (z. B. Fade)
+
+---
+
+## 📝 Meta-Daten aus HTML
+
+```html
+
+```
+
+→ Beim Laden des HTML werden automatisch:
+
+* `document.title` gesetzt
+* CSS-Variable `--theme-color` aktualisiert
+
+---
+
+Wenn du möchtest, kann ich dir nun einen ``-Updater bauen oder eine ViewTransition speziell für Layoutwechsel. Sag einfach:
+**„Ja, bitte meta\[name=theme-color]“** oder **„ViewTransition für Layout“**.
diff --git a/resources/js/docs/state.md b/resources/js/docs/state.md
new file mode 100644
index 00000000..6e15b1d3
--- /dev/null
+++ b/resources/js/docs/state.md
@@ -0,0 +1,197 @@
+# 📦 Framework-Module: `state.js` & `events.js`
+
+Diese Dokumentation beschreibt die Verwendung der globalen **State-Verwaltung** und des **Event-/Action-Dispatching** in deinem Framework.
+
+---
+
+## 📁 `core/state.js` – Globale Zustandsverwaltung
+
+### 🔹 `createState(key, initialValue)`
+
+Erzeugt einen flüchtigen (nicht-persistenten) globalen Zustand.
+
+```js
+const username = createState('username', 'Gast');
+
+console.log(username.get()); // → "Gast"
+
+username.set('Michael');
+```
+
+---
+
+### 🔹 `createPersistentState(key, defaultValue)`
+
+Wie `createState`, aber mit `localStorage`-Speicherung.
+
+```js
+const theme = createPersistentState('theme', 'light');
+theme.set('dark'); // Wird gespeichert
+```
+
+---
+
+### 🔹 `state.subscribe(callback)`
+
+Reagiert auf Änderungen des Zustands.
+
+```js
+username.subscribe((val) => {
+ console.log('Neuer Benutzername:', val);
+});
+```
+
+---
+
+### 🔹 `bindStateToElement({ state, element, property, attributeName })`
+
+Bindet einen State an ein DOM-Element.
+
+```js
+bindStateToElement({
+ state: username,
+ element: document.querySelector('#userDisplay'),
+ property: 'text'
+});
+```
+
+```html
+
+```
+
+---
+
+### 🔹 `bindInputToState(inputElement, state)`
+
+Erzeugt eine **Zwei-Wege-Bindung** zwischen Eingabefeld und State.
+
+```js
+bindInputToState(document.querySelector('#nameInput'), username);
+```
+
+```html
+
+```
+
+---
+
+### 🔹 `initStateBindings()`
+
+Initialisiert alle Elemente mit `data-bind-*` automatisch.
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```js
+initStateBindings(); // Einmalig in initApp() aufrufen
+```
+
+---
+
+### 🔹 `createComputedState([dependencies], computeFn)`
+
+Leitet einen State aus anderen ab.
+
+```js
+const first = createState('first', 'Max');
+const last = createState('last', 'Mustermann');
+
+const fullName = createComputedState([first, last], (f, l) => \`\${f} \${l}\`);
+```
+
+---
+
+### 🔹 `enableUndoRedoForState(state)`
+
+Fügt Undo/Redo-Funktionalität hinzu.
+
+```js
+const message = createState('message', '');
+const history = enableUndoRedoForState(message);
+
+history.undo();
+history.redo();
+```
+
+---
+
+### 🔹 `enableStateLogging(state, label?)`
+
+Gibt alle Änderungen in der Konsole aus.
+
+```js
+enableStateLogging(username, 'Benutzername');
+```
+
+---
+
+## 📁 `core/events.js` – EventBus & ActionDispatcher
+
+### 🔹 `on(eventName, callback)`
+
+Abonniert benutzerdefinierte Events.
+
+```js
+on('user:login', user => {
+ console.log('Login:', user.name);
+});
+```
+
+---
+
+### 🔹 `emit(eventName, payload)`
+
+Sendet ein Event global.
+
+```js
+emit('user:login', { name: 'Max', id: 123 });
+```
+
+---
+
+### 🔹 `registerActionListener(callback)`
+
+Reagiert auf alle ausgelösten Aktionen.
+
+```js
+registerActionListener(({ type, payload }) => {
+ if (type === 'counter/increment') {
+ payload.state.set(payload.state.get() + 1);
+ }
+});
+```
+
+---
+
+### 🔹 `dispatchAction(type, payload?)`
+
+Löst eine benannte Aktion aus.
+
+```js
+dispatchAction('counter/increment', { state: counter });
+```
+
+---
+
+## ✅ Zusammenfassung
+
+| Funktion | Zweck |
+|----------|-------|
+| `createState` / `createPersistentState` | Reaktiver globaler Zustand |
+| `bindStateToElement` / `bindInputToState` | DOM-Bindings |
+| `createComputedState` | Abhängiger Zustand |
+| `enableUndoRedoForState` | History / Undo |
+| `enableStateLogging` | Debugging |
+| `on` / `emit` | Lose gekoppelte Event-Kommunikation |
+| `registerActionListener` / `dispatchAction` | Zentrale Aktionssteuerung |
diff --git a/resources/js/main.js b/resources/js/main.js
index 81faac4f..ca3c820b 100644
--- a/resources/js/main.js
+++ b/resources/js/main.js
@@ -1 +1,48 @@
import '../css/styles.css';
+
+import { initApp } from './core/init.js';
+
+// resources/js/app.js (dein Einstiegspunkt)
+import { registerSW } from 'virtual:pwa-register';
+
+const updateSW = registerSW({
+ onNeedRefresh() {
+ const reload = confirm('🔄 Neue Version verfügbar. Seite neu laden?');
+ if (reload) updateSW(true);
+ },
+ onOfflineReady() {
+ console.log('📦 Offline-Inhalte sind bereit.');
+ }
+});
+
+registerSW({
+ onRegistered(reg) {
+ console.log('Service Worker registriert:', reg);
+ },
+ onRegisterError(error) {
+ console.error('Service Worker Fehler:', error);
+ }
+});
+
+document.addEventListener("DOMContentLoaded", () => {
+ initApp();
+});
+
+function isHtmlAttributeSupported(elementName, attribute) {
+ const element = document.createElement(elementName);
+ return attribute in element;
+}
+
+
+let closedAttr = document.getElementById('my-dialog');
+if(! 'closedby' in closedAttr) {
+ alert('oh no');
+}
+
+/*
+if (isHtmlAttributeSupported('dialog', 'closedby')) {
+ alert('Attribut wird unterstützt!');
+} else {
+ alert('Nicht unterstützt!');
+}
+*/
diff --git a/resources/js/modules/config.js b/resources/js/modules/config.js
new file mode 100644
index 00000000..ec5a7159
--- /dev/null
+++ b/resources/js/modules/config.js
@@ -0,0 +1,25 @@
+export const moduleConfig = {
+ 'noise': {
+ selector: '.noise-overlay',
+ toggleKey: 'g',
+ className: 'grainy',
+ enableTransition: true
+ },
+ 'shortcut-handler.js': {
+ debug: false
+ },
+ 'scrollfx': {
+ selector: '.fade-in-on-scroll, .zoom-in',
+ offset: 0.8,
+ baseDelay: 0.075,
+ once: true
+ },
+ 'scroll-timeline': {
+ attribute: 'data-scroll-step',
+ triggerPoint: 0.4,
+ once: false
+ },
+ 'smooth-scroll': {
+ 'speed': 0.2,
+ }
+};
diff --git a/resources/js/modules/example-module/index.js b/resources/js/modules/example-module/index.js
new file mode 100644
index 00000000..14a3cd6d
--- /dev/null
+++ b/resources/js/modules/example-module/index.js
@@ -0,0 +1,37 @@
+// modules/example-module/index.js
+import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
+
+let frameId = 'example-module';
+let resizeHandler = null;
+
+export function init(config = {}) {
+ console.log('[example-module] init');
+
+ // z. B. Event-Listener hinzufügen
+ resizeHandler = () => {
+ console.log('Fenstergröße geändert');
+ };
+ window.addEventListener('resize', resizeHandler);
+
+ // Scroll- oder Frame-Logik
+ registerFrameTask(frameId, () => {
+ // wiederkehrende Aufgabe
+ const scrollY = window.scrollY;
+ // ggf. transformieren oder Werte speichern
+ }, { autoStart: true });
+}
+
+export function destroy() {
+ console.log('[example-module] destroy');
+
+ // EventListener entfernen
+ if (resizeHandler) {
+ window.removeEventListener('resize', resizeHandler);
+ resizeHandler = null;
+ }
+
+ // FrameTask entfernen
+ unregisterFrameTask(frameId);
+
+ // weitere Aufräumarbeiten, z.B. Observer disconnect
+}
diff --git a/resources/js/modules/index.js b/resources/js/modules/index.js
new file mode 100644
index 00000000..3b927a58
--- /dev/null
+++ b/resources/js/modules/index.js
@@ -0,0 +1,47 @@
+import { moduleConfig } from './config.js';
+import { Logger } from '../core/logger.js';
+
+export const activeModules = new Map(); // key: modulename → { mod, config }
+
+export async function registerModules() {
+ const modules = import.meta.glob('./*/index.js', { eager: true });
+
+ const domModules = new Set(
+ Array.from(document.querySelectorAll('[data-module]')).map(el => el.dataset.module).filter(Boolean)
+ );
+
+ const usedModules = new Set(domModules);
+ const fallbackMode = usedModules.size === 0;
+
+ Object.entries(modules).forEach(([path, mod]) => {
+ const name = path.split('/').slice(-2, -1)[0]; // z.B. "noise-toggle.js"
+ const config = moduleConfig[name] || {};
+
+ if(!fallbackMode && !usedModules.has(name)) {
+ Logger.info(`⏭️ [Module] Skipped (not used in DOM): ${name}`);
+ return;
+ }
+
+ if (typeof mod.init === 'function') {
+ mod.init(config);
+ activeModules.set(name, { mod, config });
+ Logger.info(`✅ [Module] Initialized: ${name}`);
+ } else {
+ Logger.warn(`⛔ [Module] No init() in ${name}`);
+ }
+ });
+
+ if (fallbackMode) {
+ Logger.info('⚠️ [Module] No data-module usage detected, fallback to full init mode');
+ }
+}
+
+export function destroyModules() {
+ for (const [name, { mod }] of activeModules.entries()) {
+ if (typeof mod.destroy === 'function') {
+ mod.destroy();
+ Logger.info(`🧹 [Module] Destroyed: ${name}`);
+ }
+ }
+ activeModules.clear();
+}
diff --git a/resources/js/modules/inertia-scroll/index.js b/resources/js/modules/inertia-scroll/index.js
new file mode 100644
index 00000000..4fc16879
--- /dev/null
+++ b/resources/js/modules/inertia-scroll/index.js
@@ -0,0 +1,63 @@
+// modules/inertia-scroll/index.js
+import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
+
+let taskId = 'inertia-scroll';
+let velocity = 0;
+let lastY = window.scrollY;
+let active = false;
+let scrollEndTimer;
+let damping = 0.9;
+let minVelocity = 0.2;
+
+export function init(config = {}) {
+ damping = typeof config.damping === 'number' ? config.damping : 0.9;
+
+ minVelocity = typeof config.minVelocity === 'number' ? contig.minVelocity : 0.1;
+
+ window.addEventListener('scroll', onScroll, { passive: true });
+
+ registerFrameTask(taskId, () => {
+ const root = document.documentElement;
+ const scrollY = window.scrollY;
+ const delta = scrollY - lastY;
+ const direction = delta > 0 ? 'down' : delta < 0 ? 'up' : 'none';
+ const speed = Math.abs(delta);
+
+ if (!active && Math.abs(velocity) > minVelocity) {
+ window.scrollTo(0, scrollY + velocity);
+ velocity *= damping; // Trägheit / Dämpfung
+ root.dataset.scrollState = 'inertia';
+ } else if (active) {
+ velocity = delta;
+ lastY = scrollY;
+ root.dataset.scrollState = 'active';
+ } else {
+ delete root.dataset.scrollState;
+ }
+
+ root.dataset.scrollDirection = direction;
+ root.dataset.scrollSpeed = speed.toFixed(2);
+ }, { autoStart: true });
+}
+
+function onScroll() {
+ active = true;
+ clearTimeout(scrollEndTimer);
+ scrollEndTimer = setTimeout(() => {
+ active = false;
+ }, 50);
+}
+
+export function destroy() {
+ window.removeEventListener('scroll', onScroll);
+ unregisterFrameTask(taskId);
+ velocity = 0;
+ lastY = window.scrollY;
+ active = false;
+ clearTimeout(scrollEndTimer);
+
+ const root = document.documentElement;
+ delete root.dataset.scrollState;
+ delete root.dataset.scrollDirection;
+ delete root.dataset.scrollSpeed;
+}
diff --git a/resources/js/modules/lightbox-trigger/index.js b/resources/js/modules/lightbox-trigger/index.js
new file mode 100644
index 00000000..2e1d5035
--- /dev/null
+++ b/resources/js/modules/lightbox-trigger/index.js
@@ -0,0 +1,26 @@
+// modules/lightbox-trigger/index.js
+import { UIManager } from '../ui/UIManager.js';
+import { Logger } from '../../core/logger.js';
+import { useEvent } from '../../core/useEvent.js';
+
+function onClick(e) {
+ const img = e.target.closest('[data-lightbox]');
+ if (!img || img.tagName !== 'IMG') return;
+
+ e.preventDefault();
+
+ UIManager.open('lightbox', {
+ content: `
`
+ });
+}
+
+export function init() {
+ Logger.info('[lightbox-trigger] init');
+ useEvent(document, 'click', onClick);
+}
+
+export function destroy() {
+ Logger.info('[lightbox-trigger] destroy');
+ // Automatische Entfernung über EventManager erfolgt über Modulkennung
+ // Kein direkter Aufruf nötig, solange removeModules() global verwendet wird
+}
diff --git a/resources/js/modules/noise/index.js b/resources/js/modules/noise/index.js
new file mode 100644
index 00000000..01b0d988
--- /dev/null
+++ b/resources/js/modules/noise/index.js
@@ -0,0 +1,42 @@
+// js/noise-toggle.js
+
+import { Logger } from "../../core/logger.js";
+
+export function init(config = {}) {
+ Logger.log('Noise Toggle Init', config);
+
+ const {
+ selector = ".noise-overlay",
+ toggleKey = "g",
+ className = "grainy",
+ enableTransition = true,
+ } = config;
+
+ const body = document.body;
+ const noiseElement = document.querySelector(selector);
+
+ if (!noiseElement) return;
+
+ const isInput = noiseElement => ["input", "textarea"].includes(noiseElement.tagName.toLowerCase());
+
+ function update() {
+ if (enableTransition) {
+ noiseElement.classList.toggle("hidden", !body.classList.contains(className));
+ } else {
+ noiseElement.style.display = body.classList.contains(className) ? "block" : "none";
+ }
+ }
+
+ update();
+
+ document.addEventListener("keydown", (e) => {
+ if (
+ e.key.toLowerCase() === toggleKey &&
+ !e.ctrlKey && !e.metaKey && !e.altKey &&
+ !isInput(e.target)
+ ) {
+ body.classList.toggle(className);
+ update();
+ }
+ })
+}
diff --git a/resources/js/modules/parallax/index.js b/resources/js/modules/parallax/index.js
new file mode 100644
index 00000000..240c8058
--- /dev/null
+++ b/resources/js/modules/parallax/index.js
@@ -0,0 +1,27 @@
+// modules/parallax/index.js
+import { Logger } from '../../core/logger.js';
+import { registerFrameTask } from '../../core/frameloop.js';
+
+export function init(config = {}) {
+ Logger.info('Parallax init');
+
+ const defaultConfig = {
+ selector: '[data-parallax]',
+ speedAttr: 'data-parallax-speed',
+ defaultSpeed: 0.5
+ };
+
+ const settings = { ...defaultConfig, ...config };
+ const elements = document.querySelectorAll(settings.selector);
+
+ function updateParallax() {
+ const scrollY = window.scrollY;
+ elements.forEach(el => {
+ const speed = parseFloat(el.getAttribute(settings.speedAttr)) || settings.defaultSpeed;
+ const offset = scrollY * speed;
+ el.style.transform = `translateY(${offset}px)`;
+ });
+ }
+
+ registerFrameTask('parallax', updateParallax, { autoStart: true });
+}
diff --git a/resources/js/modules/scroll-loop/index.js b/resources/js/modules/scroll-loop/index.js
new file mode 100644
index 00000000..a1fafdbc
--- /dev/null
+++ b/resources/js/modules/scroll-loop/index.js
@@ -0,0 +1,66 @@
+// modules/scroll-loop/index.js
+import { registerFrameTask } from '../../core/frameloop.js';
+
+export function init(config = {}) {
+ const elements = document.querySelectorAll('[data-scroll-loop]');
+
+ elements.forEach(el => {
+ const type = el.dataset.scrollType || 'translate';
+ if (type === 'translate' && el.children.length === 1) {
+ const clone = el.firstElementChild.cloneNode(true);
+ clone.setAttribute('aria-hidden', 'true');
+ el.appendChild(clone);
+ }
+ });
+
+ registerFrameTask('scroll-loop', () => {
+ const scrollY = window.scrollY;
+ const scrollX = window.scrollX;
+
+ elements.forEach(el => {
+ const factor = parseFloat(el.dataset.scrollSpeed || config.speed || 0.2);
+ const axis = el.dataset.scrollAxis || 'y';
+ const type = el.dataset.scrollType || 'translate';
+ const pause = el.dataset.loopPause === 'true';
+ const offsetStart = parseFloat(el.dataset.loopOffset || 0);
+ const limit = parseFloat(el.dataset.loopLimit || 0);
+
+ const input = axis === 'x' ? scrollX : scrollY;
+ if (limit && input > limit) return;
+ if (pause && (el.matches(':hover') || el.matches(':active'))) return;
+
+ const offset = (input + offsetStart) * factor;
+
+ switch (type) {
+ case 'translate': {
+ const base = axis === 'x' ? el.offsetWidth : el.offsetHeight;
+ const value = -(offset % base);
+ const transform = axis === 'x' ? `translateX(${value}px)` : `translateY(${value}px)`;
+ el.style.transform = transform;
+ break;
+ }
+ case 'rotate': {
+ const deg = offset % 360;
+ el.style.transform = `rotate(${deg}deg)`;
+ break;
+ }
+ case 'background': {
+ const pos = offset % 100;
+ if (axis === 'x') {
+ el.style.backgroundPosition = `${pos}% center`;
+ } else {
+ el.style.backgroundPosition = `center ${pos}%`;
+ }
+ break;
+ }
+ case 'scale': {
+ const scale = 1 + Math.sin(offset * 0.01) * 0.1;
+ el.style.transform = `scale(${scale.toFixed(3)})`;
+ break;
+ }
+ default:
+ break;
+ }
+ });
+ }, { autoStart: true });
+}
diff --git a/resources/js/modules/scroll-timeline/index.js b/resources/js/modules/scroll-timeline/index.js
new file mode 100644
index 00000000..ec62ad57
--- /dev/null
+++ b/resources/js/modules/scroll-timeline/index.js
@@ -0,0 +1,53 @@
+// modules/scroll-timeline/index.js
+import { Logger } from '../../core/logger.js';
+import { scrollSteps } from './steps.js';
+import {registerFrameTask, unregisterFrameTask} from '../../core/frameloop.js';
+
+export function init(userConfig = {}) {
+ Logger.info('ScrollTimeline init');
+
+ const defaultConfig = {
+ attribute: 'data-scroll-step',
+ triggerPoint: 0.4,
+ once: true
+ };
+
+ const config = { ...defaultConfig, ...userConfig };
+
+ const steps = Array.from(document.querySelectorAll(`[${config.attribute}]`)).map(el => ({
+ el,
+ index: parseInt(el.getAttribute(config.attribute), 10),
+ active: false
+ }));
+
+ function update() {
+ const triggerY = window.innerHeight * config.triggerPoint;
+
+ steps.forEach(step => {
+ const rect = step.el.getBoundingClientRect();
+ const isVisible = rect.top < triggerY && rect.bottom > 0;
+
+ if (isVisible && !step.active) {
+ step.active = true;
+ step.el.classList.add('active');
+ Logger.log(`➡️ ENTER step ${step.index}`);
+ scrollSteps.onEnter?.(step.index, step.el);
+ }
+
+ if (!isVisible && step.active) {
+ step.active = false;
+ step.el.classList.remove('active');
+ Logger.log(`⬅️ LEAVE step ${step.index}`);
+ if (!config.once) {
+ scrollSteps.onLeave?.(step.index, step.el);
+ }
+ }
+ });
+ }
+
+ registerFrameTask('scroll-timeline', update, { autoStart: true });
+}
+
+export function destroy () {
+ unregisterFrameTask('scroll-timeline');
+}
diff --git a/resources/js/modules/scroll-timeline/steps.js b/resources/js/modules/scroll-timeline/steps.js
new file mode 100644
index 00000000..526acf87
--- /dev/null
+++ b/resources/js/modules/scroll-timeline/steps.js
@@ -0,0 +1,33 @@
+export const scrollSteps = {
+ onEnter(index, el) {
+ el.classList.add('active');
+ document.body.dataset.activeScrollStep = index;
+ console.log(`[ScrollStep] Enter: ${index}`);
+ // Beispielaktionen
+ if (index === 1) showIntro();
+ if (index === 2) activateChart();
+ if (index === 3) revealQuote();
+ },
+
+ onLeave(index, el) {
+ el.classList.remove('active');
+ el.style.transitionDelay = '';
+ console.log(`[ScrollStep] Leave: ${index}`);
+
+ if (document.body.dataset.activeScrollStep === String(index)) {
+ delete document.body.dataset.activeScrollStep;
+ }
+
+ if (index === 1) hideIntro();
+ if (index === 2) deactivateChart();
+ if (index === 3) hideQuote();
+ }
+};
+
+
+function showIntro() { console.log('Intro sichtbar'); }
+function hideIntro() { console.log('Intro ausgeblendet'); }
+function activateChart() { console.log('Chart aktiviert'); }
+function deactivateChart() { console.log('Chart deaktiviert'); }
+function revealQuote() { console.log('Zitat eingeblendet'); }
+function hideQuote() { console.log('Zitat ausgeblendet'); }
diff --git a/resources/js/modules/scrollfx/ScrollEngine.js b/resources/js/modules/scrollfx/ScrollEngine.js
new file mode 100644
index 00000000..99f6a621
--- /dev/null
+++ b/resources/js/modules/scrollfx/ScrollEngine.js
@@ -0,0 +1,36 @@
+// src/resources/js/scrollfx/ScrollEngine.js
+
+class ScrollEngine {
+ constructor() {
+ this.triggers = new Set();
+ this.viewportHeight = window.innerHeight;
+ this._loop = this._loop.bind(this);
+
+ window.addEventListener('resize', () => {
+ this.viewportHeight = window.innerHeight;
+ });
+
+ requestAnimationFrame(this._loop);
+ }
+
+ register(trigger) {
+ this.triggers.add(trigger);
+ }
+
+ unregister(trigger) {
+ this.triggers.delete(trigger);
+ }
+
+ clear() {
+ this.triggers.clear();
+ }
+
+ _loop() {
+ this.triggers.forEach(trigger => {
+ trigger.update(this.viewportHeight);
+ });
+ requestAnimationFrame(this._loop);
+ }
+}
+
+export default new ScrollEngine();
diff --git a/resources/js/modules/scrollfx/ScrollTrigger.js b/resources/js/modules/scrollfx/ScrollTrigger.js
new file mode 100644
index 00000000..b8bbd57a
--- /dev/null
+++ b/resources/js/modules/scrollfx/ScrollTrigger.js
@@ -0,0 +1,69 @@
+// src/resources/js/scrollfx/ScrollTrigger.js
+
+export default class ScrollTrigger {
+ constructor(config) {
+ this.element = this.resolveElement(config.element);
+ this.target = config.target
+ ? this.element.querySelector(config.target)
+ : this.element;
+
+ if (config.target && !this.target) {
+ throw new Error(`Target selector '${config.target}' not found inside element '${config.element}'.`);
+ }
+
+ this.start = config.start || 'top 80%';
+ this.end = config.end || 'bottom 20%';
+ this.scrub = config.scrub || false;
+ this.onEnter = config.onEnter || null;
+ this.onLeave = config.onLeave || null;
+ this.onUpdate = config.onUpdate || null;
+
+ this._wasVisible = false;
+ this._progress = 0;
+ }
+
+ resolveElement(input) {
+ if (typeof input === 'string') {
+ const els = document.querySelectorAll(input);
+ if (els.length === 1) return els[0];
+ throw new Error(`Selector '${input}' matched ${els.length} elements, expected exactly 1.`);
+ }
+ return input;
+ }
+
+ getScrollProgress(viewportHeight) {
+ const rect = this.element.getBoundingClientRect();
+ const startPx = this.parsePosition(this.start, viewportHeight);
+ const endPx = this.parsePosition(this.end, viewportHeight);
+ const scrollRange = endPx - startPx;
+ const current = rect.top - startPx;
+ return 1 - Math.min(Math.max(current / scrollRange, 0), 1);
+ }
+
+ parsePosition(pos, viewportHeight) {
+ const [edge, value] = pos.split(' ');
+ const edgeOffset = edge === 'top' ? 0 : viewportHeight;
+ const percentage = parseFloat(value) / 100;
+ return edgeOffset - viewportHeight * percentage;
+ }
+
+ update(viewportHeight) {
+ const rect = this.element.getBoundingClientRect();
+ const inViewport = rect.bottom > 0 && rect.top < viewportHeight;
+
+ if (inViewport && !this._wasVisible) {
+ this._wasVisible = true;
+ if (this.onEnter) this.onEnter(this.target);
+ }
+
+ if (!inViewport && this._wasVisible) {
+ this._wasVisible = false;
+ if (this.onLeave) this.onLeave(this.target);
+ }
+
+ if (this.scrub && inViewport) {
+ const progress = this.getScrollProgress(viewportHeight);
+ if (this.onUpdate) this.onUpdate(this.target, progress);
+ }
+ }
+}
diff --git a/resources/js/modules/scrollfx/Tween.js b/resources/js/modules/scrollfx/Tween.js
new file mode 100644
index 00000000..75f6ad95
--- /dev/null
+++ b/resources/js/modules/scrollfx/Tween.js
@@ -0,0 +1,177 @@
+// resources/js/scrollfx/Tween.js
+
+export const Easing = {
+ linear: t => t,
+ easeInQuad: t => t * t,
+ easeOutQuad: t => t * (2 - t),
+ easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
+};
+
+function interpolate(start, end, t) {
+ return start + (end - start) * t;
+}
+
+function parseTransform(transform) {
+ const result = {
+ translateY: 0,
+ scale: 1,
+ rotate: 0
+ };
+ if (!transform || transform === 'none') return result;
+
+ const translateMatch = transform.match(/translateY\((-?\d+(?:\.\d+)?)px\)/);
+ const scaleMatch = transform.match(/scale\((-?\d+(?:\.\d+)?)\)/);
+ const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
+
+ if (translateMatch) result.translateY = parseFloat(translateMatch[1]);
+ if (scaleMatch) result.scale = parseFloat(scaleMatch[1]);
+ if (rotateMatch) result.rotate = parseFloat(rotateMatch[1]);
+
+ return result;
+}
+
+export function tweenTo(el, props = {}, duration = 300, easing = Easing.linear) {
+ const start = performance.now();
+ const initial = {};
+
+ const currentTransform = parseTransform(getComputedStyle(el).transform);
+
+ for (const key in props) {
+ if (['translateY', 'scale', 'rotate'].includes(key)) {
+ initial[key] = currentTransform[key];
+ } else {
+ const current = parseFloat(getComputedStyle(el)[key]) || 0;
+ initial[key] = current;
+ }
+ }
+
+ function animate(now) {
+ const t = Math.min((now - start) / duration, 1);
+ const eased = easing(t);
+
+ let transformParts = [];
+
+ for (const key in props) {
+ const startValue = initial[key];
+ const endValue = parseFloat(props[key]);
+ const current = interpolate(startValue, endValue, eased);
+
+ if (key === 'translateY') transformParts.push(`translateY(${current}px)`);
+ else if (key === 'scale') transformParts.push(`scale(${current})`);
+ else if (key === 'rotate') transformParts.push(`rotate(${current}deg)`);
+ else el.style[key] = current + (key === 'opacity' ? '' : 'px');
+ }
+
+ if (transformParts.length > 0) {
+ el.style.transform = transformParts.join(' ');
+ }
+
+ if (t < 1) requestAnimationFrame(animate);
+ }
+
+ requestAnimationFrame(animate);
+}
+
+export function timeline(steps = []) {
+ let index = 0;
+
+ function runNext() {
+ if (index >= steps.length) return;
+ const { el, props, duration, easing = Easing.linear, delay = 0 } = steps[index++];
+ setTimeout(() => {
+ tweenTo(el, props, duration, easing);
+ setTimeout(runNext, duration);
+ }, delay);
+ }
+
+ runNext();
+}
+
+export function tweenFromTo(el, from = {}, to = {}, duration = 300, easing = Easing.linear) {
+ for (const key in from) {
+ if (['translateY', 'scale', 'rotate'].includes(key)) {
+ el.style.transform = `${key}(${from[key]}${key === 'rotate' ? 'deg' : key === 'translateY' ? 'px' : ''})`;
+ } else {
+ el.style[key] = from[key] + (key === 'opacity' ? '' : 'px');
+ }
+ }
+ tweenTo(el, to, duration, easing);
+}
+
+// === Utility Animations ===
+
+export function fadeIn(el, duration = 400, easing = Easing.easeOutQuad) {
+ el.classList.remove('fade-out');
+ el.classList.add('fade-in');
+ tweenTo(el, { opacity: 1 }, duration, easing);
+}
+
+export function fadeOut(el, duration = 400, easing = Easing.easeInQuad) {
+ el.classList.remove('fade-in');
+ el.classList.add('fade-out');
+ tweenTo(el, { opacity: 0 }, duration, easing);
+}
+
+export function zoomIn(el, duration = 500, easing = Easing.easeOutQuad) {
+ el.classList.add('zoom-in');
+ tweenTo(el, { opacity: 1, scale: 1 }, duration, easing);
+}
+
+export function zoomOut(el, duration = 500, easing = Easing.easeInQuad) {
+ el.classList.remove('zoom-in');
+ tweenTo(el, { opacity: 0, scale: 0.8 }, duration, easing);
+}
+
+export function triggerCssAnimation(el, className, duration = 1000) {
+ el.classList.add(className);
+ setTimeout(() => {
+ el.classList.remove(className);
+ }, duration);
+}
+
+// === ScrollTrigger Presets ===
+
+import { createTrigger } from './index.js';
+
+export function fadeScrollTrigger(selector, options = {}) {
+ const elements = document.querySelectorAll(selector);
+ elements.forEach(el => {
+ createTrigger({
+ element: el,
+ start: options.start || 'top 80%',
+ end: options.end || 'bottom 20%',
+ onEnter: () => fadeIn(el),
+ onLeave: () => fadeOut(el)
+ });
+ });
+}
+
+export function zoomScrollTrigger(selector, options = {}) {
+ const elements = document.querySelectorAll(selector);
+ elements.forEach(el => {
+ createTrigger({
+ element: el,
+ start: options.start || 'top 80%',
+ end: options.end || 'bottom 20%',
+ onEnter: () => zoomIn(el),
+ onLeave: () => zoomOut(el)
+ });
+ });
+}
+
+export function fixedZoomScrollTrigger(selector, options = {}) {
+ const elements = document.querySelectorAll(selector);
+ elements.forEach(el => {
+ el.style.willChange = 'transform, opacity';
+ el.style.opacity = 0;
+ el.style.transform = 'scale(0.8)';
+
+ createTrigger({
+ element: el,
+ start: options.start || 'top 100%',
+ end: options.end || 'top 40%',
+ onEnter: () => zoomIn(el, 600),
+ onLeave: () => zoomOut(el, 400)
+ });
+ });
+}
diff --git a/resources/js/modules/scrollfx/index.js b/resources/js/modules/scrollfx/index.js
new file mode 100644
index 00000000..a06cf66b
--- /dev/null
+++ b/resources/js/modules/scrollfx/index.js
@@ -0,0 +1,57 @@
+// src/resources/js/scrollfx/index.js
+
+import ScrollEngine from './ScrollEngine.js';
+import ScrollTrigger from './ScrollTrigger.js';
+import {fadeScrollTrigger, fixedZoomScrollTrigger, zoomScrollTrigger} from "./Tween";
+
+export function createTrigger(config) {
+ const elements = typeof config.element === 'string'
+ ? document.querySelectorAll(config.element)
+ : [config.element];
+
+ const triggers = [];
+
+ elements.forEach(el => {
+ const trigger = new ScrollTrigger({ ...config, element: el });
+ ScrollEngine.register(trigger);
+ triggers.push(trigger);
+ });
+
+ return triggers.length === 1 ? triggers[0] : triggers;
+}
+
+export function init(config = {}) {
+ const {
+ selector = '.fade-in-on-scroll, .zoom-in, .fade-out, .fade',
+ offset = 0.85, // z.B. 85 % Viewport-Höhe
+ baseDelay = 0.05, // delay pro Element (stagger)
+ once = true // nur 1× triggern?
+ } = config;
+
+ const elements = Array.from(document.querySelectorAll(selector))
+ .map(el => ({ el, triggered: false }));
+
+ function update() {
+ const triggerY = window.innerHeight * offset;
+
+ elements.forEach((obj, index) => {
+ if (obj.triggered && once) return;
+
+ const rect = obj.el.getBoundingClientRect();
+
+ if (rect.top < triggerY) {
+ obj.el.style.transitionDelay = `${index * baseDelay}s`;
+ obj.el.classList.add('visible', 'entered');
+ obj.el.classList.remove('fade-out');
+ obj.triggered = true;
+ } else if (!once) {
+ obj.el.classList.remove('visible', 'entered');
+ obj.triggered = false;
+ }
+ });
+
+ requestAnimationFrame(update);
+ }
+
+ requestAnimationFrame(update);
+}
diff --git a/resources/js/modules/sidebar/index.js b/resources/js/modules/sidebar/index.js
new file mode 100644
index 00000000..0a364443
--- /dev/null
+++ b/resources/js/modules/sidebar/index.js
@@ -0,0 +1,62 @@
+import {useEvent} from "../../core";
+import {removeModules} from "../../core/removeModules";
+
+let keydownHandler = null;
+let clickHandler = null;
+
+export function init() {
+ const el = document.getElementById("sidebar-menu");
+ const button = document.getElementById('menu-toggle');
+ const aside = document.getElementById('sidebar');
+ const backdrop = document.querySelector('.backdrop');
+
+ const footer = document.querySelector('footer');
+ const headerLink = document.querySelector('header a');
+
+
+ useEvent(button, 'click', (e) => {
+ aside.classList.toggle('show');
+ //aside.toggleAttribute('inert')
+
+ let isVisible = aside.classList.contains('show');
+
+ if (isVisible) {
+ backdrop.classList.add('visible');
+
+ footer.setAttribute('inert', 'true');
+ headerLink.setAttribute('inert', 'true');
+ } else {
+ backdrop.classList.remove('visible');
+
+ footer.removeAttribute('inert');
+ headerLink.removeAttribute('inert');
+ }
+ })
+
+ keydownHandler = (e) => {
+ if(e.key === 'Escape'){
+ if(aside.classList.contains('show')){
+ aside.classList.remove('show');
+ backdrop.classList.remove('visible');
+ }
+ }
+ }
+
+ useEvent(document, 'keydown', keydownHandler)
+
+ clickHandler = (e) => {
+ aside.classList.remove('show');
+ backdrop.classList.remove('visible');
+
+ footer.removeAttribute('inert');
+ headerLink.removeAttribute('inert');
+ }
+
+ useEvent(backdrop, 'click' , clickHandler)
+}
+
+export function destroy() {
+ /*document.removeEventListener('keydown', keydownHandler);
+ document.removeEventListener('click', clickHandler);*/
+ removeModules('sidebar');
+}
diff --git a/resources/js/modules/smooth-scroll/index.js b/resources/js/modules/smooth-scroll/index.js
new file mode 100644
index 00000000..1cc22862
--- /dev/null
+++ b/resources/js/modules/smooth-scroll/index.js
@@ -0,0 +1,118 @@
+// modules/smooth-scroll/index.js
+import { Logger } from '../../core/logger.js';
+import { registerFrameTask } from '../../core/frameloop.js';
+
+/*
+export function init(config = {}) {
+ Logger.info('SmoothScroll init');
+
+ const defaultConfig = {
+ speed: 0.12,
+ scrollTarget: window,
+ interceptWheel: true,
+ interceptTouch: true,
+ interceptKeys: true
+ };
+
+ const settings = { ...defaultConfig, ...config };
+ const scrollElement = settings.scrollTarget === window ? document.scrollingElement : settings.scrollTarget;
+
+ let current = scrollElement.scrollTop;
+ let target = current;
+
+ function clampTarget() {
+ const maxScroll = scrollElement.scrollHeight - window.innerHeight;
+ target = Math.max(0, Math.min(target, maxScroll));
+ }
+
+ function update() {
+ const delta = target - current;
+ current += delta * settings.speed;
+
+ const maxScroll = scrollElement.scrollHeight - window.innerHeight;
+ current = Math.max(0, Math.min(current, maxScroll));
+
+ scrollElement.scrollTop = current;
+ }
+
+ registerFrameTask('smooth-scroll', update, { autoStart: true });
+
+ function triggerScroll(immediate = false) {
+ clampTarget();
+ if (immediate) scrollElement.scrollTop = target;
+ }
+
+ function onWheel(e) {
+ if (!settings.interceptWheel) return;
+ e.preventDefault();
+ target += e.deltaY;
+ triggerScroll(true);
+ }
+
+ let lastTouchY = 0;
+
+ function onTouchStart(e) {
+ if (!settings.interceptTouch) return;
+ lastTouchY = e.touches[0].clientY;
+ }
+
+ function onTouchMove(e) {
+ if (!settings.interceptTouch) return;
+ e.preventDefault();
+ const touch = e.changedTouches[0];
+ const dy = lastTouchY - touch.clientY;
+ target += dy;
+ lastTouchY = touch.clientY;
+ triggerScroll(true);
+ }
+
+ function onKeyDown(e) {
+ if (!settings.interceptKeys) return;
+ const keyScrollAmount = 60;
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ target += keyScrollAmount;
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ target -= keyScrollAmount;
+ break;
+ case 'PageDown':
+ e.preventDefault();
+ target += window.innerHeight * 0.9;
+ break;
+ case 'PageUp':
+ e.preventDefault();
+ target -= window.innerHeight * 0.9;
+ break;
+ case 'Home':
+ e.preventDefault();
+ target = 0;
+ break;
+ case 'End':
+ e.preventDefault();
+ target = scrollElement.scrollHeight;
+ break;
+ }
+ triggerScroll(true);
+ }
+
+ if (settings.interceptWheel) {
+ window.addEventListener('wheel', onWheel, { passive: false });
+ }
+
+ if (settings.interceptTouch) {
+ window.addEventListener('touchstart', onTouchStart, { passive: false });
+ window.addEventListener('touchmove', onTouchMove, { passive: false });
+ }
+
+ if (settings.interceptKeys) {
+ window.addEventListener('keydown', onKeyDown);
+ }
+
+ // initial scroll alignment
+ target = scrollElement.scrollTop;
+ current = target;
+}
+*/
diff --git a/resources/js/modules/sticky-fade/index.js b/resources/js/modules/sticky-fade/index.js
new file mode 100644
index 00000000..6cc8ca01
--- /dev/null
+++ b/resources/js/modules/sticky-fade/index.js
@@ -0,0 +1,67 @@
+// modules/sticky-fade/index.js
+import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
+
+let taskId = 'sticky-fade';
+let elements = [];
+let lastScrollY = window.scrollY;
+let activeMap = new WeakMap();
+let configCache = {
+ direction: false,
+ reset: false,
+};
+
+
+export function init(config = {}) {
+ elements = Array.from(document.querySelectorAll('[data-sticky-fade]'));
+ if (elements.length === 0) return;
+
+ configCache.direction = config.direction ?? false;
+ configCache.reset = config.reset ?? false;
+
+ registerFrameTask(taskId, () => {
+ const scrollY = window.scrollY;
+ const direction = scrollY > lastScrollY ? 'down' : scrollY < lastScrollY ? 'up' : 'none';
+ lastScrollY = scrollY;
+
+ const viewportHeight = window.innerHeight;
+
+ elements.forEach(el => {
+ const rect = el.getBoundingClientRect();
+ const progress = 1 - Math.min(Math.max(rect.top / viewportHeight, 0), 1);
+
+ el.style.opacity = progress.toFixed(3);
+ el.style.transform = `translateY(${(1 - progress) * 20}px)`;
+
+ if(configCache.direction) {
+ el.dataset.scrollDir = direction;
+ }
+
+ if (configCache.reset) {
+ const isVisible = progress >= 1;
+ const wasActive = activeMap.get(el) || false;
+
+ if(isVisible && !wasActive) {
+ el.classList.add('visible');
+ activeMap.set(el, true);
+ } else if(!isVisible && wasActive) {
+ el.classList.remove('visible');
+ activeMap.set(el, false);
+ }
+ }
+ });
+ }, { autoStart: true });
+}
+
+export function destroy() {
+ unregisterFrameTask(taskId);
+
+ elements.forEach(el => {
+ el.style.opacity = '';
+ el.style.transform = '';
+ el.classList.remove('visible');
+ delete el.dataset.scrollDir;
+ });
+
+ elements = [];
+ activeMap = new WeakMap();
+}
diff --git a/resources/js/modules/sticky-steps/index.js b/resources/js/modules/sticky-steps/index.js
new file mode 100644
index 00000000..09118e8f
--- /dev/null
+++ b/resources/js/modules/sticky-steps/index.js
@@ -0,0 +1,45 @@
+// modules/sticky-steps/index.js
+import { Logger } from '../../core/logger.js';
+import {registerFrameTask, unregisterFrameTask} from '../../core/frameloop.js';
+
+export function init(config = {}) {
+ Logger.info('StickySteps init');
+
+ const defaultConfig = {
+ containerSelector: '[data-sticky-container]',
+ stepSelector: '[data-sticky-step]',
+ activeClass: 'is-sticky-active',
+ datasetKey: 'activeStickyStep'
+ };
+
+ const settings = { ...defaultConfig, ...config };
+ const containers = document.querySelectorAll(settings.containerSelector);
+
+ containers.forEach(container => {
+ const steps = container.querySelectorAll(settings.stepSelector);
+ const containerOffsetTop = container.offsetTop;
+
+ function update() {
+ const scrollY = window.scrollY;
+ const containerHeight = container.offsetHeight;
+
+ steps.forEach((step, index) => {
+ const stepOffset = containerOffsetTop + index * (containerHeight / steps.length);
+ const nextStepOffset = containerOffsetTop + (index + 1) * (containerHeight / steps.length);
+
+ const isActive = scrollY >= stepOffset && scrollY < nextStepOffset;
+ step.classList.toggle(settings.activeClass, isActive);
+
+ if (isActive) {
+ container.dataset[settings.datasetKey] = index;
+ }
+ });
+ }
+
+ registerFrameTask(`sticky-steps-${container.dataset.moduleId || Math.random()}`, update, { autoStart: true });
+ });
+}
+
+export function destroy() {
+ unregisterFrameTask('sticky-steps');
+}
diff --git a/resources/js/modules/ui/UIManager.js b/resources/js/modules/ui/UIManager.js
new file mode 100644
index 00000000..283ccd42
--- /dev/null
+++ b/resources/js/modules/ui/UIManager.js
@@ -0,0 +1,23 @@
+// modules/ui/UIManager.js
+import { Modal } from './components/Modal.js';
+
+const components = {
+ modal: Modal,
+};
+
+export const UIManager = {
+ open(type, props = {}) {
+ const Component = components[type];
+ if (!Component) {
+ console.warn(`[UIManager] Unknown type: ${type}`);
+ return null;
+ }
+ const instance = new Component(props);
+ instance.open();
+ return instance;
+ },
+
+ close(instance) {
+ if (instance?.close) instance.close();
+ }
+};
diff --git a/resources/js/modules/ui/components/Dialog.js b/resources/js/modules/ui/components/Dialog.js
new file mode 100644
index 00000000..10bbf00b
--- /dev/null
+++ b/resources/js/modules/ui/components/Dialog.js
@@ -0,0 +1,41 @@
+import {useEvent} from "../../../core/useEvent";
+
+export class Dialog {
+ constructor({content = '', className = '', onClose = null} = {}) {
+ this.onClose = onClose;
+ this.dialog = document.createElement('dialog');
+ this.dialog.className = className;
+ this.dialog.innerHTML = `
+
+ `;
+
+ useEvent(this.dialog, 'click', (e) => {
+ const isOutside = !e.target.closest(className+'-content');
+ if (isOutside) this.close();
+ })
+
+ useEvent(this.dialog, 'cancel', (e) => {
+ e.preventDefault();
+ this.close();
+ })
+
+ }
+
+ open()
+ {
+ document.body.appendChild(this.dialog);
+ this.dialog.showModal?.() || this.dialog.setAttribute('open', '');
+ document.documentElement.dataset[`${this.dialog.className}Open`] = 'true';
+ }
+
+ close()
+ {
+ this.dialog.close?.() || this.dialog.removeAttribute('open');
+ this.dialog.remove();
+ delete document.documentElement.dataset[`${this.dialog.className}Open`];
+ this.onClose?.();
+ }
+}
diff --git a/resources/js/modules/ui/components/Lightbox.js b/resources/js/modules/ui/components/Lightbox.js
new file mode 100644
index 00000000..0e0303e6
--- /dev/null
+++ b/resources/js/modules/ui/components/Lightbox.js
@@ -0,0 +1,6 @@
+import { Dialog } from './Dialog.js';
+export class Lightbox extends Dialog {
+ constructor(props) {
+ super({ ...props, className: 'lightbox' });
+ }
+}
diff --git a/resources/js/modules/ui/components/Modal.js b/resources/js/modules/ui/components/Modal.js
new file mode 100644
index 00000000..3b6d4676
--- /dev/null
+++ b/resources/js/modules/ui/components/Modal.js
@@ -0,0 +1,6 @@
+import { Dialog } from './Dialog.js';
+export class Modal extends Dialog {
+ constructor(props) {
+ super({ ...props, className: 'modal' });
+ }
+}
diff --git a/resources/js/modules/ui/index.js b/resources/js/modules/ui/index.js
new file mode 100644
index 00000000..656b7942
--- /dev/null
+++ b/resources/js/modules/ui/index.js
@@ -0,0 +1,9 @@
+import { UIManager } from './UIManager.js';
+
+export function init() {
+
+ /*UIManager.open('modal', {
+ content: 'Hallo!
',
+ onClose: () => console.log('Modal wurde geschlossen')
+ });*/
+}
diff --git a/resources/js/modules/wheel-boost/index.js b/resources/js/modules/wheel-boost/index.js
new file mode 100644
index 00000000..a0fea810
--- /dev/null
+++ b/resources/js/modules/wheel-boost/index.js
@@ -0,0 +1,40 @@
+// modules/wheel-boost/index.js
+import { registerFrameTask, unregisterFrameTask } from '../../core/frameloop.js';
+
+/*
+let velocity = 0;
+let taskId = 'wheel-boost';
+let damping = 0.85;
+let boost = 1.2;
+let enabled = true;
+
+export function init(config = {}) {
+ damping = typeof config.damping === 'number' ? config.damping : 0.85;
+ boost = typeof config.boost === 'number' ? config.boost : 1.2;
+ enabled = config.enabled !== false;
+
+ if (!enabled) return;
+
+ window.addEventListener('wheel', onWheel, { passive: false });
+
+ registerFrameTask(taskId, () => {
+ if (Math.abs(velocity) > 0.1) {
+ window.scrollBy(0, velocity);
+ velocity *= damping;
+ } else {
+ velocity = 0;
+ }
+ }, { autoStart: true });
+}
+
+function onWheel(e) {
+ velocity += e.deltaY * boost;
+ e.preventDefault(); // verhindert das native Scrollen
+}
+
+export function destroy() {
+ window.removeEventListener('wheel', onWheel);
+ unregisterFrameTask(taskId);
+ velocity = 0;
+}
+*/
diff --git a/resources/js/serviceWorker.js b/resources/js/serviceWorker.js
new file mode 100644
index 00000000..181efcb8
--- /dev/null
+++ b/resources/js/serviceWorker.js
@@ -0,0 +1,23 @@
+export function registerServiceWorker() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/sw.js')
+ .then(reg => {
+ console.log('✅ Service Worker registriert:', reg.scope);
+
+ // Update found?
+ reg.onupdatefound = () => {
+ const installing = reg.installing;
+ installing.onstatechange = () => {
+ if (installing.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ console.log('🔄 Neue Version verfügbar – Seite neu laden?');
+ } else {
+ console.log('✅ Inhalte jetzt offline verfügbar');
+ }
+ }
+ };
+ };
+ })
+ .catch(err => console.error('❌ SW-Fehler:', err));
+ }
+}
diff --git a/resources/js/utils/autoLoadResponsiveVideo.js b/resources/js/utils/autoLoadResponsiveVideo.js
new file mode 100644
index 00000000..38993eab
--- /dev/null
+++ b/resources/js/utils/autoLoadResponsiveVideo.js
@@ -0,0 +1,50 @@
+// 📦 Automatischer Video-Loader (Bandbreiten- & Auflösungs-bewusst)
+// Erkennt automatisch