Admin-ToDo/Review-Seite + Staging-Self-Service-Admin-Login
- admin-todo.html: mobile-freundliche ToDo/Review-Liste (Prioritaeten, Haekchen + Notizfelder je Punkt, localStorage, Notizen-Export), admin-gated, noindex. In admin-licenses.html Nav + Backend-Werkzeuge verlinkt. - admin.php: devPin auch bei STAGING=1 (v3, Mail bewusst tot) -> Self-Service- Admin-Login ohne Mailversand. Prod unveraendert (STAGING dort nie definiert, 2FA-Mail-Pflicht bleibt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
<a href="admin-styleguide.html" class="btn-sm">🎨 Styleguide</a>
|
<a href="admin-styleguide.html" class="btn-sm">🎨 Styleguide</a>
|
||||||
<a href="admin-accounts.html" class="btn-sm">👤 Admins</a>
|
<a href="admin-accounts.html" class="btn-sm">👤 Admins</a>
|
||||||
<a href="admin-weltkueche.html" class="btn-sm">🍽️ Weltküche-Herkunft</a>
|
<a href="admin-weltkueche.html" class="btn-sm">🍽️ Weltküche-Herkunft</a>
|
||||||
|
<a href="admin-todo.html" class="btn-sm">📋 ToDo / Review</a>
|
||||||
<a href="admin.html" class="btn-sm">Logout</a>
|
<a href="admin.html" class="btn-sm">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
<a class="btn btn-outline" href="stadt-editor">🏙️ Stadt-Raumplanung-Editor</a>
|
<a class="btn btn-outline" href="stadt-editor">🏙️ Stadt-Raumplanung-Editor</a>
|
||||||
<a class="btn btn-outline" href="foto-upload">📷 Schulbuch-Bildupload</a>
|
<a class="btn btn-outline" href="foto-upload">📷 Schulbuch-Bildupload</a>
|
||||||
<a class="btn btn-outline" href="admin-weltkueche.html">🍽️ Weltküche-Herkunftsübersicht (QA)</a>
|
<a class="btn btn-outline" href="admin-weltkueche.html">🍽️ Weltküche-Herkunftsübersicht (QA)</a>
|
||||||
|
<a class="btn btn-outline" href="admin-todo.html">📋 ToDo / Review-Liste</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:.66rem;color:#8a8a8a;margin-top:.55rem">Alle nur mit Admin-Login erreichbar. Schulbuch-Upload zusätzlich per PIN (0815). Heli-Audio-Werkzeuge liegen nur lokal (<code>.LocalDeveloperTools</code>) und sind nicht auf dem Server.</div>
|
<div style="font-size:.66rem;color:#8a8a8a;margin-top:.55rem">Alle nur mit Admin-Login erreichbar. Schulbuch-Upload zusätzlich per PIN (0815). Heli-Audio-Werkzeuge liegen nur lokal (<code>.LocalDeveloperTools</code>) und sind nicht auf dem Server.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>GeoGraSim — ToDo / Review (Admin)</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="assets/fonts/inter.css">
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'Inter',system-ui,sans-serif;background:#f7f6f3;min-height:100vh;color:#2a2a2a;-webkit-text-size-adjust:100%}
|
||||||
|
.topbar{background:#fff;padding:.6rem .9rem;display:flex;align-items:center;gap:.6rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:20;flex-wrap:wrap}
|
||||||
|
.topbar h1{font-size:.9rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.45rem;min-width:0}
|
||||||
|
.topbar h1 img{height:26px}
|
||||||
|
.btn-sm{padding:.35rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.7rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none;white-space:nowrap}
|
||||||
|
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
|
||||||
|
.btn-sm.primary{background:#4a7c8a;color:#fff;border-color:#4a7c8a}
|
||||||
|
.wrap{max-width:820px;margin:0 auto;padding:.9rem}
|
||||||
|
.lead{font-size:.8rem;color:#6a6a6a;margin-bottom:.7rem;line-height:1.5}
|
||||||
|
.lead b{color:#3a3a3a}
|
||||||
|
.bar{height:8px;border-radius:99px;background:#e6e3dc;overflow:hidden;margin:.5rem 0 1rem}
|
||||||
|
.bar>i{display:block;height:100%;background:linear-gradient(90deg,#4a7c8a,#6aa );background:#4a7c8a;width:0;transition:width .3s}
|
||||||
|
.toolbar{display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:1rem}
|
||||||
|
.sec{margin-bottom:1.4rem}
|
||||||
|
.sec-h{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:#8a8a8a;margin:0 .2rem .5rem;display:flex;align-items:center;gap:.5rem}
|
||||||
|
.sec-h .pill{font-size:.6rem;padding:.12rem .5rem;border-radius:99px;font-weight:800;letter-spacing:.02em}
|
||||||
|
.p1 .pill{background:#f6dcd6;color:#a33f28}
|
||||||
|
.p2 .pill{background:#fbeccd;color:#8a6510}
|
||||||
|
.p3 .pill{background:#dceadd;color:#2f6b3a}
|
||||||
|
.p4 .pill{background:#e2e8ee;color:#42607a}
|
||||||
|
.item{background:#fff;border-radius:11px;box-shadow:0 1px 3px rgba(0,0,0,.04);border:1px solid rgba(0,0,0,.05);padding:.7rem .8rem;margin-bottom:.6rem;border-left:4px solid #ccc}
|
||||||
|
.p1 .item{border-left-color:#c9503a}.p2 .item{border-left-color:#e0a020}.p3 .item{border-left-color:#4e9c5e}.p4 .item{border-left-color:#6a8db0}
|
||||||
|
.item.done{opacity:.55}
|
||||||
|
.item-top{display:flex;gap:.6rem;align-items:flex-start}
|
||||||
|
.chk{appearance:none;-webkit-appearance:none;flex:0 0 auto;width:22px;height:22px;border:2px solid #bcbcbc;border-radius:6px;cursor:pointer;position:relative;margin-top:.05rem;background:#fff}
|
||||||
|
.chk:checked{background:#4a7c8a;border-color:#4a7c8a}
|
||||||
|
.chk:checked::after{content:"✓";position:absolute;inset:0;color:#fff;font-size:.85rem;font-weight:800;display:flex;align-items:center;justify-content:center}
|
||||||
|
.item .ttl{font-size:.88rem;font-weight:700;color:#2a2a2a;line-height:1.3}
|
||||||
|
.item.done .ttl{text-decoration:line-through}
|
||||||
|
.item .desc{font-size:.76rem;color:#6a6a6a;line-height:1.5;margin-top:.25rem}
|
||||||
|
.item .desc code{background:#f0efe9;padding:.05rem .3rem;border-radius:4px;font-size:.72rem}
|
||||||
|
.note{width:100%;margin-top:.5rem;border:1.5px solid rgba(0,0,0,.1);border-radius:7px;padding:.45rem .55rem;font-family:inherit;font-size:.8rem;resize:vertical;min-height:2.2rem;color:#2a2a2a;background:#fcfcfa}
|
||||||
|
.note:focus{outline:none;border-color:#4a7c8a;background:#fff}
|
||||||
|
.note.has{border-color:#e0a020;background:#fffdf6}
|
||||||
|
#status{padding:2rem;text-align:center;color:#8a8a8a}
|
||||||
|
dialog{border:none;border-radius:12px;padding:0;max-width:640px;width:92%;box-shadow:0 20px 60px rgba(0,0,0,.3)}
|
||||||
|
dialog::backdrop{background:rgba(0,0,0,.45)}
|
||||||
|
.dlg-h{padding:.8rem 1rem;font-weight:800;color:#4a7c8a;border-bottom:1px solid rgba(0,0,0,.07);display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.dlg-b{padding:1rem;max-height:60vh;overflow:auto}
|
||||||
|
.dlg-b textarea{width:100%;min-height:40vh;border:1px solid rgba(0,0,0,.12);border-radius:8px;padding:.6rem;font-family:ui-monospace,monospace;font-size:.75rem}
|
||||||
|
.foot{font-size:.68rem;color:#a0a0a0;text-align:center;margin:1.5rem 0 2rem}
|
||||||
|
@media(max-width:520px){ .wrap{padding:.7rem} .topbar h1{font-size:.82rem} }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="topbar">
|
||||||
|
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> 📋 ToDo / Review</h1>
|
||||||
|
<button class="btn-sm primary" onclick="showNotes()">📝 Notizen</button>
|
||||||
|
<a href="admin-licenses.html" class="btn-sm">← Admin</a>
|
||||||
|
<a href="admin.html" class="btn-sm">Logout</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status">Prüfe Zugang…</div>
|
||||||
|
|
||||||
|
<div class="wrap" id="app" style="display:none">
|
||||||
|
<p class="lead">Lebende ToDo-/Review-Liste. Häkchen = <b>erledigt/gesichtet</b>, Notizfeld je Punkt für deine Review-Gedanken. Alles wird <b>lokal in diesem Browser</b> gespeichert (kein Server) — ideal für unterwegs. Mit <b>📝 Notizen</b> holst du dir alle Notizen als Text zum Kopieren.</p>
|
||||||
|
<div class="bar"><i id="prog"></i></div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="btn-sm" onclick="filterDone()"><span id="fd">Erledigte ausblenden</span></button>
|
||||||
|
<button class="btn-sm" onclick="resetAll()">↺ Häkchen zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="foot">Stand: 2026-08-20 · gepflegt von Atlas · nur Admin</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="notesDlg">
|
||||||
|
<div class="dlg-h"><span>📝 Alle Review-Notizen</span><button class="btn-sm" onclick="document.getElementById('notesDlg').close()">✕</button></div>
|
||||||
|
<div class="dlg-b">
|
||||||
|
<textarea id="notesOut" readonly></textarea>
|
||||||
|
<div style="margin-top:.6rem;display:flex;gap:.4rem"><button class="btn-sm primary" onclick="copyNotes()">In Zwischenablage</button></div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ---- Auth-Gate (wie andere Admin-Seiten) ----
|
||||||
|
var BASE = location.pathname.replace(/\/admin-todo\.html$/,'').replace(/\/$/,'');
|
||||||
|
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 || !r.authenticated) { location.href = 'admin.html'; return; }
|
||||||
|
document.getElementById('status').style.display='none';
|
||||||
|
document.getElementById('app').style.display='';
|
||||||
|
render();
|
||||||
|
})
|
||||||
|
.catch(function(){ location.href = 'admin.html'; });
|
||||||
|
|
||||||
|
// ---- ToDo-Daten (id STABIL halten, damit Notizen erhalten bleiben) ----
|
||||||
|
var TODO = [
|
||||||
|
{ sec:'Auth-Cutover: v3 → Produktion', prio:1, items:[
|
||||||
|
{id:'csrf', t:'CSRF-Token für POST-Endpoints', d:'Aktuell nur <code>SameSite=Lax</code>. Vor jedem Prod-Cutover des JWT-Backends CSRF-Token ergänzen (Double-Submit oder synchronizer).'},
|
||||||
|
{id:'cutover-dual', t:'Doppelgleisiger Cutover (alt + neu parallel)', d:'Übergangszeit altes <code>$_SESSION</code>/Cookie UND neues JWT akzeptieren, Migration beim nächsten Login. 9 echte Lehrer + Schüler mit 30-Tage-Cookies dürfen NICHT rausfliegen.'},
|
||||||
|
{id:'admin-pending-redis', t:'admin_pending (2FA-Zwischenstand) → Redis', d:'Liegt im jwt-Modus noch in nativer $_SESSION. Für echtes k8s (Multi-Pod) nach Redis. Auf Single-Container-v3 aktuell ok.'},
|
||||||
|
{id:'reporting-fk', t:'Reporting auf student_id-FK ziehen', d:'Voraussetzung, dass Schüler ganz ohne DB-Session-Zeile auskommen (assessments.session_id → student_id).'},
|
||||||
|
{id:'prod-redis', t:'Prod-Redis + JWT_SECRET, Cutover nachts', d:'Eigener Prod-Redis-Container + JWT_SECRET/REDIS_URL in Prod-.env.production. Umstellung nachts, wenn niemand online ist.'},
|
||||||
|
]},
|
||||||
|
{ sec:'Offener Prod-Deploy (Feature-Bündel)', prio:2, items:[
|
||||||
|
{id:'deploy-batch', t:'Bündel db4b75c5..HEAD auf Prod', d:'Non-breaking (AUTH bleibt <code>session</code>). <b>Enthält den kritischen PHPMailer-vendor-Fix</b> — ohne den bricht Prod-Mailversand. vendor/ auch ins Top-Level spiegeln.'},
|
||||||
|
{id:'deploy-content', t:'Inhalt des Bündels prüfen', d:'Lehrer-Aktivitätsstreifen · Tourismusregion-Balance+UI · Weltküche-Gravitation+Artikel+Sätze · Logistik-Pause-Fix · assessment-Skip-Fix · admin-weltkueche QA · Passstraße-Karte · Lehrplan-Prod-Mapping.'},
|
||||||
|
]},
|
||||||
|
{ sec:'v3-Härtung & Infrastruktur', prio:3, items:[
|
||||||
|
{id:'v3-noindex', t:'v3 schützen (öffentlich mit Echt-Daten)', d:'noindex ist gesetzt; überlegen: Basic-Auth/IP-Schutz vor v3, da geklonte Echt-DB.'},
|
||||||
|
{id:'secret-scrub', t:'Secret-Scrub vor Bitbucket/Bruder-Deploy', d:'Prod-DB-Passwort im Klartext in getrackten Deploy-Skripten; Voll-DB-Dumps getrackt (DSGVO). → Secrets in .env, Dumps untracken, Passwort rotieren.'},
|
||||||
|
{id:'bitbucket', t:'App-Ordner → Bitbucket für neues Prod-System', d:'Bruder baut Deployment-Umgebung; wir pullen/pushen. Zwei Origins + eigene .gitignore-Regeln.'},
|
||||||
|
]},
|
||||||
|
{ sec:'Feinschliff-Backlog (Features)', prio:4, items:[
|
||||||
|
{id:'tr-stars', t:'Tourismusregion: Sterne-Kurve/Eskalation', d:'Umwelt beißt jetzt (Pass 1). Sterne-Kurve + eskalierende Ansprüche über Wellen tunen — braucht deinen Playtest (Feeling headless nicht messbar).'},
|
||||||
|
{id:'wk-gesamt', t:'Weltküche-Gesamt-Kachel (teacher.html)', d:'Aggregat-Kachel in der Lehreransicht.'},
|
||||||
|
{id:'lehrplan-switch', t:'Lehrplan-Länderschalter (D-A-CH)', d:'Umschalter AT/DE/CH/LI in der Lehrplan-/Modul-Ansicht.'},
|
||||||
|
{id:'preview-rest', t:'display_preview: klima/kofferdetektiv/stadt', d:'Fehlen noch in der Screenshot-Galerie (andere Einstiegspfade).'},
|
||||||
|
{id:'strip-v2', t:'Aktivitätsstreifen v2: „läuft noch"', d:'Aktive live_sessions einblenden (rechts-offener Balken für laufende Runden).'},
|
||||||
|
]},
|
||||||
|
{ sec:'Ältere offene Entscheidungen', prio:4, items:[
|
||||||
|
{id:'klassen-exkl', t:'Klassenaufgaben: Exklusiv-Sperre', d:'Vertagt — nur eine Sim gleichzeitig freigeben?'},
|
||||||
|
{id:'klima-save', t:'Klima Save-Cleanup nach Game-Over', d:'Konzept steht, dein OK für Implementierung offen.'},
|
||||||
|
{id:'balance-rest', t:'Balancing sonnensystem / energiemanager', d:'Aus dem großen Review offen.'},
|
||||||
|
]},
|
||||||
|
];
|
||||||
|
|
||||||
|
var KEY='ggs_todo_review_v1';
|
||||||
|
function load(){ try{ return JSON.parse(localStorage.getItem(KEY)||'{}'); }catch(e){ return {}; } }
|
||||||
|
function save(s){ localStorage.setItem(KEY, JSON.stringify(s)); }
|
||||||
|
var state = load();
|
||||||
|
var hideDone = false;
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
var host = document.getElementById('list');
|
||||||
|
host.innerHTML = '';
|
||||||
|
var total=0, done=0;
|
||||||
|
TODO.forEach(function(g){
|
||||||
|
var sec = document.createElement('div');
|
||||||
|
sec.className = 'sec p'+g.prio;
|
||||||
|
var pill = g.prio===1?'Priorität 1':g.prio===2?'Priorität 2':g.prio===3?'Priorität 3':'Backlog';
|
||||||
|
sec.innerHTML = '<div class="sec-h"><span class="pill">'+pill+'</span> '+g.sec+'</div>';
|
||||||
|
g.items.forEach(function(it){
|
||||||
|
total++;
|
||||||
|
var st = state[it.id] || {};
|
||||||
|
if (st.done) done++;
|
||||||
|
if (hideDone && st.done) return;
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = 'item' + (st.done?' done':'');
|
||||||
|
var note = (st.note||'');
|
||||||
|
el.innerHTML =
|
||||||
|
'<div class="item-top">'+
|
||||||
|
'<input type="checkbox" class="chk" '+(st.done?'checked':'')+' data-id="'+it.id+'">'+
|
||||||
|
'<div style="flex:1;min-width:0"><div class="ttl">'+it.t+'</div><div class="desc">'+it.d+'</div></div>'+
|
||||||
|
'</div>'+
|
||||||
|
'<textarea class="note'+(note?' has':'')+'" data-id="'+it.id+'" placeholder="Notiz für Review…">'+note.replace(/</g,'<')+'</textarea>';
|
||||||
|
sec.appendChild(el);
|
||||||
|
});
|
||||||
|
host.appendChild(sec);
|
||||||
|
});
|
||||||
|
document.getElementById('prog').style.width = total? Math.round(done/total*100)+'%' : '0';
|
||||||
|
host.querySelectorAll('.chk').forEach(function(c){ c.addEventListener('change', function(){ var id=c.dataset.id; state[id]=state[id]||{}; state[id].done=c.checked; save(state); render(); }); });
|
||||||
|
host.querySelectorAll('.note').forEach(function(n){ n.addEventListener('input', function(){ var id=n.dataset.id; state[id]=state[id]||{}; state[id].note=n.value; save(state); n.classList.toggle('has', !!n.value); }); });
|
||||||
|
}
|
||||||
|
function filterDone(){ hideDone=!hideDone; document.getElementById('fd').textContent = hideDone?'Erledigte einblenden':'Erledigte ausblenden'; render(); }
|
||||||
|
function resetAll(){ if(!confirm('Alle Häkchen zurücksetzen? (Notizen bleiben)')) return; Object.keys(state).forEach(function(k){ if(state[k]) state[k].done=false; }); save(state); render(); }
|
||||||
|
function showNotes(){
|
||||||
|
var out=[];
|
||||||
|
TODO.forEach(function(g){ g.items.forEach(function(it){ var st=state[it.id]||{}; if(st.note){ out.push('• '+it.t+(st.done?' [erledigt]':'')+'\n '+st.note); } }); });
|
||||||
|
document.getElementById('notesOut').value = out.length? out.join('\n\n') : '(noch keine Notizen)';
|
||||||
|
document.getElementById('notesDlg').showModal();
|
||||||
|
}
|
||||||
|
function copyNotes(){ var t=document.getElementById('notesOut'); t.select(); try{ document.execCommand('copy'); }catch(e){} if(navigator.clipboard){ navigator.clipboard.writeText(t.value); } }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -46,11 +46,13 @@ if ($action === 'login') {
|
|||||||
unset($_SESSION['admin_id']); // Sicherstellen dass nicht schon eingeloggt
|
unset($_SESSION['admin_id']); // Sicherstellen dass nicht schon eingeloggt
|
||||||
|
|
||||||
$resp = ['message' => 'PIN wurde an Ihre E-Mail gesendet.', 'email' => maskEmail($admin['email_2fa'])];
|
$resp = ['message' => 'PIN wurde an Ihre E-Mail gesendet.', 'email' => maskEmail($admin['email_2fa'])];
|
||||||
// Dev-Umgebung (localhost): es geht keine Mail raus → PIN direkt zurückgeben,
|
// Dev-Umgebung (localhost) ODER Staging (v3, STAGING=1 in .env.production, Mail
|
||||||
// damit man sich lokal anmelden kann. Auf Prod (IS_PRODUCTION) NIEMALS.
|
// bewusst tot): PIN direkt zurückgeben, damit man sich ohne Mailversand anmelden
|
||||||
if (!IS_PRODUCTION) {
|
// kann. Auf Prod (IS_PRODUCTION ohne STAGING) NIEMALS — dort greift 2FA per Mail.
|
||||||
|
$isStaging = defined('STAGING') && (STAGING === '1' || STAGING === 1 || STAGING === true);
|
||||||
|
if (!IS_PRODUCTION || $isStaging) {
|
||||||
$resp['devPin'] = $pin;
|
$resp['devPin'] = $pin;
|
||||||
$resp['message'] = 'Dev-Umgebung – kein Mailversand. PIN steht unten.';
|
$resp['message'] = ($isStaging ? 'Staging' : 'Dev-Umgebung') . ' – kein Mailversand. PIN steht unten.';
|
||||||
}
|
}
|
||||||
Response::ok($resp);
|
Response::ok($resp);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user