Compare commits
225 Commits
778634fab6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dba841830 | |||
| ff889865e1 | |||
| eea5688c76 | |||
| 50c72fa8d8 | |||
| 96a73e22b2 | |||
| 7691649f37 | |||
| bb4d3b4cb6 | |||
| b109ebb099 | |||
| 93c742b381 | |||
| ff91c489f7 | |||
| 085b0b6aa8 | |||
| 637c058507 | |||
| 5b6c25bcd9 | |||
| 77a3e2f16c | |||
| 3f29ad0533 | |||
| 248a46f829 | |||
| 2e9ad6a83f | |||
| 6009eabca1 | |||
| 41731254b1 | |||
| 1dffef8200 | |||
| 496fc59c7b | |||
| 8f89613eee | |||
| 6135dd2648 | |||
| aab9b35781 | |||
| 014eb418a6 | |||
| e901f5addb | |||
| abc8bf5149 | |||
| 54772180a2 | |||
| 4cbdb6bbb1 | |||
| b835475ef7 | |||
| ec746f250f | |||
| 6c2990aa5e | |||
| 7b1bdb8d1b | |||
| c913522a77 | |||
| 502f271f36 | |||
| 362aaba883 | |||
| 88e3c09e5c | |||
| 539f6b012e | |||
| 7e1a7c5228 | |||
| ce7bff3184 | |||
| 89d4d8587e | |||
| a4e57d7aa6 | |||
| 3bc0f418be | |||
| 0a6fa2172f | |||
| c79ef691ad | |||
| bf579310ed | |||
| 691387c4d8 | |||
| 62e903a5ec | |||
| 11c48817de | |||
| 8ec005f889 | |||
| 7de71b0e83 | |||
| b77f4ed912 | |||
| 868cdda276 | |||
| 9a1ee0fae1 | |||
| 1e1c1238bd | |||
| de104e915a | |||
| 193c6dcc8f | |||
| 74736a33aa | |||
| f7bf0f0901 | |||
| cc83493055 | |||
| abbc0580c5 | |||
| be7d9fce1a | |||
| da1e6e6e87 | |||
| bc468272ba | |||
| 4976582400 | |||
| bf5b026b46 | |||
| 77023ff7a3 | |||
| aaa40e9f1a | |||
| 0d9481cae2 | |||
| 0d765961af | |||
| 7986bb2eb6 | |||
| 6dfcf4d57f | |||
| 0532327e6a | |||
| 5d79fa3c3a | |||
| c9eb0e7764 | |||
| 3c28794012 | |||
| 5633d3f9f9 | |||
| e4b877de4b | |||
| 550a35eae8 | |||
| 47adf0497d | |||
| 48971b4588 | |||
| 2386756a20 | |||
| 0c4fb84606 | |||
| 2160a99896 | |||
| 8cc48b1bc6 | |||
| eb67cbc79f | |||
| 3c856ff5ce | |||
| 64a9a62157 | |||
| ad8c6b7d7e | |||
| 71226581fa | |||
| 540b27a2bd | |||
| 5635fa26b4 | |||
| 4e38b27c64 | |||
| 85ee58c56d | |||
| fecd8bb9c9 | |||
| 9c99359d9c | |||
| b32097b617 | |||
| 257b5e5099 | |||
| 532b5471ec | |||
| ff751f3a05 | |||
| 714abc026a | |||
| 8c316f22e8 | |||
| 8ffc43d7b2 | |||
| 9bc3ca7b26 | |||
| 13f14c26a8 | |||
| d7dd3f9de5 | |||
| 8977901e89 | |||
| f934a7f9e4 | |||
| 0c2225594b | |||
| 7f997bff84 | |||
| 423490260d | |||
| b6b6c6ccce | |||
| a68c58ac9e | |||
| c3e61f4da7 | |||
| b353f25b07 | |||
| 5fbbf506b5 | |||
| d4c7fd5009 | |||
| bf2a4cd0d1 | |||
| 4c0183f2e0 | |||
| 0c4064bb58 | |||
| 5d3cd1bdcf | |||
| 5b0505b140 | |||
| c950f954d1 | |||
| a568f81b0a | |||
| 182a87978e | |||
| 5b91a5bf84 | |||
| 0555a2d245 | |||
| 5f603cb193 | |||
| 913f161502 | |||
| 665b43e069 | |||
| d9f9f901a1 | |||
| e23ebd960e | |||
| c1f90e255d | |||
| 37f6238d05 | |||
| 5d94572569 | |||
| db4b75c5e6 | |||
| 9937fbf62a | |||
| 5655af81bf | |||
| 4bfb6539cb | |||
| f6df2eb79f | |||
| ec102b7c36 | |||
| 9ec7ad67f1 | |||
| b9bcbf9198 | |||
| 2bdaa0f6cc | |||
| b0fe3fdf86 | |||
| ad627331f0 | |||
| 75511630c8 | |||
| 81ba268ea8 | |||
| 1950542ae5 | |||
| eba9e54c70 | |||
| 8b1088a070 | |||
| fb44ec7afe | |||
| 2984d93e7e | |||
| a3c5753cd7 | |||
| 031ca87daf | |||
| 405fcc2b69 | |||
| 7f774c7b2a | |||
| bbef4856d0 | |||
| 3a2e65ebd9 | |||
| 97144495a1 | |||
| 5f6abeeb42 | |||
| ebdd1ce438 | |||
| 07010f2302 | |||
| 05ace93318 | |||
| 25faca0819 | |||
| 532956fd7d | |||
| 52e9fafd92 | |||
| e750402799 | |||
| 56d8a3217b | |||
| bfc90b78de | |||
| ff285e9af3 | |||
| d1a01915af | |||
| 5bf8f0fe03 | |||
| c83da66325 | |||
| 2db99c37ee | |||
| 4cd6302f0a | |||
| 1f00d83232 | |||
| 84386e93d0 | |||
| 504b0098a6 | |||
| 65ca8692cb | |||
| 3d1b8e49a3 | |||
| 1c2fa2e1b4 | |||
| 15ac35a7ad | |||
| 69888445c7 | |||
| cb16c3742e | |||
| 7915512195 | |||
| e7453d9072 | |||
| 1c5356422c | |||
| 7d3b3693db | |||
| 305e3a62f8 | |||
| 0357cd188e | |||
| 6e3de69b3c | |||
| c64cfda66d | |||
| e65e938b7f | |||
| c5307017e4 | |||
| 685b4700f8 | |||
| 283780994b | |||
| 108053d5da | |||
| 954461c1b9 | |||
| f8e77910ea | |||
| f9a5c84ab9 | |||
| 6e23265dbc | |||
| e06681d522 | |||
| c00b535b1f | |||
| 237c3c4d82 | |||
| 1f9d52bd2b | |||
| 2e50aaf49f | |||
| aae0c82c64 | |||
| 0febe51685 | |||
| c4276083a2 | |||
| 78a1cdca24 | |||
| f91f3f26de | |||
| a3d81b8983 | |||
| 4e3ff6447f | |||
| f282c79a25 | |||
| e9870843e8 | |||
| e7e0f922c7 | |||
| dbfb5be5a0 | |||
| 31f1744730 | |||
| 7d4e97c0bc | |||
| 3d85b7a13c | |||
| cf0afc5c02 | |||
| 6ca643f628 | |||
| d60bc2caff | |||
| 8047778a5b |
@@ -0,0 +1,42 @@
|
||||
# Relevante Infos für Lehrpersonen — Zusammenfassung
|
||||
|
||||
Stand 2026-08-19. Konsolidiert, was wir zum Lehrer-Cockpit festgehalten und gebaut haben.
|
||||
|
||||
## Die Kernfrage der Lehrperson
|
||||
> „Wer ist gerade **engagiert und macht es gut** — und wer **blödelt herum oder hängt fest**?"
|
||||
|
||||
Alles im Cockpit dient dieser Frage. Kein Zahlen-Dump, sondern wenige aussagekräftige Signale.
|
||||
|
||||
## Die 6 relevanten Signale
|
||||
| Signal | Was es beantwortet | Woher |
|
||||
|---|---|---|
|
||||
| **Fortschritt** | Wie weit durch die Sim? (%) | `progressPct` / abgeleitet je Sim |
|
||||
| **Qualität** | Blödeln vs. gut — Verhältnis **richtige ↔ falsche** Klicks (grafischer Balken) | `correctClicks`/`wrongClicks` (bisher Weltküche; andere ziehen nach) |
|
||||
| **Fleiß** | Wie viel im **gewählten Zeitraum** geschafft? (z. B. Gerichte, gelieferte Aufträge) | assessments im Intervall |
|
||||
| **Lage** | Klartext: **Stark / Solide / Viele Fehler** (kein „Kritisch"-Jargon) | `_liveMood` je Sim |
|
||||
| **Aktivität** | aktiv / idle / **pausiert** — war das Kind die ganze Stunde dabei? | Heartbeat `__activeMs`/`__idleMs`/`__paused` |
|
||||
| **Hilfe-Bedarf** | „🆘 braucht evtl. Hilfe" | `_liveNeedsHelp` je Sim (z. B. Budget < 0, viele Fehlversuche) |
|
||||
|
||||
## Die Ansichten (Menü „Ergebnisse")
|
||||
- **📅 Tag** — Zeitstrahl aller Sessions des Tages je Kind, eingefärbt nach Erfolg. Balken tragen **Sim-Kürzel/Wort** (LOG, Logistik …), Klick öffnet Detail.
|
||||
- **🔴 Aktuell** — Live-Tabelle je Sim mit **kuratierten** Spalten (LIVE_PRIMARY_FIELDS), Qualitäts-Balken, Fortschritt, Lage. Aktualisiert alle 4 s.
|
||||
- **📊 Vergleich** — aktive Kinder EINER Sim nebeneinander (Fortschritt + Qualität) → direkter „wer gut / wer nicht".
|
||||
- **📚 Gesamt** — Aggregat (Fleiß-Zeit, Modul-Karten, Matrix) über den gewählten Zeitraum.
|
||||
|
||||
## Design-Prinzipien (verbindlich)
|
||||
1. **Kuratierte Spalten je Sim** — nie Roh-State dumpen, nie interne `__`-Heartbeat-Keys zeigen. Fehlt ein Schema → gefilterter Fallback (ohne Rausch-Felder).
|
||||
2. **Klartext statt Jargon** — „Stark/Solide/Viele Fehler" statt „Kritisch/OK"; „Gerade dabei" statt „Phase".
|
||||
3. **Ehrliche Zeit** — Aktivzeit zählt **nicht** bei Spiel-Pause und **nicht** bei Tab-weg/idle (>5 min). Sonst wirkt ein pausiertes Kind fälschlich „fleißig".
|
||||
4. **Ein Wort + 3-Buchstaben-Kürzel je Sim** (SIM_LABELS) — enge Balken nutzen das Kürzel, breite das Wort.
|
||||
5. **Datenschutz** — nur was die Lehrperson pädagogisch braucht; verdeckte Antworten (z. B. Kofferdetektiv-Ziel) bleiben verdeckt, solange das Kind sucht.
|
||||
|
||||
## Was jede Sim liefern sollte (GGS_LIVE_STATE)
|
||||
- `progressPct` (0–100), ein Fortschritts-Zähler (z. B. `completed`/`total`),
|
||||
- **`correctClicks`/`wrongClicks`** (für die Qualitäts-Kennzahl — Ziel: alle Sims),
|
||||
- optional `health` (good/ok/struggling) für die Lage,
|
||||
- Heartbeat-Felder (`__activeMs` etc.) kommen automatisch aus live-client.js; bei Pause `window.__GGS_LIVE__.setPaused(true)` rufen.
|
||||
|
||||
## Offen (siehe project_backlog_2026-08-18 / project_lehrer_observability_overhaul)
|
||||
- **Aktivitäts-/Qualitätsstreifen** pro Kind über die Stunde (grün→rot, aktiv→inaktiv & bullshit→ernsthaft) — braucht Zeitreihen-Aktivitätsdaten.
|
||||
- Weltküche-**Gesamt-Kachel** (Gerichte · Ø Punkte · Fehlquote).
|
||||
- **Andere Sims** mit `correctClicks`/`wrongClicks` nachrüsten (bisher nur Weltküche).
|
||||
@@ -0,0 +1,34 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const b = await chromium.launch(); const p = await b.newPage();
|
||||
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
|
||||
await p.route('**/api/admin', r => r.fulfill({ contentType:'application/json', body: JSON.stringify({authenticated:true}) }));
|
||||
await p.goto('http://localhost/geograsim/App/admin-weltkueche.html');
|
||||
await p.waitForTimeout(2000);
|
||||
const r = await p.evaluate(() => {
|
||||
const dishes = [...document.querySelectorAll('.dish')];
|
||||
const names = dishes.map(d => d.querySelector('.nm')?.textContent);
|
||||
function dishInfo(namePart){
|
||||
const d = dishes.find(x => (x.querySelector('.nm')?.textContent||'').toLowerCase().includes(namePart));
|
||||
if(!d) return null;
|
||||
const rows = [...d.querySelectorAll('tbody tr')].map(tr => ({
|
||||
ing: tr.querySelector('.ing-nm')?.textContent.replace(/\s+/g,' ').trim(),
|
||||
ok: [...tr.querySelectorAll('.cty.ok')].map(c=>c.textContent.replace(/\s+/g,' ').trim()),
|
||||
dist: [...tr.querySelectorAll('.cty.dist')].map(c=>c.title.split(' · ')[0]),
|
||||
}));
|
||||
return rows;
|
||||
}
|
||||
const groestl = dishInfo('tiroler gröstl');
|
||||
const kart = groestl && groestl.find(r => /Kartoffel/i.test(r.ing));
|
||||
const eierRow = groestl && groestl.find(r => /Eier/i.test(r.ing));
|
||||
return {
|
||||
dishCount: dishes.length,
|
||||
sampleNames: names.slice(0,6),
|
||||
kartoffelOk: kart ? kart.ok : null,
|
||||
eierOk: eierRow ? eierRow.ok : null,
|
||||
warnCount: document.querySelectorAll('.warn').length,
|
||||
};
|
||||
});
|
||||
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
|
||||
await b.close();
|
||||
})();
|
||||
@@ -0,0 +1,19 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const r = await page.request.get('https://geograsim.at/teacher.html');
|
||||
const html = await r.text();
|
||||
console.log('STATUS', r.status(), 'LEN', html.length);
|
||||
console.log('has renderLiveGesamt:', html.includes('renderLiveGesamt'));
|
||||
console.log('has _liveQualityBar:', html.includes('_liveQualityBar'));
|
||||
console.log('has tab-bar:', html.includes('tab-bar'));
|
||||
console.log('has Ergebnisse:', html.includes('Ergebnisse'));
|
||||
console.log('has external script src (module?):', /<script[^>]+src=/i.test(html));
|
||||
// show script tags
|
||||
const scripts = [...html.matchAll(/<script[^>]*>/gi)].map(m=>m[0]);
|
||||
console.log('SCRIPT TAGS:', JSON.stringify(scripts, null, 2));
|
||||
// show first 600 chars to see redirect logic
|
||||
console.log('HEAD:', html.slice(0, 1200));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const r = await page.request.get('https://geograsim.at/teacher.html');
|
||||
const html = await r.text();
|
||||
|
||||
const idxLogin = html.indexOf('login.html');
|
||||
console.log('--- context around first login.html reference ---');
|
||||
console.log(html.slice(idxLogin - 400, idxLogin + 120));
|
||||
|
||||
for (const name of ['renderLiveGesamt', '_liveQualityBar']) {
|
||||
const idx = html.indexOf('function ' + name);
|
||||
console.log('\n--- "function ' + name + '" @' + idx + ' ---');
|
||||
if (idx >= 0) console.log(html.slice(idx - 60, idx + 120));
|
||||
}
|
||||
|
||||
const idxErg = html.indexOf('Ergebnisse');
|
||||
console.log('\n--- context around Ergebnisse ---');
|
||||
console.log(html.slice(idxErg - 220, idxErg + 80));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,29 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const r = await page.request.get('https://geograsim.at/teacher.html');
|
||||
const html = await r.text();
|
||||
|
||||
// 1) Static tab-bar markup
|
||||
const tb = html.indexOf('id="tab-bar"');
|
||||
console.log('--- #tab-bar markup ---');
|
||||
console.log(tb >= 0 ? html.slice(tb - 40, tb + 700) : 'tab-bar id NOT found');
|
||||
|
||||
// 2) Extract _liveQualityBar function body and run it in isolation
|
||||
const start = html.indexOf('function _liveQualityBar');
|
||||
// find matching braces
|
||||
let i = html.indexOf('{', start), depth = 0, end = -1;
|
||||
for (; i < html.length; i++) {
|
||||
if (html[i] === '{') depth++;
|
||||
else if (html[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
|
||||
}
|
||||
const fnSrc = html.slice(start, end);
|
||||
const fn = new Function(fnSrc + '\nreturn _liveQualityBar;')();
|
||||
const sample = fn(8, 2);
|
||||
console.log('\n--- _liveQualityBar(8,2) OUTPUT ---');
|
||||
console.log(sample);
|
||||
console.log('\ncontains "80":', String(sample).includes('80'));
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* gen-display-preview.js
|
||||
* ----------------------
|
||||
* Lokales Admin-Werkzeug: erstellt Bildschirmfotos aller Simulationen
|
||||
* (Onboarding/Start + Spielverlauf) in zwei Auflösungen (iPad + Full HD)
|
||||
* und generiert daraus eine statische Galerie-Seite.
|
||||
*
|
||||
* Ausgabe: App/.LocalDeveloperTools/display-preview/
|
||||
* shots/<id>__<vp>__<screen>.png
|
||||
* display_preview.html
|
||||
*
|
||||
* Start: node gen-display-preview.js
|
||||
*/
|
||||
const { chromium } = require('@playwright/test');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// --- Pfade ---------------------------------------------------------------
|
||||
const APP_DIR = 'C:/xampp/htdocs/geograsim/App';
|
||||
const SIMS_DIR = path.join(APP_DIR, 'sims');
|
||||
const OUT_DIR = path.join(APP_DIR, '.LocalDeveloperTools', 'display-preview');
|
||||
const SHOTS_DIR = path.join(OUT_DIR, 'shots');
|
||||
const BASE_URL = 'http://localhost/geograsim/App/sims';
|
||||
|
||||
// --- Viewports -----------------------------------------------------------
|
||||
const VIEWPORTS = [
|
||||
{ key: 'ipad', label: 'iPad', w: 1180, h: 820 },
|
||||
{ key: 'fullhd', label: 'Full HD', w: 1920, h: 1080 },
|
||||
];
|
||||
|
||||
// --- Freundliche Namen ---------------------------------------------------
|
||||
const NAMES = {
|
||||
weltkueche: '🍲 Welt-Küche',
|
||||
fluss: '🌊 Flussmanagement',
|
||||
heli: '🚁 Helikopter-Navigation',
|
||||
energiemanager: '⚡ Energiemanager',
|
||||
'eu-werkstatt': '🇪🇺 EU-Werkstatt',
|
||||
farmer: '🌾 Farmer',
|
||||
busfahrt: '🚌 Busfahrt',
|
||||
sonnensystem: '🪐 Sonnensystem',
|
||||
tourismustal: '🏔️ Tourismustal',
|
||||
tourismusregion: '🗺️ Tourismusregion',
|
||||
logistik: '🚚 Logistik',
|
||||
fluggesellschaft:'✈️ Fluggesellschaft',
|
||||
staustufen: '💧 Staustufen',
|
||||
entscheidungstag:'📅 Entscheidungstag',
|
||||
vulkan: '🌋 Vulkan',
|
||||
wal: '🐋 Wal',
|
||||
kofferdetektiv: '🧳 Kofferdetektiv',
|
||||
stadt: '🏙️ Stadt',
|
||||
};
|
||||
|
||||
// --- Sims finden ---------------------------------------------------------
|
||||
function findSims() {
|
||||
return fs.readdirSync(SIMS_DIR, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory() && fs.existsSync(path.join(SIMS_DIR, d.name, 'game.html')))
|
||||
.map(d => d.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
// --- In-Page: sichtbaren Start-/Weiter-Button klicken --------------------
|
||||
async function clickStartLike(page) {
|
||||
return await page.evaluate(() => {
|
||||
const isVisible = (el) => {
|
||||
if (!el) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 4 || r.height < 4) return false;
|
||||
if (r.bottom < 0 || r.right < 0) return false;
|
||||
const st = getComputedStyle(el);
|
||||
return st.visibility !== 'hidden' && st.display !== 'none' && st.opacity !== '0'
|
||||
&& !el.disabled && !el.classList.contains('hidden');
|
||||
};
|
||||
// 1) Prioritäts-Selektoren (bekannte Start-/Primary-Buttons der Plattform)
|
||||
const prio = [
|
||||
'.ggs-qi-btn-primary', '[data-action="start"]',
|
||||
'#btnStart', '#obs-start', '#phase-start',
|
||||
'.sn-btn-primary', '.ggs-btn-primary',
|
||||
'.era-card-cta', '.scene-btn',
|
||||
'.map-btn', '.btn.primary', '.primary', '.next',
|
||||
];
|
||||
for (const sel of prio) {
|
||||
const el = [...document.querySelectorAll(sel)].find(isVisible);
|
||||
if (el) { el.click(); return sel + ' :: ' + (el.textContent || '').trim().slice(0, 24); }
|
||||
}
|
||||
// 2) Text-basiert — führende Emojis/Pfeile/Ziffern strippen, dann Anfang prüfen
|
||||
const clean = (s) => (s || '').replace(/^[^\p{L}]+/u, '').trim();
|
||||
const rx = /^(los geht|los!|los →|los\b|start|starten|jetzt starten|neue partie|partie starten|spielen|spiel starten|beginnen|anfangen|einsteigen|erntejahr starten|routenplanung starten|in dieser welt starten|szene|weiter|verstanden|fertig|karte wählen|auswählen)/i;
|
||||
const cands = [...document.querySelectorAll('button, a.btn, a[role=button], .btn, [role=button], [onclick]')];
|
||||
const el = cands.find(e => {
|
||||
if (!isVisible(e)) return false;
|
||||
const t = clean(e.textContent);
|
||||
return t.length > 0 && t.length < 44 && rx.test(t);
|
||||
});
|
||||
if (el) { el.click(); return 'text :: ' + (el.textContent || '').trim().slice(0, 24); }
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
// --- Ein Screenshot ------------------------------------------------------
|
||||
async function shoot(page, file) {
|
||||
try {
|
||||
await page.screenshot({ path: file }); // Viewport-Ausschnitt (exakte Auflösung)
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(' ! Screenshot-Fehler:', e.message.slice(0, 80));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const sims = findSims();
|
||||
// Optional: nur bestimmte Sims neu schießen (HTML wird immer komplett neu erzeugt).
|
||||
const onlyArg = (process.argv.find(a => a.startsWith('--only=')) || '').slice(7);
|
||||
const shootList = onlyArg ? onlyArg.split(',').map(s => s.trim()).filter(Boolean) : sims;
|
||||
console.log('Gefundene Sims (' + sims.length + '):', sims.join(', '));
|
||||
if (onlyArg) console.log('Neu geschossen werden nur:', shootList.join(', '));
|
||||
fs.mkdirSync(SHOTS_DIR, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch();
|
||||
// results[id][vp] = { start:bool, play:bool, play2:bool, errs:[] }
|
||||
const results = {};
|
||||
|
||||
for (const vp of VIEWPORTS) {
|
||||
console.log('\n===== Viewport ' + vp.label + ' (' + vp.w + '×' + vp.h + ') =====');
|
||||
for (const id of shootList) {
|
||||
results[id] = results[id] || {};
|
||||
const r = { start: false, play: false, play2: false, errs: [] };
|
||||
results[id][vp.key] = r;
|
||||
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: vp.w, height: vp.h },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
page.on('pageerror', e => { if (r.errs.length < 4) r.errs.push('PE:' + e.message.slice(0, 60)); });
|
||||
page.on('console', m => { if (m.type() === 'error' && r.errs.length < 4) r.errs.push('CON:' + m.text().slice(0, 60)); });
|
||||
|
||||
const url = BASE_URL + '/' + id + '/game.html';
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
} catch (e) {
|
||||
r.errs.push('GOTO:' + e.message.slice(0, 50));
|
||||
}
|
||||
|
||||
// 1) Onboarding/Start
|
||||
await page.waitForTimeout(2500);
|
||||
r.start = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__start.png'));
|
||||
|
||||
// 2) Spielverlauf: mehrere Runden klicken (Onboarding schließen -> Menü -> Spiel -> Tutorial weg)
|
||||
let clicks = 0, last = null;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
const clicked = await clickStartLike(page);
|
||||
if (!clicked) break;
|
||||
clicks++;
|
||||
if (clicked === last) break; // gleiches Element erneut -> Abbruch
|
||||
last = clicked;
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
if (clicks > 0) {
|
||||
await page.waitForTimeout(2500);
|
||||
r.play = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__play.png'));
|
||||
|
||||
// 3) optionaler zweiter Play-Screen: noch ein Weiter/Start, falls vorhanden
|
||||
const more = await clickStartLike(page);
|
||||
if (more && more !== last) {
|
||||
await page.waitForTimeout(2000);
|
||||
r.play2 = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__play2.png'));
|
||||
}
|
||||
}
|
||||
|
||||
const flags = [r.start ? 'S' : '-', r.play ? 'P' : '-', r.play2 ? 'P2' : '--'].join(' ');
|
||||
console.log(' ' + id.padEnd(18) + '[' + flags + ']'
|
||||
+ (clicks ? ' clicks=' + clicks : ' (kein Start-Button)')
|
||||
+ (r.errs.length ? ' ' + r.errs.slice(0, 2).join(' | ') : ''));
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// --- HTML generieren ---------------------------------------------------
|
||||
const genDate = new Date().toLocaleString('de-AT', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
||||
});
|
||||
|
||||
const screens = [
|
||||
{ key: 'start', label: 'Onboarding / Start' },
|
||||
{ key: 'play', label: 'Spielverlauf' },
|
||||
{ key: 'play2', label: 'Spielverlauf (2)' },
|
||||
];
|
||||
|
||||
function cell(id, vp, screenKey) {
|
||||
const rel = 'shots/' + id + '__' + vp.key + '__' + screenKey + '.png';
|
||||
const abs = path.join(SHOTS_DIR, id + '__' + vp.key + '__' + screenKey + '.png');
|
||||
if (fs.existsSync(abs)) {
|
||||
return '<a href="' + rel + '" target="_blank" class="shot">'
|
||||
+ '<img loading="lazy" src="' + rel + '" alt="' + id + ' ' + vp.label + ' ' + screenKey + '">'
|
||||
+ '<span class="cap">' + vp.label + ' · ' + vp.w + '×' + vp.h + '</span></a>';
|
||||
}
|
||||
return '<div class="missing">— kein Screenshot —<span class="cap">' + vp.label + '</span></div>';
|
||||
}
|
||||
|
||||
let blocks = '';
|
||||
for (const id of sims) {
|
||||
const name = NAMES[id] || (id.charAt(0).toUpperCase() + id.slice(1));
|
||||
// nur Screen-Zeilen zeigen, die mindestens ein Bild haben
|
||||
const activeScreens = screens.filter(sc =>
|
||||
VIEWPORTS.some(vp => fs.existsSync(path.join(SHOTS_DIR, id + '__' + vp.key + '__' + sc.key + '.png')))
|
||||
);
|
||||
const rows = (activeScreens.length ? activeScreens : [screens[0]]).map(sc =>
|
||||
'<tr><th class="screen">' + sc.label + '</th>'
|
||||
+ VIEWPORTS.map(vp => '<td>' + cell(id, vp, sc.key) + '</td>').join('')
|
||||
+ '</tr>'
|
||||
).join('\n');
|
||||
|
||||
blocks += '\n<section class="sim" id="sim-' + id + '">'
|
||||
+ '<h2>' + name + ' <span class="simid">' + id + '</span></h2>'
|
||||
+ '<table><thead><tr><th class="screen"></th>'
|
||||
+ VIEWPORTS.map(vp => '<th>' + vp.label + ' <span class="res">' + vp.w + '×' + vp.h + '</span></th>').join('')
|
||||
+ '</tr></thead><tbody>' + rows + '</tbody></table></section>';
|
||||
}
|
||||
|
||||
const nav = sims.map(id =>
|
||||
'<a href="#sim-' + id + '">' + (NAMES[id] || id) + '</a>'
|
||||
).join('');
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Display-Vorschau (lokal)</title>
|
||||
<style>
|
||||
:root { --bg:#f4f2ec; --card:#fff; --ink:#1f2b26; --accent:#1f4b37; --muted:#6b7a72; --line:rgba(0,0,0,.08); }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--ink); }
|
||||
header { background:var(--accent); color:#fff; padding:20px 28px; position:sticky; top:0; z-index:10; box-shadow:0 2px 10px rgba(0,0,0,.15); }
|
||||
header h1 { margin:0; font-size:1.4rem; }
|
||||
header .meta { font-size:.82rem; opacity:.85; margin-top:4px; }
|
||||
nav.toc { display:flex; flex-wrap:wrap; gap:6px; padding:14px 28px; background:#e9e6dd; border-bottom:1px solid var(--line); }
|
||||
nav.toc a { font-size:.82rem; text-decoration:none; color:var(--accent); background:#fff; border:1px solid var(--line); border-radius:999px; padding:4px 11px; }
|
||||
nav.toc a:hover { background:var(--accent); color:#fff; }
|
||||
main { padding:24px 28px 60px; max-width:1500px; margin:0 auto; }
|
||||
section.sim { background:var(--card); border-radius:14px; padding:18px 20px 22px; margin-bottom:26px; box-shadow:0 4px 14px rgba(0,0,0,.06); scroll-margin-top:90px; }
|
||||
section.sim h2 { margin:0 0 14px; font-size:1.25rem; color:var(--accent); display:flex; align-items:baseline; gap:10px; }
|
||||
.simid { font-size:.72rem; font-weight:600; color:var(--muted); background:#eef0ec; padding:2px 8px; border-radius:6px; letter-spacing:.03em; }
|
||||
table { width:100%; border-collapse:collapse; table-layout:fixed; }
|
||||
thead th { text-align:left; font-size:.82rem; color:var(--muted); padding:0 0 8px; font-weight:700; }
|
||||
thead th .res { font-weight:500; opacity:.7; }
|
||||
th.screen { width:150px; vertical-align:top; font-size:.9rem; color:var(--ink); padding-top:8px; }
|
||||
td { padding:6px 6px 14px; vertical-align:top; width:calc((100% - 150px)/2); }
|
||||
a.shot { display:block; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:#0d1512; text-decoration:none; }
|
||||
a.shot img { display:block; width:100%; max-width:100%; height:auto; }
|
||||
.cap { display:block; font-size:.72rem; color:var(--muted); padding:5px 8px; background:#fafafa; border-top:1px solid var(--line); }
|
||||
a.shot .cap { color:#c8d4ce; background:#141f1a; border-top:none; }
|
||||
.missing { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; min-height:120px; border:1px dashed var(--line); border-radius:8px; color:var(--muted); font-size:.85rem; background:#faf9f5; }
|
||||
.missing .cap { background:none; border:none; }
|
||||
@media (max-width:820px){ th.screen{width:90px;} td{width:calc((100% - 90px)/2);} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Display-Vorschau (lokal)</h1>
|
||||
<div class="meta">${sims.length} Simulationen · je Onboarding/Start + Spielverlauf · iPad 1180×820 & Full HD 1920×1080 · generiert am ${genDate}</div>
|
||||
</header>
|
||||
<nav class="toc">${nav}</nav>
|
||||
<main>${blocks}
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const outFile = path.join(OUT_DIR, 'display_preview.html');
|
||||
fs.writeFileSync(outFile, html, 'utf8');
|
||||
console.log('\nHTML geschrieben: ' + outFile);
|
||||
|
||||
// --- Zusammenfassung ---------------------------------------------------
|
||||
console.log('\n===== ZUSAMMENFASSUNG =====');
|
||||
let totalShots = 0, missingPlay = [];
|
||||
for (const id of sims) {
|
||||
const parts = [];
|
||||
for (const vp of VIEWPORTS) {
|
||||
const r = (results[id] && results[id][vp.key]) || {};
|
||||
const n = (r.start ? 1 : 0) + (r.play ? 1 : 0) + (r.play2 ? 1 : 0);
|
||||
totalShots += n;
|
||||
parts.push(vp.label + ':' + n + '/3(' + (r.start ? 'S' : '-') + (r.play ? 'P' : '-') + (r.play2 ? '2' : '-') + ')');
|
||||
if (!r.play) missingPlay.push(id + '/' + vp.key);
|
||||
}
|
||||
console.log(' ' + id.padEnd(18) + parts.join(' '));
|
||||
}
|
||||
console.log('\nScreenshots gesamt: ' + totalShots);
|
||||
console.log('Fehlende Play-Screens: ' + (missingPlay.length ? missingPlay.join(', ') : 'keine'));
|
||||
console.log('Seite: http://localhost/geograsim/App/.LocalDeveloperTools/display-preview/display_preview.html');
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const res = await page.goto('http://localhost/geograsim/App/modul-weltkueche');
|
||||
await page.waitForTimeout(700);
|
||||
const r = await page.evaluate(() => {
|
||||
const sec = document.getElementById('lehrplan-bezug');
|
||||
const komps = [...document.querySelectorAll('.md-kompetenz .md-kompetenz-title')].map(e=>e.textContent.trim());
|
||||
const anchors = document.querySelectorAll('.md-anchor').length;
|
||||
const countryChips = [...document.querySelectorAll('.md-lehrplan-filters .md-chip')].map(c=>c.textContent.trim());
|
||||
const empty = document.body.textContent.includes('Noch keine Kompetenzen zugeordnet');
|
||||
return { hasSection: !!sec, kompetenzTitles: komps, anchorCount: anchors, countryChips, showsEmpty: empty };
|
||||
});
|
||||
console.log(JSON.stringify({ httpStatus: res.status(), result: r }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const res = await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(700);
|
||||
const r = await page.evaluate(() => {
|
||||
const sec = document.getElementById('lehrplan-bezug');
|
||||
const komps = [...document.querySelectorAll('.md-kompetenz .md-kompetenz-title')].map(e=>e.textContent.trim());
|
||||
const anchors = document.querySelectorAll('.md-anchor').length;
|
||||
const countryChips = [...document.querySelectorAll('.md-lehrplan-filters .md-chip')].map(c=>c.textContent.trim());
|
||||
const empty = document.body.textContent.includes('Noch keine Kompetenzen zugeordnet');
|
||||
return { hasSection: !!sec, kompetenzTitles: komps, anchorCount: anchors, countryChips, showsEmpty: empty };
|
||||
});
|
||||
console.log(JSON.stringify({ httpStatus: res.status(), result: r }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
|
||||
await page.goto('http://localhost/geograsim/App/modul-weltkueche', { waitUntil:'domcontentloaded' });
|
||||
await page.waitForTimeout(700);
|
||||
|
||||
async function snapshot() {
|
||||
return await page.evaluate(() => {
|
||||
const all = [...document.querySelectorAll('.md-anchor')];
|
||||
const visible = all.filter(a => !a.classList.contains('hidden'));
|
||||
const byType = t => visible.filter(a => a.dataset.anchorType===t).length;
|
||||
const fachCountries = {};
|
||||
visible.filter(a=>a.dataset.anchorType==='fach').forEach(a=>{ fachCountries[a.dataset.country]=(fachCountries[a.dataset.country]||0)+1; });
|
||||
return { total: all.length, visible: visible.length, fachVisible: byType('fach'), fachCountries };
|
||||
});
|
||||
}
|
||||
async function clickChip(country) {
|
||||
await page.evaluate((c) => {
|
||||
const btn = [...document.querySelectorAll('.md-chip')].find(b => (b.dataset.country||'')===c);
|
||||
if (btn) btn.click();
|
||||
}, country);
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
const start = await snapshot();
|
||||
await clickChip(''); // Alle Länder
|
||||
const alle = await snapshot();
|
||||
await clickChip('DE'); // Deutschland
|
||||
const de = await snapshot();
|
||||
await clickChip('AT'); // Österreich
|
||||
const at = await snapshot();
|
||||
|
||||
console.log(JSON.stringify({ start, alle, de, at, pageErrors: errs }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,15 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
|
||||
await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil:'domcontentloaded' });
|
||||
await page.waitForTimeout(900);
|
||||
async function snap(){ return await page.evaluate(()=>{ const all=[...document.querySelectorAll('.md-anchor')]; const vis=all.filter(a=>!a.classList.contains('hidden')); const fc={}; vis.filter(a=>a.dataset.anchorType==='fach').forEach(a=>fc[a.dataset.country]=(fc[a.dataset.country]||0)+1); return {total:all.length, visible:vis.length, fach:vis.filter(a=>a.dataset.anchorType==='fach').length, fachCountries:fc}; }); }
|
||||
async function chip(c){ await page.evaluate(c=>{const b=[...document.querySelectorAll('.md-chip')].find(x=>(x.dataset.country||'')===c); if(b)b.click();},c); await page.waitForTimeout(200); }
|
||||
await chip(''); const alle=await snap();
|
||||
await chip('DE'); const de=await snap();
|
||||
await chip('AT'); const at=await snap();
|
||||
console.log(JSON.stringify({alle,de,at,pageErrors:errs},null,2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const b = await chromium.launch(); const p = await b.newPage();
|
||||
await p.addInitScript(() => {
|
||||
window.__GGS__ = { simId: 'test', baseUrl: '' };
|
||||
window.fetch = () => Promise.resolve({ ok:true, json:()=>Promise.resolve({}) }); // Heartbeat stummschalten
|
||||
});
|
||||
await p.goto('about:blank');
|
||||
await p.addScriptTag({ path: 'C:/xampp/htdocs/geograsim/App/assets/js/live-client.js' });
|
||||
const read = () => p.evaluate(() => window.__GGS_LIVE__ ? window.__GGS_LIVE__.activeMs : null);
|
||||
const set = (v) => p.evaluate((v) => window.__GGS_LIVE__.setPaused(v), v);
|
||||
await p.waitForTimeout(300);
|
||||
const a1 = await read();
|
||||
await p.waitForTimeout(2500); const a2 = await read(); // sollte gestiegen sein
|
||||
await set(true);
|
||||
await p.waitForTimeout(2500); const a3 = await read(); // sollte eingefroren sein
|
||||
await set(false);
|
||||
await p.waitForTimeout(2500); const a4 = await read(); // sollte wieder steigen
|
||||
const grewNormal = (a2 - a1) > 1500;
|
||||
const frozen = (a3 - a2) < 300;
|
||||
const resumed = (a4 - a3) > 1500;
|
||||
console.log(JSON.stringify({ a1,a2,a3,a4, grewNormal, frozen, resumed,
|
||||
verdict: (grewNormal && frozen && resumed) ? 'PASS' : 'FAIL' }, null, 2));
|
||||
await b.close();
|
||||
})();
|
||||
@@ -0,0 +1,74 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const out = {};
|
||||
|
||||
// 1) modul-weltkueche: Lehrplan-Bezug jetzt vorhanden?
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(1000);
|
||||
out.modulWeltkueche = await page.evaluate(() => ({
|
||||
kompetenzTitles: [...document.querySelectorAll('.md-kompetenz-title')].map(e=>e.textContent.trim()),
|
||||
anchorCount: document.querySelectorAll('.md-anchor').length,
|
||||
showsEmpty: document.body.textContent.includes('Noch keine Kompetenzen zugeordnet'),
|
||||
}));
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// 2) Weltküche-Sim: Artikel + Ukraine-Feedback
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
|
||||
await page.goto('https://geograsim.at/sims/weltkueche/game.html', { waitUntil:'domcontentloaded' });
|
||||
await page.waitForTimeout(2000);
|
||||
out.weltkueche = await page.evaluate(async () => {
|
||||
try {
|
||||
if (!DISHES.length) await loadWeltkuecheData();
|
||||
const dish = DISHES.find(d=>d.id==='tiroler-groestl'); current={dish};
|
||||
buildTaskFor('kartoffel');
|
||||
return {
|
||||
kartoffelArt: ING.kartoffel.art,
|
||||
badArticles: Object.entries(ING).filter(([k,v])=>!['der','die','das'].includes(v&&v.art)).map(([k])=>k),
|
||||
wrongUA: explainChoice('kartoffel','ukraine',false).text,
|
||||
};
|
||||
} catch(e){ return { error:String(e) }; }
|
||||
});
|
||||
out.weltkueche.pageErrors = errs;
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// 3) teacher.html: lädt + neue Funktionen da (direkter Link, den das Produkt nutzt)
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
|
||||
// teacher.html leitet ohne Login auf login.html um — Navigation abfangen, sofort auswerten
|
||||
await page.route('**/login.html', r=>r.abort());
|
||||
await page.goto('https://geograsim.at/teacher.html', { waitUntil:'domcontentloaded' }).catch(()=>{});
|
||||
await page.waitForTimeout(1500);
|
||||
out.teacher = await page.evaluate(() => ({
|
||||
hasRenderGesamt: typeof renderLiveGesamt === 'function',
|
||||
hasQualityBar: typeof _liveQualityBar === 'function',
|
||||
qualitySample: (typeof _liveQualityBar==='function') ? _liveQualityBar(8,2) : null,
|
||||
tabLabels: [...document.querySelectorAll('#tab-bar .tab')].map(t=>t.textContent.trim()),
|
||||
})).catch(e=>({evalError:String(e)}));
|
||||
out.teacher.pageErrors = errs;
|
||||
await page.close();
|
||||
}
|
||||
|
||||
// 4) Tourismusregion passstrasse Karte
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
const mc = await page.request.get('https://geograsim.at/sims/tourismusregion/js/maps-custom.json');
|
||||
const j = await mc.json();
|
||||
out.tourismus = {
|
||||
keys: Object.keys(j),
|
||||
passBergaufPoints: j.passstrasse?.routes?.bergauf?.length ?? null,
|
||||
passSlots: (j.passstrasse?.slots||[]).length,
|
||||
};
|
||||
await page.close();
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const b = await chromium.launch(); const p = await b.newPage();
|
||||
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
|
||||
await p.route('**/login.html', r=>r.abort());
|
||||
await p.goto('http://localhost/geograsim/App/teacher.html',{waitUntil:'domcontentloaded'}).catch(()=>{});
|
||||
await p.waitForTimeout(1200);
|
||||
const r = await p.evaluate(() => ({
|
||||
hasSimWord: typeof simWord==='function',
|
||||
hasSimCode: typeof simCode==='function',
|
||||
logistikWord: typeof simWord==='function' ? simWord('logistik') : null,
|
||||
logistikCode: typeof simCode==='function' ? simCode('logistik') : null,
|
||||
unknownCode: typeof simCode==='function' ? simCode('irgendwas') : null,
|
||||
logistikFields: (typeof LIVE_PRIMARY_FIELDS==='object' && LIVE_PRIMARY_FIELDS.logistik) ? LIVE_PRIMARY_FIELDS.logistik.map(f=>f.label) : null,
|
||||
liveShortLogistik: typeof LIVE_SIM_SHORT==='object' ? LIVE_SIM_SHORT.logistik : null,
|
||||
// Kürzel-Eindeutigkeit
|
||||
codesUnique: (function(){ if(typeof SIM_LABELS!=='object') return null; var seen={},dup=[]; Object.keys(SIM_LABELS).forEach(k=>{var c=SIM_LABELS[k].c; if(seen[c])dup.push(c); seen[c]=1;}); return dup; })(),
|
||||
}));
|
||||
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
|
||||
await b.close();
|
||||
})();
|
||||
@@ -0,0 +1,26 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
|
||||
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
|
||||
const res = await page.goto('http://localhost/geograsim/App/teacher');
|
||||
await page.waitForTimeout(1000);
|
||||
const r = await page.evaluate(() => {
|
||||
const tabs = [...document.querySelectorAll('#tab-bar .tab')].map(t => t.textContent.trim());
|
||||
let toggle = null;
|
||||
try { toggle = (typeof _liveToggleHtml === 'function') ? _liveToggleHtml() : null; } catch(e){ toggle = 'ERR:'+e.message; }
|
||||
return {
|
||||
tabLabels: tabs,
|
||||
hasSetLiveMode: typeof setLiveMode === 'function',
|
||||
hasRenderGesamt: typeof renderLiveGesamt === 'function',
|
||||
hasQualityBar: typeof _liveQualityBar === 'function',
|
||||
qualitySample: (typeof _liveQualityBar === 'function') ? _liveQualityBar(8,2) : null,
|
||||
toggleHasGesamt: (typeof toggle === 'string') ? toggle.includes('Gesamt') : toggle,
|
||||
toggleHasTag: (typeof toggle === 'string') ? toggle.includes('Tag') : toggle,
|
||||
};
|
||||
});
|
||||
console.log(JSON.stringify({ httpStatus: res.status(), result: r, pageErrors: errors }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const b = await chromium.launch(); const p = await b.newPage({ viewport:{width:1180,height:820} });
|
||||
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
|
||||
await p.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
|
||||
await p.waitForTimeout(1500);
|
||||
await p.click('.map-btn'); // Route klicken → Spiel startet → Willkommenskarte
|
||||
await p.waitForTimeout(2000);
|
||||
const r = await p.evaluate(() => {
|
||||
const card = document.getElementById('cardText');
|
||||
const txt = card ? card.textContent : '';
|
||||
return {
|
||||
started: !!window.__ttState,
|
||||
cardHasEnvText: /Umwelt ist deine Landschaft/.test(txt),
|
||||
cardHasNaturschutz: /Naturschutzgebiet/.test(txt),
|
||||
envKpi: document.getElementById('kEnv')?.textContent,
|
||||
};
|
||||
});
|
||||
console.log(JSON.stringify({ result:r, pageErrors: errs.filter(e=>!/404|Failed to load resource/.test(e)) }, null, 2));
|
||||
await b.close();
|
||||
})();
|
||||
@@ -0,0 +1,38 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
|
||||
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
|
||||
|
||||
// 1) maps-custom.json direkt prüfen
|
||||
const mc = await page.request.get('http://localhost/geograsim/App/sims/tourismusregion/js/maps-custom.json');
|
||||
const mcJson = await mc.json();
|
||||
const ps = mcJson.passstrasse || null;
|
||||
|
||||
// 2) Hintergrundbild erreichbar?
|
||||
const img = await page.request.get('http://localhost/geograsim/App/sims/tourismusregion/assets/maps/passstrasse.png');
|
||||
|
||||
// 3) Sim-Startscreen laden, Kartenliste prüfen
|
||||
const res = await page.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
|
||||
await page.waitForTimeout(2500);
|
||||
const startInfo = await page.evaluate(() => {
|
||||
const list = document.getElementById('mapList');
|
||||
const labels = list ? [...list.querySelectorAll('*')].map(e=>e.textContent).filter(t=>t && t.length<40) : [];
|
||||
const hasPass = !!(list && /Passstra/i.test(list.textContent||''));
|
||||
return { mapListText: (list?list.textContent:'').replace(/\s+/g,' ').trim().slice(0,300), hasPass };
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
httpStatus: res.status(),
|
||||
passInMapsCustom: !!ps,
|
||||
routeKeys: ps ? Object.keys(ps.routes||{}) : null,
|
||||
bergaufPoints: ps && ps.routes && ps.routes.bergauf ? ps.routes.bergauf.length : null,
|
||||
slotCount: ps ? (ps.slots||[]).length : null,
|
||||
bgImageStatus: img.status(),
|
||||
startInfo,
|
||||
pageErrors: errors
|
||||
}, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,30 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1180, height: 820 } }); // iPad Landscape
|
||||
const errs=[]; page.on('pageerror',e=>errs.push(e.message)); page.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
|
||||
await page.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const before = await page.evaluate(() => ({
|
||||
hasPlayerName: !!document.getElementById('playerName'),
|
||||
hasClassCode: !!document.getElementById('classCode'),
|
||||
hasBtnStart: !!document.getElementById('btnStart'),
|
||||
hasBtnSave: !!document.getElementById('btnSave'),
|
||||
mapBtnCount: document.querySelectorAll('.map-btn').length,
|
||||
startOverlayHidden: document.getElementById('startOverlay').classList.contains('hidden'),
|
||||
}));
|
||||
|
||||
// ② Klick auf erste Route sollte SOFORT starten
|
||||
await page.click('.map-btn');
|
||||
await page.waitForTimeout(2500);
|
||||
const after = await page.evaluate(() => ({
|
||||
startOverlayHidden: document.getElementById('startOverlay').classList.contains('hidden'),
|
||||
gameStarted: !!window.__ttState,
|
||||
mapId: window.__ttState && window.__ttState.map && window.__ttState.map.id,
|
||||
}));
|
||||
|
||||
await page.screenshot({ path: 'C:/Users/herr_/AppData/Local/Temp/claude/c--xampp-htdocs-geograsim/c02572ea-7616-4524-a974-dff0e5670d2c/scratchpad/tr-ipad.png' });
|
||||
console.log(JSON.stringify({ before, after, pageErrors: errs }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,27 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
|
||||
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
|
||||
await page.goto('http://localhost/geograsim/App/sims/weltkueche/game.html');
|
||||
await page.waitForTimeout(1500);
|
||||
const r = await page.evaluate(async () => {
|
||||
try {
|
||||
if (!DISHES.length) await loadWeltkuecheData();
|
||||
const dish = DISHES.find(d => d.id === 'tiroler-groestl');
|
||||
if (!dish) return { error: 'no tiroler-groestl dish' };
|
||||
current = { dish };
|
||||
const task = buildTaskFor('kartoffel');
|
||||
const art = ING.kartoffel.art;
|
||||
const correctAT = explainChoice('kartoffel', 'oesterreich', true).text;
|
||||
const correctDE = explainChoice('kartoffel', 'deutschland', true).text;
|
||||
const wrongUA = explainChoice('kartoffel', 'ukraine', false).text;
|
||||
const bad = Object.entries(ING).filter(([k,v]) => !['der','die','das'].includes(v && v.art)).map(([k])=>k);
|
||||
return { art, taskCorrect: task.correct, correctAT, correctDE, wrongUA, badArticles: bad, ingCount: Object.keys(ING).length };
|
||||
} catch (e) { return { error: String(e), stack: e.stack }; }
|
||||
});
|
||||
console.log(JSON.stringify({ result: r, consoleErrors: errors }, null, 2));
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,38 @@
|
||||
const { chromium } = require('@playwright/test');
|
||||
(async () => {
|
||||
const b = await chromium.launch(); const p = await b.newPage();
|
||||
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
|
||||
await p.goto('http://localhost/geograsim/App/sims/weltkueche/game.html');
|
||||
await p.waitForTimeout(1500);
|
||||
const r = await p.evaluate(async () => {
|
||||
try {
|
||||
if (!DISHES.length) await loadWeltkuecheData();
|
||||
function taskFor(ingKey, heimat){
|
||||
current = { dish: { id:'test-'+ingKey, heimat: heimat, ings:[ingKey], signature:{} } };
|
||||
try { delete _taskCache[ingKey]; } catch(_){}
|
||||
return buildTaskFor(ingKey);
|
||||
}
|
||||
const dEggsDE = _wkDist('oesterreich','deutschland');
|
||||
const dEggsFR = _wkDist('oesterreich','frankreich');
|
||||
const eier = taskFor('eier','oesterreich');
|
||||
const kart = taskFor('kartoffel','oesterreich');
|
||||
const milch = taskFor('milch','oesterreich');
|
||||
// alle Zutaten müssen fehlerfrei bauen
|
||||
let buildErr = null;
|
||||
for (const k of Object.keys(ING)) {
|
||||
current = { dish:{id:'x',heimat:'oesterreich',ings:[k],signature:{}} };
|
||||
try { delete _taskCache[k]; buildTaskFor(k); } catch(e){ buildErr = k+': '+e.message; break; }
|
||||
}
|
||||
return {
|
||||
distDE: Math.round(dEggsDE), distFR: Math.round(dEggsFR),
|
||||
eierCorrect: eier.correct, eierChoices: eier.choices,
|
||||
eierDistractors: eier.choices.length - eier.correct.length,
|
||||
kartoffelCorrect: kart.correct,
|
||||
milchCorrect: milch.correct,
|
||||
buildErr, ingCount: Object.keys(ING).length,
|
||||
};
|
||||
} catch(e){ return { error:String(e), stack:e.stack }; }
|
||||
});
|
||||
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
|
||||
await b.close();
|
||||
})();
|
||||
@@ -0,0 +1,48 @@
|
||||
# Schulbuch-Konzept (Auftrag Thomas, 2026-07-12)
|
||||
|
||||
Hinter jeder Sim entsteht ein Schulbuch-Kapitel: Sachinhalte lehrplankonform,
|
||||
mit Links (Glossar-Tooltips, Sim, Quellen), präsentierbar vor/zwischen/nach dem
|
||||
Arbeiten mit der Simulation. Zwei Versionen aus EINER Quelle.
|
||||
|
||||
## Architektur
|
||||
|
||||
- **Routen:** `/buch` (Übersicht aller Kapitel) · `/buch-<sim>` (Kapitel).
|
||||
Lehrer-Version über `?lehrer=1` (Toggle-Link auf der Seite).
|
||||
TODO später: Lehrer-Version hinter Teacher-Session (wie Marktplatz-Plan).
|
||||
- **Dateien:** `App/pages/buch.php` + `App/pages/buch-<sim>.php`.
|
||||
Erste Iteration self-contained (eigenes CSS im File, Schulbuch-Look),
|
||||
bei Kapitel 3+ ggf. gemeinsames Partial extrahieren.
|
||||
- **Fotos (echte Bilder von Thomas):** Slots via PHP-Helper `buchBild($slug,
|
||||
$vorschlag)`. Sucht `App/assets/img/buch/<sim>/<slug>.{jpg,webp,png}` —
|
||||
Datei da → Bild erscheint automatisch; sonst Platzhalter-Kasten
|
||||
„📷 FOTO GESUCHT: <konkreter Vorschlag>". Thomas legt Dateien einfach ab.
|
||||
- **Glossar:** `data-glossar`-Spans + plattformweite glossar-tooltip.js.
|
||||
- **Druck:** @media print → A4, Seitenumbrüche an Kapitel-Seiten, Nav/Toggle raus.
|
||||
- **Quellen:** Jede Daten-Box nummeriert [1]…, Quellenverzeichnis am Ende
|
||||
(aus QUELLEN.md der Sim übernommen).
|
||||
|
||||
## Baukasten (Inhaltselemente)
|
||||
|
||||
- Kapitel-Opener: großes Bild, Leitfrage, Lernziele-Box, Lehrplan-Chips (D-A-CH)
|
||||
- „Seite"-Sektionen: Sachtext (~250–400 Wörter), Merke-Kasten, Daten-Kasten,
|
||||
Begriffs-Chips (Glossar), Foto
|
||||
- 🎮 Sim-Kasten: „So ist es in der Simulation" — erklärt die Mechanik & ihren
|
||||
Realitätsbezug (Schülerversion = vollständige Mechanik-Referenz)
|
||||
- ✏️ Aufgaben-Kasten: vor / während / nach dem Arbeiten, gestuft
|
||||
- 💡 Lehrer-Kästen (nur ?lehrer=1): Verlaufsvorschlag (Doppelstunde),
|
||||
didaktischer Kommentar je Seite, Stolpersteine (z. B. „Panzer-Rush endet in
|
||||
Bank-Liquidation — bewusst so"), Differenzierung, Erwartungshorizont,
|
||||
Cockpit-Tipps (welche Live-Spalten/Ergebnisse ansehen)
|
||||
|
||||
## Kapitel-Fahrplan
|
||||
|
||||
1. **Tourismustal** (gebaut 2026-07-12, beide Versionen) — Pilot
|
||||
2. Tourismusregion (Besucherlenkung/Mobilität)
|
||||
3. danach je Bestands-Sim (Busfahrt, Heli, Klima, …) nach Priorität Thomas
|
||||
|
||||
## Foto-Workflow
|
||||
|
||||
Kapitel nennt pro Slot einen konkreten Vorschlag. Thomas fotografiert real
|
||||
(Vorarlberg/Arlberg-Umfeld vorhanden), legt ab unter
|
||||
`App/assets/img/buch/<sim>/<slug>.jpg` → erscheint automatisch. Credits-Zeile
|
||||
optional via Dateiname egal; Bildunterschrift steht im Kapitel.
|
||||
@@ -0,0 +1,73 @@
|
||||
// Headless-Balance-Messung (temporär). node sim-harness.mjs
|
||||
import { createState, place, upgrade, callNextWave, tick, GOALS } from '../../App/sims/tourismusregion/js/model.js';
|
||||
import { BUILDINGS, GAME_LEN } from '../../App/sims/tourismusregion/js/data.js';
|
||||
|
||||
const ALL_TYPES = Object.keys(BUILDINGS);
|
||||
function upgradeAllMax(s) {
|
||||
for (const b of s.buildings) {
|
||||
const def = BUILDINGS[b.type];
|
||||
let guard = 0;
|
||||
while (b.level < def.cost.length && s.budget >= def.cost[b.level] && guard++ < 6) {
|
||||
const r = upgrade(s, b);
|
||||
if (!r || !r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run(strategy, mapId = 'talschleife') {
|
||||
const s = createState({ mapId, seed: 12345 });
|
||||
const dt = 0.5;
|
||||
let t = 0;
|
||||
|
||||
function tryBuildAll() {
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
const slot = s.slots[i];
|
||||
if (slot.building || slot.blocked) continue;
|
||||
for (const type of ALL_TYPES) {
|
||||
const def = BUILDINGS[type];
|
||||
if (!def || s.budget < def.cost[0]) continue;
|
||||
const r = place(s, type, i);
|
||||
if (r && r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function buildOneCafe() {
|
||||
// Minimal-Strategie: genau EIN Café (Thomas' „5-Sterne-Café macht alles grün")
|
||||
if (s.buildings.length) return;
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
if (s.slots[i].building || s.slots[i].blocked) continue;
|
||||
const r = place(s, 'cafe', i);
|
||||
if (r && r.ok) return;
|
||||
}
|
||||
}
|
||||
|
||||
let guard = 0;
|
||||
while (t < GAME_LEN && s.phase < 8 && guard++ < 2000) {
|
||||
if (strategy === 'buildAll' || strategy === 'maxedAll') tryBuildAll();
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
if (strategy === 'oneCafe') buildOneCafe();
|
||||
try { callNextWave(s); } catch (_) {}
|
||||
for (let k = 0; k < 40 && t < GAME_LEN; k++) { tick(s, dt); t += dt; }
|
||||
}
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
// Restzeit auslaufen lassen
|
||||
while (t < GAME_LEN && guard++ < 6000) { tick(s, dt); t += dt; }
|
||||
|
||||
const stars = s.stats.all.count ? s.stats.all.stars / s.stats.all.count : 0;
|
||||
return {
|
||||
strategy,
|
||||
env: Math.round(s.env),
|
||||
avgStars: +stars.toFixed(2),
|
||||
visitorsExited: s.stats.all.count,
|
||||
budget: Math.round(s.budget),
|
||||
buildings: s.buildings.length,
|
||||
traffic: Math.round(s.traffic || 0),
|
||||
goalsMet: GOALS.filter(g => g.check(s)).length + '/' + GOALS.length,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('=== Tourismusregion Balance-Messung (talschleife) ===');
|
||||
for (const strat of ['maxedAll', 'buildAll', 'oneCafe']) {
|
||||
const r = run(strat);
|
||||
console.log(`${strat.padEnd(9)} → env ${String(r.env).padStart(3)} · Ø${r.avgStars}★ · ${r.buildings} Geb. · Verkehr ${r.traffic} · Ziele ${r.goalsMet} · Budget ${r.budget}`);
|
||||
}
|
||||
@@ -23,3 +23,4 @@ secret_keys.txt
|
||||
**/.humaninput/
|
||||
**/.env
|
||||
!**/.env.example
|
||||
App/uploads/
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Display-Vorschau (lokal)</title>
|
||||
<style>
|
||||
:root { --bg:#f4f2ec; --card:#fff; --ink:#1f2b26; --accent:#1f4b37; --muted:#6b7a72; --line:rgba(0,0,0,.08); }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--ink); }
|
||||
header { background:var(--accent); color:#fff; padding:20px 28px; position:sticky; top:0; z-index:10; box-shadow:0 2px 10px rgba(0,0,0,.15); }
|
||||
header h1 { margin:0; font-size:1.4rem; }
|
||||
header .meta { font-size:.82rem; opacity:.85; margin-top:4px; }
|
||||
nav.toc { display:flex; flex-wrap:wrap; gap:6px; padding:14px 28px; background:#e9e6dd; border-bottom:1px solid var(--line); }
|
||||
nav.toc a { font-size:.82rem; text-decoration:none; color:var(--accent); background:#fff; border:1px solid var(--line); border-radius:999px; padding:4px 11px; }
|
||||
nav.toc a:hover { background:var(--accent); color:#fff; }
|
||||
main { padding:24px 28px 60px; max-width:1500px; margin:0 auto; }
|
||||
section.sim { background:var(--card); border-radius:14px; padding:18px 20px 22px; margin-bottom:26px; box-shadow:0 4px 14px rgba(0,0,0,.06); scroll-margin-top:90px; }
|
||||
section.sim h2 { margin:0 0 14px; font-size:1.25rem; color:var(--accent); display:flex; align-items:baseline; gap:10px; }
|
||||
.simid { font-size:.72rem; font-weight:600; color:var(--muted); background:#eef0ec; padding:2px 8px; border-radius:6px; letter-spacing:.03em; }
|
||||
table { width:100%; border-collapse:collapse; table-layout:fixed; }
|
||||
thead th { text-align:left; font-size:.82rem; color:var(--muted); padding:0 0 8px; font-weight:700; }
|
||||
thead th .res { font-weight:500; opacity:.7; }
|
||||
th.screen { width:150px; vertical-align:top; font-size:.9rem; color:var(--ink); padding-top:8px; }
|
||||
td { padding:6px 6px 14px; vertical-align:top; width:calc((100% - 150px)/2); }
|
||||
a.shot { display:block; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:#0d1512; text-decoration:none; }
|
||||
a.shot img { display:block; width:100%; max-width:100%; height:auto; }
|
||||
.cap { display:block; font-size:.72rem; color:var(--muted); padding:5px 8px; background:#fafafa; border-top:1px solid var(--line); }
|
||||
a.shot .cap { color:#c8d4ce; background:#141f1a; border-top:none; }
|
||||
.missing { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; min-height:120px; border:1px dashed var(--line); border-radius:8px; color:var(--muted); font-size:.85rem; background:#faf9f5; }
|
||||
.missing .cap { background:none; border:none; }
|
||||
@media (max-width:820px){ th.screen{width:90px;} td{width:calc((100% - 90px)/2);} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Display-Vorschau (lokal)</h1>
|
||||
<div class="meta">16 Simulationen · je Onboarding/Start + Spielverlauf · iPad 1180×820 & Full HD 1920×1080 · generiert am 19.08.2026, 15:06</div>
|
||||
</header>
|
||||
<nav class="toc"><a href="#sim-busfahrt">🚌 Busfahrt</a><a href="#sim-energiemanager">⚡ Energiemanager</a><a href="#sim-entscheidungstag">📅 Entscheidungstag</a><a href="#sim-eu-werkstatt">🇪🇺 EU-Werkstatt</a><a href="#sim-farmer">🌾 Farmer</a><a href="#sim-fluggesellschaft">✈️ Fluggesellschaft</a><a href="#sim-fluss">🌊 Flussmanagement</a><a href="#sim-heli">🚁 Helikopter-Navigation</a><a href="#sim-logistik">🚚 Logistik</a><a href="#sim-sonnensystem">🪐 Sonnensystem</a><a href="#sim-staustufen">💧 Staustufen</a><a href="#sim-tourismusregion">🗺️ Tourismusregion</a><a href="#sim-tourismustal">🏔️ Tourismustal</a><a href="#sim-vulkan">🌋 Vulkan</a><a href="#sim-wal">🐋 Wal</a><a href="#sim-weltkueche">🍲 Welt-Küche</a></nav>
|
||||
<main>
|
||||
<section class="sim" id="sim-busfahrt"><h2>🚌 Busfahrt <span class="simid">busfahrt</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/busfahrt__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__ipad__start.png" alt="busfahrt iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/busfahrt__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__fullhd__start.png" alt="busfahrt Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/busfahrt__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__ipad__play.png" alt="busfahrt iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/busfahrt__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__fullhd__play.png" alt="busfahrt Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-energiemanager"><h2>⚡ Energiemanager <span class="simid">energiemanager</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/energiemanager__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__start.png" alt="energiemanager iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__start.png" alt="energiemanager Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/energiemanager__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__play.png" alt="energiemanager iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__play.png" alt="energiemanager Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/energiemanager__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__play2.png" alt="energiemanager iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__play2.png" alt="energiemanager Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-entscheidungstag"><h2>📅 Entscheidungstag <span class="simid">entscheidungstag</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/entscheidungstag__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__ipad__start.png" alt="entscheidungstag iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/entscheidungstag__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__fullhd__start.png" alt="entscheidungstag Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/entscheidungstag__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__ipad__play.png" alt="entscheidungstag iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/entscheidungstag__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__fullhd__play.png" alt="entscheidungstag Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-eu-werkstatt"><h2>🇪🇺 EU-Werkstatt <span class="simid">eu-werkstatt</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/eu-werkstatt__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__ipad__start.png" alt="eu-werkstatt iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/eu-werkstatt__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__fullhd__start.png" alt="eu-werkstatt Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/eu-werkstatt__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__ipad__play.png" alt="eu-werkstatt iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/eu-werkstatt__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__fullhd__play.png" alt="eu-werkstatt Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-farmer"><h2>🌾 Farmer <span class="simid">farmer</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/farmer__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__ipad__start.png" alt="farmer iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/farmer__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__fullhd__start.png" alt="farmer Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/farmer__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__ipad__play.png" alt="farmer iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/farmer__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__fullhd__play.png" alt="farmer Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-fluggesellschaft"><h2>✈️ Fluggesellschaft <span class="simid">fluggesellschaft</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/fluggesellschaft__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__ipad__start.png" alt="fluggesellschaft iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluggesellschaft__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__fullhd__start.png" alt="fluggesellschaft Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/fluggesellschaft__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__ipad__play.png" alt="fluggesellschaft iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluggesellschaft__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__fullhd__play.png" alt="fluggesellschaft Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-fluss"><h2>🌊 Flussmanagement <span class="simid">fluss</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/fluss__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluss__ipad__start.png" alt="fluss iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluss__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluss__fullhd__start.png" alt="fluss Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-heli"><h2>🚁 Helikopter-Navigation <span class="simid">heli</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/heli__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__ipad__start.png" alt="heli iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/heli__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__fullhd__start.png" alt="heli Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/heli__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__ipad__play.png" alt="heli iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/heli__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__fullhd__play.png" alt="heli Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-logistik"><h2>🚚 Logistik <span class="simid">logistik</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/logistik__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__ipad__start.png" alt="logistik iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/logistik__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__fullhd__start.png" alt="logistik Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/logistik__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__ipad__play.png" alt="logistik iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/logistik__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__fullhd__play.png" alt="logistik Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-sonnensystem"><h2>🪐 Sonnensystem <span class="simid">sonnensystem</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/sonnensystem__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__start.png" alt="sonnensystem iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__start.png" alt="sonnensystem Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/sonnensystem__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__play.png" alt="sonnensystem iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__play.png" alt="sonnensystem Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/sonnensystem__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__play2.png" alt="sonnensystem iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__play2.png" alt="sonnensystem Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-staustufen"><h2>💧 Staustufen <span class="simid">staustufen</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/staustufen__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__ipad__start.png" alt="staustufen iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/staustufen__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__fullhd__start.png" alt="staustufen Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/staustufen__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__ipad__play.png" alt="staustufen iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/staustufen__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__fullhd__play.png" alt="staustufen Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-tourismusregion"><h2>🗺️ Tourismusregion <span class="simid">tourismusregion</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/tourismusregion__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__ipad__start.png" alt="tourismusregion iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismusregion__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__fullhd__start.png" alt="tourismusregion Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/tourismusregion__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__ipad__play.png" alt="tourismusregion iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismusregion__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__fullhd__play.png" alt="tourismusregion Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-tourismustal"><h2>🏔️ Tourismustal <span class="simid">tourismustal</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/tourismustal__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__ipad__start.png" alt="tourismustal iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismustal__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__fullhd__start.png" alt="tourismustal Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/tourismustal__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__ipad__play.png" alt="tourismustal iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismustal__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__fullhd__play.png" alt="tourismustal Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-vulkan"><h2>🌋 Vulkan <span class="simid">vulkan</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/vulkan__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__start.png" alt="vulkan iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__start.png" alt="vulkan Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/vulkan__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__play.png" alt="vulkan iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__play.png" alt="vulkan Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/vulkan__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__play2.png" alt="vulkan iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__play2.png" alt="vulkan Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-wal"><h2>🐋 Wal <span class="simid">wal</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/wal__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__ipad__start.png" alt="wal iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/wal__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__fullhd__start.png" alt="wal Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/wal__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__ipad__play.png" alt="wal iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/wal__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__fullhd__play.png" alt="wal Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
<section class="sim" id="sim-weltkueche"><h2>🍲 Welt-Küche <span class="simid">weltkueche</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/weltkueche__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__ipad__start.png" alt="weltkueche iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/weltkueche__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__fullhd__start.png" alt="weltkueche Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
|
||||
<tr><th class="screen">Spielverlauf</th><td><a href="shots/weltkueche__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__ipad__play.png" alt="weltkueche iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/weltkueche__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__fullhd__play.png" alt="weltkueche Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* regen-gameplay-shots.cjs
|
||||
* ------------------------
|
||||
* Ersetzt die "Spielverlauf"-Screenshots (play / play2) der Display-Vorschau
|
||||
* durch ECHTE Spielszenen: Auswahl-Screens (Welt/Karte/Szenario) werden
|
||||
* betreten und das Onboarding wird zuverlässig weggeklickt (Skip-Buttons +
|
||||
* mehrseitige "Weiter"-Tutorials, auch wenn der Button unter dem Fold liegt),
|
||||
* bevor der Screenshot fällt.
|
||||
*
|
||||
* Ersetzt NUR die play/play2-Bilder in-place; die start-Bilder (Onboarding)
|
||||
* und display_preview.html bleiben unangetastet, damit die Galerie
|
||||
* unverändert funktioniert.
|
||||
*
|
||||
* Start (aus dem playwright-Ordner wegen node_modules):
|
||||
* cd /c/xampp/htdocs/geograsim/.dontDeploy/playwright && \
|
||||
* NODE_PATH="C:\\xampp\\htdocs\\geograsim\\.dontDeploy\\playwright\\node_modules" \
|
||||
* node ../../App/.LocalDeveloperTools/display-preview/regen-gameplay-shots.cjs
|
||||
*
|
||||
* Optional: --only=sonnensystem,wal --vp=ipad
|
||||
*/
|
||||
const { chromium } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
const SHOTS_DIR = 'C:/xampp/htdocs/geograsim/App/.LocalDeveloperTools/display-preview/shots';
|
||||
const BASE_URL = 'http://localhost/geograsim/App/sims';
|
||||
|
||||
const VIEWPORTS = [
|
||||
{ key: 'ipad', w: 1180, h: 820 },
|
||||
{ key: 'fullhd', w: 1920, h: 1080 },
|
||||
];
|
||||
|
||||
// Sims mit "play"-Screenshot in der Galerie (fluss hat keinen play -> weggelassen).
|
||||
const SIMS = [
|
||||
{ id: 'busfahrt' },
|
||||
{ id: 'energiemanager', play2: true },
|
||||
{ id: 'entscheidungstag' },
|
||||
{ id: 'eu-werkstatt' },
|
||||
{ id: 'farmer' },
|
||||
{ id: 'fluggesellschaft' },
|
||||
{ id: 'heli' },
|
||||
{ id: 'logistik' },
|
||||
{ id: 'sonnensystem', play2: true },
|
||||
{ id: 'staustufen' },
|
||||
{ id: 'tourismusregion' },
|
||||
{ id: 'tourismustal' },
|
||||
{ id: 'vulkan', play2: true },
|
||||
{ id: 'wal' },
|
||||
{ id: 'weltkueche' },
|
||||
];
|
||||
|
||||
// ---- In-Page: EIN Onboarding-Schritt wegklicken -------------------------
|
||||
async function skipStep(page) {
|
||||
return await page.evaluate(() => {
|
||||
// "loose": gerendert, aber darf auch unter dem Fold liegen (.click() geht trotzdem)
|
||||
const rendered = (el) => {
|
||||
if (!el) return false;
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 6 || r.height < 6) return false;
|
||||
const s = getComputedStyle(el);
|
||||
return s.visibility !== 'hidden' && s.display !== 'none' && +s.opacity > 0.05
|
||||
&& !el.disabled && el.getAttribute('aria-hidden') !== 'true'
|
||||
&& !el.classList.contains('hidden');
|
||||
};
|
||||
const clean = (s) => (s || '').replace(/\s+/g, ' ').trim();
|
||||
const norm = (s) => clean(s).replace(/[^\p{L}\p{N} ]+/gu, '').trim().toLowerCase();
|
||||
|
||||
// Namensfelder füllen
|
||||
document.querySelectorAll('input[type=text],input[type=search],input:not([type])').forEach((i) => {
|
||||
if (rendered(i) && !i.value) {
|
||||
i.value = 'Demo';
|
||||
i.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
i.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
|
||||
// 1) Explizite Skip-Buttons (schließen das GANZE Tutorial auf einmal)
|
||||
const skipSel = [
|
||||
'#emOnbSkip', '#tutorial-skip', '#heliOnbSkip', '#snOnbSkip',
|
||||
'[onclick*="skipTutorial"]', '[onclick*="closeTutorial"]',
|
||||
'[onclick*="skipOnboarding"]', '[onclick*="skipHeliOnboarding"]',
|
||||
];
|
||||
for (const sel of skipSel) {
|
||||
const el = [...document.querySelectorAll(sel)].find(rendered);
|
||||
if (el) { el.click(); return 'skip:' + sel; }
|
||||
}
|
||||
// Skip per Text
|
||||
{
|
||||
const el = [...document.querySelectorAll('button,a,[role=button]')].find(
|
||||
(e) => rendered(e) && /^(uberspringen|tutorial uberspringen|einfuhrung uberspringen|nicht mehr anzeigen)$/.test(norm(e.textContent))
|
||||
);
|
||||
if (el) { el.click(); return 'skiptext'; }
|
||||
}
|
||||
|
||||
// 2) "Weiter"/"Los geht's" NUR innerhalb eines sichtbaren Overlays
|
||||
// (verhindert das Anklicken von Gameplay-Buttons wie Vulkan "Weiter →")
|
||||
const overSel = '.ggs-modal-overlay,.ggs-overlay,.ggs-overlay-card,.sn-overlay,'
|
||||
+ '.overlay,.overlay-card,.splash-overlay,.lg-splash-overlay,.modal,'
|
||||
+ '.st-hint-overlay,.st-hint-modal,#vulk-overlay,#vulk-modal,.ggs-qi-back,'
|
||||
+ '[id*="Onb"],[id*="onboard"],[id*="overlay" i],[id*="modal" i],'
|
||||
+ '[class*="onb"],[class*="welcome"],[class*="tutorial"],[class*="intro"],'
|
||||
+ '[class*="willkommen"],[class*="overlay" i],[class*="ggs-qi"]';
|
||||
const overlays = [...document.querySelectorAll(overSel)].filter((o) => {
|
||||
if (!rendered(o)) return false;
|
||||
const r = o.getBoundingClientRect();
|
||||
return r.width > 240 && r.height > 150;
|
||||
});
|
||||
const proceedRx = /^(weiter|weiter gehts|los gehts|los geht s|auf gehts|verstanden|alles klar|habe verstanden|fertig|los arbeiten|jetzt starten|jetzt loslegen|jetzt spielen|spiel starten|neue partie|partie starten|los|okay|ok|schliessen|start|starten|beginnen)$/;
|
||||
for (const ov of overlays) {
|
||||
const btns = [...ov.querySelectorAll('button,a,[role=button],.btn')].filter(rendered);
|
||||
let el = btns.find((b) => proceedRx.test(norm(b.textContent)));
|
||||
if (!el) el = btns.find((b) => /^(x|close)$/.test(norm(b.textContent)) || /(^|\s)(close|ggs-modal-close)(\s|$)/i.test(b.className));
|
||||
if (el) { el.click(); return 'proceed:' + norm(el.textContent).slice(0, 24); }
|
||||
}
|
||||
return null;
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
// Noch ein Onboarding-Overlay sichtbar? (Heuristik für den Report)
|
||||
async function onboardingStillVisible(page) {
|
||||
return await page.evaluate(() => {
|
||||
const rx = /(willkommen|wusstest du|anleitung|was lerne ich|so funktioniert|einf(ü|u)hrung|beobachtungs-?aufgaben|dein kreislauf|uberspringen|überspringen|vulkanologie|observatorium|du erkundest die planeten|was tust du)/i;
|
||||
const els = [...document.querySelectorAll('.overlay,.ggs-overlay,.ggs-modal-overlay,.sn-overlay,.st-hint-overlay,#vulk-overlay,[class*="onb"],[class*="tutorial"],[class*="intro"],[class*="welcome"],[class*="overlay" i]')];
|
||||
return els.some((el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.width < 240 || r.height < 150) return false;
|
||||
const s = getComputedStyle(el);
|
||||
if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity < 0.05 || el.classList.contains('hidden')) return false;
|
||||
return rx.test((el.textContent || '').slice(0, 500));
|
||||
});
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
async function clickSel(page, sel) {
|
||||
return await page.evaluate((sel) => {
|
||||
const el = [...document.querySelectorAll(sel)].find((e) => {
|
||||
const r = e.getBoundingClientRect();
|
||||
const s = getComputedStyle(e);
|
||||
return r.width > 4 && r.height > 4 && s.visibility !== 'hidden' && s.display !== 'none' && !e.classList.contains('hidden');
|
||||
});
|
||||
if (el) { el.click(); return true; }
|
||||
return false;
|
||||
}, sel).catch(() => false);
|
||||
}
|
||||
|
||||
async function clickByText(page, source) {
|
||||
return await page.evaluate((src) => {
|
||||
const rx = new RegExp(src, 'i');
|
||||
const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().replace(/[^\p{L}\p{N} ]+/gu, '').trim();
|
||||
const el = [...document.querySelectorAll('button,a,[role=button],.btn')].find((e) => {
|
||||
const r = e.getBoundingClientRect();
|
||||
const s = getComputedStyle(e);
|
||||
if (r.width < 6 || r.height < 6 || s.visibility === 'hidden' || s.display === 'none' || e.classList.contains('hidden')) return false;
|
||||
return rx.test(norm(e.textContent));
|
||||
});
|
||||
if (el) { el.click(); return true; }
|
||||
return false;
|
||||
}, source).catch(() => false);
|
||||
}
|
||||
|
||||
async function skipLoop(page, rounds = 12) {
|
||||
const seq = [];
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const r = await skipStep(page);
|
||||
if (!r) break;
|
||||
seq.push(r);
|
||||
await page.waitForTimeout(420);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
// ---- pro-Sim: Auswahl-Screen betreten, dann Tutorial wegklicken ---------
|
||||
const ENTRY = {
|
||||
// heli: Flugkarte braucht PHP-injizierte Waypoints (WP), die beim direkten
|
||||
// Aufruf von game.html fehlen (-> "WP is not defined"). Daher bleibt der
|
||||
// stabile Gameplay-Screen die Einsatzzentrale (Missionsauswahl nach Onboarding).
|
||||
'wal': async (page) => {
|
||||
await clickSel(page, '.era-card-cta'); // "In dieser Welt starten →"
|
||||
await page.waitForTimeout(1800);
|
||||
await skipLoop(page);
|
||||
},
|
||||
'tourismusregion': async (page) => {
|
||||
await clickSel(page, '#mapList .map-btn, .map-btn'); // Wegkarte wählen
|
||||
await page.waitForTimeout(3000); // 3D-Szene
|
||||
await skipLoop(page); // "Willkommen …" weg
|
||||
},
|
||||
'tourismustal': async (page) => {
|
||||
await clickSel(page, '[data-scene]'); // Szene wählen
|
||||
await page.waitForTimeout(3000);
|
||||
await skipLoop(page); // "Die Gäste …" weg
|
||||
},
|
||||
'eu-werkstatt': async (page) => {
|
||||
await clickSel(page, '.scenario-card'); // Szenario betreten
|
||||
await page.waitForTimeout(1600);
|
||||
await skipLoop(page, 8); // Konzept-Intro weg
|
||||
await page.waitForTimeout(900);
|
||||
await clickByText(page, 'los gehts|los geht|^weiter|verstanden|alles klar');
|
||||
await page.waitForTimeout(700);
|
||||
await skipLoop(page, 4);
|
||||
},
|
||||
};
|
||||
|
||||
// Nach dem Wegklicken des Onboardings noch aufräumen, um die echte Szene zu zeigen.
|
||||
const POST = {
|
||||
'vulkan': async (page) => {
|
||||
// "Los geht's" öffnet direkt das erste Aufgaben-Modal (#vulk-overlay).
|
||||
// Schließen -> darunter liegt die 3D-Vulkanszene + Steuerpanel (Slider).
|
||||
await page.evaluate(() => { const o = document.getElementById('vulk-overlay'); if (o) o.classList.remove('show'); });
|
||||
await page.waitForTimeout(500);
|
||||
},
|
||||
};
|
||||
|
||||
const PLAY2 = {
|
||||
'vulkan': async (page) => { await clickSel(page, '#erupt-btn'); await page.waitForTimeout(3200); },
|
||||
'sonnensystem': async (page) => { await clickSel(page, '#obs-open-btn'); await page.waitForTimeout(1800); await skipLoop(page, 4); await page.waitForTimeout(1200); },
|
||||
'energiemanager': async (page) => { await clickSel(page, '#btnRunDay'); await page.waitForTimeout(3200); },
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const onlyArg = (process.argv.find((a) => a.startsWith('--only=')) || '').slice(7);
|
||||
const vpArg = (process.argv.find((a) => a.startsWith('--vp=')) || '').slice(5);
|
||||
const list = onlyArg ? SIMS.filter((s) => onlyArg.split(',').includes(s.id)) : SIMS;
|
||||
const vps = vpArg ? VIEWPORTS.filter((v) => v.key === vpArg) : VIEWPORTS;
|
||||
|
||||
const browser = await chromium.launch({
|
||||
args: ['--use-gl=swiftshader', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader'],
|
||||
});
|
||||
|
||||
for (const vp of vps) {
|
||||
console.log(`\n===== ${vp.key} (${vp.w}x${vp.h}) =====`);
|
||||
for (const sim of list) {
|
||||
const ctx = await browser.newContext({ viewport: { width: vp.w, height: vp.h }, deviceScaleFactor: 1 });
|
||||
const page = await ctx.newPage();
|
||||
const errs = [];
|
||||
page.on('pageerror', (e) => { if (errs.length < 3) errs.push('PE:' + e.message.slice(0, 50)); });
|
||||
|
||||
const url = `${BASE_URL}/${sim.id}/game.html`;
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 40000 });
|
||||
} catch (e) { errs.push('GOTO:' + e.message.slice(0, 40)); }
|
||||
await page.waitForTimeout(3200); // 3D / CDN
|
||||
|
||||
let seq = await skipLoop(page, 14); // sofortiges Onboarding
|
||||
if (ENTRY[sim.id]) await ENTRY[sim.id](page); // Auswahl-Screen + Tutorial
|
||||
await page.waitForTimeout(2000);
|
||||
// Zweiter Durchlauf: 3D-Sims (vulkan/sonnensystem) laden ihr Onboarding
|
||||
// asynchron erst NACH dem ersten Skip -> hier erneut wegklicken.
|
||||
seq = seq.concat(await skipLoop(page, 10));
|
||||
if (POST[sim.id]) await POST[sim.id](page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const stuck = await onboardingStillVisible(page);
|
||||
let ok = false;
|
||||
try {
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, `${sim.id}__${vp.key}__play.png`) });
|
||||
ok = true;
|
||||
} catch (e) { errs.push('SHOT:' + e.message.slice(0, 40)); }
|
||||
|
||||
let ok2 = null;
|
||||
if (sim.play2 && PLAY2[sim.id]) {
|
||||
try {
|
||||
await PLAY2[sim.id](page);
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, `${sim.id}__${vp.key}__play2.png`) });
|
||||
ok2 = true;
|
||||
} catch (e) { ok2 = false; errs.push('SHOT2:' + e.message.slice(0, 40)); }
|
||||
}
|
||||
|
||||
console.log(
|
||||
' ' + sim.id.padEnd(18)
|
||||
+ (ok ? 'play OK' : 'play FAIL')
|
||||
+ (ok2 === null ? '' : ok2 ? ' · play2 OK' : ' · play2 FAIL')
|
||||
+ (stuck ? ' [!! ONBOARDING NOCH SICHTBAR]' : '')
|
||||
+ ' clicks=' + seq.length + (seq.length ? ' (' + seq.slice(0, 6).join(',') + ')' : '')
|
||||
+ (errs.length ? ' ' + errs.slice(0, 2).join(' | ') : '')
|
||||
);
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
console.log('\nFertig. Galerie: http://localhost/geograsim/App/.LocalDeveloperTools/display-preview/display_preview.html');
|
||||
})();
|
||||
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 714 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,272 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GeoGraSim — Test-Checkliste (19.08.2026)</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#f4f2ec; --card:#fff; --ink:#242018; --muted:#7a7266; --line:rgba(0,0,0,.08);
|
||||
--accent:#4a7c8a; --accent-d:#356070; --ok:#4a8a5a; --warn:#c58a2a; --sim:#7a5ca8;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--ink);line-height:1.5;padding-bottom:4rem}
|
||||
header{position:sticky;top:0;z-index:10;background:#fff;border-bottom:1px solid var(--line);
|
||||
padding:.9rem 1.2rem;display:flex;align-items:center;gap:1rem;flex-wrap:wrap;box-shadow:0 1px 6px rgba(0,0,0,.04)}
|
||||
header h1{font-size:1.15rem;font-weight:900;color:var(--accent-d);letter-spacing:-.02em}
|
||||
header .sub{font-size:.72rem;color:var(--muted)}
|
||||
.progress{margin-left:auto;display:flex;align-items:center;gap:.6rem;font-size:.8rem;font-weight:700;color:var(--accent-d)}
|
||||
.bar{width:160px;height:9px;border-radius:99px;background:#e4e0d6;overflow:hidden}
|
||||
.bar > div{height:100%;background:var(--ok);width:0;transition:width .3s}
|
||||
.reset{font-size:.66rem;color:var(--muted);border:1px solid var(--line);background:#fff;border-radius:6px;padding:.25rem .5rem;cursor:pointer}
|
||||
.wrap{max-width:920px;margin:1.4rem auto;padding:0 1.1rem}
|
||||
.intro{font-size:.86rem;color:var(--muted);margin-bottom:1.4rem;background:#fff;border:1px solid var(--line);border-radius:10px;padding:.8rem 1rem}
|
||||
.intro code{background:#eee9df;padding:.05rem .35rem;border-radius:4px;font-size:.85em}
|
||||
section{margin-bottom:1.8rem}
|
||||
.sec-h{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--accent);
|
||||
margin:0 0 .6rem .2rem;display:flex;align-items:center;gap:.5rem}
|
||||
.sec-h .cnt{font-size:.62rem;color:var(--muted);font-weight:600;letter-spacing:.02em}
|
||||
.item{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:.85rem 1rem;margin-bottom:.7rem;
|
||||
box-shadow:0 1px 3px rgba(0,0,0,.03);transition:opacity .2s}
|
||||
.item.done{opacity:.55}
|
||||
.item-top{display:flex;gap:.7rem;align-items:flex-start}
|
||||
.item-top input[type=checkbox]{width:22px;height:22px;margin-top:.1rem;accent-color:var(--ok);cursor:pointer;flex-shrink:0}
|
||||
.item-title{font-weight:800;font-size:.98rem}
|
||||
.item.done .item-title{text-decoration:line-through;text-decoration-color:rgba(0,0,0,.3)}
|
||||
.tag{display:inline-block;font-size:.58rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
|
||||
padding:.12rem .5rem;border-radius:99px;vertical-align:middle;margin-left:.5rem}
|
||||
.tag.live{background:#dceadd;color:#2f6b3a}
|
||||
.tag.pending{background:#fdeecd;color:#8a6510}
|
||||
.body{padding-left:calc(22px + .7rem);margin-top:.4rem;font-size:.86rem;color:#4a463d}
|
||||
.body .row{margin:.28rem 0}
|
||||
.body b{color:var(--ink)}
|
||||
.k{display:inline-block;min-width:74px;font-weight:700;color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.03em}
|
||||
a.url{color:var(--accent-d);font-family:ui-monospace,monospace;font-size:.82em;text-decoration:none;border-bottom:1px dotted var(--accent)}
|
||||
a.url:hover{background:#eaf2f4}
|
||||
.exp{background:#f0f6f1;border-left:3px solid var(--ok);border-radius:0 6px 6px 0;padding:.35rem .6rem;margin-top:.35rem;font-size:.82rem;color:#33613f}
|
||||
.note{width:100%;margin-top:.5rem;border:1px solid var(--line);border-radius:7px;padding:.4rem .55rem;font-family:inherit;
|
||||
font-size:.82rem;resize:vertical;min-height:0;height:2rem;background:#fcfbf8;color:var(--ink)}
|
||||
.note::placeholder{color:#b3ac9e}
|
||||
footer{text-align:center;color:var(--muted);font-size:.72rem;margin:2rem 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<h1>🧪 GeoGraSim — Test-Checkliste</h1>
|
||||
<div class="sub">Alle Erledigungen seit den Weltküche-Lebensmitteln · Stand 19.08.2026 · lokal testen</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<span id="pcount">0 / 0</span>
|
||||
<div class="bar"><div id="pbar"></div></div>
|
||||
<button class="reset" onclick="if(confirm('Alle Haken + Notizen zurücksetzen?')){localStorage.clear();location.reload();}">reset</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="intro">
|
||||
Basis-URL lokal: <code>http://localhost/geograsim/App/</code> · <b>Haken & Notizen</b> werden im Browser gespeichert.
|
||||
<span class="tag live">live</span> = auf Prod, <span class="tag pending">bereit</span> = committed, kommt mit dem nächsten Deploy.
|
||||
Für Admin-Seiten zuerst über <a class="url" href="http://localhost/geograsim/App/admin.html" target="_blank">admin.html</a> anmelden (Username <b>Thomas</b> → PIN ist lokal vorausgefüllt).
|
||||
</div>
|
||||
|
||||
<!-- ================= WELTKÜCHE (SIM) ================= -->
|
||||
<section data-sec="Weltküche – Simulation"></section>
|
||||
|
||||
<!-- ================= WELTKÜCHE (LEHRPLAN/ADMIN) ================= -->
|
||||
<section data-sec="Weltküche – Lehrplan & Admin"></section>
|
||||
|
||||
<!-- ================= LEHRER-COCKPIT ================= -->
|
||||
<section data-sec="Lehrer-Cockpit (teacher.html)"></section>
|
||||
|
||||
<!-- ================= TOURISMUSREGION ================= -->
|
||||
<section data-sec="Tourismusregion"></section>
|
||||
|
||||
<!-- ================= LOGISTIK & PLATTFORM ================= -->
|
||||
<section data-sec="Logistik & Plattform"></section>
|
||||
|
||||
<!-- ================= ADMIN & AUTH ================= -->
|
||||
<section data-sec="Admin & Auth-Fundament"></section>
|
||||
</div>
|
||||
|
||||
<footer>GeoGraSim · Test-Checkliste · lokal generiert 19.08.2026</footer>
|
||||
|
||||
<script>
|
||||
// Alle Test-Items als Daten — daraus wird die Liste gebaut.
|
||||
const BASE = 'http://localhost/geograsim/App/';
|
||||
function u(path){ return BASE + path; }
|
||||
const ITEMS = {
|
||||
'Weltküche – Simulation': [
|
||||
{ id:'wk-artikel', t:'Korrekte Artikel für alle 70 Zutaten', tag:'live',
|
||||
w:'der/die/das je Zutat („die Kartoffel", „der Pfeffer", „das Olivenöl") statt Platzhalter.',
|
||||
url:'sims/weltkueche/game.html', steps:'Ein Gericht spielen, Zutat lösen/falsch klicken – im Feedback-Satz steht der richtige Artikel.',
|
||||
exp:'Nie mehr „der/die/das Kartoffel" – immer „die Kartoffel" usw.' },
|
||||
{ id:'wk-ukraine', t:'Verdrehter Herkunfts-Satz korrigiert (Ukraine-Fall)', tag:'live',
|
||||
w:'Bei fernem Großproduzenten (z. B. Ukraine-Kartoffel im AT-Gericht) sachlich korrektes Feedback.',
|
||||
url:'sims/weltkueche/game.html', steps:'Tiroler Gröstl → Kartoffel → ein fernes Land wie Ukraine anklicken.',
|
||||
exp:'„…in unseren Handel kommt davon kaum etwas… nimmt man die Kartoffel aus Österreich oder einem Nachbarland." (nicht mehr „…stammt die Kartoffel von dort").' },
|
||||
{ id:'wk-heimat', t:'Heimat-Banner neu formuliert', tag:'live',
|
||||
w:'Erklärt „bevorzugt aus AT, aber AT deckt den Bedarf nicht allein".',
|
||||
url:'sims/weltkueche/game.html', steps:'AT-Gericht, eine Heimat-Zutat lösen → grünes Banner lesen.',
|
||||
exp:'„Zutaten werden zwar bevorzugt aus Österreich verwendet, aber Österreich allein deckt den Bedarf nicht ab – darum zählt auch ein Nachbarland."' },
|
||||
{ id:'wk-gravitation', t:'Herkunft per Handels-Gravitation (Nähe × Menge)', tag:'pending',
|
||||
w:'Zweit-Lieferant = nächster großer Nachbar, nicht ferner Großproduzent.',
|
||||
url:'sims/weltkueche/game.html', steps:'Ein AT-Gericht mit Eiern/Kartoffel spielen – richtige Länder prüfen.',
|
||||
exp:'AT-Eier → Österreich + Deutschland (NICHT Frankreich), obwohl FR mehr Eier erzeugt.' },
|
||||
{ id:'wk-punkte', t:'Punktefix: „alle Falschen zuerst = 0"', tag:'pending',
|
||||
w:'Strafe pro Fehlklick = 100 ÷ Anzahl Distraktoren.',
|
||||
url:'sims/weltkueche/game.html', steps:'Bei einer Zutat ALLE falschen Länder zuerst anklicken, dann die richtigen.',
|
||||
exp:'Ergebnis 0 Punkte. Banner zeigt die tatsächliche Strafe pro Fehlklick.' },
|
||||
{ id:'wk-phase', t:'Live-State zeigt echten Fortschritt statt „forschen"', tag:'live',
|
||||
w:'GGS_LIVE_STATE-Phase = „Gericht X · Zutat Y/Z".',
|
||||
url:'teacher.html', steps:'Als Lehrer im Cockpit „Aktuell" ein Weltküche-Kind ansehen (oder Live-State im Sim prüfen).',
|
||||
exp:'Phase/Info zeigt „Gericht 2 · Zutat 3/5" – nicht dauerhaft „forschen".' },
|
||||
{ id:'wk-submit', t:'Backend-Submit + Persistenz geschaffter Gerichte', tag:'live',
|
||||
w:'Pro fertigem Gericht ein assessments-Eintrag; geschaffte Gerichte bleiben nach Reload.',
|
||||
url:'sims/weltkueche/game.html', steps:'Ein Gericht abschließen, Seite neu laden.',
|
||||
exp:'Bereits geschaffte Gerichte bleiben erkennbar; im Lehrer-Cockpit taucht das Ergebnis auf.' },
|
||||
],
|
||||
'Weltküche – Lehrplan & Admin': [
|
||||
{ id:'wk-lehrplan', t:'Lehrplan-Bezug auf der Weltküche-Modulseite', tag:'live',
|
||||
w:'Kompetenz „Welternährung, Konsum und globale Disparitäten" + Anker.',
|
||||
url:'modul-weltkueche', steps:'Seite öffnen, zu „Lehrplan-Bezug" scrollen.',
|
||||
exp:'Kompetenzen + Anker sichtbar (nicht mehr „Noch keine Kompetenzen zugeordnet").' },
|
||||
{ id:'wk-filter', t:'Länder-Filter im Lehrplan-Bezug wirkt', tag:'live',
|
||||
w:'Filter-Chips (Alle/AT/DE/CH/FL) blenden Fach-Anker wirklich um.',
|
||||
url:'modul-weltkueche', steps:'Bei „Lehrplan-Bezug" auf Deutschland, dann Österreich klicken.',
|
||||
exp:'Sichtbare Fach-Anker ändern sich je Land (vorher passierte nichts).' },
|
||||
{ id:'wk-admin', t:'Admin-QA: Weltküche-Herkunftsübersicht (NEU)', tag:'pending',
|
||||
w:'Pro Gericht × Zutat richtige Länder + Distraktoren; „nur Auffällige"-Filter.',
|
||||
url:'admin-weltkueche.html', steps:'Als Admin einloggen, Seite öffnen. Suche/Kategorie testen, „nur Auffällige" aktivieren.',
|
||||
exp:'30 Gerichte; Kartoffel/Eier → 🟢 AT+DE. „Nur Auffällige" zeigt ~5 Kandidaten zum Prüfen.' },
|
||||
],
|
||||
'Lehrer-Cockpit (teacher.html)': [
|
||||
{ id:'tc-menu', t:'Menü „Live" → „Ergebnisse" mit Tag · Aktuell · Vergleich · Gesamt', tag:'pending',
|
||||
w:'Umbenannt + 4. Unteransicht „Gesamt" (alte Ergebnis-Ansicht).',
|
||||
url:'teacher.html', steps:'Als Lehrer einloggen, Klasse wählen, Tab „Ergebnisse".',
|
||||
exp:'Vier Unteransichten; „Gesamt" zeigt die frühere Ergebnis-/Auswertungsansicht.' },
|
||||
{ id:'tc-quali', t:'Qualitäts-Kennzahl (richtig ↔ falsch als Balken)', tag:'pending',
|
||||
w:'Grafischer Balken (grün/rot) je Kind in Aktuell + Vergleich.',
|
||||
url:'teacher.html', steps:'Ergebnisse → Aktuell/Vergleich bei aktivem Weltküche-Kind.',
|
||||
exp:'Zweifarbiger Balken + %-Wert („blödeln vs. gut" sichtbar).' },
|
||||
{ id:'tc-labels', t:'Klartext-Labels statt Jargon', tag:'pending',
|
||||
w:'„Stark/Solide/Viele Fehler" statt „Kritisch/OK"; „Gerade dabei" statt „Phase".',
|
||||
url:'teacher.html', steps:'Aktuell/Vergleich ansehen.',
|
||||
exp:'Keine „Kritisch"-Bezeichnung mehr.' },
|
||||
{ id:'tc-simlabels', t:'Sim-Bezeichner: Wort + 3-Buchstaben-Kürzel', tag:'pending',
|
||||
w:'Kein „Log L1" mehr — enger Balken zeigt Kürzel (LOG), breiter das Wort.',
|
||||
url:'teacher.html', steps:'Ergebnisse → Tag, Session-Balken ansehen.',
|
||||
exp:'Balken tragen LOG/Logistik statt „Log L1".' },
|
||||
{ id:'tc-logistik', t:'Logistik hat kuratierte Spalten (statt Roh-Dump)', tag:'pending',
|
||||
w:'Stufe · Budget · Geliefert · Unterwegs · Offen · Verspätet · Fortschritt.',
|
||||
url:'teacher.html', steps:'Ergebnisse → Aktuell mit aktivem Logistik-Kind.',
|
||||
exp:'Sinnvolle Spalten – keine simId/__activeMs/timestamp-Spalten mehr.' },
|
||||
{ id:'tc-streifen', t:'Aktivitätsstreifen: Farbe je Sim, Höhe=Erfolg, Rahmen=Zustand', tag:'pending',
|
||||
w:'Fertig = geschlossener Rahmen · läuft = rechts offen · Abbruch = gestrichelt + ✖.',
|
||||
url:'teacher.html', steps:'Ergebnisse → Tag, mit Sessions eines Tages.',
|
||||
exp:'Balken je Sim gefärbt, höher = besser; Rahmen/✖ zeigen den Zustand; Legende erklärt es.' },
|
||||
{ id:'tc-assess', t:'assessment-Skip-Fix (keine leeren Ping-Zeilen mehr)', tag:'pending',
|
||||
w:'„started/running"-Pings landen nicht mehr als leere Zeilen in Ergebnisse/Tag.',
|
||||
url:'teacher.html', steps:'Nach etwas Spielzeit Ergebnisse/Tag ansehen.',
|
||||
exp:'Nur echte Abschlüsse als Balken – kein leeres Rauschen.' },
|
||||
],
|
||||
'Tourismusregion': [
|
||||
{ id:'tr-start', t:'Startseite: keine Name/Klasscode-Felder, Klick auf Route startet', tag:'live',
|
||||
w:'Felder entfernt; Klick auf eine Wegkarte startet sofort (kein „Partie starten").',
|
||||
url:'sims/tourismusregion/game.html', steps:'Sim öffnen, eine Wegkarte anklicken.',
|
||||
exp:'Keine Eingabefelder; Partie startet direkt bei Kartenklick.' },
|
||||
{ id:'tr-ipad', t:'iPad-Responsive (Panels decken die Karte nicht mehr zu)', tag:'live',
|
||||
w:'Panels mit Höhen-Deckel + eigene iPad-Landscape-Regeln. AM IPAD TESTEN!',
|
||||
url:'sims/tourismusregion/game.html', steps:'Am echten iPad (Landscape) öffnen und ein Gebäude antippen.',
|
||||
exp:'KPIs/Baumenü/Info-Panel überdecken die Karte nicht; Info-Panel scrollt statt zu überziehen.' },
|
||||
{ id:'tr-lift', t:'Seilbahn hat eine Talstation (nicht mehr „transparent")', tag:'live',
|
||||
w:'Talstation mit Dach + Umlenkrad unten am Lift.',
|
||||
url:'sims/tourismusregion/game.html', steps:'Seilbahn bauen (Berg erschließen), auf die Talstation schauen.',
|
||||
exp:'Solider Baukörper unten – kein durchsichtiger Lift-Anfang.' },
|
||||
{ id:'tr-save', t:'Abschluss: kein „Ergebnis speichern"-Button (Auto-Save)', tag:'live',
|
||||
w:'Automatisches Speichern am Partie-Ende; Button entfernt.',
|
||||
url:'sims/tourismusregion/game.html', steps:'Eine Partie zu Ende bringen.',
|
||||
exp:'Hinweis „automatisch gespeichert", kein Speichern-Button.' },
|
||||
{ id:'tr-pass', t:'Neue Karte „Passstraße"', tag:'live',
|
||||
w:'Route bergauf (136 Punkte) + 41 Bauslots + Hintergrundbild.',
|
||||
url:'sims/tourismusregion/game.html', steps:'Im Startbildschirm „Passstraße" wählen.',
|
||||
exp:'Karte lädt, Route + Bauslots passen aufs Bild (optisch prüfen).' },
|
||||
{ id:'tr-balance', t:'Balance: Umwelt beißt jetzt (Übertourismus)', tag:'pending',
|
||||
w:'Zu viel Bauen + Verkehr senken die Umwelt spürbar; Umwelt-Malus wirkt.',
|
||||
url:'sims/tourismusregion/game.html', steps:'Viel/dicht bauen und laufen lassen; Umwelt-KPI beobachten.',
|
||||
exp:'Umwelt fällt deutlich (Richtung <45), umweltbewusste Gäste werden unzufriedener.' },
|
||||
{ id:'tr-erklaer', t:'Schüler-Erklärung: Umwelt & Naturschutzgebiet', tag:'pending',
|
||||
w:'Willkommenskarte + Warnkarte bei Umwelt<50 + klarere Naturschutz-Beschreibung.',
|
||||
url:'sims/tourismusregion/game.html', steps:'Neu starten → Willkommenskarte lesen; Umwelt absacken lassen → Warnkarte.',
|
||||
exp:'Karte erklärt Umwelt-Mechanik + Naturschutzgebiet; Warnkarte erscheint bei <50.' },
|
||||
],
|
||||
'Logistik & Plattform': [
|
||||
{ id:'lg-pause', t:'Logistik-Pause friert die Aktivzeit ein', tag:'pending',
|
||||
w:'Bei Pause zählt die „Seit"-Zeit im Cockpit nicht weiter.',
|
||||
url:'sims/logistik/game.html', steps:'Logistik spielen, ⏸ drücken, im Lehrer-Cockpit die Aktivzeit beobachten.',
|
||||
exp:'Aktivzeit steht still, solange pausiert; läuft nach dem Fortsetzen weiter.' },
|
||||
{ id:'pf-scripts', t:'Wrapper-Bug behoben (/teacher lud kein Script mehr)', tag:'live',
|
||||
w:'Favicon wurde ins letzte </head> injiziert – zerschoss ein JS-Popup-Script.',
|
||||
url:'teacher', steps:'Die /teacher-Route (Wrapper) öffnen.',
|
||||
exp:'Seite lädt sauber, keine „Invalid token"-Konsolenfehler. (teacher.html direkt lief immer.)' },
|
||||
],
|
||||
'Admin & Auth-Fundament': [
|
||||
{ id:'ad-login', t:'Lokaler Admin-Login ohne Mailversand (Dev-PIN)', tag:'pending',
|
||||
w:'PIN wird lokal vorausgefüllt; Mailer-Fehler sperrt nicht mehr aus. Auf Prod bleibt 2FA-Mail Pflicht.',
|
||||
url:'admin.html', steps:'Username Thomas + Passwort → „Anmelden".',
|
||||
exp:'PIN-Feld ist vorausgefüllt („Dev: … vorausgefüllt"), „Bestätigen" → eingeloggt.' },
|
||||
{ id:'auth-found', t:'Auth-Fundament JWT + Redis (schlafend, non-breaking)', tag:'pending',
|
||||
w:'firebase/php-jwt + predis + Jwt/SessionStore + Session-Kanalisierung. AUTH_BACKEND=session.',
|
||||
url:'', steps:'Kein direkter Test nötig — nur sicherstellen, dass Login/Admin normal funktionieren.',
|
||||
exp:'Alles läuft wie bisher (das Fundament ist inaktiv, bis der JWT-Cutover kommt).' },
|
||||
],
|
||||
};
|
||||
|
||||
const listWrap = document.querySelector('.wrap');
|
||||
document.querySelectorAll('section[data-sec]').forEach(sec => {
|
||||
const name = sec.getAttribute('data-sec');
|
||||
const items = ITEMS[name] || [];
|
||||
let html = '<div class="sec-h">' + name + ' <span class="cnt">' + items.length + ' Punkte</span></div>';
|
||||
items.forEach(it => {
|
||||
const urlHtml = it.url ? '<a class="url" href="' + u(it.url) + '" target="_blank" rel="noopener">' + u(it.url) + '</a>' : '<span style="color:#b3ac9e">—</span>';
|
||||
html += ''
|
||||
+ '<div class="item" data-id="' + it.id + '">'
|
||||
+ '<div class="item-top">'
|
||||
+ '<input type="checkbox" data-check="' + it.id + '">'
|
||||
+ '<div style="flex:1"><span class="item-title">' + it.t + '</span>'
|
||||
+ '<span class="tag ' + it.tag + '">' + (it.tag==='live'?'live':'bereit') + '</span></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="body">'
|
||||
+ '<div class="row"><span class="k">Was</span> ' + it.w + '</div>'
|
||||
+ '<div class="row"><span class="k">Öffnen</span> ' + urlHtml + '</div>'
|
||||
+ '<div class="row"><span class="k">Testen</span> ' + it.steps + '</div>'
|
||||
+ '<div class="exp">✓ Erwartet: ' + it.exp + '</div>'
|
||||
+ '<textarea class="note" data-note="' + it.id + '" placeholder="Notiz / gefundene Abweichung…"></textarea>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
sec.innerHTML = html;
|
||||
});
|
||||
|
||||
// Persistenz + Fortschritt
|
||||
const checks = document.querySelectorAll('input[data-check]');
|
||||
function refresh(){
|
||||
let done = 0;
|
||||
checks.forEach(c => { if(c.checked){ done++; c.closest('.item').classList.add('done'); } else c.closest('.item').classList.remove('done'); });
|
||||
document.getElementById('pcount').textContent = done + ' / ' + checks.length;
|
||||
document.getElementById('pbar').style.width = (checks.length? (done/checks.length*100):0) + '%';
|
||||
}
|
||||
checks.forEach(c => {
|
||||
const key = 'chk_' + c.getAttribute('data-check');
|
||||
c.checked = localStorage.getItem(key) === '1';
|
||||
c.addEventListener('change', () => { localStorage.setItem(key, c.checked?'1':'0'); refresh(); });
|
||||
});
|
||||
document.querySelectorAll('textarea[data-note]').forEach(t => {
|
||||
const key = 'note_' + t.getAttribute('data-note');
|
||||
t.value = localStorage.getItem(key) || '';
|
||||
t.addEventListener('input', () => localStorage.setItem(key, t.value));
|
||||
});
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,3 +14,18 @@ DB_PORT=3306
|
||||
|
||||
# ElevenLabs Sound Effects API — nur für generate-sounds.py (Dev-Zeit)
|
||||
# ELEVENLABS_API_KEY=...
|
||||
|
||||
# ── Auth-Backend (JWT + Redis) ──────────────────────────────────────────
|
||||
# 'session' = Legacy PHP-Datei-Sessions (Default, ändert nichts).
|
||||
# 'jwt' = JWT-Access-Token (HttpOnly-Cookie) + Redis-Session/Refresh.
|
||||
AUTH_BACKEND=session
|
||||
|
||||
# Signaturschlüssel für JWT (HS256). PFLICHT sobald AUTH_BACKEND=jwt.
|
||||
# Erzeugen mit: openssl rand -hex 32
|
||||
# Pro Umgebung UNTERSCHIEDLICH (dev ≠ prod). NIEMALS committen.
|
||||
# JWT_SECRET=
|
||||
|
||||
# Redis (Session-/Refresh-Store). Lokal: docker-compose.dev.yml → 127.0.0.1:6379
|
||||
REDIS_URL=tcp://127.0.0.1:6379
|
||||
# JWT_TTL=900 # Access-Token-Lebensdauer in Sekunden (Default 15 min)
|
||||
# JWT_REFRESH_TTL=2592000 # Session/Refresh in Sekunden (Default 30 Tage)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Sensible Dateien niemals ausliefern (Defense-in-Depth), auch wenn sie im
|
||||
# Docroot liegen und die -f-Regel unten sie sonst direkt serviert.
|
||||
<FilesMatch "^\.env">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(sh|sql|bak)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
# Backend-Editor-UI (statisch) nicht direkt ausliefern — nur über die
|
||||
# admin-geschützte Route /stadt-editor (pages/stadt-editor.php) erreichbar.
|
||||
<Files "stadt-editor.html">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
RewriteEngine On
|
||||
# Kein RewriteBase — Apache nutzt den Request-Pfad automatisch.
|
||||
# Funktioniert fuer alle Deploy-Ziele:
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- ============================================================
|
||||
-- Tourismusregion: Komplett-Integration (module_info + Glossar + Lehrplan)
|
||||
-- Stand: 2026-07-12 · Atlas · idempotent
|
||||
-- Schwester-Sim von Tourismustal (strikt getrennt): wellenbasierte
|
||||
-- Routen-Sim — Gäste ziehen über Wegkarten, Infrastruktur wirkt nur in
|
||||
-- Reichweite. Kern-Didaktik: Besucherlenkung, Mobilität, Standort am Weg.
|
||||
-- ============================================================
|
||||
|
||||
-- 1) Modul
|
||||
INSERT INTO module_info (
|
||||
module_id, title, subtitle, short_desc, long_desc, learning_goals,
|
||||
short_desc_easy, long_desc_easy, learning_goals_easy,
|
||||
duration_min, age_min, age_max, icon, card_image, page_url, play_url,
|
||||
status, sort_order
|
||||
) VALUES (
|
||||
'tourismusregion',
|
||||
'Tourismusregion',
|
||||
'Gäste lenken auf den Wegen deiner Region',
|
||||
'Gästegruppen ziehen über die Wege deiner Region – baue Wirtshäuser, Bahnen und Attraktionen in Reichweite, lenke Besucherströme und halte Umwelt und Budget im Lot.',
|
||||
'Acht Gästewellen ziehen über die Wegkarte deiner Region: Familien, Wandergruppen, Radfahrer:innen, Bus-Reisegruppen – jede mit eigenen Wünschen, eigenem Budget und eigenem Verkehrsmittel. Deine Gebäude wirken nur auf Gäste in ihrer Reichweite: Der Standort am Weg entscheidet. Zwischen den Wellen planst du in Baupausen, Ereignisse und A/B-Entscheidungen fordern dein Urteil. Fünf Wegkarten (Talschleife bis Passstraße), vier Saisonen und ein Endbericht mit Regionsprofil zeigen, ob du zufriedene Gäste, regionale Wertschöpfung, intakte Umwelt UND kluge Besucherlenkung geschafft hast.',
|
||||
'Besucherströme und Besucherlenkung verstehen · Standortwahl an Verkehrswegen begründen · Zielgruppen und ihre Mobilität unterscheiden (zu Fuß, Rad, Bus, Auto) · Wertschöpfung entlang einer Route erfassen · Zielkonflikte Umwelt/Verkehr/Wirtschaft abwägen (GW-Lehrplan 2023: 3.5–3.8, 3.17; übergreifend Verkehrs- und Mobilitätsbildung)',
|
||||
'Gäste wandern über die Wege. Baue Häuser an den Weg und lenke die Gäste gut.',
|
||||
'In deiner Region gibt es Wege. Auf den Wegen kommen Gäste: Familien, Wanderer, Rad-Fahrer, Bus-Gruppen. Baue Gast-Häuser und Attraktionen an die Wege. Nur Gäste in der Nähe nutzen sie. Nach 8 Gäste-Wellen siehst du: War deine Region gut?',
|
||||
'Du lernst: Wo baut man am besten? Wie lenkt man viele Besucher? Welche Gäste kommen wie an – zu Fuß, mit Rad oder Bus?',
|
||||
25, 9, 15,
|
||||
0xF09F97BAEFB88F,
|
||||
'card-tourismusregion.webp',
|
||||
'modul-tourismusregion',
|
||||
'tourismusregion',
|
||||
'beta', 95
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title=VALUES(title), subtitle=VALUES(subtitle), short_desc=VALUES(short_desc),
|
||||
long_desc=VALUES(long_desc), learning_goals=VALUES(learning_goals),
|
||||
short_desc_easy=VALUES(short_desc_easy), long_desc_easy=VALUES(long_desc_easy),
|
||||
learning_goals_easy=VALUES(learning_goals_easy),
|
||||
duration_min=VALUES(duration_min), age_min=VALUES(age_min), age_max=VALUES(age_max),
|
||||
icon=VALUES(icon), card_image=VALUES(card_image), page_url=VALUES(page_url),
|
||||
play_url=VALUES(play_url), status=VALUES(status), sort_order=VALUES(sort_order);
|
||||
|
||||
-- 2) Glossar: neuer Kernbegriff Besucherlenkung
|
||||
INSERT INTO glossar (key_slug, title, short, text, short_easy, text_easy, category, sort_order, image_path) VALUES
|
||||
('besucherlenkung', 'Besucherlenkung',
|
||||
'Besucherströme gezielt steuern – durch Wege, Angebote, Beschilderung und Mobilität – damit weder Natur noch Orte überlastet werden.',
|
||||
'Besucherlenkung heißt: Eine Region steuert, WO und WIE sich Gäste bewegen – über Wegenetze, Attraktions-Standorte, Beschilderung, Parkraum und öffentliche Anreise (Bus statt Auto). Gute Lenkung verteilt Ströme, schützt sensible Naturräume und entlastet Ortskerne; schlechte Lenkung erzeugt Staus, Trampelpfade und Übertourismus-Hotspots. Werkzeuge reichen von Bushaltestellen und attraktiven Alternativrouten bis zu Obergrenzen an Ausflugszielen.',
|
||||
'Man lenkt, wohin die Gäste gehen – damit es nirgends zu voll wird.',
|
||||
'Viele Gäste an einem Ort sind ein Problem. Besucher-Lenkung heißt: Man verteilt die Gäste. Zum Beispiel mit Wegen, Schildern und Bussen. So bleibt die Natur heil und kein Ort ist zu voll.',
|
||||
'gesellschaft', 215,
|
||||
'assets/img/glossar/besucherlenkung.webp')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title=VALUES(title), short=VALUES(short), text=VALUES(text),
|
||||
short_easy=VALUES(short_easy), text_easy=VALUES(text_easy),
|
||||
category=VALUES(category), sort_order=VALUES(sort_order), image_path=VALUES(image_path);
|
||||
|
||||
-- Modul-Zuordnung: neuer Begriff + passende bestehende Begriffe
|
||||
INSERT INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'tourismusregion' FROM glossar WHERE key_slug IN (
|
||||
'besucherlenkung',
|
||||
'standortfaktor','angebot-nachfrage','wertschoepfung','infrastruktur',
|
||||
'saisonalitaet','uebertourismus','raumnutzungskonflikt','diversifizierung',
|
||||
'sanfter-tourismus','tagesausgaben','tourismus','nachhaltigkeit'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE module_id=VALUES(module_id);
|
||||
|
||||
-- 3) Lehrplan: Modul an bestehende Kompetenzen hängen
|
||||
-- (Anker hängen an den Kompetenzen und gelten damit automatisch mit.)
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'primaer' FROM kompetenzen WHERE key_slug='tourismus-raumentwicklung'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'sekundaer' FROM kompetenzen WHERE key_slug='verkehrserziehung-sicherheit'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'sekundaer' FROM kompetenzen WHERE key_slug='systemisches-denken'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
@@ -0,0 +1,57 @@
|
||||
-- ============================================================
|
||||
-- Buch-Kapitel Busfahrt <-> Glossar (2026-07-15)
|
||||
-- 5 neue Glossar-Eintraege + Modul-Mapping fuer alle im Kapitel
|
||||
-- verwendeten g()-Slugs. Idempotent (INSERT IGNORE), mehrfach
|
||||
-- ausfuehrbar, MySQL-kompatibel. Encoding: UTF-8.
|
||||
-- Anwenden: mysql --default-character-set=utf8mb4 -u root geograsim < diese Datei
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- Neue Glossar-Eintraege ----------
|
||||
|
||||
INSERT IGNORE INTO glossar (key_slug, title, short, text, category, short_easy, text_easy) VALUES
|
||||
('fernbus', 'Fernbus',
|
||||
'Linienbus für lange Strecken zwischen Städten – meist günstig und klimafreundlich.',
|
||||
'Ein Fernbus verbindet Städte über große Entfernungen im Linienverkehr – anders als der Stadtbus, der nur innerhalb eines Ortes unterwegs ist. In Deutschland wurde der Fernbusmarkt 2013 geöffnet; seither ist ein dichtes Netz entstanden, Marktführer Flix verbindet über 8.000 Ziele in mehr als 40 Ländern. Typische Fahrgäste sind junge Leute, Familien und Schulklassen, denn die Tickets sind meist deutlich billiger als Bahn oder Flugzeug. Weil sich viele Menschen ein Fahrzeug teilen, verursacht ein Fernbus nur rund 30 g Treibhausgase pro [Personenkilometer](glossar:personenkilometer) – fast so wenig wie der Fernzug und nur ein Bruchteil von Auto oder Flugzeug.',
|
||||
'verkehr',
|
||||
'Ein Bus für weite Fahrten von Stadt zu Stadt.',
|
||||
'Ein Fern-Bus fährt von einer Stadt in eine andere Stadt.\nDie Fahrt ist oft sehr weit.\nDie Fahr-Karte kostet wenig Geld.\nViele Menschen teilen sich einen Bus.\nDarum ist der Fern-Bus gut für die Umwelt.'),
|
||||
|
||||
('transitverkehr', 'Transitverkehr',
|
||||
'Verkehr, der ein Land nur durchquert – Start und Ziel liegen in anderen Ländern.',
|
||||
'Transitverkehr durchquert ein Land oder eine Region, ohne dort zu starten oder anzukommen. Österreich ist wegen seiner Lage in der Mitte Europas ein typisches Transitland: Ein Lkw von München nach Verona fährt über den [Brenner](glossar:brennerkorridor) quer durch Tirol. 2025 rollten rund 2,42 Millionen Lkw über den Brenner – fast dreimal so viele wie über alle Schweizer Alpenübergänge zusammen. Die Menschen an der Strecke tragen Lärm, Stau und Abgase, der Nutzen entsteht aber vor allem anderswo. Deshalb wird politisch gestritten, wie viel Transit eine Region verträgt – etwa mit Lkw-Fahrverboten oder dem Bau des Brennerbasistunnels für die Bahn.',
|
||||
'verkehr',
|
||||
'Verkehr, der nur durch ein Land hindurchfährt.',
|
||||
'Viele Last-Wagen fahren nur durch Österreich hindurch.\nSie kommen aus einem anderen Land.\nSie fahren in ein anderes Land.\nDas heißt Transit-Verkehr.\nDer Transit-Verkehr macht Lärm und schlechte Luft.\nDarum gibt es oft Streit darüber.'),
|
||||
|
||||
('personenkilometer', 'Personenkilometer',
|
||||
'Maßeinheit der Verkehrsleistung: eine Person legt einen Kilometer zurück.',
|
||||
'Ein Personenkilometer (Pkm) bedeutet: Eine Person reist einen Kilometer weit. Fahren 50 Personen 100 km weit mit dem Bus, sind das 50 · 100 = 5.000 Personenkilometer. Mit dieser Einheit lassen sich Verkehrsmittel fair vergleichen, denn die [Emissionen](glossar:emissionen) eines Fahrzeugs werden auf alle Mitfahrenden aufgeteilt. Ein gut besetzter Reisebus kommt so auf nur rund 30 g Treibhausgase pro Personenkilometer, ein durchschnittlich besetztes Auto auf 164 g. Entscheidend ist die Auslastung: Ein fast leerer Bus kann pro Kopf schlechter abschneiden als ein voll besetztes Auto.',
|
||||
'einheit',
|
||||
'Ein Mensch fährt einen Kilometer weit.',
|
||||
'Ein Personen-Kilometer heißt:\nEin Mensch fährt einen Kilometer weit.\n50 Menschen im Bus auf 1 Kilometer sind 50 Personen-Kilometer.\nSo kann man Bus, Auto und Flugzeug gut vergleichen.\nEin voller Bus ist gut für die Umwelt.'),
|
||||
|
||||
('zollunion', 'Zollunion',
|
||||
'Staatenverbund ohne Zölle untereinander und mit gemeinsamen Zöllen nach außen.',
|
||||
'In einer Zollunion erheben die Mitgliedsländer untereinander keine Zölle mehr und wenden an ihren Außengrenzen gemeinsame Zollsätze an. Die EU-Zollunion besteht seit 1968: Waren können zwischen den EU-Staaten ohne Warenkontrollen und Zollpapiere gehandelt werden. Sie ist nicht dasselbe wie der [Schengen-Raum](glossar:schengen), der die Personenkontrollen abschafft: Die Schweiz gehört zu Schengen, aber nicht zur EU-Zollunion – deshalb kontrolliert der [Zoll](glossar:zoll) an der Schweizer Grenze Waren, obwohl niemand den Pass zeigen muss. Das Vereinigte Königreich war umgekehrt lange Mitglied der Zollunion, aber nie Teil von Schengen.',
|
||||
'recht',
|
||||
'Länder-Gruppe ohne Zoll an den Grenzen untereinander.',
|
||||
'Die EU-Länder sind eine Zoll-Union.\nDas heißt: Waren kosten an der Grenze keinen Zoll.\nNiemand kontrolliert die Waren im Bus oder Last-Wagen.\nDie Schweiz ist nicht in der Zoll-Union.\nAn der Schweizer Grenze werden Waren kontrolliert.'),
|
||||
|
||||
('binnengrenze', 'Binnengrenze',
|
||||
'Grenze zwischen zwei Mitgliedstaaten innerhalb eines Verbunds wie dem Schengen-Raum.',
|
||||
'Eine Binnengrenze ist eine Grenze zwischen zwei Staaten, die beide demselben Verbund angehören – im Schengen-Raum etwa die Grenze zwischen Österreich und Deutschland. An den Binnengrenzen des [Schengen-Raums](glossar:schengen) gibt es keine systematischen Personenkontrollen mehr: Busse, Autos und Züge fahren einfach durch, nur ein Schild zeigt den Staatswechsel an. Das Gegenstück ist die [EU-Außengrenze](glossar:eu-aussengrenze), an der weiterhin streng kontrolliert wird. In Ausnahmefällen – etwa bei Großereignissen oder starker Migration – dürfen Staaten vorübergehend wieder Kontrollen an Binnengrenzen einführen, wie es Österreich und Deutschland mehrfach getan haben.',
|
||||
'geografie',
|
||||
'Eine Grenze zwischen zwei Ländern im Schengen-Raum.',
|
||||
'Eine Binnen-Grenze liegt zwischen zwei Schengen-Ländern.\nZum Beispiel zwischen Österreich und Deutschland.\nDort gibt es keine Pass-Kontrolle.\nDer Bus fährt einfach durch.\nNur ein Schild zeigt: Jetzt bist du in einem anderen Land.');
|
||||
|
||||
-- ---------- Modul-Mapping: alle im Buch-Kapitel verwendeten Slugs ----------
|
||||
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'busfahrt' FROM glossar WHERE key_slug IN (
|
||||
'hauptstadt', 'wahrzeichen', 'topografie', 'auftrag', 'umweg',
|
||||
'massstab', 'luftlinie', 'tarif', 'marge', 'maut', 'vignette',
|
||||
'go-box', 'lenkzeit', 'tachograph', 'pauschalreise', 'emissionen',
|
||||
'klimawandel', 'tourismus', 'kinetose', 'schengen', 'eu-aussengrenze',
|
||||
'zoll', 'brennerkorridor', 'alpenpass',
|
||||
'fernbus', 'transitverkehr', 'personenkilometer', 'zollunion', 'binnengrenze'
|
||||
);
|
||||
@@ -0,0 +1,101 @@
|
||||
-- ============================================================
|
||||
-- Buch-Kapitel Energiemanager ↔ Glossar (2026-07-15)
|
||||
-- 8 neue Glossar-Einträge + Modul-Mapping für alle Buch-Begriffe
|
||||
-- Idempotent (INSERT IGNORE), mehrfach ausführbar. UTF-8.
|
||||
-- Anwenden: mysql --default-character-set=utf8mb4 -u root geograsim < diese_datei.sql
|
||||
-- ============================================================
|
||||
|
||||
INSERT IGNORE INTO glossar (key_slug, title, short, text, category, short_easy, text_easy) VALUES
|
||||
('hertz', 'Hertz (Hz)',
|
||||
'Einheit für Frequenz: Wie oft sich etwas pro Sekunde wiederholt – 1 Hz = einmal pro Sekunde.',
|
||||
'Hertz (Hz) ist die Einheit für die Frequenz – also dafür, wie oft sich ein Vorgang pro Sekunde wiederholt. 1 Hertz bedeutet: einmal pro Sekunde. Benannt ist die Einheit nach dem deutschen Physiker Heinrich Hertz (1857–1894). Im europäischen Stromnetz wechselt der Wechselstrom 50-mal pro Sekunde die Richtung – die [Netzfrequenz](glossar:netzfrequenz) beträgt 50 Hz. Auch Töne werden in Hertz gemessen: Der Kammerton a hat 440 Hz, Menschen hören ungefähr von 20 Hz bis 20.000 Hz.',
|
||||
'einheit',
|
||||
'Hertz sagt: Wie oft passiert etwas in einer Sekunde?',
|
||||
'Hertz ist eine Einheit. Die Abkürzung ist Hz.\n1 Hertz heißt: einmal in der Sekunde.\nUnser Strom wechselt 50-mal in der Sekunde die Richtung.\nMan sagt: Das Strom-Netz hat 50 Hertz.\nAuch Töne misst man in Hertz.'),
|
||||
|
||||
('gigawattstunde', 'Gigawattstunde (GWh)',
|
||||
'Eine Million Kilowattstunden – Einheit für sehr große Strommengen, etwa den Tagesverbrauch ganzer Städte.',
|
||||
'1 Gigawattstunde (GWh) entspricht 1.000 Megawattstunden oder 1.000.000 [Kilowattstunden](glossar:kilowattstunde). In dieser Größenordnung rechnet man bei großen Kraftwerken, Speicherseen und dem Verbrauch ganzer Städte. Ein durchschnittlicher Haushalt in Österreich verbraucht rund 3.500 kWh pro Jahr – mit 1 GWh kommen also etwa 285 Haushalte ein ganzes Jahr aus. Der Speichersee eines großen [Pumpspeicherkraftwerks](glossar:pumpspeicher) kann viele GWh Energie aufnehmen. Noch größer ist die Terawattstunde (TWh): 1 TWh = 1.000 GWh – Österreich verbraucht rund 65 TWh Strom pro Jahr.',
|
||||
'einheit',
|
||||
'1 Gigawatt-Stunde ist sehr viel Strom: 1 Million Kilowatt-Stunden.',
|
||||
'Die Abkürzung ist GWh.\n1 GWh sind 1 Million Kilowatt-Stunden.\nDas ist sehr viel Strom.\nUngefähr 285 Familien brauchen so viel Strom in einem ganzen Jahr.\nMit GWh misst man große Kraft-Werke und große Speicher.'),
|
||||
|
||||
('verbundnetz', 'Verbundnetz',
|
||||
'Zusammenschluss vieler nationaler Stromnetze zu einem großen Netz – ganz Kontinentaleuropa schwingt gemeinsam im 50-Hz-Takt.',
|
||||
'Ein Verbundnetz entsteht, wenn viele Länder ihre [Stromnetze](glossar:stromnetz) zusammenschalten. Das kontinentaleuropäische Verbundnetz reicht von Portugal bis Polen und von Dänemark bis in die Türkei – alle angeschlossenen Kraftwerke und Verbraucher arbeiten synchron mit derselben [Netzfrequenz](glossar:netzfrequenz) von 50 Hz. Der große Vorteil: Die Länder helfen einander aus. Fällt irgendwo ein Kraftwerk aus oder liefert das Wetter zu wenig, gleichen die Nachbarländer die Lücke sofort mit aus. Wie empfindlich das System ist, zeigte der 8. Jänner 2021: Eine Störung in Kroatien trennte das europäische Netz in zwei Teile, die Frequenz fiel auf 49,74 Hz – nur automatische Schutzmaßnahmen verhinderten einen großflächigen [Blackout](glossar:blackout).',
|
||||
'energie',
|
||||
'Viele Länder teilen sich ein großes Strom-Netz.',
|
||||
'Viele Länder in Europa haben ihre Strom-Netze verbunden.\nSo entsteht ein riesiges Netz.\nAlle Kraft-Werke arbeiten im gleichen Takt.\nDie Länder helfen sich gegenseitig.\nHat ein Land zu wenig Strom, schicken die Nachbarn Strom.'),
|
||||
|
||||
('stromboerse', 'Strombörse',
|
||||
'Marktplatz, auf dem Strom für jede Stunde des nächsten Tages gehandelt wird – der Preis entsteht aus Angebot und Nachfrage.',
|
||||
'An der Strombörse kaufen und verkaufen Kraftwerksbetreiber, Stromhändler und große Verbraucher elektrische Energie – meist für jede einzelne Stunde des nächsten Tages. Der Preis entsteht wie auf jedem Markt aus Angebot und Nachfrage: Liefern Sonne und Wind viel, ist Strom billig – an manchen Mittagen sogar gratis oder mit negativem Preis. Wird Strom knapp, etwa in einer [Dunkelflaute](glossar:dunkelflaute), schießen die Preise in die Höhe: im November 2024 zeitweise von rund 79 auf über 800 Euro pro Megawattstunde. Die wichtigste Strombörse für Mitteleuropa ist die EPEX SPOT in Paris. [Pumpspeicherkraftwerke](glossar:pumpspeicher) verdienen an den Preisunterschieden: Sie kaufen billigen Überschussstrom zum Pumpen und verkaufen teuren Strom zur Abendspitze.',
|
||||
'wirtschaft',
|
||||
'An der Strom-Börse wird Strom gekauft und verkauft.',
|
||||
'Strom hat einen Preis.\nDer Preis entsteht an der Strom-Börse.\nDas ist ein Markt-Platz für Strom.\nGibt es viel Strom, ist der Strom billig.\nGibt es wenig Strom, ist der Strom teuer.'),
|
||||
|
||||
('kernenergie', 'Kernenergie',
|
||||
'Stromerzeugung durch Kernspaltung von Uran – liefert verlässlich viel Strom ohne CO₂, hinterlässt aber radioaktiven Abfall.',
|
||||
'In einem Kernkraftwerk wird der Kern von Uran-Atomen gespalten. Dabei entsteht enorme Hitze, die Wasserdampf erzeugt und über [Turbinen](glossar:turbine) Strom produziert. Kernkraftwerke liefern gleichmäßig sehr viel Strom und stoßen dabei kein CO₂ aus – deshalb tragen sie in vielen Ländern die [Grundlast](glossar:grundlast). Die Kehrseiten: radioaktiver Abfall, der hunderttausende Jahre sicher gelagert werden muss, und das Risiko schwerer Unfälle wie in Tschernobyl (1986) und Fukushima (2011). Die Schweiz erzeugt rund 28 % ihres Stroms aus Kernkraft, Frankreich sogar etwa 65 %. Österreich hat sich 1978 per Volksabstimmung dagegen entschieden – das fertig gebaute Kraftwerk Zwentendorf ging nie in Betrieb.',
|
||||
'energie',
|
||||
'Kern-Kraft-Werke machen Strom aus Uran.',
|
||||
'Ein Kern-Kraft-Werk spaltet winzige Teilchen: Atome.\nDabei entsteht sehr viel Hitze.\nMit der Hitze macht das Kraft-Werk Strom.\nDer Müll davon strahlt aber sehr lange und ist gefährlich.\nÖsterreich hat kein Kern-Kraft-Werk.\nDie Schweiz hat mehrere.'),
|
||||
|
||||
('laufwasserkraftwerk', 'Laufwasserkraftwerk',
|
||||
'Wasserkraftwerk an einem Fluss, das die ständige Strömung nutzt – läuft Tag und Nacht und trägt die Grundlast.',
|
||||
'Ein Laufwasserkraftwerk liegt direkt an einem Fluss und nutzt dessen stetige Strömung: Das Wasser fließt ununterbrochen durch die [Turbinen](glossar:turbine) und erzeugt rund um die Uhr Strom. Anders als ein [Pumpspeicherkraftwerk](glossar:pumpspeicher) kann es keine Energie speichern – es produziert so viel, wie der Fluss gerade Wasser führt. Dafür liefert es sehr verlässlich und günstig, weshalb Laufwasserkraftwerke einen großen Teil der [Grundlast](glossar:grundlast) tragen. In Österreich stehen große Laufwasserkraftwerke an Donau, Inn, Enns und Ill – das größte ist das Donaukraftwerk Altenwörth in Niederösterreich.',
|
||||
'energie',
|
||||
'Ein Kraft-Werk am Fluss. Es macht Tag und Nacht Strom.',
|
||||
'Ein Lauf-Wasser-Kraft-Werk steht an einem Fluss.\nDas Fluss-Wasser fließt durch das Kraft-Werk.\nDabei dreht es eine Turbine.\nDie Turbine macht Strom.\nDas geht Tag und Nacht.\nDenn der Fluss fließt immer.'),
|
||||
|
||||
('netzbetreiber', 'Netzbetreiber',
|
||||
'Unternehmen, das Stromleitungen betreibt und das Gleichgewicht im Netz überwacht – in Österreich z. B. die Austrian Power Grid (APG).',
|
||||
'Netzbetreiber sind die Unternehmen hinter den Stromleitungen: Sie bauen und warten die Netze und sorgen dafür, dass Erzeugung und Verbrauch in jeder Sekunde übereinstimmen. Übertragungsnetzbetreiber wie die Austrian Power Grid (APG) in Österreich, Swissgrid in der Schweiz oder die vier großen Betreiber in Deutschland steuern die Hochspannungs-„Autobahnen“ des Stroms; Verteilnetzbetreiber bringen den Strom bis zur Steckdose. In Leitwarten beobachten Fachleute rund um die Uhr die [Netzfrequenz](glossar:netzfrequenz), erstellen Prognosen für Wetter und Verbrauch und rufen bei Bedarf [Regelreserve](glossar:regelreserve) ab. Droht ein Ungleichgewicht, greifen sie ein – notfalls bis zum kontrollierten [Lastabwurf](glossar:lastabwurf).',
|
||||
'energie',
|
||||
'Netz-Betreiber sind Firmen. Sie kümmern sich um die Strom-Leitungen.',
|
||||
'Der Strom fließt durch viele Leitungen.\nUm die Leitungen kümmern sich eigene Firmen.\nSie heißen Netz-Betreiber.\nSie passen auf: Es muss immer genau genug Strom da sein.\nFachleute schauen dafür Tag und Nacht auf das Netz.'),
|
||||
|
||||
('lageenergie', 'Lageenergie',
|
||||
'Energie, die ein Körper durch seine Höhe besitzt – auch Höhenenergie oder potenzielle Energie genannt.',
|
||||
'Wer etwas nach oben hebt, steckt Arbeit hinein – diese Energie ist danach als Lageenergie (auch Höhenenergie oder potenzielle Energie) gespeichert. Je schwerer der Körper und je größer die Höhe, desto mehr Lageenergie: Sie ergibt sich aus Masse · Fallhöhe · Erdbeschleunigung. Beim Herabfallen oder Herabfließen wandelt sie sich in Bewegungsenergie um. Genau das nutzt ein [Pumpspeicherkraftwerk](glossar:pumpspeicher): Mit überschüssigem Strom wird Wasser in einen hoch gelegenen Bergsee gepumpt – dort „wartet“ die Energie als Lageenergie, bis das Wasser wieder ins Tal rauscht und über [Turbinen](glossar:turbine) neuen Strom erzeugt.',
|
||||
'physik',
|
||||
'Lage-Energie ist die Energie von Dingen, die hoch oben sind.',
|
||||
'Hebe einen Stein hoch.\nDas kostet dich Kraft.\nDie Kraft steckt jetzt im Stein.\nMan sagt: Der Stein hat Lage-Energie.\nLässt du den Stein los, fällt er.\nGenauso speichert ein Berg-See Energie: Das Wasser oben kann beim Hinunter-Fließen Strom machen.');
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Modul-Mapping: neue Einträge
|
||||
-- ------------------------------------------------------------
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='hertz';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='gigawattstunde';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='verbundnetz';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='stromboerse';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='kernenergie';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='laufwasserkraftwerk';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='netzbetreiber';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='lageenergie';
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Modul-Mapping: alle bereits im Kapitel verlinkten Bestandsbegriffe
|
||||
-- (idempotent — stellt sicher, dass die Glossar-Modulseite alle Buch-Begriffe zeigt)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='stromnetz';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='netzfrequenz';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='lastprofil';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='energiemix';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='wasserkraft';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='erneuerbar';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='windenergie';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='photovoltaik';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='turbine';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='lastausgleich';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='residuallast';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='dunkelflaute';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='stromspeicher';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='pumpspeicher';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='regelreserve';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='wirkungsgrad';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='megawatt';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='grundlast';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='spitzenlast';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='lastabwurf';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'energiemanager' FROM glossar WHERE key_slug='blackout';
|
||||
@@ -0,0 +1,64 @@
|
||||
-- ============================================================
|
||||
-- Schulbuch-Kapitel EU-Werkstatt ↔ Glossar
|
||||
-- 5 neue Glossar-Einträge + Modul-Mapping (alle Buch-Begriffe)
|
||||
-- Idempotent (INSERT IGNORE), MySQL-kompatibel, keine harten IDs.
|
||||
-- Datum: 2026-07-15
|
||||
-- ============================================================
|
||||
|
||||
INSERT IGNORE INTO glossar (key_slug, title, short, text, category, short_easy, text_easy) VALUES
|
||||
('trilog', 'Trilog',
|
||||
'Informelle Verhandlungsrunde von EU-Parlament, Ministerrat und Kommission, um bei einem Gesetz schneller einen Kompromiss zu finden.',
|
||||
'Können sich EU-Parlament und Ministerrat bei einem Gesetzesvorschlag nicht einigen, treffen sich Verhandler:innen beider Seiten mit der Kommission zu einer vertraulichen Kompromissrunde — dem Trilog („Dreiergespräch“).\n\nDer Trilog steht in keinem EU-Vertrag. Er hat sich als Praxis entwickelt, weil er das Gesetzgebungsverfahren stark beschleunigt: Die große Mehrheit der EU-Gesetze wird heute schon nach der ersten Lesung mit einem Trilog-Kompromiss beschlossen.\n\nKritiker:innen bemängeln, dass die Runden hinter verschlossenen Türen stattfinden und schwer nachvollziehbar sind. Das Ergebnis muss aber immer noch offiziell von Parlament und Ministerrat bestätigt werden — ohne deren Zustimmung gilt kein Trilog-Kompromiss.',
|
||||
'gesellschaft',
|
||||
'Ein Treffen von Parlament, Minister-Rat und Kommission. Dort suchen sie einen Kompromiss für ein Gesetz.',
|
||||
'Manchmal sind sich Parlament und Minister-Rat nicht einig.\nDann treffen sich Vertreter von beiden mit der Kommission.\nDieses Treffen heißt Trilog.\nTrilog bedeutet: Dreier-Gespräch.\nDort suchen sie gemeinsam einen Kompromiss.\nAm Ende müssen Parlament und Minister-Rat trotzdem noch Ja sagen.'),
|
||||
|
||||
('binnenmarkt', 'Binnenmarkt',
|
||||
'Der gemeinsame Markt der EU: Waren, Personen, Dienstleistungen und Kapital bewegen sich frei über die Binnengrenzen.',
|
||||
'Der Binnenmarkt ist das wirtschaftliche Herzstück der EU. Seit 1993 gelten darin die vier Grundfreiheiten: Waren, Personen, Dienstleistungen und Kapital dürfen sich frei zwischen den Mitgliedsländern bewegen — ohne Zölle und ohne Warenkontrollen an den Binnengrenzen.\n\nEin Tischlereibetrieb aus Vorarlberg kann so ohne Zollpapiere nach Deutschland liefern, eine Wienerin ohne Arbeitserlaubnis in den Niederlanden arbeiten.\n\nAuch Nicht-EU-Länder können teilnehmen: Norwegen, Island und Liechtenstein sind über den EWR dabei, die Schweiz teilweise über bilaterale Verträge.\n\nDamit der Binnenmarkt funktioniert, braucht es gemeinsame Regeln — etwa für Produktsicherheit oder Ladekabel. Deshalb entsteht ein großer Teil der EU-Gesetze genau für ihn.',
|
||||
'wirtschaft',
|
||||
'Der gemeinsame Markt der EU-Länder. Waren und Menschen können sich frei bewegen.',
|
||||
'Die EU-Länder haben einen gemeinsamen Markt.\nEr heißt Binnen-Markt.\nWaren kann man frei in ein anderes EU-Land verkaufen.\nEs gibt keinen Zoll zwischen den EU-Ländern.\nMenschen dürfen in jedem EU-Land arbeiten und wohnen.\nDafür braucht es gemeinsame Regeln.'),
|
||||
|
||||
('transparenzregister', 'Transparenzregister',
|
||||
'Öffentliche EU-Liste, in die sich Lobby-Organisationen eintragen müssen, bevor sie Einfluss auf die EU-Politik nehmen.',
|
||||
'Das Transparenzregister ist eine öffentliche Datenbank der EU. Wer als Firma, Verband oder Organisation Einfluss auf EU-Gesetze nehmen will, muss sich dort eintragen — mit Namen, Themen, Auftraggebern und ungefährem Lobby-Budget.\n\nÜber 12.000 Interessenvertretungen sind registriert: Konzerne wie Apple und Bayer genauso wie Umweltorganisationen oder Verbraucherschützer:innen.\n\nOhne Eintrag bekommen Lobbyist:innen keine offiziellen Treffen mit Kommissar:innen und keinen dauerhaften Zugang zum EU-Parlament.\n\nJede:r kann online nachschauen, wer in Brüssel zu welchem Thema Einfluss nimmt. So bleibt Lobbyismus erlaubt — aber sichtbar.',
|
||||
'gesellschaft',
|
||||
'Eine öffentliche Liste der EU. Dort müssen alle Lobby-Gruppen eingetragen sein.',
|
||||
'Viele Firmen und Vereine wollen EU-Gesetze beeinflussen.\nDas nennt man Lobby-Arbeit.\nDafür gibt es eine Regel:\nSie müssen in einer öffentlichen Liste stehen.\nDie Liste heißt Transparenz-Register.\nJeder Mensch kann die Liste im Internet ansehen.'),
|
||||
|
||||
('ewr', 'Europäischer Wirtschaftsraum (EWR)',
|
||||
'Abkommen, das den EU-Binnenmarkt auf Norwegen, Island und Liechtenstein ausweitet — ohne EU-Mitgliedschaft.',
|
||||
'Der Europäische Wirtschaftsraum (EWR) verbindet seit 1994 die EU-Länder mit Norwegen, Island und Liechtenstein. Die drei Länder machen beim Binnenmarkt mit: Sie übernehmen die meisten Binnenmarkt-Regeln, dürfen bei EU-Gesetzen aber nicht mitentscheiden.\n\nDie Schweiz stimmte 1992 knapp gegen den EWR-Beitritt (50,3 % Nein) und regelt ihr Verhältnis zur EU seither über mehr als 100 bilaterale Verträge.\n\nFür Vorarlberg besonders greifbar: Das Nachbarland Liechtenstein ist seit 1995 EWR-Mitglied — Waren und Arbeitskräfte bewegen sich dort fast so frei wie innerhalb der EU.',
|
||||
'wirtschaft',
|
||||
'Ein Vertrag: Norwegen, Island und Liechtenstein machen beim EU-Markt mit. Sie sind aber nicht in der EU.',
|
||||
'EWR heißt: Europäischer Wirtschafts-Raum.\nDas ist ein Vertrag mit der EU.\nNorwegen, Island und Liechtenstein machen mit.\nSie dürfen am gemeinsamen Markt teilnehmen.\nAber sie sind nicht in der EU.\nDie Schweiz hat 1992 Nein zum EWR gesagt.'),
|
||||
|
||||
('subsidiaritaet', 'Subsidiarität',
|
||||
'Grundsatz der EU: Entscheidungen sollen auf der niedrigsten Ebene fallen, die das Problem gut lösen kann.',
|
||||
'Subsidiarität ist ein Grundprinzip der EU (Artikel 5 EU-Vertrag): Die Union soll nur dann handeln, wenn ein Ziel auf EU-Ebene besser erreicht werden kann als von den einzelnen Ländern, Regionen oder Gemeinden.\n\nGrenzüberschreitende Themen wie Roaming-Gebühren oder einheitliche Ladekabel kann kein Land allein regeln — hier ist die EU-Ebene sinnvoll. Den Stundenplan einer Schule in Bregenz kann dagegen das Land Vorarlberg viel besser festlegen als Brüssel.\n\nDie nationalen Parlamente wachen darüber mit: Meinen genug von ihnen, ein EU-Vorschlag verletze die Subsidiarität, muss die Kommission ihn noch einmal überprüfen.',
|
||||
'gesellschaft',
|
||||
'Eine Regel der EU: Jede Entscheidung soll dort fallen, wo man das Problem am besten lösen kann.',
|
||||
'Die EU hat eine wichtige Regel.\nSie heißt Subsidiarität.\nDie Regel sagt: Entscheide so nah bei den Menschen wie möglich.\nDie Gemeinde regelt, was die Gemeinde gut kann.\nDas Land regelt, was das Land gut kann.\nDie EU regelt nur, was alle Länder gemeinsam besser können.');
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Modul-Mapping: ALLE Glossar-Begriffe des Buch-Kapitels
|
||||
-- (neue + bestehende g()-Slugs; INSERT IGNORE = idempotent)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='trilog';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='binnenmarkt';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='transparenzregister';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='ewr';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='subsidiaritaet';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='eu-kommission';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='eu-parlament';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='rat-der-eu';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='europaeischer-rat';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='europaeische-buergerinitiative';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='eu-petition';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='initiativrecht';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='mitentscheidung';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='eu-gesetz';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='eu-zustaendigkeit';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='lobbyismus';
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id) SELECT id, 'eu-werkstatt' FROM glossar WHERE key_slug='freiwillige-vereinbarung';
|
||||
@@ -0,0 +1,70 @@
|
||||
-- ============================================================
|
||||
-- Buch-Glossar: Kapitel "Landwirtschaft weltweit" (farmer)
|
||||
-- Neue Fachbegriffe + Modul-Mapping für alle Kapitel-Begriffe
|
||||
-- Idempotent (INSERT IGNORE), mehrfach ausführbar, MySQL-kompatibel.
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- 1) Neue Glossar-Einträge ----------
|
||||
INSERT IGNORE INTO glossar (key_slug, title, short, text, category, short_easy, text_easy) VALUES
|
||||
('anbaugrenze', 'Anbaugrenze',
|
||||
'Die klimatische Grenze, jenseits derer eine Nutzpflanze nicht mehr wächst.',
|
||||
'Jede Nutzpflanze hat Ansprüche an Wärme, Wasser und Wachstumszeit. Die Anbaugrenze ist der Punkt, an dem eine dieser Bedingungen nicht mehr erfüllt ist — dahinter misslingt die Ernte, egal wie sorgfältig gearbeitet wird. Wichtige Grenzen sind Frost (die Banane stirbt in der ersten Frostnacht), eine zu kurze [Vegetationsperiode](glossar:vegetationsperiode) im Norden und die Höhengrenze am Berg. Anbaugrenzen sind der Grund, warum Kaffee, Bananen und Datteln über den [Welthandel](glossar:welthandel) zu uns kommen. Der Klimawandel verschiebt viele dieser Grenzen gerade polwärts und bergauf.',
|
||||
'geografie',
|
||||
'Die Grenze, ab der eine Pflanze nicht mehr wachsen kann.',
|
||||
'Jede Pflanze braucht das richtige Wetter.\nIst es zu kalt oder zu trocken, wächst sie nicht.\nDiese Grenze heißt Anbau-Grenze.\nDie Banane wächst nicht im kalten Norden.\nDarum kommt sie aus einem warmen Land.'),
|
||||
|
||||
('vegetationsperiode', 'Vegetationsperiode',
|
||||
'Die Zeit im Jahr, in der es warm genug für Pflanzenwachstum ist.',
|
||||
'Die Vegetationsperiode ist die Anzahl der Tage pro Jahr, an denen die Temperatur zum Wachsen ausreicht — meist gerechnet ab einer Tagesmitteltemperatur von etwa 5 °C. In den Tropen dauert sie das ganze Jahr, in Mitteleuropa rund 200 Tage, im hohen Norden nur wenige Wochen. Ist die Vegetationsperiode zu kurz, reifen Pflanzen wie Mais oder Reis nicht mehr aus, bevor der Frost kommt — deshalb bildet sie eine wichtige [Anbaugrenze](glossar:anbaugrenze). Der Klimawandel verlängert die Vegetationsperiode in vielen Regionen.',
|
||||
'biologie',
|
||||
'Die Zeit im Jahr, in der Pflanzen wachsen können.',
|
||||
'Pflanzen wachsen nur, wenn es warm genug ist.\nDiese warme Zeit im Jahr heißt Vegetations-Periode.\nBei uns dauert sie etwa ein halbes Jahr.\nIm Norden ist der Sommer sehr kurz.\nDort reift das Getreide oft nicht mehr.'),
|
||||
|
||||
('waldgrenze', 'Waldgrenze',
|
||||
'Die Höhe am Berg, über der kein zusammenhängender Wald mehr wächst.',
|
||||
'Am Berg wird es je 100 Höhenmeter etwa 0,6 °C kälter. Ab einer bestimmten Höhe ist der Sommer zu kurz und zu kalt, damit Bäume überleben — das ist die Waldgrenze. In den Alpen liegt sie je nach Lage bei rund 1.800 bis 2.200 Metern. Darüber folgen nur noch Zwergsträucher, Almweiden und schließlich Fels und Eis (siehe [Höhenstufe](glossar:hoehenstufe)). Über der Waldgrenze endet auch der Ackerbau; genutzt wird das Land dann als Almweide.',
|
||||
'geografie',
|
||||
'Die Höhe am Berg, ab der kein Wald mehr wächst.',
|
||||
'Je höher der Berg, desto kälter wird es.\nGanz oben ist es zu kalt für Bäume.\nAb dieser Höhe wächst kein Wald mehr.\nDas nennt man Wald-Grenze.\nIn den Alpen ist das etwa bei 2000 Metern.'),
|
||||
|
||||
('almwirtschaft', 'Alm- und Alpwirtschaft',
|
||||
'Sommerliche Weidewirtschaft auf hochgelegenen Bergwiesen über der Waldgrenze.',
|
||||
'Bei der Alm- (österreichisch/bayerisch) bzw. Alpwirtschaft (schweizerisch) treibt man das Vieh im Sommer auf die hochgelegenen Bergweiden über der [Waldgrenze](glossar:waldgrenze). Das nutzt Futter, das sonst niemand ernten könnte, schont die Talwiesen für das Winterheu und hält die Kulturlandschaft offen — ohne Beweidung würde vieles verbuschen. Aus der Almmilch entsteht hochwertiger Bergkäse. Alm- und Alpwirtschaft prägen Österreich, die Schweiz und Süddeutschland; allein Österreich zählt rund 8.400 Almen.',
|
||||
'geografie',
|
||||
'Kühe verbringen den Sommer oben auf der Berg-Weide.',
|
||||
'Im Sommer gehen die Kühe hoch auf den Berg.\nDie Wiese dort oben heißt Alm oder Alp.\nDas Gras dort ist frisch und gut.\nAus der Milch macht man Berg-Käse.\nDas gibt es in Österreich und in der Schweiz.'),
|
||||
|
||||
('bodenversiegelung', 'Bodenversiegelung',
|
||||
'Das dauerhafte Zubauen von Boden mit Beton, Asphalt oder Gebäuden.',
|
||||
'Bodenversiegelung bedeutet, dass fruchtbarer Boden mit Straßen, Hallen, Häusern oder Parkplätzen bedeckt wird. Versiegelter Boden kann kein Wasser mehr aufnehmen, keine Pflanzen mehr tragen und ist praktisch für immer verloren — denn Ackerboden wächst nur extrem langsam nach. Im dicht besiedelten Österreich wurden 2024 täglich rund 6,75 Hektar Boden neu verbraucht. Weil guter Boden nicht vermehrbar ist, ist der sparsame Umgang damit ein zentrales Ziel der [Nachhaltigkeit](glossar:nachhaltigkeit).',
|
||||
'umwelt',
|
||||
'Guter Boden wird mit Beton zugebaut und ist dann verloren.',
|
||||
'Manchmal baut man auf ein Feld eine Straße oder ein Haus.\nDann liegt Beton auf dem Boden.\nDurch den Beton kann nichts mehr wachsen.\nDer gute Boden ist für immer weg.\nDarum soll man Felder nicht zubauen.'),
|
||||
|
||||
('anbauguertel', 'Anbaugürtel',
|
||||
'Ein breites Band um die Erde, in dem dieselbe Nutzpflanze vorherrscht.',
|
||||
'Weil jede Pflanze ihre passende [Klimazone](glossar:klimazone) braucht, ordnet sich die Weltlandwirtschaft in Anbaugürtel — Bänder rund um die Erde, in denen eine Kultur dominiert. Der Weizengürtel liegt in den gemäßigten Zonen (US-Prärie, Ukraine, Mitteleuropa), der Reisgürtel im warm-feuchten Asien, der Kaffeegürtel im tropischen Hochland. Weil diese Gürtel klimatisch festgelegt sind, muss vieles über den [Welthandel](glossar:welthandel) zwischen den Kontinenten transportiert werden. Der Klimawandel verschiebt die Anbaugürtel langsam Richtung Pole.',
|
||||
'geografie',
|
||||
'Ein großes Gebiet auf der Erde, wo überall die gleiche Pflanze wächst.',
|
||||
'Manche Pflanzen wachsen in großen Streifen um die Erde.\nDas nennt man Anbau-Gürtel.\nWeizen wächst in einem milden Gürtel wie bei uns.\nReis wächst im warmen und nassen Asien.\nKaffee wächst nur in den warmen Tropen.'),
|
||||
|
||||
('welternaehrung', 'Welternährung',
|
||||
'Die Frage, wie alle Menschen der Erde genug zu essen bekommen.',
|
||||
'Welternährung meint die Aufgabe, die gesamte Weltbevölkerung — rund 8 Milliarden Menschen — ausreichend und gesund mit Nahrung zu versorgen. Grundnahrungsmittel wie Weizen und Reis spielen dabei die Hauptrolle. Obwohl weltweit genug Nahrung erzeugt wird, hungerten 2024 etwa 673 Millionen Menschen — vor allem wegen Armut, Kriegen und Missernten. Der Klimawandel bedroht die Welternährung zusätzlich, weil Dürren und Wetterextreme Ernten vernichten und [Anbaugrenzen](glossar:anbaugrenze) verschieben.',
|
||||
'gesellschaft',
|
||||
'Dass alle Menschen auf der Welt genug zu essen haben.',
|
||||
'Auf der Erde leben sehr viele Menschen.\nAlle brauchen jeden Tag Essen.\nWelt-Ernährung heißt: Bekommen alle genug?\nLeider haben noch immer viele Menschen Hunger.\nDer Klima-Wandel macht das Problem größer.');
|
||||
|
||||
-- ---------- 2) Modul-Mapping für ALLE Kapitel-Begriffe ----------
|
||||
-- ids IMMER per Subquery, nie hardcoden. INSERT IGNORE = idempotent.
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'farmer' FROM glossar WHERE key_slug IN (
|
||||
-- neue Begriffe
|
||||
'anbaugrenze','vegetationsperiode','waldgrenze','almwirtschaft',
|
||||
'bodenversiegelung','anbauguertel','welternaehrung',
|
||||
-- bereits per g() im Kapitel verlinkte Bestandsbegriffe
|
||||
'klimazone','eignungsklasse','koeppen-geiger','klimawandel','subtropisch-cfa',
|
||||
'hoehenstufe','welthandel','reefer-container','verderblich','aquakultur',
|
||||
'plantage','importpreis-abschlag','welt-hafen','ueberweidung','bodenmuedigkeit',
|
||||
'monokultur','krankheits-ausbruch','treibhausgas','co2-fussabdruck','nachhaltigkeit'
|
||||
);
|
||||