diff --git a/_template/README.md b/_template/README.md
new file mode 100644
index 0000000..49aff64
--- /dev/null
+++ b/_template/README.md
@@ -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.
diff --git a/_template/api.php b/_template/api.php
new file mode 100644
index 0000000..b286e8e
--- /dev/null
+++ b/_template/api.php
@@ -0,0 +1,75 @@
+ '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;
diff --git a/_template/bootstrap.php b/_template/bootstrap.php
new file mode 100644
index 0000000..19995b0
--- /dev/null
+++ b/_template/bootstrap.php
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+ = htmlspecialchars($moduleName) ?> – philippurbschat.de
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System Ready
+
+
+ = htmlspecialchars($moduleName) ?>
+
+
+ = htmlspecialchars($moduleDesc) ?>
+
+
+
+
+
+
+
+
+
+
Modul Workspace
+
+ Hier startet dein neues Mini-Projekt. Du hast vollen Zugriff auf Tailwind CSS, den zentralen Gemini API-Key und das Dark-Design der Hauptseite.
+
+
+
+
+
+
+ Schnelltest: Gemini API Relay
+
+
+
+
+ Ausführen →
+
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($moduleName) ?> • Philipp Urbschat Lab
+
+
+
+
+
diff --git a/_template/module.json b/_template/module.json
new file mode 100644
index 0000000..b920354
--- /dev/null
+++ b/_template/module.json
@@ -0,0 +1,7 @@
+{
+ "name": "Neues Projekt",
+ "badge": "Labor",
+ "description": "Experimentelles Mini-Projekt",
+ "adminOnly": false,
+ "order": 10
+}
diff --git a/dinos/api_proxy.php b/dinos/api_proxy.php
index 3500eb5..80ebff4 100644
--- a/dinos/api_proxy.php
+++ b/dinos/api_proxy.php
@@ -1,8 +1,11 @@
[
'slug' => 'dinos',
'name' => 'Dino Generator',
'badge' => 'AI Art',
'description' => 'KI Dino-Bilderstellung & Google Photos Sync',
'path' => '/dinos',
- 'adminOnly' => false
+ 'adminOnly' => false,
+ 'order' => 1
],
'plants' => [
'slug' => 'plants',
@@ -15,7 +25,8 @@ class ModuleService {
'badge' => 'AI Vision',
'description' => 'Pflanzen-Tracking, Pflege & KI-Erkennung',
'path' => '/plants',
- 'adminOnly' => false
+ 'adminOnly' => false,
+ 'order' => 2
],
'storymachine' => [
'slug' => 'storymachine',
@@ -23,32 +34,110 @@ class ModuleService {
'badge' => 'Creative',
'description' => 'Interaktive Kinder-Geschichtenmaschine',
'path' => '/storymachine',
- 'adminOnly' => false
+ 'adminOnly' => false,
+ 'order' => 3
],
'test' => [
'slug' => 'test',
- 'name' => 'Test',
- 'badge' => 'Test',
- 'description' => 'Interaktive Web Audio API Synthese & Soundtest',
+ 'name' => 'Test & Lab',
+ 'badge' => 'Audio',
+ 'description' => 'Modul-Showcase & Web Audio Synthesizer',
'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 {
- return self::$modules[$slug] ?? null;
+ $all = self::getAll();
+ return $all[$slug] ?? null;
}
public static function getAccessibleForUser(array $userProjects, bool $isAdmin = false): array {
$accessible = [];
- foreach (self::$modules as $slug => $module) {
+ foreach (self::getAll() as $slug => $module) {
if ($isAdmin) {
$accessible[$slug] = $module;
- } elseif (in_array($slug, $userProjects, true) && !$module['adminOnly']) {
+ } elseif (in_array($slug, $userProjects, true) && empty($module['adminOnly'])) {
$accessible[$slug] = $module;
}
}
@@ -56,6 +145,7 @@ class ModuleService {
}
public static function isValidModule(string $slug): bool {
- return isset(self::$modules[$slug]);
+ $all = self::getAll();
+ return isset($all[$slug]);
}
}
diff --git a/plants/api.php b/plants/api.php
index e1c73dd..72e8b41 100644
--- a/plants/api.php
+++ b/plants/api.php
@@ -1,13 +1,22 @@
load();
+if (file_exists(__DIR__ . '/.env') && class_exists('Dotenv\Dotenv')) {
+ $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
+ $dotenv->safeLoad();
+}
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
@@ -107,8 +116,21 @@ try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("UPDATE users SET ai_credits = ai_credits - 1 WHERE id = ? AND ai_credits >= 1");
$stmt->execute([$currentUserId]);
- if ($stmt->rowCount() === 0) throw new Exception('Nicht genügend KI-Credits (1 benötigt).', 402);
- $response_data = callGeminiAPI($json_data['payload'], $_ENV['GEMINI_API_KEY']);
+ $geminiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('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();
echo json_encode($response_data);
break;
diff --git a/plants/api_proxy.php b/plants/api_proxy.php
index 7995910..afd2adb 100644
--- a/plants/api_proxy.php
+++ b/plants/api_proxy.php
@@ -1,11 +1,19 @@
load();
+// Lade Abhängigkeiten und .env-Variablen
+if (file_exists(__DIR__ . '/vendor/autoload.php')) {
+ require_once __DIR__ . '/vendor/autoload.php';
+}
+
+if (file_exists(__DIR__ . '/.env') && class_exists('Dotenv\Dotenv')) {
+ $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
+ $dotenv->safeLoad();
+}
header('Content-Type: application/json');
@@ -15,8 +23,21 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
-// Dein geheimer API-Schlüssel wird sicher aus der .env-Datei geladen
-$apiKey = $_ENV['GEMINI_API_KEY'];
+// API-Schlüssel: Erst lokal prüfen, Fallback auf ../home/.env
+$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');
$request_data = json_decode($json_input, true);
diff --git a/plants/auth_api.php b/plants/auth_api.php
index db1993f..9fab337 100644
--- a/plants/auth_api.php
+++ b/plants/auth_api.php
@@ -1,6 +1,9 @@
diff --git a/storymachine/module.json b/storymachine/module.json
new file mode 100644
index 0000000..b188e94
--- /dev/null
+++ b/storymachine/module.json
@@ -0,0 +1,7 @@
+{
+ "name": "Story Machine",
+ "badge": "Creative",
+ "description": "Interaktive Kinder-Geschichtenmaschine",
+ "adminOnly": false,
+ "order": 3
+}
diff --git a/test/bootstrap.php b/test/bootstrap.php
new file mode 100644
index 0000000..bb912f7
--- /dev/null
+++ b/test/bootstrap.php
@@ -0,0 +1,58 @@
+
- Erster Login-Check: Erfolgreich!
-
+
+
-
-
+
-
-