From b6500bebe1d31c2c28bfbb3dcc6324a051a499b3 Mon Sep 17 00:00:00 2001 From: breitenbach76 Date: Wed, 10 Jun 2026 08:22:00 +0200 Subject: [PATCH] app update final --- 00_Konzept/README_konzept.md | 2 + 04_Tablet-Quiz/app/DEPLOY.md | 47 +++++- 04_Tablet-Quiz/app/feedback.php | 58 +++++++ 04_Tablet-Quiz/app/index.html | 289 +++++++++++++++++++++++++++++--- 04_Tablet-Quiz/app/sw.js | 2 +- PROJEKTSTAND.md | 38 ++++- 6 files changed, 406 insertions(+), 30 deletions(-) create mode 100644 04_Tablet-Quiz/app/feedback.php diff --git a/00_Konzept/README_konzept.md b/00_Konzept/README_konzept.md index d07369d..f5642c6 100644 --- a/00_Konzept/README_konzept.md +++ b/00_Konzept/README_konzept.md @@ -130,6 +130,8 @@ formcodiert. Figuren werden **gestellt, nicht gesteckt**; es gibt **zwei** Orte: **R | A** (oben) und **C | I** (unten). Die beteiligten Rollen werden zusätzlich in die passende RACI-Zone gestellt — sichtbar wird nicht nur *wer*, sondern *in welcher Verantwortung*. **A** hat genau einen Platz (genau eine Rolle accountable). + Didaktische Gewichtung (Frank): **R und A** sind die Pflicht und unbedingt zu + durchdenken (obere Zeile R | A), **C und I** sind ergänzend / nice-to-have. Alle Standfelder sind Ø 22 (gleich wie die Puck-Mulden — dieselben Ø-20-Figuren). Details & Designvarianten: [`../02_Spielfiguren/`](../02_Spielfiguren/). diff --git a/04_Tablet-Quiz/app/DEPLOY.md b/04_Tablet-Quiz/app/DEPLOY.md index 8fe2f1b..0783e14 100644 --- a/04_Tablet-Quiz/app/DEPLOY.md +++ b/04_Tablet-Quiz/app/DEPLOY.md @@ -1,7 +1,14 @@ # Deployment — SLC-Workshop Companion (App) **Auftrag für die Server-Claude / Ops:** Diese App **statisch** ausliefern. Kein -Build-Schritt, kein Backend, keine Secrets, keine Datenbank. +Build-Schritt, keine Secrets, keine Datenbank. + +> **Neu (v0.10): kleiner Feedback-Endpoint.** Die App sammelt jetzt Workshop-Feedback +> (Phasen + Prüfschritte) und **speichert es auf dem Server** zur späteren Auswertung. +> Dafür braucht es **einen** schlanken POST-Sammelpunkt (Flat-File, **keine Datenbank**). +> Referenz liegt bei: [`feedback.php`](feedback.php). Ist der Endpoint (noch) nicht +> aktiv, geht **nichts verloren**: die App hält das Feedback lokal vor (Retry-Queue) +> und der Header-Button **„⇩ Feedback"** exportiert es als Backup-Datei. ## Was das ist - Eine **statische Single-Page-PWA**. Alle Inhalte (Stationen, Quiz, Use-Cases) @@ -11,6 +18,7 @@ Build-Schritt, kein Backend, keine Secrets, keine Datenbank. - `manifest.webmanifest` — PWA-Manifest - `sw.js` — Service Worker (Offline-Cache der App-Shell) - `icon.svg` — App-Icon + - `feedback.php` — **optionaler** Feedback-Sammelpunkt (Flat-File, JSON Lines) ## Ziel Den Ordner `04_Tablet-Quiz/app/` als **statisches Web-Root** über den vorhandenen @@ -47,11 +55,44 @@ slc.example.intern { } ``` +## Feedback-Endpoint (empfohlen — für die Auswertung) + +Die App schickt jedes gespeicherte Feedback per `POST` als JSON an den im +`` hinterlegten Pfad (Default: `feedback.php`, +gleiche Domain). Der Service Worker fasst POSTs nicht an — der Offline-Cache stört +also nicht. + +**Variante A — PHP (Referenz, einfachste Option).** `feedback.php` liegt im App-Ordner. +Voraussetzung: `php-fpm` aktiv und das App-Verzeichnis für PHP freigegeben; das +Datenverzeichnis `feedback-data/` muss vom Webserver-User beschreibbar sein. + +```nginx +# innerhalb des server{}-Blocks von oben: +location ~ \.php$ { + include fastcgi_params; + fastcgi_pass unix:/run/php/php-fpm.sock; # Pfad anpassen + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; +} +``` +Daten landen in `feedback-data/feedback.jsonl` (eine JSON-Zeile pro Feedback) → +später für die Auswertung einlesen/zu CSV konvertieren. + +**Variante B — kein PHP verfügbar.** Endpoint auf einen beliebigen JSON-POST-Empfänger +zeigen lassen (z. B. ein kleines Node-/Worker-Skript, das den Body an eine Datei +anhängt) und im `` dessen URL eintragen. + +**Variante C — gar kein Server-Save.** Auch ohne Endpoint funktioniert alles: das +Feedback bleibt in der Retry-Queue und kann über den Header-Button **„⇩ Feedback"** +als JSON-Datei exportiert werden. + ## Verifikation nach dem Deploy -1. URL öffnen → Startbildschirm „SLC Companion · Schritt 1 · Action Card". +1. URL öffnen → Startbildschirm „SLC Companion". 2. DevTools → Application → **Service Workers**: `sw.js` ist *activated*. 3. Flugmodus/Offline → Seite neu laden → App lädt weiterhin (Offline-Cache). -4. Einen Durchlauf spielen → „Debrief" → **↓ Markdown / ↓ JSON** lädt eine Datei. +4. Einen Major-Durchlauf spielen, an einem Gate Feedback eintragen → **„💾 Feedback + speichern"**. Bei aktivem Endpoint erscheint kein Fehler in der Konsole; in + `feedback-data/feedback.jsonl` taucht eine neue Zeile auf. Ohne Endpoint: Header + **„⇩ Feedback"** lädt die Backup-Datei. ## Updates - Bei neuem Stand: `git pull`. Wenn sich App-Assets geändert haben, in `sw.js` diff --git a/04_Tablet-Quiz/app/feedback.php b/04_Tablet-Quiz/app/feedback.php new file mode 100644 index 0000000..9c1981a --- /dev/null +++ b/04_Tablet-Quiz/app/feedback.php @@ -0,0 +1,58 @@ + true, 'service' => 'slc-feedback', 'hint' => 'POST JSON to store feedback']); + exit; +} + +$raw = file_get_contents('php://input'); +if ($raw === false || strlen($raw) === 0 || strlen($raw) > 65536) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'empty or oversized body']); + exit; +} + +$data = json_decode($raw, true); +if (!is_array($data)) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'invalid json']); + exit; +} + +// Serverseitigen Eingangszeitstempel + Roh-Metadaten ergänzen (nicht überschreibend). +$data['_received'] = gmdate('c'); +$data['_ip'] = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; + +$dir = __DIR__ . '/feedback-data'; +$file = $dir . '/feedback.jsonl'; +if (!is_dir($dir)) { @mkdir($dir, 0775, true); } + +$line = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; +$fp = @fopen($file, 'ab'); +if ($fp === false) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'cannot open data file (Schreibrechte prüfen)']); + exit; +} +flock($fp, LOCK_EX); +fwrite($fp, $line); +flock($fp, LOCK_UN); +fclose($fp); + +echo json_encode(['ok' => true]); diff --git a/04_Tablet-Quiz/app/index.html b/04_Tablet-Quiz/app/index.html index ba26e3d..a6745a7 100644 --- a/04_Tablet-Quiz/app/index.html +++ b/04_Tablet-Quiz/app/index.html @@ -6,6 +6,8 @@ SLC-Workshop — Companion-App + + @@ -176,6 +178,10 @@ .sHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap} .sHead .phaseChip{font-size:15px;padding:8px 18px;letter-spacing:.8px} .sHead .sId{color:var(--ink);font-size:18px;font-weight:800;font-variant-numeric:tabular-nums} + .tiefeBadge{font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;cursor:help;white-space:nowrap} + .tiefeBadge.bearbeitet{background:#e7f5ec;color:#177a44;border:1px solid #b8e0c7} + .tiefeBadge.entwurf{background:#f0f1f4;color:#6b7280;border:1px solid #dfe2e8} + .sHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .sTitle{font-size:22px;font-weight:700;line-height:1.25;margin:10px 0 4px} .caseLine{color:var(--muted);font-size:13px;margin:0 0 22px} .lead{font-size:16px;margin:0 0 16px} @@ -184,6 +190,7 @@ .crit{margin:8px 0 0;padding-left:20px;color:var(--muted)} .crit li{margin:4px 0} .critHead{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px} + .gateZiel{font-size:14px;line-height:1.5;background:var(--soft);border-left:3px solid var(--accent);border-radius:8px;padding:10px 12px;margin:0 0 12px} .gateCrit{display:flex;flex-direction:column;gap:8px;margin:0 0 16px} .critItem{display:flex;align-items:flex-start;gap:10px;text-align:left;padding:11px 14px;border:1px solid var(--line);border-radius:10px;background:var(--soft);color:var(--ink);cursor:pointer;font-size:14px;line-height:1.45;animation:critIn .22s ease} .critItem:hover{border-color:#c9d2dd} @@ -208,6 +215,10 @@ .raciLegend .rlHead{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;margin-bottom:8px} .raciLegend .rlRow{display:flex;align-items:flex-start;gap:10px;margin:5px 0;font-size:14px;line-height:1.45} .raciLegend .rlRow .raciBadge{flex:none;margin-top:1px} + .raciLegend .rlGroup{margin-top:8px;padding-top:6px;border-top:1px dashed var(--line)} + .raciLegend .rlGroup:first-of-type{border-top:0;padding-top:0} + .raciLegend .rlGroupHead{font-size:11px;font-weight:700;color:var(--muted);margin:2px 0 4px} + .raciLegend .rlGroupHead.kern{color:var(--ink)} /* Aufgaben-Kasten (Anweisung hervorgehoben) */ .frageBox{background:#f7f9fc;border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:12px;padding:14px 18px;margin:6px 0;font-size:16px;line-height:1.55} .frageBox .frageLabel{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:700;margin-bottom:5px} @@ -288,6 +299,24 @@ .slcDonut.clickable .donutSeg{cursor:pointer;transition:opacity .12s} .slcDonut.clickable .donutSeg:hover{opacity:.82} .slcDonut .donutSeg.bad{stroke:var(--bad);stroke-width:4} + .slcDonut path.dim{opacity:.32} + .slcDonut path.current{stroke:var(--ink);stroke-width:3;filter:drop-shadow(0 1px 4px rgba(0,0,0,.28))} + .slcOverview{margin:4px 0 14px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;background:#fbfcfe} + .slcOverview .slcOvHead{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;text-align:center;margin-bottom:4px} + .slcOverview .slcDonut{width:180px;height:180px} + /* Feedback-Cards (Phase / Dokument) */ + .fbCard{margin:14px 0 4px;padding:14px 16px;border:1px solid var(--line);border-radius:12px;background:var(--soft)} + .fbCard .fbHead{font-size:15px;font-weight:800;margin-bottom:10px} + .fbCard .fbOpt{font-weight:600;color:var(--muted);font-size:12px} + .fbCard .fbQ{display:block;font-size:13.5px;font-weight:600;margin:10px 0 5px;line-height:1.4} + .fbCard textarea.fbInput{width:100%;box-sizing:border-box;font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);resize:vertical} + .fbCard textarea.fbInput:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)} + .fbScaleRow{display:flex;gap:8px;margin:2px 0 4px} + .fbScale{width:42px;height:42px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);font-weight:800;font-size:16px;cursor:pointer} + .fbScale:hover{border-color:#9fb0c4} + .fbScale.sel{background:var(--accent);color:#fff;border-color:var(--accent)} + .fbActions{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px} + .fbSaved{color:#177a44;font-weight:700;font-size:13px} .akteItem.flash{animation:akteFlash 1.6s ease-out} @keyframes akteFlash{0%,55%{background:#fff3bf}100%{background:transparent}} .slcCap{font-size:11px;color:var(--muted);margin-top:4px;text-align:center} @@ -357,13 +386,14 @@
SLC Workshop Companion
- v0.9 + v0.10
+
@@ -752,13 +782,21 @@ const STATIONEN = [ beschreibung:"Entry-Gate der Transition: Die SOR entscheidet zuerst, ob das Vorhaben überhaupt in die Transition darf — und wenn ja, ob die Service-Komponenten neu entwickelt oder nur konfiguriert werden. Erfordert SOR-Approval (Budget- und Ressourcenimplikationen).", umfasst:["Aufwandsschätzung (Build vs. Configure)","Technische Risiken","Budget-Abgleich","ggf. Lieferanten-Einbindung","SOR-Vorlage für Freigabe"], artefakt:"Gate-Entscheidung + SOR-Vorlage", + ziel:"Sicherstellen, dass nur Vorhaben in die weitere Entwicklung gehen, die ausreichend geplant sind und für die ein grundsätzliches Commitment bezüglich Ressourcen besteht (Budget, Betrieb & Support, Projektressourcen).", + // Schritt A: Freigabe-Entscheidung (Frank-Template). Schritt B: Routing (pfade). + freigabe:[ + ["Freigabe","Ausreichend geplant, Ressourcen-Commitment liegt vor → weiter zum Build-/Konfigurations-Routing"], + ["Freigabe mit Auflagen","Weiter zum Routing; definierte Auflagen müssen nachgearbeitet werden"], + ["Zurück an Design","Planung oder Commitment unzureichend → zurück in die Design-Phase"], + ["Ablehnung","Vorhaben wird nicht weiterverfolgt — erfordert SOR-Eskalation"] + ], pfade:[ ["Entwicklung","Neuentwicklung/wesentliche Anpassung → weiter zu tr_02"], ["Konfiguration","Bestehende Komponenten konfigurieren → springt zu tr_05"] ], pruef:[ ["Design-Vollständigkeit","Ist das Service Design Document vollständig?"], - ["Budget-Verfügbarkeit","Ist Budget für die Strategie verfügbar?"], + ["Budget-Verfügbarkeit","Ist Budget für die Implementierung verfügbar?"], ["Projektressourcen","Können Ressourcen mobilisiert werden?"], ["Betriebs-/Support-Bereitschaft","Grundsätzliches Commitment vorhanden?"] ], @@ -851,6 +889,7 @@ const STATIONEN = [ { id:"tr_09", phase:"transition", typ:"gate", gateNr:2, name:"Gate 2: Entry-Prüfung nach Build", beschreibung:"Validierung der Übergabefähigkeit nach Abschluss des Builds. Dies ist eine SO-Einzelentscheidung (keine Gremienentscheidung).", + ziel:"Sicherstellen, dass die grundlegenden Voraussetzungen für das Ausrollen eines Service gegeben sind.", umfasst:["Prüfung der Übergabe-Vollständigkeit","Prüfung des Abnahme-Status","Ressourcen-Outlook (Betrieb/Support vorbereitet)","Pilot-Entscheidung (falls erforderlich)","Dokumentation im Transition-Steckbrief"], artefakt:"Gate-2-Entscheidung (Transition-Steckbrief)", pfade:[ @@ -899,6 +938,7 @@ const STATIONEN = [ { id:"tr_12", phase:"transition", typ:"gate", gateNr:3, name:"Gate 3: Go-Live-Freigabe", beschreibung:"Portfolio-Freigabe und formale Aktivierung des Services. Exit-Gate der Transition — SOR-Entscheidung nach dem Konsent-Prinzip.", + ziel:"Sicherstellen, dass nur Services für den produktiven Betrieb aktiviert werden, die ausreichend geprüft und qualitätsgesichert sind und für die sowohl im Betrieb als auch im Support genügend Ressourcen zur Verfügung stehen.", umfasst:["Prüfung der Portfolio-Konsistenz","Prüfung der Betriebsreife (SO-Bestätigung)","Prüfung der Support-Bereitschaft","Prüfung der SLA/SLO-Vereinbarung","Prüfung der Auflagen-Erfüllung aus Gate 2","Bewertung der Geschäftskritikalität","Formale Aufnahme ins Service-Portfolio"], artefakt:"Gate-3-Entscheidung + Portfolio-Aufnahme", pfade:[ @@ -1119,8 +1159,9 @@ const STATIONEN = [ RACI + Quiz hier abgeleitet (Franks Entwurf nennt nur die Aktivitaeten). */ { id:"rv_01", phase:"review", typ:"aktivitaet", name:"Durchführen von Service-Reviews", - beschreibung:"Den Service systematisch auswerten und die Ergebnisse im Service-Review-Dokument festhalten.", - umfasst:["KPIs & Monitoring auswerten","Problems & Incidents auswerten","Kundenfeedback sammeln/einholen","zugrunde liegende Infrastruktur bewerten","Service-Review-Dokument ausfüllen"], + beschreibung:"Den Service systematisch auswerten und die Ergebnisse im Service-Review-Dokument festhalten (4 Dimensionen → Handlungsempfehlung).", + ziel:"Eine fundierte Entscheidung ermöglichen, ob der Service unverändert weiterbetrieben werden kann oder ob Änderungen erforderlich sind.", + umfasst:["KPIs & Monitoring auswerten","Problems & Incidents auswerten","Kundenfeedback sammeln/einholen","zugrunde liegende Infrastruktur bewerten","Bewertung über 4 Dimensionen: Leistungserbringung · Betriebsstabilität · Nutzerzufriedenheit · Zukunftsfähigkeit","Handlungsempfehlung ableiten: Weiterbetrieb · Normal-Change · Major-Change"], artefakt:"Service-Review-Dokument", raci:[["service_owner","A"],["betriebsteam","R"],["service_support_team","C"],["problem_manager","C"]], quiz:[ @@ -1221,9 +1262,9 @@ const ARTEFAKTE = { A13:{name:"Wissensdatenbank-Eintrag", phase:"support", live:true, was:"Standardlösungen, Known Errors, Workarounds, FAQ und Anleitungen — das zentrale Arbeitsmittel für 1st und 2nd Level Support.", warum:"Es beschleunigt den Support, sichert konsistente Antworten und entlastet die höheren Support-Level."}, - A14:{name:"Service-Review-Bericht", phase:"review", - was:"Das „Service Performance & Improvement Review“: Bewertung über 4 Dimensionen (Leistung, Stabilität, Nutzerzufriedenheit, Zukunftsfähigkeit) per Ampel und eine Handlungsempfehlung (CONTINUE / IMPROVEMENT / REDESIGN / RETIRE).", - warum:"Es liefert die strukturierte Grundlage für die SOR-Entscheidung über die Zukunft des Service."}, + A14:{name:"Service-Review-Dokument", phase:"review", + was:"Verantwortlich: Service-Owner. Bewertung über 4 Dimensionen mit Leitfrage — Leistungserbringung (liefert der Service den erwarteten Nutzen?), Betriebsstabilität (läuft er störungsarm und beherrschbar?), Nutzerzufriedenheit (wie bewerten die Nutzer den Service?) und Zukunftsfähigkeit (ist er mittelfristig tragfähig?). Daraus eine Handlungsempfehlung: Weiterbetrieb (ggf. mit Monitoring-Fokus) · Änderung als Normal-Change · Änderung als Major-Change.", + warum:"Es ermöglicht eine fundierte Entscheidung, ob der Service unverändert weiterbetrieben werden kann oder ob Änderungen erforderlich sind — die Grundlage für die SOR-Bewertung."}, A15:{name:"DPM-Rücklauf", phase:"review", was:"Die Übergabe an den Demand-Lifecycle — Variante A: Neuer Demand (Redesign/Erweiterung) oder Variante B: Retirement-Plan / Decommissioning-Auftrag (Stilllegung).", warum:"Es schließt den Lebenszyklus: größere Änderungen oder das Ende eines Service laufen kontrolliert über den Demand-Lifecycle, nicht „nebenbei“."} @@ -1246,6 +1287,20 @@ const FOLDS_INTO = { op_02:"A6", op_05:"A9", op_07:"A11", sp_03:"A10", sp_04:"A10", sp_05:"A10", sp_06:"A10", sp_08:"A10", sp_10:"A11" }; +/* Bearbeitungstiefe (Frank): "bearbeitet" = eigene Arbeitsergebnisse/Templates, + alles andere "entwurf" (konzeptioneller Best-Practice-Entwurf). Fuer Spieler sichtbar. */ +const BEARBEITET = new Set(["ds_01","tr_01","tr_09","tr_12","rv_01"]); +function stationTiefe(id){ return BEARBEITET.has(id) ? "bearbeitet" : "entwurf"; } +function tiefeBadge(id){ + const t = stationTiefe(id); + return t==="bearbeitet" + ? `● Bearbeitet` + : `○ Entwurf`; +} +/* Dokumente mit eigenem Detail-Feedback (Frank): Gate 1/2/3 + Service Review. */ +const FEEDBACK_DOCS = { + tr_01:"Gate 1", tr_09:"Gate 2", tr_12:"Gate 3", rv_01:"Service Review" +}; function addArtefakt(a){ if(a){ S.akte = S.akte || {}; S.akte[a] = true; } } // Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station @@ -1270,7 +1325,8 @@ function defaultState(){ freigabeDone:false, freigabeWrong:null, entryDone:false, entryWrong:null, bonusReveal:false, bonusDone:{}, servicesDone:{}, akteFlash:null, svcModalSel:null, - index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0, + index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0, gate1Approval:null, + feedback:{}, feedbackSaved:{}, feedbackQueue:[], actStep:0, actReveal:false, actDone:false, arteWrong:null, picks:{}, done:{}, akte:{}, loopback:null, revisit:{}, endReason:null, endGate:null }; @@ -1462,6 +1518,7 @@ function renderMainIntro(){

${card.text}

Als Major Change wird der Service neu eingeführt und durchläuft den kompletten Lifecycle ab Design. Die Freigaben fallen unterwegs an den Gates (SOR bzw. Service Owner) an — hier müsst ihr nichts einordnen. Das Einordnen (Change-Art · Freigabe · Einstieg) kommt danach bei den Bonus-Varianten.

Service-Beschreibung

${serviceDetailHtml(S.service)}
+ ${slcOverview("design","Start — ihr durchlauft den vollen Lifecycle ab Design")}
@@ -1580,15 +1637,17 @@ function renderFreigabe(){ } } -/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App) */ -function phaseDonut(wrongPh, clickable){ +/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App). + currentPh: hebt die aktuelle/nächste Phase hervor, dimmt die übrigen. */ +function phaseDonut(wrongPh, clickable, currentPh){ const order=["design","transition","operation","support","review"]; const cx=100,cy=100,R=92,r=46,seg=72,start=-90-seg/2; const P=(a,rad)=>[ (cx+rad*Math.cos(a*Math.PI/180)).toFixed(1), (cy+rad*Math.sin(a*Math.PI/180)).toFixed(1) ]; let segs="", labels=""; order.forEach((ph,i)=>{ const a0=start+i*seg, a1=a0+seg, o0=P(a0,R), o1=P(a1,R), i1=P(a1,r), i0=P(a0,r); - const cls = (clickable?"donutSeg":"") + (ph===wrongPh?" bad":""); + const cls = (clickable?"donutSeg":"") + (ph===wrongPh?" bad":"") + + (currentPh ? (ph===currentPh?" current":" dim") : ""); segs+=``; const mid=a0+seg/2, L=P(mid,(R+r)/2); labels+=`${PHASEN[ph].label}`; @@ -1597,6 +1656,15 @@ function phaseDonut(wrongPh, clickable){ labels+=`Lifecycle`; return `${segs}${labels}`; } +/* Gesamtbild-Einblendung für den Major-Walk (Frank: Start, Phasenübergänge, Schluss). + phase = hervorzuhebende Phase (oder null am Schluss). Nur im Major-Modus genutzt. */ +function slcOverview(phase, caption){ + const cap = caption || (phase ? `Ihr seid in der Phase ${PHASEN[phase].label}` : `Alle Phasen durchlaufen`); + return `
+
Service-Lifecycle — Gesamtbild
+
${phaseDonut(null, false, phase)}
${cap}
+
`; +} /* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */ function renderEntry(){ @@ -1752,7 +1820,7 @@ function enterStation(idx){ } S.index = idx; S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act"; - S.gatePick = null; S.quizIndex = 0; S.gateDeciderDone = false; S.gateCrit = 0; + S.gatePick = null; S.quizIndex = 0; S.gateDeciderDone = false; S.gateCrit = 0; S.gate1Approval = null; S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null; S.akteFlash = null; document.body.classList.remove("akteOpen"); } @@ -1784,16 +1852,21 @@ function raciTable(st){ const rows = st.raci.map(([r,c])=>`${roleLabel(r)}${c}`).join(""); return `${rows}
RolleRACI
`; } -/* RACI-Legende (deutsch) — wird bei der RACI-Frage immer mit angezeigt. */ +/* RACI-Legende (deutsch) — wird bei der RACI-Frage immer mit angezeigt. + Frank: R + A sind die Pflicht (durchdenken!), C + I ergänzend (nice-to-have). */ function raciLegendHtml(){ - const items = [ + const kern = [ ["R","Responsible","verantwortlich für die Durchführung — erledigt die Aufgabe operativ"], - ["A","Accountable","rechenschaftspflichtig — trägt die Ergebnisverantwortung (genau eine Rolle)"], + ["A","Accountable","rechenschaftspflichtig — trägt die Ergebnisverantwortung (genau eine Rolle)"] + ]; + const erg = [ ["C","Consulted","konsultiert — wird vorab um Rat/Beitrag gefragt"], ["I","Informed","informiert — wird über das Ergebnis in Kenntnis gesetzt"] ]; + const row = ([l,en,de])=>`
${l}${en} — ${de}
`; return `
Wofür RACI steht
` + - items.map(([l,en,de])=>`
${l}${en} — ${de}
`).join("") + + `
★ Pflicht — unbedingt durchdenken
${kern.map(row).join("")}
` + + `
ergänzend — nice-to-have
${erg.map(row).join("")}
` + `
`; } @@ -1821,7 +1894,7 @@ function renderRun(){
${cardBig}
-
${chip}${st.id}
+
${chip}${st.id}${tiefeBadge(st.id)}

${st.name}

${stepBody}
@@ -1841,7 +1914,7 @@ function activitySteps(st){ frage:`Welche Rollen setzen diese Aktivität operativ um — wer sorgt für die Umsetzung (Responsible)? Stellt deren Figuren auf die Mulden des Station-Pucks.`, auf:`

Operativ verantwortlich (R)

${(st.raci.filter(([r,c])=>c.includes("R")).map(([r,c])=>`${roleLabel(r)}${c==="A/R"?" (zugleich A)":""}`).join("")) || '— (keine eigene R-Rolle)'}

Das sind die „Macher" der Aktivität. Wer dafür geradesteht (A) sowie beratend (C) bzw. informiert (I) ist, klärt Schritt 3.

` }, { label:"RACI", - frage:`Ergänzt nun die vollständige RACI: Wer ist Accountable (trägt die Verantwortung), wer Consulted, wer Informed — zusätzlich zu den Responsible aus Schritt 2? Sortiert die Figuren ins Aktiv-Feld (R·A·C·I).`, + frage:`Vervollständigt die RACI. Wichtig zuerst: Wer ist Accountable (trägt die Ergebnisverantwortung — genau eine Rolle)? Zusammen mit den Responsible aus Schritt 2 sind R + A die Pflicht. Consulted und Informed danach ergänzen (nice-to-have). Sortiert die Figuren ins Aktiv-Feld (R·A·C·I).`, legend: raciLegendHtml(), auf:`

RACI (vollständig)

${raciTable(st)}` }, { label:"Artefakt", artefakt:true, @@ -1878,6 +1951,9 @@ function renderActivity(st){

Gut gemacht! Ihr habt ${st.id} — ${st.name} durchgespielt.

${phaseLine}
+ ${phaseEnd && S.mode==="main" ? slcOverview(next.phase, `Weiter in der Phase ${PHASEN[next.phase].label}`) : ``} + ${FEEDBACK_DOCS[st.id] ? docFeedbackBlock(st.id) : ``} + ${phaseEnd ? phaseFeedbackBlock(st.phase) : ``}
@@ -1975,6 +2051,7 @@ function renderGate(st){ const reqLine = req.length === 0 ? `` : blocked ? `
🔒 Gate öffnet nicht — es fehlt in der Akte: ${missing.map(a=>a+" "+ARTEFAKTE[a].name).join(" · ")}. Erzeugt diese Artefakte zuerst.
` : `
✓ Alle geforderten Artefakte liegen in der Akte (${req.join(" · ")}).
`; + const zielLine = st.ziel ? `
Ziel: ${st.ziel}
` : ``; // Prüf-Kriterien als nacheinander erscheinende Checkboxen (nur Anzeige, nicht blockierend) const pruef = st.pruef || []; @@ -1994,8 +2071,34 @@ function renderGate(st){
${critItems}
${allChecked?`

Alle Kriterien geprüft. Geforderte Artefakte in der Akte (siehe oben)? Pflicht-Figuren am Gate-Puck?

`:``}` : ``; + // Zweistufiges Gate (Gate 1): erst Freigabe-Entscheidung, dann Build-/Konfig-Routing. + if(st.freigabe){ + if(S.gate1Approval==null){ + const fopts = st.freigabe.map(([n,d],i)=> + ``).join(""); + return ` + ${revisitNote} + ${zielLine} +

Entscheidet: ${roleLabel(keeper)}

+ ${reqLine} + ${critWrap} +
Freigabe-Entscheidung
+
${fopts}
`; + } + // Freigabe erteilt (0/1) → Routing-Schritt + const fa = st.freigabe[S.gate1Approval]; + return ` + ${revisitNote} +
✓ Freigabe-Entscheidung: ${fa[0]}
+
Build-/Konfigurations-Routing
+

Da freigegeben: Werden die Service-Komponenten neu entwickelt oder bestehende nur konfiguriert?

+
${opts}
+
`; + } + return ` ${revisitNote} + ${zielLine}

Entscheidet: ${roleLabel(keeper)}

${reqLine} ${critWrap} @@ -2023,6 +2126,8 @@ function renderGateDone(st){

Weiter geht's mit der Phase ${PHASEN[target.phase].label}.

`; } + const overview = (phaseEnd && S.mode==="main") + ? slcOverview(target.phase, `Weiter in der Phase ${PHASEN[target.phase].label}`) : ``; return `
Entscheidung getroffen
@@ -2030,6 +2135,9 @@ function renderGateDone(st){ ${sorNote} ${feedback}
+ ${overview} + ${FEEDBACK_DOCS[st.id] ? docFeedbackBlock(st.id) : ``} + ${phaseEnd ? phaseFeedbackBlock(st.phase) : ``}
@@ -2037,6 +2145,117 @@ function renderGateDone(st){
`; } +/* ====================== FEEDBACK (Frank: Phase + Dokument, Server-Save) ====================== + Erfasst zwei Arten von Feedback und speichert sie auf dem Server (POST an + FEEDBACK_ENDPOINT) — mit Retry-Queue (offline-fest) und localStorage-Archiv als + Backup für den manuellen Export. Die Erfassung blockt den Spielfluss nie (optional). */ +const FEEDBACK_ENDPOINT = ((document.querySelector('meta[name="slc-feedback-endpoint"]')||{}).content) || "feedback.php"; +const FB_QUEUE = "slc-fb-queue", FB_ARCHIVE = "slc-fb-archive", FB_SESSION = "slc-fb-session"; +function fbReadArr(k){ try{ return JSON.parse(localStorage.getItem(k)||"[]"); }catch(e){ return []; } } +function fbWriteArr(k,a){ localStorage.setItem(k, JSON.stringify(a)); } +function escapeHtml(s){ return (s==null?"":""+s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])); } +function feedbackSessionId(){ + let id = localStorage.getItem(FB_SESSION); + if(!id){ id = "s-"+Math.random().toString(36).slice(2,10)+"-"+Date.now().toString(36); localStorage.setItem(FB_SESSION, id); } + return id; +} +function fbCtx(){ + return { service: S.service!=null ? USE_CASES[S.service].service : null, + serviceIdx: S.service, + change: S.change!=null ? CHANGE_TYPES[S.change] : null, + mode: S.mode }; +} + +/* Phasen-Feedback (2 Freitexte, am Phasenende) */ +function phaseFeedbackBlock(phase){ + const key = "phase:"+phase; + const fb = (S.feedback && S.feedback[key]) || {}; + const saved = S.feedbackSaved && S.feedbackSaved[key]; + return `
+
📝 Feedback zur Phase ${PHASEN[phase].label} · optional
+ + + + +
${saved?`✓ gespeichert`:``}
+
`; +} +/* Dokument-Feedback (Skala 1–5 + 2 Freitexte) für Gate 1/2/3 + Service Review */ +function docFeedbackBlock(sid){ + const key = "doc:"+sid; + const fb = (S.feedback && S.feedback[key]) || {}; + const saved = S.feedbackSaved && S.feedbackSaved[key]; + const label = FEEDBACK_DOCS[sid] || sid; + const scale = [1,2,3,4,5].map(n=>``).join(""); + return `
+
📝 Feedback zum Prüfschritt „${label}" · optional
+ +
${scale}
+ + + + +
${saved?`✓ gespeichert`:``}
+
`; +} + +/* Ein Feedback-Card aus dem DOM lesen; nur committen, wenn Inhalt vorhanden. */ +function commitFeedbackCard(card){ + const key = card.dataset.fbkey; + const rec = (S.feedback && S.feedback[key]) || {}; + card.querySelectorAll('.fbInput[data-f]').forEach(t=>{ rec[t.dataset.f] = t.value; }); + const sel = card.querySelector('.fbScale.sel'); if(sel) rec.skala = +sel.dataset.n; + S.feedback = S.feedback || {}; S.feedback[key] = rec; + const hasContent = (rec.relevant||rec.ueberfluessig||rec.fehlt||rec.weiteres||rec.skala); + if(!hasContent) return false; + S.feedbackSaved = S.feedbackSaved || {}; S.feedbackSaved[key] = true; + const record = Object.assign({ + id: key+"@"+feedbackSessionId(), + key, type: card.dataset.fbtype, target: card.dataset.target, + ts: new Date().toISOString(), session: feedbackSessionId() + }, fbCtx(), rec); + // Archiv + Queue (außerhalb des Spielstands, übersteht „Neu starten") + const arch = fbReadArr(FB_ARCHIVE).filter(r=>r.id!==record.id); arch.push(record); fbWriteArr(FB_ARCHIVE, arch); + const q = fbReadArr(FB_QUEUE).filter(r=>r.id!==record.id); q.push(record); fbWriteArr(FB_QUEUE, q); + return true; +} +/* Alle aktuell sichtbaren Feedback-Cards committen (z. B. beim Weiterblättern). */ +function captureFeedbackInputs(){ + let any=false; + document.querySelectorAll('.fbCard').forEach(card=>{ if(commitFeedbackCard(card)) any=true; }); + if(any){ save(); flushFeedbackQueue(); } + return any; +} +/* Offene Server-Submissions abarbeiten (Retry). Fehlt der Endpoint, bleibt alles in der Queue. */ +function flushFeedbackQueue(){ + const q = fbReadArr(FB_QUEUE); + if(!q.length || !navigator.onLine) return; + q.slice().forEach(record=>{ + fetch(FEEDBACK_ENDPOINT, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(record) }) + .then(r=>{ if(r.ok){ fbWriteArr(FB_QUEUE, fbReadArr(FB_QUEUE).filter(x=>x.id!==record.id)); } }) + .catch(()=>{ /* offline / kein Endpoint → bleibt für späteren Retry in der Queue */ }); + }); +} +/* Manueller Export (Backup) — lädt das gesammelte Feedback als JSON herunter. */ +function exportFeedback(){ + const data = fbReadArr(FB_ARCHIVE); + if(!data.length){ alert("Noch kein Feedback erfasst."); return; } + const pending = fbReadArr(FB_QUEUE).length; + const blob = new Blob([JSON.stringify({exported:new Date().toISOString(), pendingUpload:pending, items:data}, null, 2)], {type:"application/json"}); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "slc-feedback-"+new Date().toISOString().slice(0,10)+".json"; + document.body.appendChild(a); a.click(); a.remove(); +} +function wireFeedback(){ + document.querySelectorAll('.fbCard .fbScale[data-n]').forEach(btn=>{ + btn.onclick = ()=>{ btn.parentNode.querySelectorAll('.fbScale').forEach(x=>x.classList.remove('sel')); btn.classList.add('sel'); }; + }); + document.querySelectorAll('.fbCard .fbSaveBtn').forEach(btn=>{ + btn.onclick = ()=>{ const card = btn.closest('.fbCard'); commitFeedbackCard(card); save(); flushFeedbackQueue(); draw(); }; + }); +} + /* ====================== WIRING ====================== */ function wire(st){ const b = id => $("#"+id); @@ -2055,12 +2274,13 @@ function wire(st){ }; }); if(b("actBack")) b("actBack").onclick = ()=>{ + captureFeedbackInputs(); if(S.actDone){ S.actDone=false; } else if(S.actReveal){ S.actReveal=false; } else if((S.actStep||0)>0){ S.actStep--; S.actReveal=true; } save(); draw(); }; - if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; enterStation(S.index+1); save(); draw(); }; - if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); }; + if(b("nextStation")) b("nextStation").onclick = ()=>{ captureFeedbackInputs(); S.done[st.id]=true; enterStation(S.index+1); save(); draw(); }; + if(b("finish")) b("finish").onclick = ()=>{ captureFeedbackInputs(); S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); }; // Gate if(b("gateDeciderNext")) b("gateDeciderNext").onclick = ()=>{ S.gateDeciderDone=true; save(); draw(); }; $("#panel").querySelectorAll(".critItem[data-ci]").forEach(el=>{ @@ -2071,8 +2291,23 @@ function wire(st){ $("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{ el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); }; }); - if(b("gateBack")) b("gateBack").onclick = ()=>{ S.stage="gate"; save(); draw(); }; - if(b("gateNext")) b("gateNext").onclick = ()=>{ gateGoto(st, S.gatePick||0); }; + // Gate 1 — Freigabe-Entscheidung (Frank-Template): 0/1 → Routing, 2 → zurück an Design, 3 → Ablehnung. + $("#panel").querySelectorAll(".freigabeOpt[data-fi]").forEach(el=>{ + el.onclick = ()=>{ const fi=+el.dataset.fi; + if(fi<=1){ S.gate1Approval=fi; save(); draw(); return; } + S.done[st.id]=true; + if(fi===2){ // Zurück an Design — echte Rückschleife, Gate wird danach erneut vorgelegt + S.loopback = { gateId: st.id, gateNr: st.gateNr, untilIdx: S.index }; + enterStation(0); + } else { // Ablehnung + S.view="end"; S.endReason="rejected"; S.endGate=st.id; + } + save(); draw(); }; + }); + if(b("gateApprovalBack")) b("gateApprovalBack").onclick = ()=>{ S.gate1Approval=null; save(); draw(); }; + if(b("gateBack")) b("gateBack").onclick = ()=>{ captureFeedbackInputs(); S.stage="gate"; save(); draw(); }; + if(b("gateNext")) b("gateNext").onclick = ()=>{ captureFeedbackInputs(); gateGoto(st, S.gatePick||0); }; + wireFeedback(); } /* ====================== ABSCHLUSS-SCREEN ====================== */ @@ -2088,20 +2323,28 @@ function renderEnd(){ : `

Service-Lifecycle durchlaufen

Ihr habt den Change von der Einordnung bis zur Umsetzung begleitet. Im Workshop schließt hier das gemeinsame Debriefing am Tisch an (Reflexion der Stationen, offene Fragen).

`; + const ending = (!rejected && S.mode==="main") + ? slcOverview(null, `Alle Phasen durchlaufen — der Lifecycle ist komplett`) + phaseFeedbackBlock("review") + : ``; $("#panel").innerHTML = `
Abschluss

${icon} ${head}

${USE_CASES[S.service].service} · ${CHANGE_TYPES[S.change]}
${box} + ${ending}
`; - $("#toBonus").onclick = ()=>{ S.view="bonusPick"; save(); draw(); }; + wireFeedback(); + $("#toBonus").onclick = ()=>{ captureFeedbackInputs(); S.view="bonusPick"; save(); draw(); }; } /* ====================== INIT ====================== */ (function init(){ + flushFeedbackQueue(); // offene Feedback-Submissions nachreichen + window.addEventListener("online", flushFeedbackQueue); // bei Reconnect erneut versuchen + $("#fbExportBtn").onclick = exportFeedback; $("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } }; const closeOverlays = ()=> document.body.classList.remove("navOpen","akteOpen","rollenOpen"); $("#stationsBtn").onclick = ()=>{ const o=document.body.classList.contains("navOpen"); closeOverlays(); if(!o) document.body.classList.add("navOpen"); }; diff --git a/04_Tablet-Quiz/app/sw.js b/04_Tablet-Quiz/app/sw.js index daf8191..40a9a38 100644 --- a/04_Tablet-Quiz/app/sw.js +++ b/04_Tablet-Quiz/app/sw.js @@ -1,5 +1,5 @@ /* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */ -const CACHE = "slc-companion-v31"; +const CACHE = "slc-companion-v32"; const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"]; // Action-Card-Grafiken (cards/s-c.png) fuer Offline vorab cachen (alle 24). const CARDS = []; diff --git a/PROJEKTSTAND.md b/PROJEKTSTAND.md index 360592e..a5d66b4 100644 --- a/PROJEKTSTAND.md +++ b/PROJEKTSTAND.md @@ -4,7 +4,7 @@ Chat). Hier steht, **wo wir stehen, was entschieden wurde, was offen ist** und wie man App lokal startet/deployt. -**Stand:** 2026-06-09 · **Branch:** `feat/redesign-und-companion-app` · App **v0.7** +**Stand:** 2026-06-10 · **Branch:** `feat/redesign-und-companion-app` · App **v0.10** **Remote:** `https://git.1789.cloud/patrick/SLC_Game.git` --- @@ -76,8 +76,11 @@ Service gilt als bereits eingeführt) → zurück zur Bonus-Auswahl → „Servi zurück, blendet einen Nacharbeits-Banner ein und legt das Gate danach **erneut** vor (Hinweis „Erneute Vorlage"). „Ablehnung" → eigener **End-Screen** (SOR-Eskalation). (z. B. Gate 1 „Konfiguration" → `tr_05`; SOR→DPM→Mission Board.) -- **Kein App-Debrief mehr:** Der Export-Dialog (Markdown/JSON) wurde entfernt; das - Workshop-**Debriefing am Tisch** bleibt (im Done-Screen erwähnt). +- **Kein Lauf-Debrief mehr** (Markdown/JSON-Export des Durchlaufs raus); Workshop-**Debriefing + am Tisch** bleibt (im Done-Screen erwähnt). +- **Strukturiertes Feedback (v0.10, Frank):** Phasen-Feedback (2 Freitexte) an Phasenenden + + Dokument-Feedback (Skala 1–5 + 2 Freitexte) an Gate 1/2/3 + Service Review. **Server-Save** + via POST an `feedback.php` (Retry-Queue + localStorage-Archiv + manueller „⇩ Feedback"-Export). - **4 Change-Arten** (Major/Normal/Standard/Emergency), 24 Karten. Multiple-Choice ist aus dem Hauptfluss raus (Daten liegen noch in `index.html`, ungenutzt). - **Inhalte sind in `index.html` eingebettet** (noch keine YAML-Pipeline). @@ -108,6 +111,35 @@ Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:** ## 5. Offene Punkte / nächste Schritte +**Frank-Feedback 2026-06-10 (Workshop-/Spiel-Ebene, umgesetzt in App v0.10 — Entscheidungen Frank + Patrick):** +- [x] **RACI-Betonung:** Volle RACI bleibt; **R + A** sind als „Pflicht — unbedingt durchdenken" + hervorgehoben, **C + I** als „ergänzend / nice-to-have". Legende zweigeteilt, Schritt-3-Text + umformuliert (`activitySteps`, `raciLegendHtml`). +- [x] **GRAFIK SLC (Gesamtbild) 6×** im **Major-Walk**: Start (Main-Intro), an den 4 Phasenübergängen + (`renderActivity`/`renderGateDone`) und am Schluss (`renderEnd`) — jeweils mit Highlight der + aktuellen/nächsten Phase (`slcOverview` auf Basis von `phaseDonut`). **Bonus unberührt.** +- [x] **„Bearbeitet"/„Entwurf" sichtbar:** Badge im Stations-Header (`stationTiefe`/`tiefeBadge`). + Bearbeitet = `ds_01, tr_01, tr_09, tr_12, rv_01`; alles andere Entwurf. +- [x] **Gate 1 zweistufig:** erst **Freigabe-Entscheidung** (Freigabe / mit Auflagen / **Zurück an + Design** [Rückschleife] / **Ablehnung** [End-Screen]), dann **Routing** Entwicklung/Konfiguration. + Gate-Templates: `ziel` an Gate 1/2/3 ergänzt; Gate-1-Prüfdim. „Budget für die **Implementierung**". +- [x] **Service-Review-Template (Frank):** A14 + rv_01 auf 4 Dimensionen (Leistungserbringung, + Betriebsstabilität, Nutzerzufriedenheit, Zukunftsfähigkeit) + Empfehlung **Weiterbetrieb / + Normal-Change / Major-Change** umgestellt (**ersetzt** CONTINUE/IMPROVEMENT/REDESIGN/RETIRE). +- [x] **Feedback-Erfassung + Server-Save** (re-aktiviert nach v0.7-Entfernung des Exports): + - **Phasen-Feedback** (2 Freitexte) an jedem Phasenende; **Dokument-Feedback** (Skala 1–5 + + 2 Freitexte) an **Gate 1/2/3 + Service Review** (`FEEDBACK_DOCS`). + - **POST** an `` (Default `feedback.php`), **Retry-Queue + + localStorage-Archiv** (offline-fest, übersteht „Neu starten"), Header-Button **„⇩ Feedback"** + für manuellen JSON-Export als Backup. + - **Server:** Referenz-Collector `04_Tablet-Quiz/app/feedback.php` (Flat-File `feedback-data/feedback.jsonl`, + keine DB). **DEPLOY.md** angepasst (statisch **+ ein** kleiner POST-Endpoint; PHP/Node/„ohne"-Varianten). +- **Offen / fachlich mit Frank zu bestätigen:** + - **Retirement/Außerbetriebnahme:** in Franks Review-Empfehlung bewusst nicht mehr enthalten — + bestätigen, dass das so gewollt ist (vgl. `review-phase_arbeitsstand-frank.md`). + - **Server-Endpoint provisionieren:** PHP (php-fpm) am Webserver aktivieren **oder** Endpoint-URL + im Meta-Tag umbiegen. Bis dahin greift Queue + Export (kein Datenverlust). + **Frank-Feedback 2026-06-08 (Workshop-Ebene, entscheiden Frank + Patrick):** - [x] **Main/Bonus-Flow** umgesetzt (nur Major = voller Walk; Bonus = einordnen + Kurz-Auflösung). - [x] **Aufgabe „Freigabe-Stelle"** ergänzt (SOR/DPM/MB · SO · keine[Standard/Emergency]).