1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
118 lines
4.7 KiB
HTML
118 lines
4.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>GeoGraSim V2 — Plattform-Konfiguration</title>
|
||
<link rel="stylesheet" href="./assets/css/design-system-v2.css">
|
||
<style>
|
||
body { background: var(--ggs-bg); min-height: 100dvh; padding: var(--ggs-gap-lg); }
|
||
.wrap { max-width: 900px; margin: 0 auto; }
|
||
h1 { color: var(--ggs-fjord-dark); margin-bottom: var(--ggs-gap-md); }
|
||
.group { background: var(--ggs-white); border: 1px solid var(--ggs-border); border-radius: var(--ggs-radius-md); padding: var(--ggs-gap-lg); margin-bottom: var(--ggs-gap-md); }
|
||
.group h2 { font-size: 0.85rem; text-transform: uppercase; color: var(--ggs-text-muted); margin-bottom: var(--ggs-gap-md); }
|
||
.row { display: grid; grid-template-columns: 1fr 180px 90px; gap: var(--ggs-gap-md); align-items: center; padding: var(--ggs-gap-sm) 0; border-bottom: 1px solid var(--ggs-border); }
|
||
.row:last-child { border-bottom: none; }
|
||
.row label { font-weight: 600; font-size: 0.9rem; color: var(--ggs-text); }
|
||
.row .help { font-size: 0.72rem; color: var(--ggs-text-muted); margin-top: 2px; }
|
||
.row .range { font-size: 0.7rem; color: var(--ggs-text-light); margin-top: 2px; }
|
||
.row input { width: 100%; }
|
||
.status { font-size: 0.75rem; color: var(--ggs-text-muted); padding: 4px 8px; border-radius: var(--ggs-radius-sm); }
|
||
.status.saved { background: var(--ggs-moss-light); color: var(--ggs-moss-dark); }
|
||
.status.error { background: var(--ggs-coral-light); color: var(--ggs-coral); }
|
||
.topbar { display: flex; align-items: center; gap: var(--ggs-gap-md); margin-bottom: var(--ggs-gap-lg); }
|
||
.topbar .spacer { flex: 1; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="wrap">
|
||
<div class="topbar">
|
||
<h1>⚙ Plattform-Konfiguration</h1>
|
||
<div class="spacer"></div>
|
||
<a href="./teacher.html" class="ggs-btn ggs-btn-secondary">Zurück zum Cockpit</a>
|
||
<button class="ggs-btn ggs-btn-secondary" id="logoutBtn">Abmelden</button>
|
||
</div>
|
||
|
||
<div id="content">
|
||
<p style="color:var(--ggs-text-muted)">Wird geladen …</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
import { auth } from './assets/js/auth-client.js';
|
||
|
||
if (!auth.token || auth.role !== 'admin') {
|
||
alert('Nur Admins dürfen die Plattform-Konfiguration ändern.');
|
||
location.replace('./login.html');
|
||
}
|
||
|
||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||
await auth.logout(); location.replace('./login.html');
|
||
});
|
||
|
||
const groupLabels = {
|
||
telemetry: '📡 Telemetrie',
|
||
auth: '🔐 Authentifizierung',
|
||
general: '⚙ Allgemein',
|
||
};
|
||
|
||
async function load() {
|
||
const r = await auth.api('GET', 'admin/platform-config.php');
|
||
const html = Object.entries(r.groups).map(([gname, entries]) => `
|
||
<div class="group">
|
||
<h2>${escapeHtml(groupLabels[gname] || gname)}</h2>
|
||
${entries.map(e => `
|
||
<div class="row" data-key="${escapeAttr(e.key)}">
|
||
<div>
|
||
<label for="i-${escapeAttr(e.key)}">${escapeHtml(e.label)}</label>
|
||
<div class="help">${escapeHtml(e.help || '')}</div>
|
||
${e.min !== null && e.max !== null ? `<div class="range">Bereich: ${e.min} – ${e.max}</div>` : ''}
|
||
</div>
|
||
<input class="ggs-input" id="i-${escapeAttr(e.key)}" type="${e.type === 'int' ? 'number' : 'text'}"
|
||
value="${escapeAttr(e.value)}"
|
||
${e.min !== null ? `min="${e.min}"` : ''}
|
||
${e.max !== null ? `max="${e.max}"` : ''}>
|
||
<span class="status"></span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`).join('');
|
||
document.getElementById('content').innerHTML = html;
|
||
|
||
document.querySelectorAll('.row input').forEach(input => {
|
||
let timer = null;
|
||
input.addEventListener('input', () => {
|
||
clearTimeout(timer);
|
||
const status = input.closest('.row').querySelector('.status');
|
||
status.textContent = '…';
|
||
status.className = 'status';
|
||
timer = setTimeout(() => save(input, status), 800);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function save(input, status) {
|
||
const key = input.closest('.row').dataset.key;
|
||
try {
|
||
await auth.api('PATCH', 'admin/platform-config.php', { key, value: input.value });
|
||
status.textContent = '✓ gespeichert';
|
||
status.className = 'status saved';
|
||
setTimeout(() => { status.textContent = ''; status.className = 'status'; }, 2000);
|
||
} catch (e) {
|
||
status.textContent = '✗ ' + e.message;
|
||
status.className = 'status error';
|
||
}
|
||
}
|
||
|
||
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||
function escapeAttr(s) { return escapeHtml(s); }
|
||
|
||
load().catch(err => {
|
||
document.getElementById('content').innerHTML = `<p style="color:var(--ggs-coral)">Fehler: ${err.message}</p>`;
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|