v16
This commit is contained in:
parent
acd40599ab
commit
32c2556ec9
3 changed files with 254 additions and 44 deletions
|
|
@ -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 <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)">📁 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 ====================== */
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue