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:{},
@ -1159,7 +1200,10 @@ function draw(){
renderCardBadge();
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 = [];

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:** `3647129`
**Stand:** 2026-06-09 · **Branch:** `feat/redesign-und-companion-app` · App **v0.7**
**Remote:** `https://git.1789.cloud/patrick/SLC_Game.git`
---
@ -51,9 +51,16 @@ Operations/Service-Owner/Support. Mix aus **Vermittlung** (Lifecycle + Stationen
keine Gate-Karte mehr**.
### 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** → **Abschluss-Screen** („abgeschlossen" oder „abgelehnt").
**Flow (v0.7 · Main/Bonus, nach Frank-Feedback):** Das Deck zeigt **nur die 6 Main-Karten**
(= Major je Service). Main ziehen → **3 Aufgaben** (1. Change-Art · 2. **Freigabe-Stelle**:
SOR/DPM/MB · SO · keine[Standard] · keine[Emergency] · 3. Einstieg-Phase; je „nochmal" bis
richtig) → **voller Stationen-Durchlauf** → Abschluss → **Bonus-Auswahl** (die 3 Varianten
dieses Service: Standard/Emergency/Normal). Bonus-Karte: dieselben 3 Aufgaben, danach
**Kurz-Auflösung** „welche Phasen sind noch relevant / fallen weg" — **kein voller Walk**
(Service gilt als bereits eingeführt) → zurück zur Bonus-Auswahl → „Service abschließen"
→ nächster Service (gespielte Services im Deck als ✓ markiert).
> Hintergrund: Nur der **Major** rechtfertigt den vollen SLC; die anderen Change-Arten
> werden nur eingeordnet (Franks Kritik am „künstlichen" Durchspielen jeder Karte).
- **Aktivitäts-Station, schrittweiser Mikro-Ablauf** (4 Fragen, je einzeln + Auflösung):
(1) Was steckt hinter der Überschrift? (2) Beteiligte Rollen → Figuren auf die
**Puck-Mulden**; (3) **RACI** → Figuren ins **Aktiv-Feld (R·A·C·I)** (mit RACI-Legende);
@ -96,6 +103,21 @@ 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
**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]).
- [x] **Normal-Legende** „strukturierter Bewertungs- und **Freigabe**prozess" (nicht „Gate-Prozess").
- [x] **„Verräter"-Sätze** aus 5 Standard-Karten raus (Quartalspflege, Monatsroutine, Patchday,
GeoServer-Update, Patch-Quartal).
- [x] **Einstiegs-Frage** umformuliert: „Wo würde die Umsetzung starten (nach Freigabe)?".
- [ ] **Wartet auf Frank:** 3 „Normal"-Karten (Rotstift gefragt · Vierstellig, bitte · Falsch
beschriftet) sind eigentlich **Standard** → neue **Normal**-Texte aus Franks Word übernehmen.
- [ ] **Wartet auf Frank:** Service-Steckbrief-Beiblatt (6 Services: Was ist der Service, was der
IT-Provider-Anteil) aus Franks Inhalten.
- [ ] **Klären (Frank + Patrick):** **Problem-Manager**-Rolle — in App vorhanden (`sp_09/sp_10`),
war Frank neu. Gewollt oder YAML-Artefakt?
- [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.