Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit

- Design-System (assets/css/design-system.css) mit 21 Komponenten,
  iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player,
  Glossar-Tooltips
- Templates (sims/template.html + student/teacher-Dashboard)
- Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache,
  4c iPad), content-architecture.md, crash-recovery.md, music-registry.md
- Admin-Infrastruktur: admin-modules.html + api/admin-modules.php
  (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul)
- Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings
  an Klima, Glossar, Lehrplan, Fluss
- Zentrale SFX-Pipeline (scripts/generate-sounds.py)
- DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat)
- Logo + Inter-Font lokal
- PHP-APIs: admin, glossar, levels, licenses, progress, waypoints,
  assignments, profile, tickets
- Spielsprache entfernt (admin-modules, admin-levels, schueler)
- Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 11:51:14 +02:00
parent 2136bbbe62
commit 9a61f55cb1
134 changed files with 1012826 additions and 690 deletions
+358
View File
@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Administration</title>
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;min-height:100vh}
.topbar{background:#fff;padding:.5rem 1rem;display:flex;align-items:center;gap:.6rem;border-bottom:1px solid rgba(0,0,0,.06);flex-wrap:wrap;position:sticky;top:0;z-index:100}
.topbar h1{font-size:.9rem;font-weight:800;color:#1f4e5a;display:flex;align-items:center;gap:.5rem}
.topbar h1 img{height:28px}
.topbar .spacer{flex:1}
.nav-btn{padding:.35rem .7rem;border:1.5px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.7rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none}
.nav-btn:hover{background:#dae8ec;color:#4a7c8a}
.nav-btn.on{background:#1f4e5a;color:#fff;border-color:#1f4e5a}
.wrap{max-width:1100px;margin:0 auto;padding:1rem}
.card{background:#fff;border-radius:10px;padding:1rem;box-shadow:0 1px 4px rgba(0,0,0,.03);border:1px solid rgba(0,0,0,.04);margin-bottom:1rem}
.card-h{font-size:.62rem;font-weight:700;color:#4a7c8a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem}
.btn{padding:.4rem .8rem;border:none;border-radius:6px;background:#4a7c8a;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer}
.btn:hover{background:#3a6470}
.btn-outline{background:transparent;color:#4a7c8a;border:1.5px solid #4a7c8a}
select,input[type=text],input[type=number]{padding:.35rem .5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:6px;font-size:.75rem;font-family:inherit}
/* Sim Cards Grid */
.sim-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.8rem;margin-bottom:1.5rem}
.sim-card{background:#fff;border-radius:12px;overflow:hidden;cursor:pointer;border:2px solid rgba(0,0,0,.05);transition:all .2s}
.sim-card:hover{border-color:#4a7c8a;transform:translateY(-2px);box-shadow:0 6px 20px rgba(74,124,138,.1)}
.sim-card.active{border-color:#1f4e5a;box-shadow:0 0 0 3px rgba(31,78,90,.15)}
.sim-card img{width:100%;height:100px;object-fit:cover}
.sim-card-body{padding:.6rem}
.sim-card-body h3{font-size:.78rem;font-weight:700;color:#1f4e5a}
.sim-card-body p{font-size:.6rem;color:#8a8a8a}
/* Slider rows */
.slider-row{display:grid;grid-template-columns:140px 1fr 40px;gap:8px;align-items:center;margin-bottom:.4rem;font-size:.75rem}
.slider-row label{color:#4a4a4a}
.slider-row input[type=range]{width:100%;accent-color:#4a7c8a}
.slider-row .sval{text-align:right;font-weight:700;color:#1f4e5a;font-variant-numeric:tabular-nums}
/* Waypoint rows */
.wp-row{display:grid;grid-template-columns:100px 1fr 80px 80px 70px 60px 110px 50px;gap:5px;align-items:center;padding:.4rem .2rem;border-bottom:1px solid rgba(0,0,0,.03);font-size:.7rem}
.wp-row:hover{background:#f0f8fa}
.wp-header{font-weight:700;font-size:.58rem;color:#8a8a8a;text-transform:uppercase;border-bottom:2px solid #dae8ec}
.wp-type{font-size:.58rem;padding:1px 5px;border-radius:3px;font-weight:600}
.wp-type-base{background:#f2eacc;color:#8a7020}
.wp-type-city{background:#dae8ec;color:#4a7c8a}
.wp-type-village{background:#f0eeea;color:#666}
.wp-type-peak{background:#dceadd;color:#3a6b3e}
.wp-type-target{background:#f2ddd8;color:#8a3a2a}
.wp-map-mini{width:110px;height:65px;border-radius:5px;border:1px solid rgba(0,0,0,.06)}
/* Level cards */
.level-card{border:1.5px solid rgba(0,0,0,.06);border-radius:8px;padding:.7rem;margin-bottom:.4rem}
.level-card h4{font-size:.78rem;font-weight:700;color:#1f4e5a}
.level-card .meta{font-size:.62rem;color:#8a8a8a}
/* Modal */
.modal{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:1000}
.modal-card{background:#fff;border-radius:14px;padding:1.2rem;max-width:600px;width:95%;max-height:90vh;overflow-y:auto}
.modal-card h3{font-size:.95rem;font-weight:800;color:#1f4e5a;margin-bottom:.6rem}
.modal-card label{display:block;font-size:.65rem;font-weight:600;color:#4a4a4a;margin-top:.5rem}
.modal-card input,.modal-card select{width:100%}
#editMap{height:400px;border-radius:8px;margin-top:.5rem}
.hidden{display:none}
/* Tab content */
.tab-content{display:none}
.tab-content.active{display:block}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/geograsim_t.svg" alt="">Administration</h1>
<span class="spacer"></span>
<a href="#" class="nav-btn on" onclick="showTab('sims')">Simulationen</a>
<a href="#" class="nav-btn" onclick="showTab('waypoints')">📍 Waypoints</a>
<a href="heli" class="nav-btn" target="_blank">🗺️ Gesamtkarte</a>
<a href="admin-modules.html" class="nav-btn">🧩 Module</a>
<a href="admin-licenses.html" class="nav-btn">🔑 Lizenzen</a>
<a href="admin.html" class="nav-btn">Logout</a>
</div>
<div class="wrap">
<!-- TAB: Simulationen -->
<div class="tab-content active" id="tab-sims">
<div class="card">
<div class="card-h">Simulationen — Wähle ein Modul zur Konfiguration</div>
<div class="sim-grid" id="simGrid"></div>
</div>
<div id="simDetail"></div>
</div>
<!-- TAB: Waypoints -->
<div class="tab-content" id="tab-waypoints">
<div class="card">
<div class="card-h">📍 Waypoints — Koordinaten (<span id="wp-count">0</span>)</div>
<div style="display:flex;gap:.4rem;margin-bottom:.5rem;flex-wrap:wrap">
<select id="wp-filter-region" onchange="loadWaypoints()"><option value="">Alle</option><option value="vorarlberg">Vorarlberg</option><option value="tirol">Tirol</option></select>
<select id="wp-filter-type" onchange="loadWaypoints()"><option value="">Alle Typen</option><option value="base">Basis</option><option value="city">Stadt</option><option value="village">Dorf</option><option value="peak">Gipfel</option><option value="target">Einsatzort</option></select>
<button class="btn" onclick="openAdd()">+ Waypoint</button>
</div>
<div class="wp-row wp-header"><div>Key</div><div>Name</div><div>Lat</div><div>Lon</div><div>Region</div><div>Typ</div><div>Karte</div><div></div></div>
<div id="wp-list"></div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal hidden" id="editModal">
<div class="modal-card">
<h3 id="editTitle">Waypoint bearbeiten</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div><label>Key</label><input type="text" id="e-key" placeholder="bregenz"></div>
<div><label>Name</label><input type="text" id="e-name" placeholder="Bregenz"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.5rem">
<div><label>Lat</label><input type="number" id="e-lat" step="0.00001"></div>
<div><label>Lon</label><input type="number" id="e-lon" step="0.00001"></div>
<div><label>Region</label><select id="e-region"><option value="vorarlberg">Vorarlberg</option><option value="tirol">Tirol</option></select></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div><label>Typ</label><select id="e-type"><option value="village">Dorf</option><option value="city">Stadt</option><option value="base">Basis</option><option value="peak">Gipfel</option><option value="target">Einsatzort</option><option value="pass">Pass</option></select></div>
<div><label>Info</label><input type="text" id="e-info" placeholder="Beschreibung"></div>
</div>
<p style="font-size:.68rem;color:#8a8a8a;margin-top:.4rem">Klicke auf die Karte um die Position zu setzen:</p>
<div id="editMap"></div>
<div style="display:flex;gap:.4rem;margin-top:.6rem">
<button class="btn" onclick="saveEdit()">Speichern</button>
<button class="btn btn-outline" onclick="closeEdit()">Abbrechen</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var BASE = (window.__GGS__?.baseUrl) || '';
if (!BASE) { var p = location.pathname, i = p.indexOf('/geograsim'); if (i !== -1) { var rest = p.substring(i + '/geograsim'.length); BASE = rest.indexOf('/App/') === 0 ? p.substring(0, i) + '/geograsim/App' : p.substring(0, i) + '/geograsim'; } }
async function api(endpoint, data) {
var opts = data ? {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify(data)} : {credentials:'same-origin'};
return (await fetch(BASE + '/api/' + endpoint, opts)).json();
}
// ==========================================
// TABS
// ==========================================
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.toggle('active', el.id === 'tab-' + tab); });
document.querySelectorAll('.nav-btn').forEach(function(b) { b.classList.remove('on'); });
event.target.classList.add('on');
if (tab === 'waypoints') loadWaypoints();
}
// ==========================================
// SIMULATIONEN
// ==========================================
var SIMS = [
{id:'klima', name:'Klimawächter V2', icon:'🌍', img:'assets/img/card-climate.png',
params:{
start_budget:{min:100,max:2000,def:600,label:'Start-Budget (Mio €)'},
start_population:{min:1000,max:20000,def:6000,label:'Start-Bevölkerung'},
income_per_10k:{min:50,max:500,def:220,label:'Einnahmen pro 10.000 Bürger'},
budget_loss:{min:-2000,max:0,def:-500,label:'Pleite-Grenze (Mio €, negativ)'},
event_frequency:{min:0,max:1,def:0.3,step:0.1,label:'Ereignis-Häufigkeit (01)'},
time_limit:{min:30,max:150,def:75,label:'Simulationsdauer (Runden)'}
}},
{id:'sim-05', name:'Klimawächter 2D (V1)', icon:'🌡️', img:'assets/img/card-climate.png',
params:{start_co2:{min:300,max:600,def:425,label:'Start-CO₂ (ppm)'},start_temp:{min:14,max:18,def:15.2,label:'Start-Temperatur (°C)'},budget:{min:200,max:2000,def:950,label:'Budget (Mrd)'},income:{min:100,max:500,def:290,label:'Einkommen/Jahr'},damage_mult:{min:0.5,max:3,def:1,label:'Schadensmultiplikator'}}},
{id:'sim-05-3d', name:'Klimawächter 3D (V1)', icon:'🌊', img:'assets/img/card-climate.png',
params:{start_co2:{min:300,max:600,def:425,label:'Start-CO₂ (ppm)'},start_temp:{min:14,max:18,def:15.2,label:'Start-Temperatur (°C)'},budget:{min:200,max:2000,def:950,label:'Budget (Mrd)'},income:{min:100,max:500,def:290,label:'Einkommen/Jahr'}}},
{id:'sim-07', name:'Erdbeben', icon:'🌋', img:'assets/img/card-tectonic.png',
params:{quake_freq:{min:1,max:10,def:3,label:'Erdbeben-Häufigkeit'},build_cost:{min:50,max:500,def:200,label:'Baukosten'},population:{min:1000,max:50000,def:10000,label:'Bevölkerung'}}},
{id:'sim-09', name:'Energiemix', icon:'⚡', img:'assets/img/card-energy.png',
params:{demand_growth:{min:0,max:5,def:2,label:'Nachfrage-Wachstum (%/Jahr)'},co2_target:{min:20,max:80,def:50,label:'CO₂-Reduktionsziel (%)'},budget:{min:500,max:5000,def:2000,label:'Budget (Mio)'}}},
{id:'sim-10', name:'Lieferketten', icon:'👕', img:'assets/img/card-globe.png',
params:{price_weight:{min:0,max:100,def:40,label:'Preis-Gewichtung (%)'},fair_weight:{min:0,max:100,def:30,label:'Fairness-Gewichtung (%)'},co2_weight:{min:0,max:100,def:30,label:'CO₂-Gewichtung (%)'}}},
{id:'sim-11', name:'Regenwald', icon:'🌴', img:'assets/img/card-climate.png', params:{}},
{id:'sim-12', name:'Flussmanagement', icon:'🏞️', img:'assets/img/card-fluss.png',
params:{flood_freq:{min:1,max:10,def:3,label:'Hochwasser-Häufigkeit'},budget:{min:500,max:5000,def:2000,label:'Budget'},bio_weight:{min:0,max:100,def:40,label:'Biodiversitäts-Gewichtung (%)'}}},
{id:'sim-13', name:'Stadt & Raumplanung', icon:'🏗️', img:'assets/img/card-city.png',
params:{rounds:{min:5,max:20,def:10,label:'Runden'},budget_per_round:{min:100,max:1000,def:300,label:'Budget/Runde'},grid_size:{min:6,max:12,def:8,label:'Raster-Größe'}}},
{id:'sim-14', name:'Helikopter-Navigation', icon:'🚁', img:'assets/img/card-heli.png',
params:{houses:{min:0,max:14,def:8,label:'Häuser'},trees_tall:{min:0,max:14,def:6,label:'Hohe Bäume'},flocks:{min:0,max:8,def:2,label:'Vogelschwärme'},airliners:{min:0,max:3,def:1,label:'Verkehrsflugzeuge'},sportplanes:{min:0,max:4,def:2,label:'Sportflieger'},storms:{min:0,max:4,def:1,label:'Gewitterzellen'}}},
];
var selectedSim = null;
function renderSimGrid() {
var html = '';
SIMS.forEach(function(s) {
var cls = selectedSim === s.id ? ' active' : '';
html += '<div class="sim-card'+cls+'" onclick="selectSim(\''+s.id+'\')">';
html += '<img src="'+s.img+'" alt="">';
html += '<div class="sim-card-body"><h3>'+s.icon+' '+s.name+'</h3><p>'+Object.keys(s.params).length+' Parameter</p></div>';
html += '</div>';
});
document.getElementById('simGrid').innerHTML = html;
}
async function selectSim(id) {
selectedSim = id;
renderSimGrid();
var sim = SIMS.find(function(s){return s.id===id;});
if (!sim) return;
// Bestehende Levels laden
var levels = await api('levels?game_id=' + id);
if (levels.error) levels = [];
var html = '<div class="card"><div class="card-h">'+sim.icon+' '+sim.name+' — Level-Konfiguration</div>';
var params = sim.params || {};
var paramKeys = Object.keys(params);
// Für jedes der 3 Standard-Levels (Leicht/Mittel/Schwer) alle Schieberegler anzeigen
var levelNames = ['Leicht','Mittel','Schwer'];
var levelColors = ['#5a8a5e','#c4a35a','#c85c4a'];
levelNames.forEach(function(lName, li) {
var existingLevel = levels.find(function(l){return l.level_name===lName;});
var existingParams = existingLevel ? existingLevel.params : {};
var levelId = existingLevel ? existingLevel.id : 0;
html += '<div style="border-left:4px solid '+levelColors[li]+';padding:.6rem .8rem;margin-bottom:.6rem;background:rgba(0,0,0,.01);border-radius:0 8px 8px 0">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">';
html += '<strong style="font-size:.85rem;color:'+levelColors[li]+'">'+lName+'</strong>';
html += '<button class="btn" style="font-size:.62rem;padding:.2rem .6rem" onclick="saveLevel(\''+id+'\',\''+lName+'\','+li+','+levelId+')">Speichern</button>';
html += '</div>';
paramKeys.forEach(function(key) {
var p = params[key];
var val = existingParams[key] !== undefined ? existingParams[key] : p.def;
var sliderId = 'sl-'+li+'-'+key;
var valId = 'sv-'+li+'-'+key;
html += '<div class="slider-row">';
html += '<label>'+p.label+'</label>';
html += '<input type="range" min="'+p.min+'" max="'+p.max+'" value="'+val+'" step="'+(p.max-p.min > 10 ? 1 : 0.1)+'" id="'+sliderId+'" oninput="document.getElementById(\''+valId+'\').textContent=this.value">';
html += '<span class="sval" id="'+valId+'">'+val+'</span>';
html += '</div>';
});
html += '</div>';
});
html += '</div>';
document.getElementById('simDetail').innerHTML = html;
}
async function saveLevel(gameId, levelName, levelIndex, existingId) {
var sim = SIMS.find(function(s){return s.id===gameId;});
var params = {};
Object.keys(sim.params||{}).forEach(function(key) {
var el = document.getElementById('sl-'+levelIndex+'-'+key);
if (el) params[key] = parseFloat(el.value);
});
var r = await api('levels', {action:'save', gameId:gameId, levelId:existingId||undefined, levelName:levelName, scenario:'standard', params:params, sortOrder:levelIndex+1});
if (r.error) { alert(r.error); return; }
alert(levelName + ' gespeichert!');
selectSim(gameId);
}
async function deleteLevel(id, gameId) {
if (!confirm('Level löschen?')) return;
await api('levels', {action:'delete', levelId:id});
selectSim(gameId);
}
// ==========================================
// WAYPOINTS
// ==========================================
var allWaypoints = [];
async function loadWaypoints() {
var region = document.getElementById('wp-filter-region').value;
var type = document.getElementById('wp-filter-type').value;
var url = 'waypoints?';
if (region) url += 'region=' + region + '&';
if (type) url += 'type=' + type;
var wps = await api(url);
if (wps.error) wps = [];
allWaypoints = wps;
document.getElementById('wp-count').textContent = wps.length;
var html = '';
wps.forEach(function(w) {
var typeClass = 'wp-type-' + w.wp_type;
var typeLabel = {base:'Basis',city:'Stadt',village:'Dorf',peak:'Gipfel',target:'Ziel',pass:'Pass'}[w.wp_type] || w.wp_type;
var lat = parseFloat(w.lat).toFixed(5), lon = parseFloat(w.lon).toFixed(5);
html += '<div class="wp-row">';
html += '<div style="font-family:monospace;font-size:.65rem;color:#4a7c8a;font-weight:600">'+w.wp_key+'</div>';
html += '<div><strong>'+w.name+'</strong>'+(w.info?'<br><span style="font-size:.58rem;color:#8a8a8a">'+w.info+'</span>':'')+'</div>';
html += '<div style="font-variant-numeric:tabular-nums;font-size:.65rem">'+lat+'</div>';
html += '<div style="font-variant-numeric:tabular-nums;font-size:.65rem">'+lon+'</div>';
html += '<div style="font-size:.58rem;color:#8a8a8a">'+w.region+'</div>';
html += '<div><span class="wp-type '+typeClass+'">'+typeLabel+'</span></div>';
html += '<div><img class="wp-map-mini" src="https://staticmap.openstreetmap.de/staticmap.php?center='+lat+','+lon+'&zoom=13&size=110x65&markers='+lat+','+lon+',ol-marker" loading="lazy"></div>';
html += '<div><button style="padding:.2rem .4rem;font-size:.6rem;border:1px solid rgba(0,0,0,.08);border-radius:4px;background:#fff;cursor:pointer" onclick="openEdit(\''+w.wp_key+'\')">✏️</button></div>';
html += '</div>';
});
document.getElementById('wp-list').innerHTML = html;
}
var editMap = null, editMarker = null;
function openEdit(key) {
var w = allWaypoints.find(function(x){return x.wp_key===key;});
if (!w) return;
document.getElementById('editTitle').textContent = 'Bearbeiten: ' + w.name;
document.getElementById('e-key').value = w.wp_key;
document.getElementById('e-name').value = w.name;
document.getElementById('e-lat').value = parseFloat(w.lat);
document.getElementById('e-lon').value = parseFloat(w.lon);
document.getElementById('e-region').value = w.region;
document.getElementById('e-type').value = w.wp_type;
document.getElementById('e-info').value = w.info || '';
document.getElementById('editModal').classList.remove('hidden');
setTimeout(function() {
if (!editMap) {
editMap = L.map('editMap').setView([w.lat, w.lon], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:18}).addTo(editMap);
editMap.on('click',function(e){document.getElementById('e-lat').value=e.latlng.lat.toFixed(5);document.getElementById('e-lon').value=e.latlng.lng.toFixed(5);if(editMarker)editMap.removeLayer(editMarker);editMarker=L.marker(e.latlng).addTo(editMap);});
} else { editMap.setView([w.lat,w.lon],14); editMap.invalidateSize(); }
if(editMarker)editMap.removeLayer(editMarker);
editMarker=L.marker([w.lat,w.lon]).addTo(editMap);
}, 200);
}
function openAdd() {
document.getElementById('editTitle').textContent = 'Neuer Waypoint';
['e-key','e-name','e-lat','e-lon','e-info'].forEach(function(id){document.getElementById(id).value='';});
document.getElementById('editModal').classList.remove('hidden');
setTimeout(function(){if(!editMap){editMap=L.map('editMap').setView([47.3,10.5],9);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:18}).addTo(editMap);editMap.on('click',function(e){document.getElementById('e-lat').value=e.latlng.lat.toFixed(5);document.getElementById('e-lon').value=e.latlng.lng.toFixed(5);if(editMarker)editMap.removeLayer(editMarker);editMarker=L.marker(e.latlng).addTo(editMap);});}else{editMap.setView([47.3,10.5],9);editMap.invalidateSize();}if(editMarker){editMap.removeLayer(editMarker);editMarker=null;}},200);
}
function closeEdit(){document.getElementById('editModal').classList.add('hidden');}
async function saveEdit() {
var r = await api('waypoints',{action:'save',wpKey:document.getElementById('e-key').value.trim().toLowerCase().replace(/[^a-z0-9_]/g,''),name:document.getElementById('e-name').value.trim(),lat:parseFloat(document.getElementById('e-lat').value),lon:parseFloat(document.getElementById('e-lon').value),region:document.getElementById('e-region').value,wpType:document.getElementById('e-type').value,info:document.getElementById('e-info').value.trim()});
if(r.error){alert(r.error);return;}
closeEdit(); loadWaypoints();
}
// ==========================================
// INIT
// ==========================================
fetch(BASE+'/api/admin',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({action:'status'})})
.then(function(r){return r.json()}).then(function(r){
if(!r.authenticated){location.href='admin.html';return;}
renderSimGrid();
});
</script>
</body>
</html>