diff --git a/.gitignore b/.gitignore
index ca51de8..e2bb057 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ secret_keys.txt
**/.humaninput/
**/.env
!**/.env.example
+App/uploads/
diff --git a/App/pages/foto-upload.php b/App/pages/foto-upload.php
new file mode 100644
index 0000000..44b4389
--- /dev/null
+++ b/App/pages/foto-upload.php
@@ -0,0 +1,227 @@
+/ — dieser
+ * Ordner ist per .htaccess gesperrt (Originale enthalten EXIF/GPS!).
+ * Atlas holt sie vom Server, strippt EXIF, skaliert und baut sie in die Slots ein.
+ */
+require_once __DIR__ . '/../php/config/app.php';
+$bp = BASE_PATH;
+
+$UPLOAD_BASE = __DIR__ . '/../uploads/buch-fotos';
+$MAX_BYTES = 40 * 1024 * 1024; // 40 MB je Datei
+$MAX_FILES = 12; // je Absenden
+$ALLOWED_EXT = ['jpg','jpeg','png','heic','heif','webp','tif','tiff'];
+
+/* ---------- Slots aus den Kapiteln parsen ---------- */
+$ORDER = ['tourismustal','tourismusregion','kofferdetektiv','busfahrt','logistik',
+ 'weltkueche','farmer','klima','fluss','energiemanager','heli',
+ 'sonnensystem','eu-werkstatt'];
+$KAPITEL = [];
+foreach ($ORDER as $id) {
+ $file = __DIR__ . "/buch-{$id}.php";
+ if (!is_file($file)) continue;
+ $src = file_get_contents($file);
+ $titel = preg_match('/
(.*?)<\/h1>/s', $src, $m) ? trim(strip_tags($m[1])) : $id;
+ preg_match_all("/buchBild\(\s*'((?:[^'\\\\]|\\\\.)*)'\s*,\s*'((?:[^'\\\\]|\\\\.)*)'/s", $src, $mm, PREG_SET_ORDER);
+ $slots = [];
+ foreach ($mm as $b) {
+ $slug = stripcslashes($b[1]);
+ $vorschlag = stripcslashes($b[2]);
+ // Leicht- und Normal-Version rufen denselben Slot auf → längste Beschreibung gewinnt
+ if (!isset($slots[$slug]) || mb_strlen($vorschlag) > mb_strlen($slots[$slug])) {
+ $slots[$slug] = $vorschlag;
+ }
+ }
+ if ($slots) $KAPITEL[$id] = ['titel' => $titel, 'slots' => $slots];
+}
+
+/* ---------- Helfer ---------- */
+function slotBildPfad(string $sim, string $slug): ?string {
+ foreach (['jpg','jpeg','webp','png'] as $ext) {
+ $rel = "assets/img/buch/{$sim}/{$slug}.{$ext}";
+ if (is_file(__DIR__ . '/../' . $rel)) return $rel;
+ }
+ return null;
+}
+function pendingUploads(string $base, string $sim, string $slug): array {
+ $dir = "{$base}/{$sim}";
+ if (!is_dir($dir)) return [];
+ $out = [];
+ foreach (glob($dir . '/' . $slug . '__*') as $f) {
+ if (substr($f, -9) === '.note.txt') continue;
+ $out[] = ['name' => basename($f), 'kb' => (int)round(filesize($f) / 1024)];
+ }
+ return $out;
+}
+function safeName(string $s): string {
+ $s = preg_replace('/[^A-Za-z0-9._-]+/', '-', $s);
+ return trim(mb_substr($s, 0, 80), '-.') ?: 'foto';
+}
+
+/* ---------- Upload verarbeiten (PRG-Muster) ---------- */
+$fehler = [];
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $sim = $_POST['sim'] ?? '';
+ $slug = preg_replace('/[^a-z0-9-]/', '', $_POST['slug'] ?? '');
+ $note = trim($_POST['note'] ?? '');
+
+ $gueltig = ($sim === 'sonstiges' && $slug !== '')
+ || (isset($KAPITEL[$sim]) && isset($KAPITEL[$sim]['slots'][$slug]));
+ if (!$gueltig) {
+ $fehler[] = 'Unbekannter Foto-Slot.';
+ } else {
+ $dir = "{$UPLOAD_BASE}/{$sim}";
+ if (!is_dir($dir)) mkdir($dir, 0775, true);
+ // Ordner absichern: Originale (mit GPS!) dürfen nie öffentlich abrufbar sein
+ if (!is_file($UPLOAD_BASE . '/.htaccess')) {
+ file_put_contents($UPLOAD_BASE . '/.htaccess', "Require all denied\n");
+ }
+ $ts = date('Ymd-His');
+ $gespeichert = 0;
+ if (!empty($_FILES['fotos']) && is_array($_FILES['fotos']['name'])) {
+ $n = min(count($_FILES['fotos']['name']), $MAX_FILES);
+ for ($i = 0; $i < $n; $i++) {
+ if ($_FILES['fotos']['error'][$i] === UPLOAD_ERR_NO_FILE) continue;
+ if ($_FILES['fotos']['error'][$i] !== UPLOAD_ERR_OK) { $fehler[] = 'Upload-Fehler bei Datei ' . ($i + 1) . ' (Code ' . $_FILES['fotos']['error'][$i] . ').'; continue; }
+ if ($_FILES['fotos']['size'][$i] > $MAX_BYTES) { $fehler[] = htmlspecialchars($_FILES['fotos']['name'][$i]) . ' ist größer als 40 MB.'; continue; }
+ $orig = $_FILES['fotos']['name'][$i];
+ $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
+ if (!in_array($ext, $ALLOWED_EXT, true)) { $fehler[] = htmlspecialchars($orig) . ': Dateityp .' . htmlspecialchars($ext) . ' nicht erlaubt.'; continue; }
+ $ziel = "{$dir}/{$slug}__{$ts}__" . ($i + 1) . '__' . safeName($orig);
+ if (move_uploaded_file($_FILES['fotos']['tmp_name'][$i], $ziel)) $gespeichert++;
+ else $fehler[] = htmlspecialchars($orig) . ' konnte nicht gespeichert werden.';
+ }
+ }
+ if ($gespeichert === 0 && !$fehler) $fehler[] = 'Keine Datei ausgewählt.';
+ if ($note !== '' && ($gespeichert > 0 || $sim === 'sonstiges')) {
+ file_put_contents("{$dir}/{$slug}__{$ts}.note.txt", $note . "\n");
+ }
+ if ($gespeichert > 0 && !$fehler) {
+ header("Location: ?ok={$gespeichert}&sim=" . urlencode($sim) . "&slug=" . urlencode($slug) . "#{$sim}-{$slug}");
+ exit;
+ }
+ }
+}
+$ok = isset($_GET['ok']) ? (int)$_GET['ok'] : 0;
+$okAnker = $ok ? (($_GET['sim'] ?? '') . '-' . ($_GET['slug'] ?? '')) : '';
+
+/* Übersichtszahlen */
+$gesamt = 0; $mitFoto = 0; $wartend = 0;
+foreach ($KAPITEL as $sim => $k) {
+ foreach ($k['slots'] as $slug => $v) {
+ $gesamt++;
+ if (slotBildPfad($sim, $slug)) $mitFoto++;
+ if (pendingUploads($UPLOAD_BASE, $sim, $slug)) $wartend++;
+ }
+}
+?>
+
+
+
+
+
+📷 Foto-Backend · Schulbuch · GeoGraSim
+
+
+
+
+
📷 Foto-Backend zum Schulbuch
+
Alle Foto-Slots aus den 13 Schulbuch-Kapiteln mit ihren
+ Foto-Wünschen. Zu jedem Slot kannst du direkt Fotos hochladen (mehrere auf einmal, auch HEIC
+ vom iPhone/iPad) und eine Notiz dazuschreiben — Atlas holt sie ab, entfernt die GPS-Daten,
+ verkleinert sie und baut sie ein.
+
📷 = $gesamt ?> Slots
+
✅ = $mitFoto ?> eingebaut
+
📥 = $wartend ?> mit wartenden Uploads
+
+
🔒 Hochgeladene Original-Dateien sind nicht öffentlich abrufbar
+ (Ordner gesperrt) — deine GPS-Daten bleiben privat. Veröffentlicht wird nur die bereinigte
+ Version im Kapitel.
+
+
✅ = $ok ?> Foto= $ok > 1 ? 's' : '' ?> hochgeladen — danke! Atlas kümmert sich um Zuschnitt und Einbau.
+
⚠️ = $f ?>
+
+
+
+ $k): ?>
+
= htmlspecialchars($k['titel']) ?>
+ $vorschlag):
+ $bild = slotBildPfad($sim, $slug);
+ $pend = pendingUploads($UPLOAD_BASE, $sim, $slug);
+ $status = $bild ? 'hat' : ($pend ? 'wartet' : 'offen');
+ ?>
+
+
+ = $slug ?>
+ = $bild ? '✅ Foto eingebaut' : ($pend ? '📥 Upload wartet auf Einbau' : '📷 Foto gesucht') ?>
+
+
= htmlspecialchars($vorschlag) ?>
+

+
📥 = htmlspecialchars($p['name']) ?> (= $p['kb'] ?> KB)
+
+
+
+
+
+
Sonstiges / ohne Zuordnung
+
+
Fotos, die zu keinem Slot passen, aber gut sein könnten — mit kurzer
+ Beschreibung, wofür du sie siehst. Atlas ordnet sie zu oder legt neue Slots an.
+
+
+
+
+