feat: inkubator-standard & modul-autodiscovery
- Dynamische Modulerkennung via module.json in ModuleService.php - Kaskadierender Env-Loader fuer zentrale API-Keys (GEMINI_API_KEY) - Graceful Auth fuer alle Subprojekte (dinos, plants, storymachine, test) - Wiederverwendbares Starter-Template in _template/ - test-Modul auf philippurbschat.de Dark-Design modernisiert
This commit is contained in:
parent
7ebfb9cb63
commit
f393beb7ff
23 changed files with 758 additions and 306 deletions
41
_template/README.md
Normal file
41
_template/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 🚀 Modul-Starter-Template für philippurbschat.de
|
||||||
|
|
||||||
|
Dieses Verzeichnis dient als Kopiervorlage für neue Mini-Projekte und Experimente.
|
||||||
|
|
||||||
|
## In 30 Sekunden startklar:
|
||||||
|
|
||||||
|
1. **Ordner duplizieren:**
|
||||||
|
Kopiere diesen `_template`-Ordner und gib ihm den Namen deines neuen Projekts (z.B. `synth`, `todo`, `scanner`).
|
||||||
|
```bash
|
||||||
|
cp -r _template mein-projekt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Metadaten in `module.json` anpassen:**
|
||||||
|
Öffne `mein-projekt/module.json` und passe Name, Badge und Beschreibung an:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Mein Tolles Tool",
|
||||||
|
"badge": "AI Lab",
|
||||||
|
"description": "Was das Tool macht",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Fertig!**
|
||||||
|
* Das Modul erscheint sofort automatisch in der Benutzerverwaltung unter `philippurbschat.de/admin/users`.
|
||||||
|
* Es erbt automatisch das Dark-Theme, Tailwind v4 und Typografie von `philippurbschat.de`.
|
||||||
|
* Es hat sofortigen Zugriff auf `GEMINI_API_KEY` aus der zentralen Konfiguration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features & Architektur
|
||||||
|
|
||||||
|
* **Graceful Auth (`bootstrap.php`):**
|
||||||
|
* Auf `philippurbschat.de`: Vollständig geschützt durch Session & Rechteverwaltung.
|
||||||
|
* Standalone-Betrieb: Wenn du den Ordner als eigene Website auslagerst, läuft er ohne Änderungen direkt weiter.
|
||||||
|
* **Hierarchische API-Keys:**
|
||||||
|
* Rufe in PHP `module_env('GEMINI_API_KEY')` auf.
|
||||||
|
* Wenn im Modulordner eine eigene `.env` liegt, hat diese Vorrang. Andernfalls greift automatisch der globale Key aus `/home/.env`.
|
||||||
|
* **Sicherer API-Proxy (`api.php`):**
|
||||||
|
* Verhindert das Offenlegen deiner API-Schlüssel im Frontend.
|
||||||
75
_template/api.php
Normal file
75
_template/api.php
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Modul-API-Proxy: Sicherer serverseitiger Relay für Gemini & externe APIs
|
||||||
|
*
|
||||||
|
* - Hält den API-Key auf dem Server geheim
|
||||||
|
* - Nutzt automatisch den zentralen Key aus /home/.env oder lokale .env
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
// Nur POST-Anfragen erlauben
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Nur POST-Anfragen sind erlaubt.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = module_env('GEMINI_API_KEY');
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'API-Schlüssel (GEMINI_API_KEY) ist weder lokal noch zentral konfiguriert.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Payload lesen
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$requestData = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE || !isset($requestData['prompt'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Ungültige Anfrage: "prompt" wird benötigt.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prompt = trim($requestData['prompt']);
|
||||||
|
$model = $requestData['model'] ?? 'gemini-2.5-flash';
|
||||||
|
|
||||||
|
// Google Gemini API Aufruf
|
||||||
|
$endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . urlencode($model) . ':generateContent?key=' . $apiKey;
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'contents' => [
|
||||||
|
[
|
||||||
|
'parts' => [
|
||||||
|
['text' => $prompt]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||||
|
CURLOPT_TIMEOUT => 30
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Verbindungsfehler zur API: ' . $curlError]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code($httpCode);
|
||||||
|
echo $response;
|
||||||
72
_template/bootstrap.php
Normal file
72
_template/bootstrap.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Modul-Bootstrap: Graceful Auth & Universeller Env-Loader
|
||||||
|
*
|
||||||
|
* - Auf philippurbschat.de: Prüft zentralen Login & Benutzerrechte über ../auth.php
|
||||||
|
* - Standalone-Betrieb: Läuft autark weiter, wenn ../auth.php nicht existiert
|
||||||
|
* - API-Keys: Liest zuerst lokale .env, greift bei Bedarf auf ../home/.env zu
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Graceful Auth (im Webbetrieb)
|
||||||
|
$authPath = __DIR__ . '/../auth.php';
|
||||||
|
$isHostedOnMainSite = file_exists($authPath);
|
||||||
|
|
||||||
|
if ($isHostedOnMainSite && php_sapi_name() !== 'cli') {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Nutzer-Kontext
|
||||||
|
$currentUser = $_SESSION['user_email'] ?? 'Gast / Standalone';
|
||||||
|
$isAdmin = !empty($_SESSION['is_admin']);
|
||||||
|
|
||||||
|
// 3. Hierarchischer Env-Loader
|
||||||
|
if (!function_exists('module_env')) {
|
||||||
|
function module_env(string $key, ?string $default = null): ?string {
|
||||||
|
static $envCache = null;
|
||||||
|
|
||||||
|
if ($envCache === null) {
|
||||||
|
$envCache = [];
|
||||||
|
$candidates = [
|
||||||
|
__DIR__ . '/.env', // 1. Lokale Modul-Konfiguration
|
||||||
|
__DIR__ . '/../home/.env', // 2. Zentrale philippurbschat.de Konfiguration
|
||||||
|
__DIR__ . '/../.env' // 3. Root-Konfiguration
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if ($lines !== false) {
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
if (strpos($line, '=') !== false) {
|
||||||
|
list($k, $v) = explode('=', $line, 2);
|
||||||
|
$cleanKey = trim($k);
|
||||||
|
// Frühere (spezifischere) Dateien haben Vorrang
|
||||||
|
if (!isset($envCache[$cleanKey])) {
|
||||||
|
$envCache[$cleanKey] = trim(trim($v), '"\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($envCache[$key])) {
|
||||||
|
return $envCache[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sysEnv = getenv($key);
|
||||||
|
if ($sysEnv !== false && $sysEnv !== '') {
|
||||||
|
return $sysEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_ENV[$key] ?? $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
_template/index.php
Normal file
152
_template/index.php
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
// Modul-Metadaten laden
|
||||||
|
$moduleDescriptor = file_exists(__DIR__ . '/module.json')
|
||||||
|
? json_decode(file_get_contents(__DIR__ . '/module.json'), true)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
$moduleName = $moduleDescriptor['name'] ?? 'Neues Projekt';
|
||||||
|
$moduleBadge = $moduleDescriptor['badge'] ?? 'Lab';
|
||||||
|
$moduleDesc = $moduleDescriptor['description'] ?? 'Experimentelles Web-Modul';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= htmlspecialchars($moduleName) ?> – philippurbschat.de</title>
|
||||||
|
|
||||||
|
<!-- Zentrale Styles von philippurbschat.de -->
|
||||||
|
<link rel="stylesheet" href="/css/app.css">
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
|
||||||
|
<!-- Standalone-Fallback: Falls außerhalb von philippurbschat.de gehostet -->
|
||||||
|
<script>
|
||||||
|
window.addEventListener('error', function(e) {
|
||||||
|
if (e.target && e.target.tagName === 'LINK' && e.target.href.includes('/css/app.css')) {
|
||||||
|
const cdn = document.createElement('script');
|
||||||
|
cdn.src = 'https://cdn.tailwindcss.com';
|
||||||
|
document.head.appendChild(cdn);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-950 text-slate-200 min-h-screen font-sans flex flex-col antialiased selection:bg-fuchsia-500 selection:text-white">
|
||||||
|
|
||||||
|
<!-- Top Navigation Bar -->
|
||||||
|
<header class="border-b border-slate-800/80 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
|
||||||
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3.5 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a href="/" class="text-xs font-mono text-slate-400 hover:text-cyan-400 transition-colors flex items-center gap-1.5 group">
|
||||||
|
<span class="group-hover:-translate-x-0.5 transition-transform">←</span> philippurbschat.de
|
||||||
|
</a>
|
||||||
|
<span class="text-slate-700">/</span>
|
||||||
|
<span class="text-xs font-mono text-slate-300 font-medium"><?= htmlspecialchars(basename(__DIR__)) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 text-cyan-400 uppercase tracking-wider">
|
||||||
|
<?= htmlspecialchars($moduleBadge) ?>
|
||||||
|
</span>
|
||||||
|
<span class="text-[11px] font-mono text-slate-500 hidden sm:inline-block">
|
||||||
|
<?= htmlspecialchars($currentUser) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content Container -->
|
||||||
|
<main class="grow max-w-5xl mx-auto px-4 sm:px-6 py-10 w-full">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
<span class="text-xs font-mono uppercase tracking-[0.2em] text-emerald-400">System Ready</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold font-mono tracking-tight text-white mb-2">
|
||||||
|
<?= htmlspecialchars($moduleName) ?>
|
||||||
|
</h1>
|
||||||
|
<p class="text-slate-400 text-sm max-w-2xl">
|
||||||
|
<?= htmlspecialchars($moduleDesc) ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App Sandbox Card -->
|
||||||
|
<div class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 sm:p-8 backdrop-blur shadow-2xl relative overflow-hidden">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-cyan-500/30 to-transparent"></div>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xs font-mono uppercase tracking-widest text-slate-400 mb-2">Modul Workspace</h2>
|
||||||
|
<p class="text-sm text-slate-300 leading-relaxed">
|
||||||
|
Hier startet dein neues Mini-Projekt. Du hast vollen Zugriff auf Tailwind CSS, den zentralen Gemini API-Key und das Dark-Design der Hauptseite.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interaktiver AI-Testbereich -->
|
||||||
|
<div class="pt-4 border-t border-slate-800/80">
|
||||||
|
<label for="promptInput" class="block text-xs font-mono uppercase text-slate-400 mb-2">
|
||||||
|
Schnelltest: Gemini API Relay
|
||||||
|
</label>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
|
<input type="text" id="promptInput" placeholder="Stelle eine kurze Frage..."
|
||||||
|
value="Erkläre Quantencomputer in einem prägnanten Satz."
|
||||||
|
class="grow bg-slate-950/80 border border-slate-700/80 rounded-xl px-4 py-3 text-sm text-slate-200 placeholder:text-slate-600 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all font-mono">
|
||||||
|
<button id="sendBtn" class="bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 hover:border-cyan-500/60 font-mono text-xs uppercase tracking-wider px-6 py-3 rounded-xl transition-all duration-200 flex items-center justify-center gap-2 cursor-pointer active:scale-95">
|
||||||
|
<span>Ausführen</span> →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output Area -->
|
||||||
|
<div id="outputContainer" class="hidden mt-4 p-4 rounded-xl bg-slate-950/90 border border-slate-800/90 font-mono text-xs text-slate-300 whitespace-pre-wrap leading-relaxed"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="border-t border-slate-900 py-6 text-center text-xs font-mono text-slate-600">
|
||||||
|
<?= htmlspecialchars($moduleName) ?> • Philipp Urbschat Lab
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const promptInput = document.getElementById('promptInput');
|
||||||
|
const outputContainer = document.getElementById('outputContainer');
|
||||||
|
|
||||||
|
sendBtn.addEventListener('click', async () => {
|
||||||
|
const prompt = promptInput.value.trim();
|
||||||
|
if (!prompt) return;
|
||||||
|
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.innerHTML = 'Verarbeite...';
|
||||||
|
outputContainer.classList.remove('hidden');
|
||||||
|
outputContainer.textContent = 'Verbinde mit Gemini API...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('api.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ prompt: prompt })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
outputContainer.textContent = 'Fehler: ' + data.error;
|
||||||
|
outputContainer.classList.add('text-rose-400');
|
||||||
|
} else {
|
||||||
|
outputContainer.classList.remove('text-rose-400');
|
||||||
|
const reply = data.candidates?.[0]?.content?.parts?.[0]?.text || JSON.stringify(data, null, 2);
|
||||||
|
outputContainer.textContent = reply;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
outputContainer.textContent = 'Netzwerkfehler: ' + err.message;
|
||||||
|
outputContainer.classList.add('text-rose-400');
|
||||||
|
} finally {
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.innerHTML = '<span>Ausführen</span> →';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
_template/module.json
Normal file
7
_template/module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Neues Projekt",
|
||||||
|
"badge": "Labor",
|
||||||
|
"description": "Experimentelles Mini-Projekt",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
// DATEI: dinos/api_proxy.php
|
// DATEI: dinos/api_proxy.php
|
||||||
|
|
||||||
$current_project = 'dinos';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@
|
||||||
* 2026-02-23
|
* 2026-02-23
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1. Projekt identifizieren und schützen
|
// 1. Projekt identifizieren und schützen (Graceful Auth)
|
||||||
$current_project = 'dinos';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once '../auth.php'; // Falls du deine globale Auth hast, sonst auskommentieren
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Nutzerdaten abrufen
|
// 2. Nutzerdaten abrufen
|
||||||
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'Gast';
|
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'Gast';
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@
|
||||||
* FEATURES: Robustere Fehlerbehandlung, Safety-Check für PromptFeedback, Konfigurierbare API-Modelle
|
* FEATURES: Robustere Fehlerbehandlung, Safety-Check für PromptFeedback, Konfigurierbare API-Modelle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1. Projekt identifizieren und schützen
|
// 1. Projekt identifizieren und schützen (Graceful Auth)
|
||||||
$current_project = 'dinos';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once '../auth.php'; // Falls du deine globale Auth hast, sonst auskommentieren
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Nutzerdaten abrufen
|
// 2. Nutzerdaten abrufen
|
||||||
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'Gast';
|
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'Gast';
|
||||||
|
|
|
||||||
7
dinos/module.json
Normal file
7
dinos/module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Dino Generator",
|
||||||
|
"badge": "AI Art",
|
||||||
|
"description": "KI Dino-Bilderstellung & Google Photos Sync",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,11 @@
|
||||||
* Nimmt AJAX Request entgegen und sendet an Google Photos.
|
* Nimmt AJAX Request entgegen und sendet an Google Photos.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$current_project = 'dinos';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
require_once 'google_helper.php';
|
require_once 'google_helper.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
<?php
|
<?php
|
||||||
|
/**
|
||||||
|
* ModuleService - Dynamische Auto-Discovery für Subprojekte & Module
|
||||||
|
*
|
||||||
|
* Jedes Verzeichnis im Root mit einer `module.json` wird automatisch als Modul registriert
|
||||||
|
* und steht sofort in der Benutzer- und Rechteverwaltung zur Verfügung.
|
||||||
|
*/
|
||||||
class ModuleService {
|
class ModuleService {
|
||||||
private static $modules = [
|
private static ?array $cachedModules = null;
|
||||||
|
|
||||||
|
// Fallback-Definitionen, falls Dateisystem-Zugriff blockiert ist
|
||||||
|
private static array $defaultFallback = [
|
||||||
'dinos' => [
|
'dinos' => [
|
||||||
'slug' => 'dinos',
|
'slug' => 'dinos',
|
||||||
'name' => 'Dino Generator',
|
'name' => 'Dino Generator',
|
||||||
'badge' => 'AI Art',
|
'badge' => 'AI Art',
|
||||||
'description' => 'KI Dino-Bilderstellung & Google Photos Sync',
|
'description' => 'KI Dino-Bilderstellung & Google Photos Sync',
|
||||||
'path' => '/dinos',
|
'path' => '/dinos',
|
||||||
'adminOnly' => false
|
'adminOnly' => false,
|
||||||
|
'order' => 1
|
||||||
],
|
],
|
||||||
'plants' => [
|
'plants' => [
|
||||||
'slug' => 'plants',
|
'slug' => 'plants',
|
||||||
|
|
@ -15,7 +25,8 @@ class ModuleService {
|
||||||
'badge' => 'AI Vision',
|
'badge' => 'AI Vision',
|
||||||
'description' => 'Pflanzen-Tracking, Pflege & KI-Erkennung',
|
'description' => 'Pflanzen-Tracking, Pflege & KI-Erkennung',
|
||||||
'path' => '/plants',
|
'path' => '/plants',
|
||||||
'adminOnly' => false
|
'adminOnly' => false,
|
||||||
|
'order' => 2
|
||||||
],
|
],
|
||||||
'storymachine' => [
|
'storymachine' => [
|
||||||
'slug' => 'storymachine',
|
'slug' => 'storymachine',
|
||||||
|
|
@ -23,32 +34,110 @@ class ModuleService {
|
||||||
'badge' => 'Creative',
|
'badge' => 'Creative',
|
||||||
'description' => 'Interaktive Kinder-Geschichtenmaschine',
|
'description' => 'Interaktive Kinder-Geschichtenmaschine',
|
||||||
'path' => '/storymachine',
|
'path' => '/storymachine',
|
||||||
'adminOnly' => false
|
'adminOnly' => false,
|
||||||
|
'order' => 3
|
||||||
],
|
],
|
||||||
'test' => [
|
'test' => [
|
||||||
'slug' => 'test',
|
'slug' => 'test',
|
||||||
'name' => 'Test',
|
'name' => 'Test & Lab',
|
||||||
'badge' => 'Test',
|
'badge' => 'Audio',
|
||||||
'description' => 'Interaktive Web Audio API Synthese & Soundtest',
|
'description' => 'Modul-Showcase & Web Audio Synthesizer',
|
||||||
'path' => '/test',
|
'path' => '/test',
|
||||||
'adminOnly' => false
|
'adminOnly' => false,
|
||||||
|
'order' => 4
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function getAll(): array {
|
/**
|
||||||
return self::$modules;
|
* Ermittelt den Webroot-Pfad der Domain
|
||||||
|
*/
|
||||||
|
private static function getRootPath(): string {
|
||||||
|
return dirname(__DIR__, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest alle Module dynamisch aus dem Dateisystem
|
||||||
|
*/
|
||||||
|
public static function getAll(bool $refresh = false): array {
|
||||||
|
if (self::$cachedModules !== null && !$refresh) {
|
||||||
|
return self::$cachedModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootPath = self::getRootPath();
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
// Ignorierte Systemverzeichnisse
|
||||||
|
$ignoredDirs = ['.git', '.vscode', 'home', 'philcore', 'node_modules', 'vendor', '_template'];
|
||||||
|
|
||||||
|
if (is_dir($rootPath)) {
|
||||||
|
$entries = scandir($rootPath);
|
||||||
|
if ($entries !== false) {
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
if ($entry === '.' || $entry === '..' || in_array($entry, $ignoredDirs, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleDir = $rootPath . '/' . $entry;
|
||||||
|
if (!is_dir($moduleDir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$descriptorFile = $moduleDir . '/module.json';
|
||||||
|
if (file_exists($descriptorFile)) {
|
||||||
|
$jsonContent = @file_get_contents($descriptorFile);
|
||||||
|
$data = $jsonContent ? json_decode($jsonContent, true) : null;
|
||||||
|
|
||||||
|
if (is_array($data)) {
|
||||||
|
// Ignorieren, falls explizit deaktiviert
|
||||||
|
if (isset($data['enabled']) && $data['enabled'] === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modules[$entry] = [
|
||||||
|
'slug' => $entry,
|
||||||
|
'name' => $data['name'] ?? ucfirst($entry),
|
||||||
|
'badge' => $data['badge'] ?? 'Tool',
|
||||||
|
'description' => $data['description'] ?? '',
|
||||||
|
'path' => '/' . $entry,
|
||||||
|
'adminOnly' => !empty($data['adminOnly']),
|
||||||
|
'order' => isset($data['order']) ? (int)$data['order'] : 999
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls keine Module gefunden wurden, Fallback nutzen
|
||||||
|
if (empty($modules)) {
|
||||||
|
$modules = self::$defaultFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortierung nach 'order', dann nach Name
|
||||||
|
uasort($modules, function ($a, $b) {
|
||||||
|
$orderA = $a['order'] ?? 999;
|
||||||
|
$orderB = $b['order'] ?? 999;
|
||||||
|
if ($orderA !== $orderB) {
|
||||||
|
return $orderA <=> $orderB;
|
||||||
|
}
|
||||||
|
return strcasecmp($a['name'] ?? '', $b['name'] ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
self::$cachedModules = $modules;
|
||||||
|
return self::$cachedModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getBySlug(string $slug): ?array {
|
public static function getBySlug(string $slug): ?array {
|
||||||
return self::$modules[$slug] ?? null;
|
$all = self::getAll();
|
||||||
|
return $all[$slug] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getAccessibleForUser(array $userProjects, bool $isAdmin = false): array {
|
public static function getAccessibleForUser(array $userProjects, bool $isAdmin = false): array {
|
||||||
$accessible = [];
|
$accessible = [];
|
||||||
foreach (self::$modules as $slug => $module) {
|
foreach (self::getAll() as $slug => $module) {
|
||||||
if ($isAdmin) {
|
if ($isAdmin) {
|
||||||
$accessible[$slug] = $module;
|
$accessible[$slug] = $module;
|
||||||
} elseif (in_array($slug, $userProjects, true) && !$module['adminOnly']) {
|
} elseif (in_array($slug, $userProjects, true) && empty($module['adminOnly'])) {
|
||||||
$accessible[$slug] = $module;
|
$accessible[$slug] = $module;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +145,7 @@ class ModuleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isValidModule(string $slug): bool {
|
public static function isValidModule(string $slug): bool {
|
||||||
return isset(self::$modules[$slug]);
|
$all = self::getAll();
|
||||||
|
return isset($all[$slug]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
<?php
|
<?php
|
||||||
$current_project = 'plants';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
session_start();
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
require_once 'db.php';
|
require_once 'db.php';
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
}
|
||||||
|
|
||||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
if (file_exists(__DIR__ . '/.env') && class_exists('Dotenv\Dotenv')) {
|
||||||
$dotenv->load();
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->safeLoad();
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
|
|
@ -107,8 +116,21 @@ try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
$stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1");
|
$stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1");
|
||||||
$stmt->execute([$currentUserId]);
|
$stmt->execute([$currentUserId]);
|
||||||
if ($stmt->rowCount() === 0) throw new Exception('Nicht genügend KI-Credits (1 benötigt).', 402);
|
$geminiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: '';
|
||||||
$response_data = callGeminiAPI($json_data['payload'], $_ENV['GEMINI_API_KEY']);
|
if (empty($geminiKey) && file_exists(__DIR__ . '/../home/.env')) {
|
||||||
|
foreach (file(__DIR__ . '/../home/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
if (strpos($line, '=') !== false) {
|
||||||
|
list($k, $v) = explode('=', $line, 2);
|
||||||
|
if (trim($k) === 'GEMINI_API_KEY') {
|
||||||
|
$geminiKey = trim(trim($v), '"\'');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response_data = callGeminiAPI($json_data['payload'], $geminiKey);
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
echo json_encode($response_data);
|
echo json_encode($response_data);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
$current_project = 'plants';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
// Lade die Abhängigkeiten und die .env-Variablen
|
// Lade Abhängigkeiten und .env-Variablen
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
$dotenv->load();
|
}
|
||||||
|
|
||||||
|
if (file_exists(__DIR__ . '/.env') && class_exists('Dotenv\Dotenv')) {
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||||
|
$dotenv->safeLoad();
|
||||||
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
|
@ -15,8 +23,21 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dein geheimer API-Schlüssel wird sicher aus der .env-Datei geladen
|
// API-Schlüssel: Erst lokal prüfen, Fallback auf ../home/.env
|
||||||
$apiKey = $_ENV['GEMINI_API_KEY'];
|
$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: '';
|
||||||
|
if (empty($apiKey) && file_exists(__DIR__ . '/../home/.env')) {
|
||||||
|
foreach (file(__DIR__ . '/../home/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
if (strpos($line, '=') !== false) {
|
||||||
|
list($k, $v) = explode('=', $line, 2);
|
||||||
|
if (trim($k) === 'GEMINI_API_KEY') {
|
||||||
|
$apiKey = trim(trim($v), '"\'');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$json_input = file_get_contents('php://input');
|
$json_input = file_get_contents('php://input');
|
||||||
$request_data = json_decode($json_input, true);
|
$request_data = json_decode($json_input, true);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
$current_project = 'plants';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
require_once 'db.php'; // Stellt die $pdo-Verbindung her
|
require_once 'db.php'; // Stellt die $pdo-Verbindung her
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
// Version: 01.10.2025 16:15 (FINAL & COMPLETE)
|
// Version: 01.10.2025 16:15 (FINAL & COMPLETE)
|
||||||
$current_project = 'plants';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once '../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
// Version: 01.10.2025 12:15 (Layout restored)
|
// Version: 01.10.2025 12:15 (Layout restored)
|
||||||
$current_project = 'plants';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once '../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
|
|
||||||
7
plants/module.json
Normal file
7
plants/module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Plant Tracker",
|
||||||
|
"badge": "AI Vision",
|
||||||
|
"description": "Pflanzen-Tracking, Pflege & KI-Erkennung",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
<?php
|
<?php
|
||||||
// api_proxy.php
|
// api_proxy.php
|
||||||
|
|
||||||
$current_project = 'storymachine';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once __DIR__ . '/../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Setze den Antwort-Header auf JSON, damit der Browser weiß, was er empfängt.
|
// 1. Setze den Antwort-Header auf JSON, damit der Browser weiß, was er empfängt.
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
$current_project = 'storymachine';
|
$authFile = __DIR__ . '/../auth.php';
|
||||||
require_once '../auth.php';
|
if (file_exists($authFile)) {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authFile;
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
|
||||||
7
storymachine/module.json
Normal file
7
storymachine/module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Story Machine",
|
||||||
|
"badge": "Creative",
|
||||||
|
"description": "Interaktive Kinder-Geschichtenmaschine",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 3
|
||||||
|
}
|
||||||
58
test/bootstrap.php
Normal file
58
test/bootstrap.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Modul-Bootstrap für test
|
||||||
|
*/
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$authPath = __DIR__ . '/../auth.php';
|
||||||
|
$isHostedOnMainSite = file_exists($authPath);
|
||||||
|
|
||||||
|
if ($isHostedOnMainSite && php_sapi_name() !== 'cli') {
|
||||||
|
$current_project = basename(__DIR__);
|
||||||
|
require_once $authPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUser = $_SESSION['user_email'] ?? 'Gast / Standalone';
|
||||||
|
$isAdmin = !empty($_SESSION['is_admin']);
|
||||||
|
|
||||||
|
if (!function_exists('module_env')) {
|
||||||
|
function module_env(string $key, ?string $default = null): ?string {
|
||||||
|
static $envCache = null;
|
||||||
|
if ($envCache === null) {
|
||||||
|
$envCache = [];
|
||||||
|
$candidates = [
|
||||||
|
__DIR__ . '/.env',
|
||||||
|
__DIR__ . '/../home/.env',
|
||||||
|
__DIR__ . '/../.env'
|
||||||
|
];
|
||||||
|
foreach ($candidates as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if ($lines !== false) {
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
|
if (strpos($line, '=') !== false) {
|
||||||
|
list($k, $v) = explode('=', $line, 2);
|
||||||
|
$cleanKey = trim($k);
|
||||||
|
if (!isset($envCache[$cleanKey])) {
|
||||||
|
$envCache[$cleanKey] = trim(trim($v), '"\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($envCache[$key])) {
|
||||||
|
return $envCache[$key];
|
||||||
|
}
|
||||||
|
$sysEnv = getenv($key);
|
||||||
|
if ($sysEnv !== false && $sysEnv !== '') {
|
||||||
|
return $sysEnv;
|
||||||
|
}
|
||||||
|
return $_ENV[$key] ?? $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
371
test/index.php
371
test/index.php
|
|
@ -1,268 +1,143 @@
|
||||||
<?php
|
<?php
|
||||||
// Session starten, falls noch nicht geschehen
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Einfache Logout-Logik direkt in dieser Datei
|
// Modul-Metadaten
|
||||||
if (isset($_GET['logout'])) {
|
$moduleDescriptor = file_exists(__DIR__ . '/module.json')
|
||||||
session_destroy();
|
? json_decode(file_get_contents(__DIR__ . '/module.json'), true)
|
||||||
header("Location: /");
|
: [];
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Projekt identifizieren und schützen
|
$moduleName = $moduleDescriptor['name'] ?? 'Test & Lab';
|
||||||
$current_project = 'test';
|
$moduleBadge = $moduleDescriptor['badge'] ?? 'Audio';
|
||||||
require_once __DIR__ . '/../auth.php';
|
$moduleDesc = $moduleDescriptor['description'] ?? 'Modul-Showcase & Web Audio Synthesizer';
|
||||||
|
|
||||||
// 2. Nutzerdaten abrufen
|
|
||||||
$loggedInUser = isset($_SESSION['user_email']) ? $_SESSION['user_email'] : 'neuankoemmling@beispiel.de';
|
|
||||||
$isAdmin = !empty($_SESSION['is_admin']);
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Erster Login-Check: Erfolgreich!</title>
|
<title><?= htmlspecialchars($moduleName) ?> – philippurbschat.de</title>
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: #ff00ff;
|
|
||||||
--secondary: #00ff00;
|
|
||||||
--bg: #222;
|
|
||||||
--text: #fff;
|
|
||||||
--accent: #ffff00;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
<link rel="stylesheet" href="/css/app.css">
|
||||||
background-color: var(--bg);
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
color: var(--text);
|
|
||||||
font-family: 'Comic Sans MS', 'Comic Sans', cursive, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar {
|
<!-- Standalone-Fallback -->
|
||||||
position: fixed;
|
<script>
|
||||||
top: 15px;
|
window.addEventListener('error', function(e) {
|
||||||
left: 15px;
|
if (e.target && e.target.tagName === 'LINK' && e.target.href.includes('/css/app.css')) {
|
||||||
z-index: 100;
|
const cdn = document.createElement('script');
|
||||||
display: flex;
|
cdn.src = 'https://cdn.tailwindcss.com';
|
||||||
gap: 10px;
|
document.head.appendChild(cdn);
|
||||||
font-family: sans-serif;
|
}
|
||||||
font-size: 13px;
|
}, true);
|
||||||
}
|
</script>
|
||||||
|
|
||||||
.top-bar a {
|
|
||||||
color: #fff;
|
|
||||||
background: #000;
|
|
||||||
padding: 8px 14px;
|
|
||||||
border: 2px solid var(--accent);
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: transform 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar a:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-bar a.admin-btn {
|
|
||||||
color: #000;
|
|
||||||
background: var(--secondary);
|
|
||||||
border-color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: #333;
|
|
||||||
border: 8px solid var(--primary);
|
|
||||||
padding: 40px;
|
|
||||||
max-width: 600px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 30px;
|
|
||||||
transform: rotate(-1.5deg);
|
|
||||||
box-shadow: 20px 20px 0px var(--secondary);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: 4px dashed var(--accent);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
transform: rotate(2deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--secondary);
|
|
||||||
text-shadow: 3px 3px 0px var(--primary);
|
|
||||||
font-size: 2.2rem;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
background: #444;
|
|
||||||
padding: 5px 15px;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: 2px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
text-align: center;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#whoopee-cushion {
|
|
||||||
width: 160px;
|
|
||||||
height: 160px;
|
|
||||||
background: #ff4d4d;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 6px solid #b30000;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
transition: transform 0.05s;
|
|
||||||
box-shadow: 0 12px 0 #b30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#whoopee-cushion:active {
|
|
||||||
transform: translateY(10px) scale(0.92);
|
|
||||||
box-shadow: 0 2px 0 #b30000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#whoopee-cushion::after {
|
|
||||||
content: "HIER DRÜCKEN!";
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fart-cloud {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(180, 255, 100, 0.7);
|
|
||||||
border-radius: 50%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
animation: float-up 1.5s ease-out forwards;
|
|
||||||
filter: blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float-up {
|
|
||||||
0% { transform: translateY(0) scale(0.5); opacity: 0.8; }
|
|
||||||
100% { transform: translateY(-150px) scale(4); opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
|
||||||
display: block;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 30px auto 0;
|
|
||||||
background: var(--accent);
|
|
||||||
color: #000;
|
|
||||||
padding: 12px 25px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 10px;
|
|
||||||
transform: rotate(-2deg);
|
|
||||||
border: 4px solid #000;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn:hover {
|
|
||||||
transform: rotate(0deg) scale(1.1);
|
|
||||||
background: #ff3300;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shake {
|
|
||||||
animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
10%, 90% { transform: translate3d(-2px, 0, 0) rotate(-1.5deg); }
|
|
||||||
20%, 80% { transform: translate3d(4px, 0, 0) rotate(-2deg); }
|
|
||||||
30%, 50%, 70% { transform: translate3d(-6px, 0, 0) rotate(-1deg); }
|
|
||||||
40%, 60% { transform: translate3d(6px, 0, 0) rotate(-1.8deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-slate-950 text-slate-200 min-h-screen font-sans flex flex-col antialiased selection:bg-cyan-500 selection:text-white">
|
||||||
<div class="top-bar">
|
|
||||||
<a href="/">← Website</a>
|
|
||||||
<?php if ($isAdmin): ?>
|
|
||||||
<a href="/admin" class="admin-btn">Admin-Bereich</a>
|
|
||||||
<a href="/admin/users" class="admin-btn">Benutzerverwaltung</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" id="app-container">
|
<!-- Top Navigation Bar -->
|
||||||
<header class="header">
|
<header class="border-b border-slate-800/80 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
|
||||||
<h1>Willkommen beim ersten Check! 🎉</h1>
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3.5 flex items-center justify-between">
|
||||||
<div class="user-info">
|
<div class="flex items-center gap-3">
|
||||||
Eingeloggt als: <strong><?php echo htmlspecialchars($loggedInUser); ?></strong>
|
<a href="/" class="text-xs font-mono text-slate-400 hover:text-cyan-400 transition-colors flex items-center gap-1.5 group">
|
||||||
|
<span class="group-hover:-translate-x-0.5 transition-transform">←</span> philippurbschat.de
|
||||||
|
</a>
|
||||||
|
<span class="text-slate-700">/</span>
|
||||||
|
<span class="text-xs font-mono text-slate-300 font-medium">test</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 text-cyan-400 uppercase tracking-wider">
|
||||||
<main class="main-content">
|
<?= htmlspecialchars($moduleBadge) ?>
|
||||||
<p><strong>Glückwunsch!</strong> Deine Zugangsdaten sind korrekt und du bist erfolgreich im System gelandet.</p>
|
</span>
|
||||||
<p style="font-size: 0.9rem;">Um sicherzugehen, dass auch interaktive Elemente bei dir funktionieren, teste bitte unser hochmodernes Belohnungsmodul:</p>
|
<?php if ($isAdmin): ?>
|
||||||
|
<a href="/admin/users" class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-fuchsia-500/30 bg-fuchsia-500/10 text-fuchsia-400 hover:bg-fuchsia-500/20 transition-colors uppercase tracking-wider">
|
||||||
<div class="fart-section" style="margin-top: 30px;">
|
Benutzer
|
||||||
<div id="whoopee-cushion" title="Funktionstest für Sound und Animation"></div>
|
</a>
|
||||||
<p id="status-msg" style="margin-top: 20px; font-style: italic; color: #888;">Hinweis: Sound bitte einschalten!</p>
|
<?php endif; ?>
|
||||||
|
<span class="text-[11px] font-mono text-slate-500 hidden sm:inline-block">
|
||||||
|
<?= htmlspecialchars($currentUser) ?>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<footer>
|
<!-- Main Content -->
|
||||||
<!-- Link angepasst auf Parameter-Logout -->
|
<main class="grow max-w-5xl mx-auto px-4 sm:px-6 py-10 w-full">
|
||||||
<a href="?logout=1" class="logout-btn">Test beendet? Hier abmelden!</a>
|
<!-- Header -->
|
||||||
</footer>
|
<div class="mb-10">
|
||||||
</div>
|
<div class="inline-flex items-center gap-2 mb-3">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"></span>
|
||||||
|
<span class="text-xs font-mono uppercase tracking-[0.2em] text-cyan-400">Audio Lab</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold font-mono tracking-tight text-white mb-2">
|
||||||
|
<?= htmlspecialchars($moduleName) ?>
|
||||||
|
</h1>
|
||||||
|
<p class="text-slate-400 text-sm max-w-2xl">
|
||||||
|
<?= htmlspecialchars($moduleDesc) ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sandbox & Synth Card -->
|
||||||
|
<div id="synth-container" class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 sm:p-8 backdrop-blur shadow-2xl relative overflow-hidden transition-transform duration-200">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-cyan-500/40 to-transparent"></div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||||
|
<!-- Info Section -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h2 class="text-xs font-mono uppercase tracking-widest text-slate-400">Web Audio API Test</h2>
|
||||||
|
<p class="text-sm text-slate-300 leading-relaxed">
|
||||||
|
Dieses Modul dient als Referenzimplementierung für das neue Modulsystem. Es demonstriert dynamische Web Audio Synthese (Oszillatoren, Rauschgeneratoren und Biquad-Filter) im Browser.
|
||||||
|
</p>
|
||||||
|
<div class="p-4 rounded-xl bg-slate-950/70 border border-slate-800/80 font-mono text-xs space-y-2">
|
||||||
|
<div class="text-slate-400">STATUS: <span id="audio-status" class="text-emerald-400">Bereit für Signaltest</span></div>
|
||||||
|
<div class="text-slate-500 text-[11px]">System: Graceful Auth • Auto-Discovered</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interactive Trigger -->
|
||||||
|
<div class="flex flex-col items-center justify-center p-8 rounded-xl bg-slate-950/60 border border-slate-800/60 text-center">
|
||||||
|
<button id="trigger-sound" class="w-32 h-32 rounded-full bg-gradient-to-br from-cyan-500/20 to-fuchsia-500/20 border-2 border-cyan-500/50 hover:border-cyan-400 hover:shadow-[0_0_30px_rgba(6,182,212,0.3)] transition-all duration-300 flex flex-col items-center justify-center gap-2 group cursor-pointer active:scale-95 select-none">
|
||||||
|
<svg class="w-8 h-8 text-cyan-400 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-[10px] font-mono uppercase tracking-widest text-slate-300 group-hover:text-white">Signal Starten</span>
|
||||||
|
</button>
|
||||||
|
<p class="text-xs font-mono text-slate-500 mt-4">Klicken zum Erzeugen eines synthetischen Klanges</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="border-t border-slate-900 py-6 text-center text-xs font-mono text-slate-600">
|
||||||
|
<?= htmlspecialchars($moduleName) ?> • Philipp Urbschat Lab
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const cushion = document.getElementById('whoopee-cushion');
|
const triggerBtn = document.getElementById('trigger-sound');
|
||||||
const container = document.getElementById('app-container');
|
const synthContainer = document.getElementById('synth-container');
|
||||||
const statusMsg = document.getElementById('status-msg');
|
const statusMsg = document.getElementById('audio-status');
|
||||||
|
|
||||||
function playOrganicFart() {
|
function playSynthesizerTone() {
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||||
|
|
||||||
const duration = 0.4 + Math.random() * 0.5;
|
const duration = 0.4 + Math.random() * 0.4;
|
||||||
const mainGain = audioCtx.createGain();
|
const mainGain = audioCtx.createGain();
|
||||||
|
|
||||||
const osc = audioCtx.createOscillator();
|
const osc = audioCtx.createOscillator();
|
||||||
const oscGain = audioCtx.createGain();
|
const oscGain = audioCtx.createGain();
|
||||||
osc.type = 'sawtooth';
|
osc.type = 'sawtooth';
|
||||||
osc.frequency.setValueAtTime(45 + Math.random() * 20, audioCtx.currentTime);
|
osc.frequency.setValueAtTime(55 + Math.random() * 25, audioCtx.currentTime);
|
||||||
osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + duration);
|
osc.frequency.exponentialRampToValueAtTime(25, audioCtx.currentTime + duration);
|
||||||
|
|
||||||
const lfo = audioCtx.createOscillator();
|
const lfo = audioCtx.createOscillator();
|
||||||
const lfoGain = audioCtx.createGain();
|
const lfoGain = audioCtx.createGain();
|
||||||
lfo.type = 'square';
|
lfo.type = 'square';
|
||||||
lfo.frequency.setValueAtTime(14 + Math.random() * 8, audioCtx.currentTime);
|
lfo.frequency.setValueAtTime(12 + Math.random() * 6, audioCtx.currentTime);
|
||||||
lfoGain.gain.setValueAtTime(0.8, audioCtx.currentTime);
|
lfoGain.gain.setValueAtTime(0.7, audioCtx.currentTime);
|
||||||
lfo.connect(lfoGain);
|
lfo.connect(lfoGain);
|
||||||
lfoGain.connect(oscGain.gain);
|
lfoGain.connect(oscGain.gain);
|
||||||
|
|
||||||
const bufferSize = audioCtx.sampleRate * duration;
|
const bufferSize = Math.floor(audioCtx.sampleRate * duration);
|
||||||
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
||||||
const noiseData = noiseBuffer.getChannelData(0);
|
const noiseData = noiseBuffer.getChannelData(0);
|
||||||
for (let i = 0; i < bufferSize; i++) {
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
|
@ -272,15 +147,15 @@ $isAdmin = !empty($_SESSION['is_admin']);
|
||||||
noise.buffer = noiseBuffer;
|
noise.buffer = noiseBuffer;
|
||||||
const noiseFilter = audioCtx.createBiquadFilter();
|
const noiseFilter = audioCtx.createBiquadFilter();
|
||||||
noiseFilter.type = 'bandpass';
|
noiseFilter.type = 'bandpass';
|
||||||
noiseFilter.frequency.setValueAtTime(400, audioCtx.currentTime);
|
noiseFilter.frequency.setValueAtTime(380, audioCtx.currentTime);
|
||||||
|
|
||||||
const lowPass = audioCtx.createBiquadFilter();
|
const lowPass = audioCtx.createBiquadFilter();
|
||||||
lowPass.type = 'lowpass';
|
lowPass.type = 'lowpass';
|
||||||
lowPass.frequency.setValueAtTime(450, audioCtx.currentTime);
|
lowPass.frequency.setValueAtTime(420, audioCtx.currentTime);
|
||||||
lowPass.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + duration);
|
lowPass.frequency.exponentialRampToValueAtTime(70, audioCtx.currentTime + duration);
|
||||||
|
|
||||||
mainGain.gain.setValueAtTime(0, audioCtx.currentTime);
|
mainGain.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||||
mainGain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + 0.05);
|
mainGain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.05);
|
||||||
mainGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
|
mainGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
|
||||||
|
|
||||||
osc.connect(oscGain);
|
osc.connect(oscGain);
|
||||||
|
|
@ -298,29 +173,13 @@ $isAdmin = !empty($_SESSION['is_admin']);
|
||||||
lfo.stop(audioCtx.currentTime + duration);
|
lfo.stop(audioCtx.currentTime + duration);
|
||||||
noise.stop(audioCtx.currentTime + duration);
|
noise.stop(audioCtx.currentTime + duration);
|
||||||
|
|
||||||
statusMsg.textContent = "Interaktionstest: Bestanden! 👍";
|
statusMsg.textContent = "Signal generiert (Synthese erfolgreich)";
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCloud() {
|
triggerBtn.addEventListener('click', () => {
|
||||||
const cloud = document.createElement('div');
|
playSynthesizerTone();
|
||||||
cloud.classList.add('fart-cloud');
|
synthContainer.classList.add('ring-1', 'ring-cyan-500/50');
|
||||||
const size = Math.random() * 60 + 20;
|
setTimeout(() => synthContainer.classList.remove('ring-1', 'ring-cyan-500/50'), 300);
|
||||||
cloud.style.width = size + 'px';
|
|
||||||
cloud.style.height = size + 'px';
|
|
||||||
const rect = cushion.getBoundingClientRect();
|
|
||||||
cloud.style.left = (rect.left + rect.width / 2 - size / 2 + (Math.random() * 100 - 50)) + 'px';
|
|
||||||
cloud.style.top = (rect.top + rect.height / 2 - size / 2) + 'px';
|
|
||||||
document.body.appendChild(cloud);
|
|
||||||
setTimeout(() => cloud.remove(), 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
cushion.addEventListener('click', () => {
|
|
||||||
playOrganicFart();
|
|
||||||
container.classList.add('shake');
|
|
||||||
setTimeout(() => container.classList.remove('shake'), 400);
|
|
||||||
for(let i = 0; i < 12; i++) {
|
|
||||||
setTimeout(createCloud, i * 80);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
7
test/module.json
Normal file
7
test/module.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "Test & Lab",
|
||||||
|
"badge": "Audio",
|
||||||
|
"description": "Modul-Showcase & Web Audio Synthesizer",
|
||||||
|
"adminOnly": false,
|
||||||
|
"order": 4
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue