This commit is contained in:
breitenbach76 2026-06-09 00:13:53 +02:00
parent acd40599ab
commit 32c2556ec9
3 changed files with 254 additions and 44 deletions

View file

@ -253,6 +253,14 @@
.deckCard:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.16)}
.deckCard img{display:block;width:100%;height:auto}
.cardThumb{display:block;width:150px;border-radius:8px;margin:0 auto 14px;box-shadow:0 2px 10px rgba(0,0,0,.12)}
.deckRow.mainRow,.deckRow.bonusRow{grid-template-columns:repeat(3,1fr)}
.deckCard{position:relative}
.deckCard.mainCard,.deckCard.bonusCard{display:flex;flex-direction:column}
.deckMeta{padding:8px 10px;text-align:left}
.deckMeta b{display:block;font-size:14px;color:var(--ink);line-height:1.25}
.deckMeta span{display:block;font-size:11px;color:var(--muted);margin-top:2px}
.deckCard.svcDone{opacity:.6}
.svcBadge{position:absolute;top:8px;right:8px;background:var(--ok);color:#fff;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
.choiceGrid{display:flex;flex-direction:column;gap:8px;margin:10px 0}
.choiceGrid.grid2{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
@media(max-width:440px){.choiceGrid.grid2{grid-template-columns:1fr}}
@ -288,7 +296,7 @@
<body>
<header>
<div class="brand">SLC&nbsp;<b>Companion</b></div>
<span class="tag">v0.6</span>
<span class="tag">v0.7</span>
<div id="cardBadge" class="cardBadge"></div>
<div class="spacer"></div>
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁&nbsp;Akte</button>
@ -415,7 +423,7 @@ const CHANGE_LEGEND = [
"Vollständige Bewertung, Business Case, ausführliche Planung & Tests, Kommunikationsplan",
"Durchläuft den vollen Lebenszyklus ab dem Design (alle Gates)"],
bsp:"Rechenzentrumsumzug, Austausch eines Kernsystems, organisationsweite Plattformmigration." },
{ idee:"Der Regelfall für alles, was nicht vorab genehmigt ist und kein Notfall ist. Durchläuft den Bewertungs- und Gate-Prozess.",
{ idee:"Der Regelfall für alles, was nicht vorab genehmigt ist und kein Notfall ist. Durchläuft einen strukturierten Bewertungs- und Freigabeprozess.",
bed:["Änderungsantrag (RfC) wird erfasst",
"Risiko- und Impact-Bewertung wird durchgeführt",
"Freigabe an den Gates durch die SOR (Gate 2 durch den Service Owner) vor Umsetzung",
@ -436,6 +444,37 @@ const CHANGE_LEGEND = [
];
// Anzeige-Reihenfolge der Change-Arten (Indizes in CHANGE_TYPES): Standard, Emergency, Normal, Major
const CT_ORDER = [2, 3, 1, 0];
/* Aufgabe 2: Wo wird der Change freigegeben? (Cluster nach Frank; Index parallel zu CHANGE_TYPES) */
const FREIGABE_OPTIONS = [
"SOR / DPM / Mission Board",
"Service Owner (SO)",
"Keine Freigabe — Standard (vorab genehmigt)",
"Keine Freigabe — Emergency (nachgelagert)"
];
const FREIGABE_ORDER = [0, 1, 2, 3];
const FREIGABE_CORRECT = [0, 1, 2, 3]; // Major→SOR/DPM/MB · Normal→SO · Standard→keine · Emergency→keine
const FREIGABE_GRUND = [
"Ein Major Change wird in der <b>SOR</b> freigegeben. Reicht deren Ressourcen- und Entscheidungshoheit nicht, wird daraus ein <b>Demand</b> — über den DPM ans <b>Mission Board</b>.",
"Ein Normal Change wird vom <b>Service Owner</b> freigegeben und umgesetzt; er berührt die SOR nicht (kein Gate 3).",
"Ein Standard Change ist über den Standard-Change-Katalog <b>generell vorab autorisiert</b> — keine Einzelfreigabe nötig.",
"Ein Emergency Change wird <b>sofort</b> umgesetzt; die formale Freigabe (Gate 3 / SOR) erfolgt <b>nachgelagert</b> zur Dokumentation."
];
/* Bonus-Karten: Service ist bereits live. Welche Phasen sind für diese Change-Art
noch relevant — und welche fallen weg? (Index parallel zu CHANGE_TYPES; 0/Major ungenutzt) */
const BONUS_AUFLOESUNG = [
null,
{ relevant:["Transition (verkürzt, meist Konfiguration)","Operation"],
wegfall:["Design (Service existiert bereits)","voller Review"],
text:"Der Service läuft schon — ein neues Design entfällt. Der Normal Change steigt in der <b>Transition</b> ein (meist der Konfigurationspfad), wird vom <b>Service Owner</b> freigegeben (Gate 2) und geht zurück in den <b>Betrieb</b>. Ein kompletter Review-Durchlauf ist für diese überschaubare Änderung nicht nötig." },
{ relevant:["Operation (laufender Betrieb)"],
wegfall:["Design","Transition & Gates","Review"],
text:"Ein Standard Change ist über den Katalog vorab autorisiert. Er wird <b>direkt im laufenden Betrieb</b> umgesetzt — keine Design- oder Transition-Phase, keine Gate-Freigabe, kein Review." },
{ relevant:["beschleunigte Umsetzung / Deployment","Operation & Support","nachgelagerte Freigabe + Doku"],
wegfall:["Design","reguläre Vorab-Freigabe an den Gates"],
text:"Beim Emergency Change zählt Tempo: Der Fix wird <b>sofort</b> ausgerollt, um die Störung zu beheben. Die formale Freigabe (Gate 3 / SOR) und die Dokumentation erfolgen <b>nachgelagert</b>; danach geht der Service in den normalen Betrieb zurück." }
];
// Feste, EINMALIG gemischte Deck-Reihenfolge ([service, change]) — bei jedem Start gleich, nicht gruppiert.
const DECK_ORDER = [[2,1],[0,3],[4,0],[1,2],[5,3],[3,0],[0,1],[2,3],[4,2],[1,0],[5,1],[3,2],
[2,0],[0,2],[4,3],[1,3],[5,0],[3,1],[2,2],[0,0],[4,1],[1,1],[5,2],[3,3]];
@ -445,7 +484,7 @@ const USE_CASES = [
changes:[
{titel:"Open Source von oben!", text:"Der OB gibt die Richtung vor: Die proprietäre VDI-Lösung soll auf eine Open-Source-Alternative (OpenStack + Thin-Client) umgestellt werden."},
{titel:"Tapetenwechsel", text:"Die Stadt bekommt ein neues Logo — der Desktop-Hintergrund aller virtuellen Arbeitsplätze muss angepasst werden."},
{titel:"Quartalspflege", text:"Das turnusmäßige Firmware-Update der VDI-Host-Hypervisoren steht an — im Standard-Change-Katalog längst hinterlegt."},
{titel:"Quartalspflege", text:"Das turnusmäßige Firmware-Update der VDI-Host-Hypervisoren steht an."},
{titel:"Blackout!", text:"Ein Stromausfall reißt ein ganzes VDI-Host-Cluster aus dem Betrieb — die Sitzungen müssen sofort auf ein Backup-Cluster migriert werden."}
]},
{ service:"Managed VPN-Access Service",
@ -453,7 +492,7 @@ const USE_CASES = [
changes:[
{titel:"Brüssel ruft!", text:"Eine neue EU-weite IT-Sicherheitsverordnung zwingt dazu, die gesamte VPN-Architektur neu aufzustellen."},
{titel:"Heimvorteil", text:"Ein neues Intranet-Portal soll in die Split-Tunnel-Liste, damit Mitarbeitende auch aus dem Homeoffice darauf zugreifen."},
{titel:"Monatsroutine", text:"Das monatliche Firmware-Update der VPN-Appliance steht an — als Standard-Change bereits freigegeben."},
{titel:"Monatsroutine", text:"Das monatliche Firmware-Update der VPN-Appliance steht an."},
{titel:"Gephisht!", text:"Ein erfolgreicher Phishing-Angriff hat eine VPN-Zertifikatskette kompromittiert — sofort sperren und neu ausstellen."}
]},
{ service:"Online-Bürgerportal für Meldungen & Anträge",
@ -461,7 +500,7 @@ const USE_CASES = [
changes:[
{titel:"Mitreden, Pflicht!", text:"Ein neues Landesgesetz schreibt digitale Bürgerbeteiligung vor — das Portal muss um komplette Beteiligungs-Module erweitert werden."},
{titel:"Rotstift gefragt", text:"Der Bürgerservice meldet einen Rechtschreibfehler in einem statischen Hinweistext, der korrigiert werden muss."},
{titel:"Patchday", text:"Das monatliche Sicherheits-Patch des Webservers (Apache/Nginx) steht an — im Change-Katalog definiert."},
{titel:"Patchday", text:"Das monatliche Sicherheits-Patch des Webservers (Apache/Nginx) steht an."},
{titel:"Lücke im Formular!", text:"In einem Eingabe-Formular wird eine kritische XSS-Schwachstelle entdeckt — ein Hotfix muss sofort raus."}
]},
{ service:"Zentrales Dokumenten-Management-System (DMS)",
@ -477,7 +516,7 @@ const USE_CASES = [
changes:[
{titel:"Norm-Zwang", text:"Eine bundesweite Vorgabe zu EU-Standards erzwingt die komplette Migration des GIS-Stacks auf konforme Services und Datenmodelle."},
{titel:"Falsch beschriftet", text:"Das Bauamt meldet eine falsche Beschriftung eines Karten-Layers, die korrigiert werden muss."},
{titel:"GeoServer-Update", text:"Das monatliche Update der GIS-Software (GeoServer 2.23 → 2.24) steht an — im Standard-Change-Katalog."},
{titel:"GeoServer-Update", text:"Das monatliche Update der GIS-Software (GeoServer 2.23 → 2.24) steht an."},
{titel:"Schnittstelle offen!", text:"An einer Schnittstelle wird eine kritische Schwachstelle entdeckt, die unautorisierten Datenzugriff erlaubt — Dienst sofort abschalten und patchen."}
]},
{ service:"Beschaffungs- und Vertrags-System für Fachämter",
@ -485,7 +524,7 @@ const USE_CASES = [
changes:[
{titel:"Vergabe neu!", text:"Eine neue EU-Vergaberichtlinie zwingt zur Einführung von E-Invoicing und erweiterten Transparenz-Reports."},
{titel:"Vierstellig, bitte", text:"Das Finanzamt wünscht eine kleine Anpassung: aus dem Label „Kostenstelle“ wird „Kostenstelle (4-stellig)“."},
{titel:"Patch-Quartal", text:"Das quartalsweise Sicherheits-Patch des Anwendungsservers steht an — bereits im Change-Katalog."},
{titel:"Patch-Quartal", text:"Das quartalsweise Sicherheits-Patch des Anwendungsservers steht an."},
{titel:"Upload-Falle!", text:"Im Vertrags-Upload wird eine kritische Lücke entdeckt, über die sich Schadcode hochladen lässt — Endpoint sofort sperren, Hotfix einspielen."}
]}
];
@ -1056,9 +1095,11 @@ function seedAkte(entryIdx){
/* ====================== STATE ====================== */
const LS_KEY = "slc-companion-proto";
function defaultState(){
return { view:"deck", service:null, change:null,
return { view:"deck", mode:"main", service:null, change:null,
classifyDone:false, classifyWrong:null,
freigabeDone:false, freigabeWrong:null,
entryDone:false, entryWrong:null,
bonusReveal:false, bonusDone:{}, servicesDone:{},
index:0, stage:"discuss", quizIndex:0,
actStep:0, actReveal:false, actDone:false, arteWrong:null,
picks:{}, done:{}, akte:{},
@ -1157,10 +1198,13 @@ function draw(){
document.body.classList.toggle("runMode", S.view==="run");
if(S.view!=="run"){ document.body.classList.remove("navOpen","akteOpen","rollenOpen"); }
renderCardBadge();
if(S.view==="deck") return renderDeck();
if(S.view==="classify") return renderClassify();
if(S.view==="entry") return renderEntry();
if(S.view==="end") return renderEnd();
if(S.view==="deck") return renderDeck();
if(S.view==="classify") return renderClassify();
if(S.view==="freigabe") return renderFreigabe();
if(S.view==="entry") return renderEntry();
if(S.view==="bonusPick") return renderBonusPick();
if(S.view==="bonus") return renderBonus();
if(S.view==="end") return renderEnd();
renderRun();
}
@ -1174,19 +1218,25 @@ function renderCardBadge(){
/* ---------- Schritt 1: Action Card ziehen (Raster aller Karten) ---------- */
function renderDeck(){
const cards = DECK_ORDER.map(([si,ci])=>{
const c = USE_CASES[si].changes[ci];
return `<button class="deckCard" data-s="${si}" data-c="${ci}" title="${c.titel}">
<img src="cards/s${si}-c${ci}.png" alt="${c.titel}" loading="lazy"></button>`;
const cards = USE_CASES.map((u,si)=>{
const c = u.changes[0];
const done = S.servicesDone && S.servicesDone[si];
return `<button class="deckCard mainCard ${done?'svcDone':''}" data-s="${si}" title="${c.titel}">
<img src="cards/s${si}-c0.png" alt="${c.titel}" loading="lazy">
<div class="deckMeta"><b>${c.titel}</b><span>${u.service}</span></div>
${done?'<span class="svcBadge">✓ gespielt</span>':''}
</button>`;
}).join("");
$("#panel").innerHTML = `
<div class="setupHead">Schritt 1 · Action Card ziehen</div>
<h2 class="setupTitle">Welche Karte habt ihr gezogen?</h2>
<p class="muted">Tippt auf die Action Card, die ihr gezogen habt.</p>
<div class="deck"><div class="deckRow">${cards}</div></div>`;
<div class="setupHead">Schritt 1 · Service wählen (Main Action Card)</div>
<h2 class="setupTitle">Welchen Service führt ihr ein?</h2>
<p class="muted">Jede Main-Karte ist ein <b>Major Change</b> — ihr spielt den Service einmal komplett von Design bis Review durch. Die Varianten (Bonus-Karten) kommen danach.</p>
<div class="deck"><div class="deckRow mainRow">${cards}</div></div>`;
$("#panel").querySelectorAll(".deckCard").forEach(el=>{
el.onclick=()=>{ S.service=+el.dataset.s; S.change=+el.dataset.c;
S.classifyDone=false; S.classifyWrong=null; S.entryDone=false; S.entryWrong=null;
el.onclick=()=>{ S.service=+el.dataset.s; S.change=0; S.mode="main";
S.classifyDone=false; S.classifyWrong=null;
S.freigabeDone=false; S.freigabeWrong=null;
S.entryDone=false; S.entryWrong=null; S.bonusDone={};
S.view="classify"; save(); draw(); };
});
}
@ -1204,7 +1254,7 @@ function renderClassify(){
const hint = S.classifyWrong!=null
? `<div class="hint bad">Nicht ganz — überlegt nochmal und probiert es erneut.</div>` : ``;
$("#panel").innerHTML = `
<div class="setupHead">Schritt 2 · Change-Art bestimmen</div>
<div class="setupHead">Aufgabe 1 · Change-Art bestimmen</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
@ -1224,7 +1274,7 @@ function renderClassify(){
$("#backDeck").onclick=()=>{ S.view="deck"; save(); draw(); };
} else {
$("#panel").innerHTML = `
<div class="setupHead">Schritt 3 · Erfolgreiche Kategorisierung</div>
<div class="setupHead">Aufgabe 1 · Change-Art ✓</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
@ -1238,14 +1288,64 @@ function renderClassify(){
<div class="actions">
<button class="ghost" id="backDeck">← Andere Karte</button>
<div class="spacer"></div>
<button class="primary" id="toEntry">Weiter → Einstieg finden</button>
<button class="primary" id="toFreigabe">Weiter → Freigabe bestimmen</button>
</div>`;
$("#backDeck").onclick=()=>{ S.view="deck"; save(); draw(); };
$("#toFreigabe").onclick=()=>{ S.view="freigabe"; S.freigabeDone=false; S.freigabeWrong=null; save(); draw(); };
}
}
/* ---------- Aufgabe 2: Freigabe-Stelle bestimmen (retry bis richtig) ----- */
function renderFreigabe(){
const correct = FREIGABE_CORRECT[S.change];
const card = acard(S.service,S.change);
const cardBig = `<img class="classifyCard" src="cards/s${S.service}-c${S.change}.png" alt="${card.titel}">`;
if(!S.freigabeDone){
const choices = FREIGABE_ORDER.map(i=>
`<button class="choice ${S.freigabeWrong===i?'bad':''}" data-i="${i}">${FREIGABE_OPTIONS[i]}</button>`).join("");
const hint = S.freigabeWrong!=null
? `<div class="hint bad">Nicht ganz — überlegt, wer diese Change-Art freigeben darf, und probiert es erneut.</div>` : ``;
$("#panel").innerHTML = `
<div class="setupHead">Aufgabe 2 · Freigabe bestimmen</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">Change-Art: ${CHANGE_TYPES[S.change]}</div>
<h2 class="setupTitle" style="margin-top:8px">An welcher Stelle wird dieser Change freigegeben?</h2>
<p class="muted">Überlegt gemeinsam, wer über diese Change-Art entscheidet.</p>
${hint}
<div class="choiceGrid grid2">${choices}</div>
</div>
</div>
<div class="actions"><button class="ghost" id="backClassify">← zurück</button></div>`;
$("#panel").querySelectorAll(".choice").forEach(el=>{
el.onclick=()=>{ const i=+el.dataset.i;
if(i===correct){ S.freigabeWrong=null; S.freigabeDone=true; } else { S.freigabeWrong=i; }
save(); renderFreigabe(); };
});
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
} else {
$("#panel").innerHTML = `
<div class="setupHead">Aufgabe 2 · Freigabe ✓</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">✓ Freigabe: ${FREIGABE_OPTIONS[correct]}</div>
<div class="recBox"><h4>Warum?</h4>
<p style="margin:0;color:var(--ink)">${FREIGABE_GRUND[correct]}</p></div>
</div>
</div>
<div class="actions">
<button class="ghost" id="backClassify">← zurück</button>
<div class="spacer"></div>
<button class="primary" id="toEntry">Weiter → Einstieg finden</button>
</div>`;
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
$("#toEntry").onclick=()=>{ S.view="entry"; S.entryDone=false; S.entryWrong=null; save(); draw(); };
}
}
/* ---------- Schritt 4+5: Einstieg finden (Phase anklicken) -------------- */
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
function renderEntry(){
const rec = START_EMPFEHLUNG[S.change];
const recIndex = STATIONEN.findIndex(s=>s.id===rec.id);
@ -1259,27 +1359,31 @@ function renderEntry(){
const hint = S.entryWrong
? `<div class="hint bad">Diese Phase passt nicht zur Change-Art — denkt an die Definition und probiert es erneut.</div>` : ``;
$("#panel").innerHTML = `
<div class="setupHead">Schritt 4 · Einstieg finden</div>
<div class="setupHead">Aufgabe 3 · Einstieg finden</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">Change-Art: ${CHANGE_TYPES[S.change]}</div>
<h2 class="setupTitle" style="margin-top:8px">In welcher Phase startet dieser Change?</h2>
<p class="muted">Klickt auf die Lebenszyklus-Phase, in der dieser Change einsteigt.</p>
<div class="hint ok">Change-Art: ${CHANGE_TYPES[S.change]} · Freigabe: ${FREIGABE_OPTIONS[FREIGABE_CORRECT[S.change]]}</div>
<h2 class="setupTitle" style="margin-top:8px">Wo würde die Umsetzung starten — nachdem der Change freigegeben ist?</h2>
<p class="muted">Klickt auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.</p>
${hint}
<div class="phaseRow">${zones}</div>
</div>
</div>
<div class="actions"><button class="ghost" id="backClassify">← zurück</button></div>`;
<div class="actions"><button class="ghost" id="backFreigabe">← zurück</button></div>`;
$("#panel").querySelectorAll(".phaseZone").forEach(el=>{
el.onclick=()=>{ const ph=el.dataset.ph;
if(ph===correctPhase){ S.entryWrong=null; S.entryDone=true; } else { S.entryWrong=ph; }
save(); renderEntry(); };
});
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
$("#backFreigabe").onclick=()=>{ S.view="freigabe"; save(); draw(); };
} else {
const isBonus = S.mode==="bonus";
const startBtn = isBonus
? `<button class="primary" id="toBonusDisc">Weiter → Varianten besprechen →</button>`
: `<button class="primary" id="startRun">Los geht's → voller Durchlauf</button>`;
$("#panel").innerHTML = `
<div class="setupHead">Schritt 5 · Los geht's</div>
<div class="setupHead">${isBonus?'Aufgabe 3 · Einstieg ✓':"Los geht's"}</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
@ -1290,15 +1394,99 @@ function renderEntry(){
</div>
</div>
<div class="actions">
<button class="ghost" id="backClassify">← zurück</button>
<button class="ghost" id="backFreigabe">← zurück</button>
<div class="spacer"></div>
<button class="primary" id="startRun">Los geht's →</button>
${startBtn}
</div>`;
$("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); };
$("#startRun").onclick=()=>{ seedAkte(recIndex); enterStation(recIndex); S.view="run"; save(); draw(); };
$("#backFreigabe").onclick=()=>{ S.view="freigabe"; save(); draw(); };
const sr = $("#startRun"); if(sr) sr.onclick=()=>{ seedAkte(recIndex); enterStation(recIndex); S.view="run"; save(); draw(); };
const tb = $("#toBonusDisc"); if(tb) tb.onclick=()=>{ S.bonusReveal=false; S.view="bonus"; save(); draw(); };
}
}
/* ---------- Bonus-Auswahl: Varianten des bereits live gegangenen Service - */
function renderBonusPick(){
const u = USE_CASES[S.service];
const order = [2,3,1]; // Standard, Emergency, Normal (Major war die Main-Karte)
const cards = order.map(ci=>{
const c = u.changes[ci];
const done = S.bonusDone && S.bonusDone[ci];
return `<button class="deckCard bonusCard ${done?'svcDone':''}" data-c="${ci}" title="${c.titel}">
<img src="cards/s${S.service}-c${ci}.png" alt="${c.titel}" loading="lazy">
<div class="deckMeta"><b>${c.titel}</b><span>${CHANGE_TYPES[ci]}</span></div>
${done?'<span class="svcBadge">✓ besprochen</span>':''}
</button>`;
}).join("");
const allDone = order.every(ci => S.bonusDone && S.bonusDone[ci]);
$("#panel").innerHTML = `
<div class="setupHead">Bonus · Varianten · ${u.service}</div>
<h2 class="setupTitle">Der Service läuft jetzt — welche Änderungen kommen im Betrieb?</h2>
<p class="muted">Diese drei Varianten betreffen den <b>bereits eingeführten</b> Service. Sie werden nicht komplett durchgespielt — ihr bestimmt nur <b>Change-Art</b>, <b>Freigabe</b> und <b>Einstieg</b> und besprecht, welche Phasen noch relevant sind.</p>
<div class="deck"><div class="deckRow bonusRow">${cards}</div></div>
<div class="actions">
<button class="ghost" id="bonusToDeck">← Service-Deck</button>
<div class="spacer"></div>
<button class="${allDone?'primary':'ghost'}" id="bonusFinish">${allDone?'✓ Service abschließen → nächster Service':'Service später abschließen →'}</button>
</div>`;
$("#panel").querySelectorAll(".bonusCard").forEach(el=>{
el.onclick=()=>{ S.change=+el.dataset.c; S.mode="bonus";
S.classifyDone=false; S.classifyWrong=null;
S.freigabeDone=false; S.freigabeWrong=null;
S.entryDone=false; S.entryWrong=null; S.bonusReveal=false;
S.view="classify"; save(); draw(); };
});
$("#bonusToDeck").onclick=()=>{ S.view="deck"; save(); draw(); };
$("#bonusFinish").onclick=()=>{ (S.servicesDone=S.servicesDone||{})[S.service]=true; S.view="deck"; save(); draw(); };
}
/* ---------- Bonus-Diskussion + Auflösung -------------------------------- */
function renderBonus(){
const a = BONUS_AUFLOESUNG[S.change] || {relevant:[],wegfall:[],text:""};
const card = acard(S.service,S.change);
const cardBig = `<img class="classifyCard" src="cards/s${S.service}-c${S.change}.png" alt="${card.titel}">`;
const correctPhase = STATIONEN[STATIONEN.findIndex(s=>s.id===START_EMPFEHLUNG[S.change].id)].phase;
let body;
if(!S.bonusReveal){
body = `
<div class="frageBox" style="border-left-color:var(--accent)">
<div class="frageLabel">Diskussion</div>
Der Service <b>${USE_CASES[S.service].service}</b> läuft bereits. Diskutiert gemeinsam:
<b>Welche Phasen und Aktivitäten wären für diesen ${CHANGE_TYPES[S.change]} noch relevant — und welche fallen weg?</b>
</div>
<div class="actions">
<button class="ghost" id="bonusBack">← zurück</button>
<div class="spacer"></div>
<button class="primary" id="bonusRevealBtn">Auflösen →</button>
</div>`;
} else {
const rel = (a.relevant||[]).map(x=>`<li>${x}</li>`).join("");
const weg = (a.wegfall||[]).map(x=>`<li>${x}</li>`).join("");
body = `
<div class="aufBox">
<h4 class="aufH">Relevant</h4><ul>${rel}</ul>
<h4 class="aufH">Fällt weg</h4><ul>${weg}</ul>
<p style="margin:10px 0 0">${a.text}</p>
</div>
<div class="actions">
<button class="ghost" id="bonusBack">← zurück</button>
<div class="spacer"></div>
<button class="primary" id="bonusDoneBtn">✓ Variante abschließen →</button>
</div>`;
}
$("#panel").innerHTML = `
<div class="setupHead">Bonus · ${CHANGE_TYPES[S.change]}</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">Change-Art: ${CHANGE_TYPES[S.change]} · Freigabe: ${FREIGABE_OPTIONS[FREIGABE_CORRECT[S.change]]} · Einstieg: ${PHASEN[correctPhase].label}</div>
${body}
</div>
</div>`;
const bb=$("#bonusBack"); if(bb) bb.onclick=()=>{ if(S.bonusReveal){ S.bonusReveal=false; } else { S.view="entry"; } save(); draw(); };
const br=$("#bonusRevealBtn"); if(br) br.onclick=()=>{ S.bonusReveal=true; save(); draw(); };
const bd=$("#bonusDoneBtn"); if(bd) bd.onclick=()=>{ (S.bonusDone=S.bonusDone||{})[S.change]=true; S.view="bonusPick"; save(); draw(); };
}
/* ====================== RENDER: RUN (Station) ====================== */
const GATE_FLOW = {
tr_01: ["next","tr_05"], // Entwicklung / Konfiguration (Konfig ueberspringt Build)
@ -1612,9 +1800,9 @@ function renderEnd(){
${box}
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="endRestart">Neue Action Card</button>
<button class="primary" id="toBonus">Weiter → Varianten dieses Service</button>
</div>`;
$("#endRestart").onclick = ()=>{ S = defaultState(); save(); draw(); };
$("#toBonus").onclick = ()=>{ S.view="bonusPick"; save(); draw(); };
}
/* ====================== INIT ====================== */

View file

@ -1,5 +1,5 @@
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
const CACHE = "slc-companion-v18";
const CACHE = "slc-companion-v19";
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 = [];