This commit is contained in:
breitenbach76 2026-06-06 16:38:46 +02:00
parent 36471297f5
commit 917bf1d613
3 changed files with 72 additions and 91 deletions

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>SLC-Workshop — Companion-App</title>
<meta name="description" content="Begleit-App zum SLC-Workshop-Tabletop: führt durch die Stationen, vermittelndes Quiz, Auflösung und Debrief. Offline lauffähig." />
<meta name="description" content="Begleit-App zum SLC-Workshop-Tabletop: führt durch die Stationen, vermittelndes Quiz und Auflösung. Offline lauffähig." />
<meta name="theme-color" content="#1d2430" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="icon.svg" />
@ -120,16 +120,6 @@
.actions .spacer { flex:1; }
.unclear { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:14px; }
dialog { border:none; border-radius:16px; padding:0; box-shadow: var(--shadow); max-width: 640px; width: 92%; }
dialog::backdrop { background: rgba(15,22,35,.45); }
.modal { padding: 24px; }
.modal h2 { margin:0 0 6px; }
pre.export { background:#0f1623; color:#d6e2f0; padding:14px; border-radius:10px; overflow:auto; max-height:320px; font-size:13px; }
.stat { display:flex; gap:20px; margin:14px 0; }
.stat div b { display:block; font-size:24px; }
.stat div span { color:var(--muted); font-size:12px; }
.note { font-size:12px; color:var(--muted); margin-top:18px; text-align:center; }
/* Setup-Screens (Action Card / Startpunkt) */
body:not(.runMode) aside { display:none; }
body:not(.runMode) .layout { grid-template-columns: 1fr; }
@ -193,7 +183,6 @@
<div id="cardBadge" class="cardBadge"></div>
<div class="spacer"></div>
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
<button class="primary" id="debriefBtn">Debrief</button>
</header>
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
@ -202,22 +191,6 @@
<main><div class="card" id="panel"></div></main>
</div>
<dialog id="debriefDlg">
<div class="modal">
<h2>Debrief-Export</h2>
<p style="color:var(--muted);margin-top:0">Lokale Auswertung dieses Durchlaufs — nichts wird hochgeladen.</p>
<div class="stat" id="debriefStat"></div>
<pre class="export" id="debriefText"></pre>
<div class="actions">
<button class="ghost" id="copyBtn">In Zwischenablage</button>
<button class="ghost" id="dlMd">↓ Markdown</button>
<button class="ghost" id="dlJson">↓ JSON</button>
<div class="spacer"></div>
<button class="primary" id="closeDebrief">Schließen</button>
</div>
</div>
</dialog>
<script>
/* Empfohlener Einstiegspunkt je Change-Typ (didaktische Auflösung in Schritt 2).
Reihenfolge entspricht CHANGE_TYPES. */
@ -851,7 +824,8 @@ function defaultState(){
classifyDone:false, classifyWrong:null,
entryDone:false, entryWrong:null,
index:0, stage:"discuss", quizIndex:0,
picks:{}, done:{}, unclear:{} };
picks:{}, done:{}, unclear:{},
loopback:null, revisit:{}, endReason:null, endGate:null };
}
let S = load();
function load(){ try{ return Object.assign(defaultState(), JSON.parse(localStorage.getItem(LS_KEY)||"{}")); }catch(e){ return defaultState(); } }
@ -907,6 +881,7 @@ function draw(){
if(S.view==="deck") return renderDeck();
if(S.view==="classify") return renderClassify();
if(S.view==="entry") return renderEntry();
if(S.view==="end") return renderEnd();
renderRun();
}
@ -1036,6 +1011,11 @@ const GATE_FLOW = {
};
function enterStation(idx){
idx = Math.max(0, Math.min(idx, STATIONEN.length-1));
// Echte Rueckschleife: erreicht man das Gate wieder (oder ueberholt es), Schleife schliessen.
if(S.loopback && idx >= S.loopback.untilIdx){
if(idx === S.loopback.untilIdx) (S.revisit = S.revisit || {})[STATIONEN[idx].id] = true;
S.loopback = null;
}
S.index = idx;
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
S.gatePick = null; S.quizIndex = 0;
@ -1043,9 +1023,15 @@ function enterStation(idx){
function gateGoto(st, i){
S.done[st.id] = true;
const t = (GATE_FLOW[st.id] || [])[i] || "next";
if(t==="end"){ save(); openDebrief(); return; }
if(t==="end"){ S.view="end"; S.endReason="rejected"; S.endGate=st.id; save(); draw(); return; }
if(t==="next"){ enterStation(S.index+1); }
else { const j = STATIONEN.findIndex(s=>s.id===t); enterStation(j>=0 ? j : S.index+1); }
else {
const j = STATIONEN.findIndex(s=>s.id===t);
const target = j>=0 ? j : S.index+1;
// Sprung nach hinten = echte Rueckschleife: Banner setzen, Gate wird danach erneut vorgelegt.
if(target < S.index){ S.loopback = { gateId: st.id, gateNr: st.gateNr, untilIdx: S.index }; }
enterStation(target);
}
save(); draw();
}
@ -1061,7 +1047,11 @@ function renderRun(){
const chip = st.typ==="gate"
? `<span class="phaseChip gateChip">⛩ Gate ${st.gateNr}</span>`
: `<span class="phaseChip" style="background:${ph.color}">${ph.label}</span>`;
const loopBanner = (S.loopback && S.index < S.loopback.untilIdx)
? `<div class="tourBanner"><b>Nacharbeit nach Gate ${S.loopback.gateNr}</b> — überarbeitet die folgenden Stationen; danach wird das Gate erneut vorgelegt und entscheidet neu.</div>`
: ``;
let body = `
${loopBanner}
${chip}
<div class="stationName">${st.name}</div>
<div class="stationId">${st.id}</div>
@ -1083,7 +1073,8 @@ function renderAct(st){
<div class="discuss">
<strong>Besprecht und legt am Brett ab:</strong>
<ul>
<li><b>Wer?</b> Rollen-Figuren ins <b>Aktiv-Feld (R · A · C · I)</b> stellen.</li>
<li><b>1 · Wer ist beteiligt?</b> Die Figuren der beteiligten Rollen auf die <b>Mulden des Station-Pucks</b> stellen.</li>
<li><b>2 · Wie ist die RACI?</b> Dieselben Figuren ins <b>Aktiv-Feld (R · A · C · I)</b> einsortieren.</li>
<li><b>Was passiert?</b> Kurz klären, was hier für euren Change getan wird.</li>
<li><b>Artefakt?</b> Passende <b>Artefaktkarte in die Service-Akte</b> legen (oder Status weiterschieben).</li>
</ul>
@ -1125,10 +1116,13 @@ function renderGate(st){
const pruef = (st.pruef||[]).map(([n,d])=>`<li><b>${n}</b> — ${d}</li>`).join("");
const opts = (st.pfade||[]).map(([n,d],i)=>
`<button class="choice" data-i="${i}"><b>${n}</b><br><span style="color:var(--muted);font-weight:400">${d}</span></button>`).join("");
const revisitNote = (S.revisit && S.revisit[st.id])
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft die Kriterien erneut und entscheidet neu.</div>` : ``;
return `
<div class="step">
<div class="stepHead"><span class="n"></span> Gate-Entscheidung</div>
<p>${st.beschreibung}</p>
${revisitNote}
<p><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
<h4>Prüft am Brett (Kriterien)</h4>
<ul>${pruef}</ul>
@ -1164,7 +1158,7 @@ function wire(st){
if(b("toReveal")) b("toReveal").onclick = ()=>{ S.stage="reveal"; save(); draw(); };
if(b("unclear")) b("unclear").onchange = e=>{ if(e.target.checked) S.unclear[st.id]=true; else delete S.unclear[st.id]; save(); renderList(); };
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; save(); openDebrief(); };
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
// Gate
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
@ -1173,59 +1167,33 @@ function wire(st){
if(b("gateNext")) b("gateNext").onclick = ()=>{ gateGoto(st, S.gatePick||0); };
}
/* ====================== DEBRIEF ====================== */
function quizScore(){
let correct=0, total=0;
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
const p = S.picks[pkey(st.id,qi)];
if(p!==undefined){ total++; if(p===q.richtig) correct++; }
}));
return {correct,total};
}
let DEBRIEF = { md:"", json:null };
function download(name, text, mime){
const blob = new Blob([text], {type:mime});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = name; document.body.appendChild(a); a.click();
a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000);
}
function openDebrief(){
const doneN = Object.keys(S.done).length;
const unclearList = STATIONEN.filter(st=>S.unclear[st.id]);
$("#debriefStat").innerHTML = `
<div><b>${doneN}/${STATIONEN.length}</b><span>Stationen bearbeitet</span></div>
<div><b>${unclearList.length}</b><span>als unklar markiert</span></div>`;
let md = `# SLC-Workshop — Debrief\n\n`;
md += `**Service:** ${USE_CASES[S.service].service}\n`;
md += `**Change-Typ:** ${CHANGE_TYPES[S.change]}\n`;
md += `**Action Card:** „${acard(S.service,S.change).titel}“ — ${acard(S.service,S.change).text}\n`;
md += `**Stationen bearbeitet:** ${doneN}/${STATIONEN.length}\n\n`;
md += `## Als unklar markiert\n`;
md += unclearList.length ? unclearList.map(st=>`- ${st.id} — ${st.name}`).join("\n") : "_keine_";
const stamp = new Date().toISOString().slice(0,16).replace("T"," ");
const json = {
erzeugt: stamp,
service: USE_CASES[S.service].service,
changeTyp: CHANGE_TYPES[S.change],
actionCard: acard(S.service,S.change).titel,
ausloeser: acard(S.service,S.change).text,
stationenBearbeitet: doneN,
stationenGesamt: STATIONEN.length,
unklar: unclearList.map(st=>({id:st.id, name:st.name}))
};
DEBRIEF = { md, json };
$("#debriefText").textContent = md;
$("#debriefDlg").showModal();
/* ====================== ABSCHLUSS-SCREEN ====================== */
function renderEnd(){
const rejected = S.endReason==="rejected";
const gate = rejected ? STATIONEN.find(s=>s.id===S.endGate) : null;
const head = rejected ? "Change abgelehnt" : "Durchlauf abgeschlossen";
const icon = rejected ? "✗" : "✓";
const box = rejected
? `<div class="recBox" style="border-left-color:var(--bad)">
<h4>Abgelehnt an ${gate ? "Gate "+gate.gateNr : "einem Gate"}</h4>
<p style="margin:0;color:var(--muted)">Das Vorhaben wird in dieser Form nicht weiterverfolgt. Eine endgültige Ablehnung erfordert die <b>SOR-Eskalation</b>.</p></div>`
: `<div class="recBox">
<h4>Service-Lifecycle durchlaufen</h4>
<p style="margin:0;color:var(--muted)">Ihr habt den Change von der Einordnung bis zur Umsetzung begleitet. Im Workshop schließt hier das gemeinsame <b>Debriefing am Tisch</b> an (Reflexion der Stationen, offene Fragen).</p></div>`;
$("#panel").innerHTML = `
<div class="setupHead">Abschluss</div>
<h2 class="setupTitle">${icon} ${head}</h2>
<div class="hint ${rejected?"bad":"ok"}">${USE_CASES[S.service].service} · ${CHANGE_TYPES[S.change]}</div>
${box}
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="endRestart">Neue Action Card →</button>
</div>`;
$("#endRestart").onclick = ()=>{ S = defaultState(); save(); draw(); };
}
/* ====================== INIT ====================== */
(function init(){
$("#debriefBtn").onclick = openDebrief;
$("#closeDebrief").onclick = ()=> $("#debriefDlg").close();
$("#copyBtn").onclick = ()=> navigator.clipboard?.writeText($("#debriefText").textContent);
$("#dlMd").onclick = ()=> download("slc-debrief.md", DEBRIEF.md, "text/markdown");
$("#dlJson").onclick = ()=> download("slc-debrief.json", JSON.stringify(DEBRIEF.json, null, 2), "application/json");
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
draw();
})();

View file

@ -1,5 +1,5 @@
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
const CACHE = "slc-companion-v4";
const CACHE = "slc-companion-v5";
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 30).
const CARDS = [];

View file

@ -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-06 · **Branch:** `feat/redesign-und-companion-app` · **HEAD:** `a922300`
**Stand:** 2026-06-06 · **Branch:** `feat/redesign-und-companion-app` · **HEAD:** `3647129`
**Remote:** `https://git.1789.cloud/patrick/SLC_Game.git`
---
@ -49,11 +49,18 @@ Operations/Service-Owner/Support. Mix aus **Vermittlung** (Lifecycle + Stationen
### Companion-App (`04_Tablet-Quiz/app/`) — statische PWA
**Flow:** Karten-Raster (Action Card ziehen) → **Change-Art klassifizieren** (Legende,
„nochmal" bis richtig) → **Phasen-Einstieg** (Lebenszyklus-Phase anklicken, retry) →
**Stationen** → **Debrief** (Markdown/JSON-Export).
- **Aktivitäts-Station, 2 Takte:** „Handeln am Brett" (Figuren ins RACI-Feld,
Artefaktkarte in die Akte; „Zeig mir"-Hilfe) → „Auflösung & Abgleich".
**Stationen** → **Abschluss-Screen** („abgeschlossen" oder „abgelehnt").
- **Aktivitäts-Station, 2 Takte:** „Handeln am Brett" → „Auflösung & Abgleich".
**Figuren-Regel (zweistufig):** (1) beteiligte Rollen auf die **Mulden des
Station-Pucks** stellen, (2) dieselben Figuren ins **Aktiv-Feld (R·A·C·I)**
einsortieren = RACI-Antwort. Artefaktkarte in die Akte; „Zeig mir"-Hilfe.
- **Gate-Station interaktiv:** Kriterien prüfen → Entscheidung → Konsequenz +
**Verzweigung** (z. B. Gate 1 „Konfiguration" → springt zu `tr_05`; SOR→DPM→Mission Board).
**Verzweigung**. **Echte Rückschleifen:** „Zurück an Build/Transition" springt
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).
- **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).
@ -67,6 +74,8 @@ Operations/Service-Owner/Support. Mix aus **Vermittlung** (Lifecycle + Stationen
**Kanonisch / stabil:**
- Puck-System statt Tiles; ein STL für die Bahn; Beschriftung via Etikett.
- Aktiv-Feld 2×2; Phasen-Ring; keine Magnete; kein Action-Stein.
- **Figuren-Regel (zweistufig):** Station-Puck-Mulden = „welche Rollen sind beteiligt?",
Aktiv-Feld (R·A·C·I) = „wie ist die RACI?". An Gates: Pflicht-Figuren am Gate-Puck.
**Workshop-Arbeitsstand — bewusst NICHT im Blueprint-YAML und NICHT im kanonischen
Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:**
@ -81,9 +90,13 @@ Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:**
- Details/Quelle: `00_Konzept/review-phase_arbeitsstand-frank.md`.
## 5. Offene Punkte / nächste Schritte
- [ ] **Figuren-Regel festzurren:** Aktiv-Feld = RACI-Antwort, Puck-Mulden = nur Gate-Versammlung?
- [ ] **Echte Gate-Rückschleifen** (Zurück/Ablehnung) statt vereinfachtem Weiterspringen.
- [ ] **Debrief** auf tatsächliche **Pfadlänge** statt „X/39" umstellen (Verzweigung).
- [x] **Figuren-Regel festgezurrt** (zweistufig: Puck = beteiligte Rollen, Aktiv-Feld =
RACI). In der App umgesetzt. **Offen:** in `materialliste.md` / `board-layout` /
`README_konzept.md` nachziehen.
- [x] **Echte Gate-Rückschleifen** umgesetzt (Nacharbeits-Banner + erneute Gate-Vorlage;
Ablehnung → End-Screen). **Offen ggf.:** Auflagen-Pfad sichtbar vom reinen „Freigabe"
unterscheiden (verhält sich aktuell identisch = weiter).
- [x] **App-Debrief entfernt** (Punkt „Pfadlänge X/39" damit hinfällig).
- [ ] **MC-Quiz** optional als „Wissens-Check" reaktivieren? (Daten sind noch da.)
- [ ] **YAML→Inhalts-Pipeline** (Stationsdaten aus `service-lifecycle_*.yaml`) — **braucht Zugriff aufs Blueprint-Repo**.
- [ ] Nach **Michael-Freigabe:** kanonisches Konzept (`README_konzept.md`), YAML und ggf. `bauteile-masse.svg`/`visual-prompts` final nachziehen.