SLC_Game/04_Tablet-Quiz/app/index.html
2026-06-10 13:34:14 +02:00

2465 lines
171 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<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 und Auflösung. Offline lauffähig." />
<meta name="theme-color" content="#161b24" />
<!-- Feedback-Sammelpunkt (Server). Bei Bedarf anpassen; Default: gleiche Domain, feedback.php -->
<meta name="slc-feedback-endpoint" content="feedback.php" />
<link rel="manifest" href="manifest.webmanifest" />
<link rel="apple-touch-icon" href="icon.svg" />
<link rel="icon" href="icon.svg" type="image/svg+xml" />
<style>
:root {
--bg: #161b24; /* dunkler Hintergrund (nicht voll schwarz) */
--panel: #1f2733; /* Karten/Header */
--ink: #e8edf4; /* heller Text */
--muted: #9aa7b8;
--line: #333d4d;
--accent: #e2001a; /* Freiburg-Rot */
--ok: #1f9d57;
--bad: #d23b3b;
--design: #2f80c9;
--transition: #e8862b;
--operation: #2f9e57;
--support: #18a9a0;
--review: #8358c6;
--radius: 14px;
--shadow: 0 1px 3px rgba(0,0,0,.45), 0 8px 28px rgba(0,0,0,.4);
--soft: #28313f; /* leicht erhöhte Fläche */
--okBg: rgba(31,157,87,.16);
--badBg: rgba(210,59,59,.18);
--accentBg: rgba(226,0,26,.14);
--blueBg: rgba(47,128,201,.16);
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink); background: var(--bg);
-webkit-font-smoothing: antialiased;
}
header {
display: flex; align-items: center; gap: 16px;
padding: 12px 20px; background: var(--panel);
border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 5;
}
header .brand { font-weight: 700; letter-spacing: .2px; }
header .brand b { color: var(--accent); }
header .tag { font-size: 12px; color: var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:999px; }
header .spacer { flex: 1; }
.scenario { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.scenario select, button {
font: inherit; border-radius: 10px; border: 1px solid var(--line);
background: #fff; color: var(--ink); padding: 8px 12px; cursor: pointer;
}
.scenario select { max-width: 260px; }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
button.ghost { background: #fff; }
button:disabled { opacity: .45; cursor: default; }
.progress { height: 6px; background: var(--line); }
.progress > div { height: 100%; background: var(--accent); transition: width .3s; }
.layout { display: grid; grid-template-columns: 1fr; gap: 0; min-height: calc(100% - 55px); }
/* Stationsliste als aufklappbares Overlay (Header-Button ☰ Stationen) */
aside {
position: fixed; top:0; left:0; bottom:0; width: 320px; max-width: 86vw;
background: var(--panel); border-right:1px solid var(--line); padding: 14px;
overflow:auto; z-index: 30;
transform: translateX(-100%); transition: transform .22s ease;
box-shadow: 0 0 40px rgba(20,30,50,.18);
}
body.navOpen aside { transform: translateX(0); }
.navBackdrop { position:fixed; inset:0; background:rgba(15,22,35,.42); z-index:29; opacity:0; pointer-events:none; transition:opacity .2s; }
body.navOpen .navBackdrop { opacity:1; pointer-events:auto; }
body.akteOpen .navBackdrop { opacity:1; pointer-events:auto; }
/* Service-Akte als Overlay von rechts (Header-Button 📁 Akte) */
#akteList {
position: fixed; top:0; right:0; bottom:0; width: 340px; max-width: 88vw;
background: var(--panel); border-left:1px solid var(--line); padding: 14px;
overflow:auto; z-index: 30;
transform: translateX(100%); transition: transform .22s ease;
box-shadow: 0 0 40px rgba(20,30,50,.18);
}
body.akteOpen #akteList { transform: translateX(0); }
body:not(.runMode) #akteList { display:none; }
body:not(.runMode) #akteBtn { display:none; }
#akteList h3 { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:14px 0 4px; }
.akteCount { font-size:13px; color:var(--muted); margin:2px 0 6px; font-variant-numeric:tabular-nums; }
.akteItem { display:flex; align-items:center; gap:10px; padding:7px 4px; border-radius:8px; font-size:13px; opacity:.5; }
.akteItem.have { opacity:1; }
.akteItem .aId { color:#fff; font-size:11px; font-weight:700; border-radius:6px; padding:2px 7px; min-width:30px; text-align:center; flex:none; }
.akteItem .aNm { flex:1; }
.akteItem .aNm i { color:var(--muted); font-style:italic; }
.akteItem .aChk { color:var(--ok); font-weight:700; }
/* Gate: Artefakt-Anforderung (harte Kopplung) */
.gateReq { border-radius:8px; padding:9px 12px; margin:8px 0 12px; font-size:14px; font-weight:600; line-height:1.4; }
.gateReq.ok { background:#f1faf4; color:#177a44; border:1px solid #cde6d6; }
.gateReq.bad { background:#fdf3f3; color:#b5202a; border:1px solid #f0c9c9; }
.choice[disabled] { opacity:.45; cursor:default; }
/* Rollen-Glossar als Overlay von links (Header-Button 👥 Rollen) */
#rollenList {
position: fixed; top:0; left:0; bottom:0; width: 330px; max-width: 88vw;
background: var(--panel); border-right:1px solid var(--line); padding: 14px;
overflow:auto; z-index: 30;
transform: translateX(-100%); transition: transform .22s ease;
box-shadow: 0 0 40px rgba(20,30,50,.18);
}
body.rollenOpen #rollenList { transform: translateX(0); }
body.rollenOpen .navBackdrop { opacity:1; pointer-events:auto; }
body:not(.runMode) #rollenList { display:none; }
body:not(.runMode) #rollenBtn { display:none; }
#rollenList h3 { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:14px 0 4px; }
.rolleItem { display:flex; align-items:center; gap:10px; padding:6px 4px; font-size:13px; }
.rolleItem .rDot { width:12px; height:12px; border-radius:50%; flex:none; border:1px solid rgba(0,0,0,.18); }
.rolleDetails > summary.rolleItem { cursor:pointer; list-style:none; }
.rolleDetails > summary.rolleItem::-webkit-details-marker { display:none; }
.rolleDetails > summary.rolleItem::after { content:"▸"; margin-left:auto; font-size:11px; color:var(--muted); transition:transform .15s; }
.rolleDetails[open] > summary.rolleItem::after { transform:rotate(90deg); }
.rolleDesc { margin:0 0 8px 22px; font-size:12.5px; line-height:1.45; color:var(--muted); }
.navTop { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
.navTop b { font-size:13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); }
.navTop button { border:none; background:none; font-size:20px; line-height:1; color:var(--muted); cursor:pointer; padding:4px 8px; }
aside h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); margin: 16px 0 6px; }
.stationItem {
display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:10px;
cursor:pointer; font-size: 14px;
}
.stationItem:hover { background: #f7f9fb; }
.stationItem.active { background: #eef4fb; font-weight:600; }
.stationItem .dot { width:10px; height:10px; border-radius:50%; flex:none; }
.stationItem .id { color: var(--muted); font-variant-numeric: tabular-nums; font-size:12px; }
.stationItem.done .id::after { content:" ✓"; color: var(--ok); }
main { padding: 28px clamp(20px, 5vw, 64px); overflow:auto; }
.card { background: var(--panel); border:1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 26px; max-width: 860px; margin: 0 auto; }
.phaseChip { display:inline-flex; align-items:center; gap:8px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#fff; padding:5px 12px; border-radius:999px; }
.gateChip { background: var(--accent); color:#fff; }
.stationName { font-size: 28px; font-weight: 700; margin: 14px 0 4px; line-height:1.2; }
.stationId { color: var(--muted); font-size: 14px; }
.token { font-size:13px; color:var(--muted); margin-top:10px; }
.token b { color: var(--ink); }
.ctChip { display:inline-block; margin-left:6px; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; color:var(--accent); border:1px solid var(--accent); border-radius:999px; padding:2px 9px; vertical-align:middle; }
.ctText { margin-top:8px; font-size:14px; color:var(--ink); background:#fff7f7; border-left:3px solid var(--accent); border-radius:8px; padding:10px 14px; max-width:760px; }
.step { margin-top: 22px; }
.stepHead { display:flex; align-items:center; gap:10px; font-size:12px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin-bottom:10px; }
.stepHead .n { width:22px; height:22px; border-radius:50%; background:var(--ink); color:#fff; display:grid; place-items:center; font-size:12px; }
.discuss { background:#fbfdff; border:1px dashed var(--line); border-radius:12px; padding:18px 20px; }
.discuss ul { margin:8px 0 0; padding-left:20px; }
.discuss li { margin:4px 0; }
.q { border:1px solid var(--line); border-radius:12px; padding:18px 20px; margin-bottom:14px; }
.q .frage { font-weight:600; margin-bottom:12px; }
.opts { display:grid; gap:8px; }
.opt { text-align:left; padding:12px 14px; border-radius:10px; border:1px solid var(--line); background:#fff; cursor:pointer; }
.opt:hover { border-color:#c9d2dd; }
.opt.sel { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(226,0,26,.12); }
.opt.correct { border-color: var(--ok); background:#f1faf4; }
.opt.wrong { border-color: var(--bad); background:#fdf3f3; }
.opt .mark { float:right; font-weight:700; }
.qExpl { margin-top:10px; font-size:14px; color:var(--muted); border-left:3px solid var(--line); padding-left:12px; }
.reveal h4 { margin: 18px 0 6px; font-size: 13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); }
.reveal p { margin: 0 0 8px; }
.reveal ul { margin: 4px 0; padding-left: 20px; }
table.raci { width:100%; border-collapse: collapse; font-size:14px; }
table.raci th, table.raci td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); }
table.raci th { color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.5px; }
.raciBadge { display:inline-block; min-width:22px; text-align:center; font-weight:700; border-radius:6px; padding:2px 6px; font-size:12px; background:#eef0f3; color:#5a6675; }
.raci-A { background:#fbe3e3; color:#b5202a; }
.raci-R { background:#e3eefb; color:#1f5fae; }
.raci-C { background:#fff1dd; color:#b5701a; }
.raci-I { background:#eef0f3; color:#5a6675; }
.pfade { display:grid; gap:8px; }
.pfad { border:1px solid var(--line); border-left:4px solid var(--transition); border-radius:8px; padding:10px 12px; }
.pfad b { display:block; }
/* ---- Minimal Run-Screens ---- */
.sHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.sHead .phaseChip{font-size:15px;padding:8px 18px;letter-spacing:.8px}
.sHead .sId{color:var(--ink);font-size:18px;font-weight:800;font-variant-numeric:tabular-nums}
.tiefeBadge{font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;cursor:help;white-space:nowrap}
.tiefeBadge.bearbeitet{background:#e7f5ec;color:#177a44;border:1px solid #b8e0c7}
.tiefeBadge.entwurf{background:#f0f1f4;color:#6b7280;border:1px solid #dfe2e8}
.sHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.sTitle{font-size:22px;font-weight:700;line-height:1.25;margin:10px 0 4px}
.caseLine{color:var(--muted);font-size:13px;margin:0 0 22px}
.lead{font-size:16px;margin:0 0 16px}
.todo{margin:0 0 8px;padding-left:22px}
.todo li{margin:8px 0}
.crit{margin:8px 0 0;padding-left:20px;color:var(--muted)}
.crit li{margin:4px 0}
.critHead{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px}
.gateZiel{font-size:14px;line-height:1.5;background:var(--soft);border-left:3px solid var(--accent);border-radius:8px;padding:10px 12px;margin:0 0 12px}
.gateCrit{display:flex;flex-direction:column;gap:8px;margin:0 0 16px}
.critItem{display:flex;align-items:flex-start;gap:10px;text-align:left;padding:11px 14px;border:1px solid var(--line);border-radius:10px;background:var(--soft);color:var(--ink);cursor:pointer;font-size:14px;line-height:1.45;animation:critIn .22s ease}
.critItem:hover{border-color:#c9d2dd}
.critItem.checked{border-color:rgba(31,157,87,.4);background:var(--okBg)}
.critItem .critBox{font-size:17px;flex:none;line-height:1.25}
@keyframes critIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
details.det{margin:8px 0 4px;border-top:1px solid var(--line);padding-top:12px}
details.det>summary{cursor:pointer;color:var(--muted);font-size:14px;font-weight:600;list-style:none}
details.det>summary::-webkit-details-marker{display:none}
details.det>summary::before{content:"▸ ";color:var(--muted)}
details.det[open]>summary::before{content:"▾ "}
details.det>div,details.det>p,details.det>ul{margin-top:10px}
/* Schrittweise Aktivitaet: Aufloesung */
.aufBox{background:#f1faf4;border:1px solid #cde6d6;border-left:3px solid var(--ok);border-radius:10px;padding:14px 16px;margin:14px 0 4px}
.aufBox .aufH{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
.aufBox ul{margin:6px 0 0;padding-left:20px}
.aufBox ul li{margin:3px 0}
.roleChips{display:flex;flex-wrap:wrap;gap:6px}
.roleChip{background:#eef0f3;border-radius:999px;padding:3px 11px;font-size:13px}
/* RACI-Legende */
.raciLegend{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:10px 0 4px;background:#f7f9fb}
.raciLegend .rlHead{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;margin-bottom:8px}
.raciLegend .rlRow{display:flex;align-items:flex-start;gap:10px;margin:5px 0;font-size:14px;line-height:1.45}
.raciLegend .rlRow .raciBadge{flex:none;margin-top:1px}
details.raciLegend{padding:0}
details.raciLegend .rlSummary{list-style:none;cursor:pointer;font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;padding:11px 14px;display:flex;align-items:center;gap:8px}
details.raciLegend .rlSummary::-webkit-details-marker{display:none}
details.raciLegend .rlSummary::before{content:"▸";font-size:11px;transition:transform .15s}
details.raciLegend[open] .rlSummary::before{transform:rotate(90deg)}
details.raciLegend .rlBody{padding:0 14px 12px}
.raciLegend .rlGroup{margin-top:8px;padding-top:6px;border-top:1px dashed var(--line)}
.raciLegend .rlGroup:first-of-type{border-top:0;padding-top:0}
.raciLegend .rlGroupHead{font-size:11px;font-weight:700;color:var(--muted);margin:2px 0 4px}
.raciLegend .rlGroupHead.kern{color:var(--ink)}
/* Aufgaben-Kasten (Anweisung hervorgehoben) */
.frageBox{background:#f7f9fc;border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:12px;padding:14px 18px;margin:6px 0;font-size:16px;line-height:1.55}
.frageBox .frageLabel{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:700;margin-bottom:5px}
/* Phasen-Abschluss-Feedback (an Gates) */
.phaseDone{margin:16px 0 4px;padding:16px 18px;border-radius:12px;background:#fffdf5;border:1px solid var(--line);border-left:4px solid var(--accent)}
.phaseDone .pdTitle{font-weight:800;font-size:18px;margin-bottom:6px}
.phaseDone p{color:var(--ink)}
/* Aktivitaets-Abschluss-Feedback */
.actDone{margin:16px 0 4px;padding:14px 16px;border-radius:12px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--ok)}
.actDone .adTitle{font-weight:800;font-size:16px;color:#177a44;margin-bottom:2px}
.actDone .adPhase{margin:10px 0 0;padding-top:10px;border-top:1px dashed var(--line);font-size:15px}
.actDone.big{margin:8px 0 4px;padding:22px 24px}
.actDone.big .adTitle{font-size:22px;margin-bottom:6px}
.actDone.big p{font-size:16px}
.actions { display:flex; gap:10px; align-items:center; margin-top:24px; flex-wrap:wrap; }
.actions .spacer { flex:1; }
/* Setup-Screens (Action Card / Startpunkt) */
body:not(.runMode) aside { display:none; }
body:not(.runMode) .layout { grid-template-columns: 1fr; }
body:not(.runMode) .progress { display:none; }
body:not(.runMode) #stationsBtn { display:none; }
.cardBadge { display:none; align-items:center; gap:8px; font-size:13px; }
.cardBadge .cb-svc { font-weight:600; }
.setupHead { font-size:12px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); }
.setupTitle { font-size:26px; margin:6px 0 4px; line-height:1.2; }
.muted { color:var(--muted); }
.cardForm { display:flex; gap:16px; flex-wrap:wrap; margin:18px 0; }
.cardForm label { display:flex; flex-direction:column; gap:6px; font-size:12px; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); }
.cardForm select { font:inherit; padding:10px 12px; border-radius:10px; border:1px solid var(--line); min-width:280px; background:#fff; color:var(--ink); }
.pickerList { margin:14px 0 4px; }
.pickerPhase { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:16px 0 6px; }
.pickerItem { display:flex; align-items:center; gap:10px; padding:9px 12px; border:1px solid var(--line); border-radius:10px; margin-bottom:6px; cursor:pointer; }
.pickerItem:hover { border-color:#c9d2dd; background:#f7f9fb; }
.pickerItem.sel { border-color:var(--accent); box-shadow:0 0 0 2px rgba(226,0,26,.12); }
.pickerItem .dot { width:10px; height:10px; border-radius:50%; flex:none; }
.pickerItem .id { color:var(--muted); font-size:12px; font-variant-numeric:tabular-nums; }
.pickerItem .gate { margin-left:auto; font-size:11px; font-weight:700; color:#fff; background:var(--ink); padding:1px 8px; border-radius:999px; }
.pickerItem.correct { border-color:var(--ok); background:#f1faf4; }
.pickerItem.wrongPick { border-color:var(--bad); background:#fdf3f3; }
.recBox { border:1px solid var(--line); border-left:4px solid var(--ok); border-radius:10px; padding:14px 16px; margin:14px 0; }
.recBox h4 { margin:0 0 6px; font-size:13px; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); }
/* Geführte Tour */
.tourBanner { background:#fff7f7; border:1px solid var(--line); border-left:4px solid var(--accent); border-radius:10px; padding:10px 14px; margin-bottom:16px; font-size:13px; line-height:1.45; }
.tourBanner b { color:var(--ink); }
.tourProg { font-size:12px; color:var(--muted); margin-bottom:8px; font-variant-numeric:tabular-nums; }
.tourNarr { background:#eef4fb; border-radius:12px; padding:16px 18px; margin:14px 0; font-size:16px; line-height:1.55; }
.tourNarr h4 { margin:0 0 8px; font-size:12px; text-transform:uppercase; letter-spacing:.6px; color:var(--design); }
/* ---- Neuer Einstiegs-Flow (Deck / Classify / Entry) ---- */
.deck{display:flex;flex-direction:column;gap:16px}
.deckGroup .deckSvc{font-size:13px;font-weight:700;color:var(--muted);margin-bottom:6px}
.deckRow{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
.deckCard{padding:0;border:1px solid var(--line);background:#fff;border-radius:10px;cursor:pointer;overflow:hidden;transition:transform .08s,box-shadow .08s}
.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}}
/* Klassifizieren: links große Karte, rechts Frage + Antworten */
.classifyTop{display:grid;grid-template-columns:minmax(220px,320px) 1fr;gap:26px;align-items:start;margin:18px 0 6px}
.classifyCard{display:block;width:100%;border-radius:12px;box-shadow:0 3px 16px rgba(0,0,0,.16)}
.classifyMain{min-width:0}
.classifyMain .phaseRow{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:6px}
.classifyMain .phaseZone{flex:0 1 130px;max-width:150px;padding:14px 10px;font-size:13px}
.slcOrient{display:flex;flex-direction:column;align-items:center;margin:6px 0 12px}
.slcDonut{width:240px;height:240px;max-width:88%}
.slcDonut.clickable .donutSeg{cursor:pointer;transition:opacity .12s}
.slcDonut.clickable .donutSeg:hover{opacity:.82}
.slcDonut .donutSeg.bad{stroke:var(--bad);stroke-width:4}
.slcDonut path.dim{opacity:.32}
.slcDonut path.current{stroke:var(--ink);stroke-width:3;filter:drop-shadow(0 1px 4px rgba(0,0,0,.28))}
.slcOverview{margin:4px 0 14px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;background:#fbfcfe}
.slcOverview .slcOvHead{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;text-align:center;margin-bottom:4px}
.slcOverview .slcDonut{width:180px;height:180px}
/* Feedback-Cards (Phase / Dokument) */
.fbCard{margin:14px 0 4px;padding:14px 16px;border:1px solid var(--line);border-radius:12px;background:var(--soft)}
.fbCard .fbHead{font-size:15px;font-weight:800;margin-bottom:10px}
.fbCard .fbOpt{font-weight:600;color:var(--muted);font-size:12px}
.fbCard .fbQ{display:block;font-size:13.5px;font-weight:600;margin:10px 0 5px;line-height:1.4}
.fbCard textarea.fbInput{width:100%;box-sizing:border-box;font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);resize:vertical}
.fbCard textarea.fbInput:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)}
.fbScaleRow{display:flex;gap:8px;margin:2px 0 4px}
.fbScale{width:42px;height:42px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);font-weight:800;font-size:16px;cursor:pointer}
.fbScale:hover{border-color:#9fb0c4}
.fbScale.sel{background:var(--accent);color:#fff;border-color:var(--accent)}
.fbActions{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}
.fbSaved{color:#177a44;font-weight:700;font-size:13px}
.akteItem.flash{animation:akteFlash 1.6s ease-out}
@keyframes akteFlash{0%,55%{background:#fff3bf}100%{background:transparent}}
.slcCap{font-size:11px;color:var(--muted);margin-top:4px;text-align:center}
@media(max-width:680px){.classifyTop{grid-template-columns:1fr}.classifyCard{max-width:300px;margin:0 auto}.classifyMain{margin-top:6px}}
.choice{text-align:left;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff;cursor:pointer;font-size:15px;font-weight:600}
.choice:hover{border-color:var(--ink)}
.choice.bad{border-color:var(--bad);background:#fdf3f3}
.legend{border:1px solid var(--line);border-radius:10px;padding:10px 14px;margin-top:12px;background:#f7f9fb}
.legend h4{margin:0 0 8px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
.legend dt{font-weight:700;font-size:13px}
.legend dd{margin:2px 0 8px;color:var(--muted);font-size:13px}
.lgItem{padding:8px 0;border-top:1px solid var(--line)}
.lgItem:first-of-type{border-top:0;padding-top:2px}
.lgName{font-weight:700;font-size:13px;color:var(--ink)}
.lgIdee{margin:2px 0 4px;font-size:13px;color:var(--ink)}
.lgBed{margin:4px 0;color:var(--muted);font-size:13px}
.lgBed div{margin:2px 0}
.lgBsp{font-size:12px;color:var(--muted)}
.hint{font-weight:600;margin:8px 0}
.hint.bad{color:var(--bad)} .hint.ok{color:var(--ok)}
.phaseRow{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin:14px 0}
.phaseZone{padding:22px 8px;border-radius:12px;color:#fff;font-weight:800;font-size:14px;text-align:center;cursor:pointer;border:0}
.phaseZone:hover{filter:brightness(1.06)}
.phaseZone.bad{outline:3px solid var(--bad);outline-offset:2px}
@media(max-width:760px){.deckRow{grid-template-columns:repeat(3,1fr)}.phaseRow{grid-template-columns:repeat(3,1fr)}}
/* ===== Dark-Theme-Flächen (v0.9) — überschreibt fest verdrahtete helle Farben ===== */
.scenario select, button { background:var(--soft); }
button.ghost { background:var(--soft); }
.cardForm select { background:var(--soft); }
.opt, .choice, .deckCard { background:var(--soft); }
.discuss { background:var(--soft); }
.legend, .raciLegend, .frageBox { background:var(--soft); }
.stationItem:hover, .pickerItem:hover { background:var(--soft); }
.stationItem.active { background:#26313f; }
.phaseDone, .actDone { background:var(--soft); }
.tourNarr { background:var(--blueBg); }
.tourBanner, .ctText { background:var(--accentBg); }
.gateReq.ok, .opt.correct, .pickerItem.correct { background:var(--okBg); color:var(--ok); border-color:rgba(31,157,87,.4); }
.aufBox { background:var(--okBg); border-color:rgba(31,157,87,.4); color:var(--ink); }
.gateReq.bad, .opt.wrong, .choice.bad, .pickerItem.wrongPick { background:var(--badBg); color:var(--bad); border-color:rgba(210,59,59,.4); }
.stepHead .n { background:var(--accent); color:#fff; }
.pickerItem .gate { background:#475061; color:#fff; }
.roleChip { background:var(--soft); color:var(--ink); }
.navBackdrop { background:rgba(0,0,0,.55); }
@keyframes akteFlash { 0%,55%{ background:rgba(255,206,84,.22); } 100%{ background:transparent; } }
/* Service-Beschreibung (Modal/Popup) */
.svcModal{position:fixed;inset:0;z-index:40;display:none;align-items:flex-start;justify-content:center;padding:max(24px,5vh) 16px;overflow:auto;background:rgba(0,0,0,.6)}
body.svcOpen .svcModal{display:flex}
.svcModalInner{background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);max-width:820px;width:100%;padding:20px 24px 26px}
.svcModalInner .navTop b{font-size:13px}
.svcTabs{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 6px}
.svcTab{font-size:12px;padding:6px 11px;border:1px solid var(--line);border-radius:999px;background:var(--soft);color:var(--muted);cursor:pointer}
.svcTab:hover{border-color:#c9d2dd}
.svcTab.sel{border-color:var(--accent);color:var(--ink);font-weight:700;box-shadow:0 0 0 2px rgba(226,0,26,.12)}
.svcModalTitle{font-size:22px;font-weight:800;margin:12px 0 4px;line-height:1.2}
.svcDesc{color:var(--muted);margin:0 0 6px}
.svcH{margin:16px 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--accent)}
.svcList{margin:0;padding-left:20px}
.svcList li{margin:6px 0;line-height:1.5}
.runCardWrap{display:flex;flex-direction:column;gap:6px}
#runCard{cursor:zoom-in}
.svcHintRow{font-size:12px;color:var(--muted);text-align:center}
</style>
</head>
<body>
<header>
<div class="brand">SLC&nbsp;Workshop&nbsp;<b>Companion</b></div>
<span class="tag">v0.10</span>
<div id="cardBadge" class="cardBadge"></div>
<div class="spacer"></div>
<button class="ghost" id="svcBtn" title="Ausführliche Service-Beschreibungen">&nbsp;Service-Info</button>
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁&nbsp;Service-Akte</button>
<button class="ghost" id="rollenBtn" title="Rollen-Glossar (RACI)">👥&nbsp;Rollen</button>
<button class="ghost" id="stationsBtn" title="Stationsübersicht">&nbsp;Stationen</button>
<button class="ghost" id="fbExportBtn" title="Gesammeltes Feedback als Backup-Datei exportieren">&nbsp;Feedback</button>
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
</header>
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
<div class="navBackdrop" id="navBackdrop"></div>
<div class="layout">
<aside id="stationList"></aside>
<div id="akteList"></div>
<div id="rollenList"></div>
<main><div class="card" id="panel"></div></main>
</div>
<div id="svcModal" class="svcModal"><div class="svcModalInner"></div></div>
<script>
/* Empfohlener Einstiegspunkt je Change-Typ (didaktische Auflösung in Schritt 2).
Reihenfolge entspricht CHANGE_TYPES. */
const START_EMPFEHLUNG = [
{ id:"ds_01", grund:"Ein Major Change ist strategisch getrieben und betrifft den Service grundlegend er durchläuft den vollen Lebenszyklus ab dem Design (Service-Definition)." },
{ id:"tr_01", grund:"Ein Normal Change ist geplant und dokumentiert, aber nicht strategisch. Er steigt an Gate 1 ein, wo Build-oder-Konfiguration entschieden wird meist der Konfigurationspfad (tr_05)." },
{ id:"op_03", grund:"Ein Standard Change ist vorab genehmigt und im Katalog hinterlegt. Er braucht keine Gates und kein Design, sondern wird im laufenden Betrieb umgesetzt (op_03)." },
{ id:"tr_10", grund:"Ein Emergency Change muss die Störung sofort beheben. Der Fix wird beschleunigt ausgerollt (tr_10); die formale Freigabe (Gate 3) erfolgt nachgelagert." }
];
/* Geführtes Beispiel ("for dummies"-Tour): EIN konkreter Fall durch den ganzen
Lifecycle. Online-Bürgerportal · Major Change Top-Level (neues Landesgesetz zur
digitalen Bürgerbeteiligung → Erweiterung um Beteiligungs-Module). */
const TOUR = {
service: 2, change: 0,
text: {
ds_01: "Das neue Landesgesetz verlangt Online-Beteiligung. Zuerst klären Fachamt und (designierter) Service Owner, was das Beteiligungs-Modul leisten muss: Wer nutzt es (Bürger:innen, Planungsamt)? Welchen Nutzen bringt es? Welche Fristen und Verfügbarkeiten sind gesetzlich nötig? Ergebnis ist ein erster Entwurf der Service-Definition.",
ds_02: "Jetzt entsteht das Lösungsdesign: welche Module (Online-Konsultation, Kommentar-Workflows, Veröffentlichung von Plänen), welche Schnittstellen zum bestehenden Portal und zum DMS, welche Datenschutz- und Barrierefreiheits-Vorgaben. Das ist das Service Design Document.",
ds_03: "Hier wird geplant, WIE das Modul organisatorisch eingeführt wird noch nicht technisch: Wer schult die Sachbearbeiter:innen im Planungsamt? Welche Prozesse und Rollen ändern sich? Wann kann der Betrieb übernehmen? Ergebnis: Implementation Blueprint.",
ds_04: "Letzte Vorbereitung vor der Transition: Betrieb und Support werden abgestimmt, Tools und Strukturen vorbereitet, ein Early-Life-Support-Konzept skizziert, und der Service wird vollständig im Portfolio erfasst.",
tr_01: "Erstes Tor die SOR entscheidet: bauen oder nur konfigurieren? Da völlig neue Beteiligungs-Module gebraucht werden, fällt die Entscheidung auf ENTWICKLUNG → weiter zu tr_02. (Bei einer reinen Einstellung wäre es Konfiguration, und tr_02tr_04 würden übersprungen.)",
tr_02: "Die Projektleitung steuert die Umsetzung: Abstimmung mit dem Dienstleister, der die Module baut, dazu Termine, Budget und Arbeitspakete.",
tr_03: "Projektteam bzw. Dienstleister entwickeln die Beteiligungs-Module: Formulare, Kommentar-Workflows, Veröffentlichungsfunktion und die Schnittstellen.",
tr_04: "Die fertigen Komponenten werden geprüft und angenommen (Vollständigkeit, Qualität, Dokumentation) und ans Testmanagement übergeben.",
tr_05: "Parameter, Rollen, Zugänge und die Anbindung an Portal und Service-Katalog werden eingerichtet. (Genau hier wäre der Einstieg gewesen, hätte Gate 1 'Konfiguration' entschieden.)",
tr_06: "Die Betriebs- und Supportunterlagen entstehen: Betriebshandbuch, Supportanleitungen, Monitoring-Vorgaben und Eskalationswege für das neue Modul.",
tr_07: "Das Testmanagement prüft Funktion, Integration und Abnahme etwa ob eine Online-Konsultation fristgerecht startet und endet und Eingaben korrekt gespeichert werden.",
tr_08: "Der Build ist fertig. Alle Unterlagen und Testprotokolle werden für die Prüfung an Gate 2 zusammengestellt.",
tr_09: "Zweites Tor der Service Owner entscheidet allein: Ist alles übergabefähig? Doku vollständig, Abnahme liegt vor, Betrieb und Support vorbereitet → FREIGABE → weiter zu tr_10.",
tr_10: "Das Betriebsteam rollt die Module in die produktive Umgebung aus: Systeme konfigurieren, Daten migrieren, Monitoring aktivieren, Zugänge schalten.",
tr_11: "Letzter Check vor dem Go-Live: Betriebsdoku, Überwachung und Eskalationswege werden geprüft, die Go-Live-Kommunikation an die Ämter vorbereitet und der Gate-3-Antrag erstellt.",
tr_12: "Drittes Tor die SOR entscheidet im Konsent: Go-Live? Portfolio passt, Betrieb und Support sind bereit, SLAs vereinbart → GO-LIVE. Der Service wird formal ins Portfolio aufgenommen und geht in die Operation.",
op_01: "Direkt nach dem Start läuft das Modul unter erhöhter Aufmerksamkeit (Early Life Support): enge Beobachtung, schnelle Hilfe, erste Bürger-Rückmeldungen werden aufgearbeitet. Der Service Owner entscheidet, wann der Normalbetrieb beginnt.",
op_02: "Betriebshandbuch und Betriebsprozesse sind bereitgestellt klare Regeln und Zuständigkeiten für den täglichen Betrieb des Beteiligungs-Moduls.",
op_03: "Tagesgeschäft: Benutzer und Berechtigungen, Backups, Routinepflege und die Umsetzung freigegebener Standard-Changes (z. B. ein neues Beteiligungsverfahren anlegen).",
op_04: "Es wird sichergestellt, dass genug Personal, Technik und Budget vorhanden sind inklusive Steuerung des Dienstleisters.",
op_05: "Verfügbarkeit und Performance werden überwacht besonders wichtig, wenn zum Ende einer Beteiligungsfrist viele Bürger:innen gleichzeitig zugreifen.",
op_06: "Regelmäßiger Qualitätsbericht: Wurden SLAs/SLOs eingehalten? Wie war die Verfügbarkeit während der Konsultationen? Das ist die Grundlage fürs Review.",
op_07: "Über das reine Monitoring hinaus: Trends erkennen etwa wiederkehrende Lastspitzen oder ein Formular, das auffällig oft abgebrochen wird.",
sp_01: "Der Support bekommt seinen Rahmen: Incident- und Request-Prozesse, Reaktionszeiten und Ticketkategorien für das Beteiligungs-Modul.",
sp_02: "Lösungen, Workarounds und FAQ werden gepflegt damit der 1st Level häufige Bürgerfragen schnell beantworten kann.",
sp_03: "Eingehende Tickets von Bürger:innen und Ämtern werden gesichtet, priorisiert und an die richtige Stelle geroutet.",
sp_04: "Standardanfragen werden erledigt z. B. ein Zugang für eine neue Sachbearbeiterin im Planungsamt.",
sp_05: "Typische Störungen löst der 1st Level direkt etwa wenn ein Nutzer den Kommentar-Button nicht findet (Anleitung aus der Wissensdatenbank).",
sp_06: "Knifflige Fälle gehen an den 2nd Level z. B. Kommentare werden unter hoher Last nicht gespeichert. Bleibt es ungelöst, entsteht daraus ein Problem Record.",
sp_07: "Der Fall wird sauber abgeschlossen: Lösung validiert, Nutzer informiert, Dokumentation und FAQ aktualisiert.",
sp_08: "Endgültiges Schließen inklusive Klassifizierung für die Statistik und Erkennen wiederkehrender Fälle.",
sp_09: "Ein Incident bleibt ungelöst eine strukturelle Ursache wird vermutet. Der Problem Manager legt einen Problem Record an.",
sp_10: "Mehrere ähnliche Speicher-Fehler häufen sich → sie werden geclustert und gebündelt als Problem Record erfasst.",
sp_11: "Ursachensuche: Das Speicherproblem entsteht durch einen Timeout unter Last. Es gibt einen Workaround (Eintrag in die Wissensdatenbank) und die Entscheidung, dass ein Change nötig ist.",
rv_01: "Im Service-Review werden die Betriebsdaten des Beteiligungs-Moduls ausgewertet: KPIs, die gehäuften Speicher-Incidents, Kundenfeedback und die Infrastruktur — alles fließt ins Service-Review-Dokument.",
rv_02: "Die Ergebnisse werden bewertet: Der wiederkehrende Speicher-Fehler rechtfertigt einen RFC. Der Bericht geht an die SOR, die ganzheitlich bewertet.",
rv_03: "Auf Basis der Bewertung werden Änderungsvorschläge formuliert (z. B. Last-/Timeout-Optimierung), konsolidiert und die ausgewählte Änderung beschrieben.",
rv_04: "Die Änderung wird gestartet. Es ist eine überschaubare Anpassung → Normal-Change-Logik: Umsetzung planen. (Wäre sie grundlegend, würde hier das Routing RUN/DPM/MB geklärt und ein Change-Steckbrief erstellt.)",
rv_05: "Die Änderung wird umgesetzt: Im Weg RUN führt der Service Owner sie durch, dokumentiert und schließt sie ab. Der Service läuft verbessert weiter — der Kreis schließt sich."
}
};
/* =========================================================================
STATIONEN — vollständiger Lifecycle, abgeleitet aus den Blueprint-YAMLs
(service-lifecycle_design/transition/operation/support/review.yaml v3.2 +
spm_rollen.yaml). Reihenfolge: Design → Transition (inkl. Gate 13) →
Operation ⇄ Support → Review. Quiz-Fragen sind vermittelnd formuliert.
Im Echtbetrieb generiert ein Build-Skript questions.json aus den YAMLs.
========================================================================= */
const ROLLEN = {
spm:"Service-Portfolio-Manager", sor:"Service Operations Runde (SOR)",
service_owner:"Service Owner", support_manager:"Support Manager",
problem_manager:"Problem Manager", projektleitung:"Projektleitung",
betriebsteam:"Betriebsteam", service_support_team:"Service-Support Team",
projektteam:"Projektteam", queue_koordinator:"Queue Koordinator",
first_level_agent:"1st Level Agent", second_level_agent:"2nd Level Agent",
testmanagement:"Testmanagement", architektur:"IT-Architekt",
lieferant:"Lieferant", operations_manager:"Operation Manager (AL B&C/App)", dpm:"Demand Portfolio Manager"
};
/* Rollen-Kurzbeschreibungen (1 Satz je Rolle) — von Frank/Patrick zu liefern.
Leerer String = keine Beschreibung -> Rolle bleibt eine normale Zeile. Sobald Text
eingetragen ist, wird die Rolle in der Übersicht aufklappbar (Übersicht + linkes Panel). */
const ROLLEN_DESC = {
spm:"Steuert das gesamte Service-Portfolio: entscheidet über Aufnahme, Änderung und Stilllegung, sichert Strategie, Priorisierung und Wirtschaftlichkeit, aggregiert alle Review-Berichte. Hält das Gesamtportfolio konsistent und wirtschaftlich — verhindert Wildwuchs und Doppelungen einzelner Services.",
sor:"Gremium, das Service-Aktivierungen, Betriebsfreigaben und wesentliche Anpassungen bewertet und entscheidet. Ständige Mitglieder: SPM + SO + AL B&C + AL App. Bündelt alle relevanten Perspektiven für tragfähige Go/No-Go-Entscheidungen — keine Aktivierung im Alleingang.",
service_owner:"Trägt die fachliche End-to-End-Verantwortung für einen Service über den ganzen Lifecycle: Anforderungen, Qualität, Weiterentwicklung; primärer Entscheider für Inhalt und Wert. Eine klar verantwortliche Person, die den Service „besitzt“ — sonst zerfasert die Verantwortung über viele Stellen.",
support_manager:"Verantwortet Organisation und Qualität des Service-Supports (1st & 2nd Level): Prozesse, Leitlinien, Wissensmanagement und effizientes Incident/Request-Handling. Sorgt für funktionierenden, konsistenten Support — sonst wird die Ticketbearbeitung uneinheitlich und langsam.",
problem_manager:"Identifiziert wiederkehrende oder strukturelle Störungen, führt Root-Cause-Analysen durch, steuert Problemlösungen und stellt Workarounds bereit. Bekämpft Ursachen statt Symptome — verhindert, dass dieselben Störungen immer wieder auftreten.",
projektleitung:"Plant und steuert Service-Entwicklungsprojekte, koordiniert Ressourcen und Lieferanten, sichert Termine und Qualität, liefert die Projektartefakte. Bringt Bau und Beschaffung termin- und budgetgerecht ans Ziel.",
betriebsteam:"Führt alle laufenden Betriebsaufgaben aus — von Routine bis Infrastruktur: Monitoring, Standard-Changes, Deployment, Systempflege, tiefe Diagnosen. Hält den Service täglich am Laufen und sichert Stabilität und Verfügbarkeit.",
service_support_team:"Bearbeitet Nutzeranfragen und Incidents im 1st/2nd Level, sorgt für schnelle Wiederherstellung; Bindeglied zwischen Anwendern, Betrieb und Problemmanagement. Erste Hilfe für Nutzer; hält Störungen klein und Anwender handlungsfähig.",
projektteam:"Fachlich-technisches Team für Entwicklung, Konfiguration, Tests und Dokumentation; unterstützt Übergabe und Betriebsbefähigung. Die „Hände“, die im Projekt bauen und konfigurieren.",
queue_koordinator:"Überwacht das Ticketaufkommen, verteilt und routet Tickets an die richtigen Gruppen und sichert Priorisierung und SLA-Einhaltung. Sorgt für sauberen, schnellen Ticketfluss zur richtigen Stelle.",
first_level_agent:"Erste Anlaufstelle für Nutzer: nimmt Incidents/Requests auf, löst Standardfälle, dokumentiert sauber und eskaliert fachgerecht an den 2nd Level. Löst den Großteil der Fälle schnell und entlastet die höheren Support-Level.",
second_level_agent:"Bearbeitet komplexe Störungen und fachtechnische Anfragen, führt tiefere Analysen durch und stellt Lösungen bereit; kooperiert mit Betrieb, Herstellern und Problemmanagement. Knackt die Fälle, die der 1st Level nicht lösen kann.",
testmanagement:"Plant, organisiert und verantwortet Tests (Integration, Abnahme, Regression) während Entwicklung und Transition; sichert Qualität und Betriebsreife. Stellt sicher, dass nur Funktionierendes live geht.",
architektur:"Definiert technische Standards, Zielarchitekturen und Integrationsanforderungen, bewertet Designvarianten und Risiken, sichert technische Konsistenz und Zukunftsfähigkeit. Hält Lösungen technisch konsistent und zukunftsfähig statt zu Insellösungen.",
lieferant:"Stellt externe System-, Software- oder Infrastrukturkomponenten bereit oder entwickelt spezifische Anpassungen; wird bei Build, Fehleranalyse und Komponentensupport eingebunden. Bringt externe Leistung und Know-how, wo intern nicht vorhanden.",
operations_manager:"Verantwortet den stabilen, sicheren Betrieb der Infrastruktur- und Anwendungs-Services (Netze, Server, Cloud, Fachverfahren, Software), koordiniert die Betriebsteams und sichert SLA-/Policy-Einhaltung (AL B&C + AL App, ständige SOR-Mitglieder). Stellt sicher, dass technische Basis und Anwendungen tragfähig sind, und bringt die Betriebsperspektive in die SOR-Entscheidungen.",
dpm:""
};
// Rollen-Glossar fuer das Overlay: gruppiert/eingefaerbt nach den 6 Figuren-Kategorien
// (Farbe = Filamentfarbe der Figuren). Deckt alle ROLLEN-Eintraege ab.
const ROLLEN_GRUPPEN = [
{ label:"Governance (Entscheider)", color:"#c9a227", roles:["spm","service_owner"] },
{ label:"Umfeld / Auftraggeber", color:"#7d2e3f", roles:["dpm"] },
{ label:"Operative Führung", color:"#2f80c9", roles:["projektleitung","operations_manager","support_manager","problem_manager"] },
{ label:"Operative / Fachexperten", color:"#6b7686", roles:["queue_koordinator","first_level_agent","second_level_agent","testmanagement","architektur"] },
{ label:"Externe", color:"#cfd6df", roles:["lieferant"] },
{ label:"Teams (Sonderfiguren)", color:"#2f9e57", roles:["betriebsteam","service_support_team","projektteam"] }
];
/* Eine Rollen-Zeile: mit Beschreibung -> aufklappbar (<details>), sonst schlichte Zeile. */
function rolleItemHtml(r, color){
if(!ROLLEN[r]) return "";
const dot = `<span class="rDot" style="background:${color}"></span>`;
const desc = (ROLLEN_DESC && ROLLEN_DESC[r]) ? ROLLEN_DESC[r].trim() : "";
return desc
? `<details class="rolleDetails"><summary class="rolleItem">${dot}<span>${ROLLEN[r]}</span></summary><div class="rolleDesc">${desc}</div></details>`
: `<div class="rolleItem">${dot}<span>${ROLLEN[r]}</span></div>`;
}
const PHASEN = {
design:{label:"Design", color:"var(--design)"},
transition:{label:"Transition", color:"var(--transition)"},
operation:{label:"Operation", color:"var(--operation)"},
support:{label:"Support", color:"var(--support)"},
review:{label:"Review", color:"var(--review)"}
};
/* Action Cards: 6 Services × 4 Change-Arten (aus „Use Cases mit möglichen Changes"). */
const CHANGE_TYPES = [
"Major Change",
"Normal Change",
"Standard Change",
"Emergency Change"
];
/* Legende-Inhalte, Index parallel zu CHANGE_TYPES (0 Major, 1 Normal, 2 Standard, 3 Emergency).
Quelle: DIGITOM-Definitionen. Emergency wurde nicht mitgeliefert -> bestehende Beschreibung. */
const CHANGE_LEGEND = [
{ idee:"Sonderform der Normal Change mit hohem Risiko, hohen Kosten oder breiter Auswirkung — höchste Eskalations- und Genehmigungsstufe.",
bed:["Hohes Risiko und/oder hohe Kosten und/oder weitreichende Auswirkung (viele Nutzer/Services betroffen)",
"Freigabe in der SOR; reicht deren Ressourcen-/Entscheidungshoheit nicht, wird daraus ein Demand (über DPM ans Mission Board)",
"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 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",
"Terminplanung, Test, ggf. Rollback-Plan",
"Geringes bis mittleres Risiko (die hochriskanten landen bei „Major“)"],
bsp:"Einführung einer neuen Software-Version, Konfigurationsänderung an einem produktiven System." },
{ idee:"Routine. Vorab genehmigt, weil sie oft vorkommt, das Risiko bekannt und niedrig ist und der Ablauf dokumentiert ist.",
bed:["Im Standard-Change-Katalog hinterlegtes Muster/Template für genau diese Änderung",
"Geringes, bekanntes Risiko, Auswirkung vorhersehbar",
"Wiederholbar, klar dokumentierter Ablauf",
"Keine Gate-/SOR-Einzelfreigabe nötig — generell autorisiert, läuft direkt im Betrieb"],
bsp:"Standard-Passwort-Reset, Austausch eines defekten Standard-Geräts, Einspielen eines geprüften Routine-Patches." },
{ idee:"Muss eine Störung sofort beheben — beschleunigt umgesetzt; die formale Freigabe erfolgt nachgelagert.",
bed:["Akuter Notfall / drohender oder laufender Ausfall kritischer Dienste",
"Beschleunigtes Verfahren mit Notfall-Autorisierung",
"Umsetzung sofort — Dokumentation und formale SOR-/Gate-Freigabe (Gate 3) nachgelagert"],
bsp:"Sofort-Sperrung einer kompromittierten VPN-Zertifikatskette, Notfall-Hotfix einer kritischen Sicherheitslücke." }
];
// 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]];
const USE_CASES = [
{ service:"Zentrale VDI (Virtual-Desktop-Infrastructure)",
desc:"Bereitstellung von virtuellen Windows-Desktops über das interne Rechenzentrum.",
detail:{
nutzen:[
"<b>Einheitliche Arbeitsumgebung</b> — alle Fachamt-Mitarbeitenden erhalten denselben, zentral verwalteten Windows-Desktop.",
"<b>Datensicherheit</b> — Daten verbleiben im Rechenzentrum, keine lokalen Kopien; reduziert Verlust- und Diebstahlrisiko.",
"<b>Flexibler Zugriff</b> — Nutzung von Thin-Clients, Laptops oder Tablets über RDP/HTML5 von überall.",
"<b>Schnelle Bereitstellung</b> — neue Anwendungen und Updates werden zentral ausgerollt, kein lokaler Installationsaufwand.",
"<b>Kostenersparnis</b> — geringerer Wartungs- und Lizenzaufwand für Einzelgeräte."
],
aufgaben:[
"<b>Desktop-Image-Management</b> — Erstellung und Pflege standardisierter Windows-10-Images (MSIX-Packaging).",
"<b>VDI-Host-Provisionierung</b> — VM-Hosts (VMware vSphere/Hyper-V), Storage-Pools und Connection-Broker (Horizon oder Citrix).",
"<b>Netzwerk- und QoS-Einrichtung</b> — VLAN-Segmente, Priorisierung von VDI-Traffic, Load-Balancing der Broker-Server.",
"<b>Automatisierte Image-Pipelines</b> — Build- und Deploy-Automatisierung (Packer + Ansible, Blue-Green-Deploy).",
"<b>Identity-Integration</b> — AD-basiertes RBAC, Conditional Access, MFA für alle Nutzer.",
"<b>Patch- und Update-Management</b> — regelmäßige OS- und Applikations-Patches über zentrale Tools.",
"<b>Service-Monitoring</b> — SLA = 99 % Uptime, 4-Stunden-Reaktionszeit, Monitoring von Session-Count und Host-Health (Grafana + Prometheus).",
"<b>Support & Schulung</b> — 2nd-Level-Help-Desk, Benutzerhandbuch, Schulungen für Fachamt-IT-Support."
]
},
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."},
{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",
desc:"Zentral verwalteter VPN-Dienst, der Mitarbeitenden der Fachämter sicheren Remote-Zugriff auf interne Systeme (Intranet, Fachanwendungen, Datenbanken) ermöglicht.",
detail:{
nutzen:[
"<b>Sicherer Remote-Zugriff</b> — verschlüsselte Tunnel ermöglichen Zugriff auf interne Systeme von zu Hause oder unterwegs.",
"<b>Zentralisierte Authentifizierung</b> — Single-Sign-On über AD, MFA und ggf. FIDO2-Security-Keys.",
"<b>Compliance-Erfüllung</b> — erfüllt DSGVO- und IT-Security-Vorgaben (Protokollierung, Zugriffskontrolle).",
"<b>Flexibilität für Mitarbeitende</b> — Arbeiten von jedem Gerät aus, ohne Sicherheitskompromisse.",
"<b>Transparente Überwachung</b> — vollständige Log- und Audit-Funktion für alle Verbindungen."
],
aufgaben:[
"<b>VPN-Appliance-Bereitstellung</b> — Installation hochverfügbarer VPN-VMs (FortiGate, Cisco ASA o. Ä.) inkl. Load-Balancing.",
"<b>Netzwerk- und Policy-Design</b> — VPN-Subnetze, Split-Tunnel-Regeln, Firewall-Policies (nur Port 443/8443).",
"<b>Identity-Integration</b> — SAML/AD-Anbindung, Conditional Access, MFA-Durchsetzung, optional FIDO2-Support.",
"<b>Client-Provisionierung</b> — automatisierte Verteilung von VPN-Profilen via Intune/SCCM, Dokumentation für End-User.",
"<b>Firmware- und Sicherheits-Updates</b> — regelmäßige Patch-Cycles, IPS/IDS-Monitoring, tägliche Log-Analyse.",
"<b>Monitoring & SLA-Management</b> — 99,8 % Uptime, 1-Stunden-Reaktionszeit, Dashboard (Grafana + Prometheus).",
"<b>Incident- und Change-Management</b> — 24/7-Support, Notfall-Deaktivierung bei Sicherheitsvorfällen, Dokumentation aller Änderungen."
]
},
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."},
{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",
desc:"Zentrales Web-Portal, über das Bürger*innen Meldungen (z. B. Straßenreparatur, Lärm) und Anträge (z. B. Baugenehmigung, Personalausweis) digital einreichen und den Status verfolgen.",
detail:{
nutzen:[
"<b>24/7-Zugang</b> — Bürger*innen können Meldungen (Straßenreparatur, Lärm) und Anträge (Baugenehmigung, Personalausweis) jederzeit online einreichen.",
"<b>Automatisierte Weiterleitung</b> — Eingaben werden sofort an das zuständige Fachamt geroutet, Bearbeitungszeiten sinken.",
"<b>Status-Tracking</b> — Antragsteller*innen sehen den Bearbeitungsstand in Echtzeit; reduziert Rückfragen und Telefonaufkommen.",
"<b>Digitale Signatur & revisionssichere Protokollierung</b> — erfüllt gesetzliche Vorgaben (DSGVO, OZG) und erhöht die Rechtssicherheit.",
"<b>Transparenz & Bürgernähe</b> — öffentliche Infos zu Bearbeitungszeiten und Service-Statistiken stärken das Vertrauen."
],
aufgaben:[
"<b>Anforderungsanalyse & Fachkonzept</b> — gemeinsam mit den Fachämtern (Bau, Bürger-Service, IT-Security).",
"<b>Entwicklung/Anpassung</b> der Web-Applikation (z. B. Node.js + Vue, Formular-Engine, Workflow-Engine).",
"<b>Betrieb von HA-Infrastruktur</b> — VM-Cluster, Load-Balancer, PostgreSQL-HA, tägliche Backups, Disaster-Recovery-Plan.",
"<b>Sicherheitsintegration</b> — SAML-basiertes SSO, rollenbasiertes Access-Control, MFA, WAF-Regeln, regelmäßige OWASP-Scans.",
"<b>DMZ-Einrichtung & Netzwerk-Segmente</b> — HTTPS-Only, IP-Whitelisting fürs interne Fachämter-Netz, DDoS-Schutz.",
"<b>CI/CD-Pipeline</b> (GitLab CI, Blue-Green-Deploy) für schnelle, kontrollierte Releases.",
"<b>Service-Management</b> — SLA = 99,5 % Verfügbarkeit, 2-Stunden-Reaktionszeit, Monitoring (Prometheus + Grafana), 2nd-Level-Help-Desk.",
"<b>Dokumentation & Schulungen</b> — Benutzerhandbuch, FAQ, Schulungen für Fachamt-Mitarbeitende, Knowledge-Base-Einträge."
]
},
changes:[
{titel:"Mitreden, Pflicht!", text:"Ein neues Landesgesetz schreibt digitale Bürgerbeteiligung vor — das Portal muss um komplette Beteiligungs-Module erweitert werden."},
{titel:"Licht an!", text:"Das Amt für öffentliche Ordnung möchte eine neue Mängel-Kategorie „Beleuchtung und Strom“ zu den bestehenden Kategorien hinzufügen."},
{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)",
desc:"Elektronisches Archiv, das alle ein- und ausgehenden Dokumente (PDF, Scans, E-Mails) zentral speichert, versioniert und revisionssicher archiviert.",
detail:{
nutzen:[
"<b>Revisionssichere Archivierung</b> — gesetzeskonforme Aufbewahrung von Eingangs-, Ausgangs- und Vorgangsdokumenten (1030 Jahre).",
"<b>Schnelle Volltext-Suche</b> — Metadaten- und Volltext-Index ermöglichen sofortiges Auffinden aller Unterlagen.",
"<b>Automatisierte Workflows</b> — Dokumente werden automatisch an die zuständigen Fachämter weitergeleitet, Freigabe-Ketten gesteuert.",
"<b>Platz- und Kostenersparnis</b> — Wegfall von Papierarchiven, reduzierte Lager- und Verwaltungskosten.",
"<b>Transparenz & Nachvollziehbarkeit</b> — lückenlose Protokollierung aller Zugriffe und Änderungen für Audits."
],
aufgaben:[
"<b>Systemauswahl & Anpassung</b> — Installation und Konfiguration einer DMS-Lösung (z. B. Alfresco, SharePoint) inkl. Metadaten-Schema und OCR-Engine.",
"<b>Infrastruktur-Bereitstellung</b> — HA-VM-Cluster, verschlüsselter SAN-Storage (TB-Scale), tägliche Snapshots und Langzeit-Backups.",
"<b>Sicherheits- und Identity-Integration</b> — AD-basiertes RBAC, MFA, Verschlüsselung at-rest und in-transit, Audit-Log-Management.",
"<b>Workflow-Engine-Implementierung</b> — Automatisierung von Dokumenten-Freigabe- und Weiterleitungsprozessen für alle Fachämter.",
"<b>Monitoring & Performance</b> — Index-Health-Checks, Speicher-Auslastungs-Monitoring, SLA = 99,8 % Verfügbarkeit (Grafana + Prometheus).",
"<b>Support & Schulung</b> — 2nd-Level-Help-Desk, Benutzerhandbuch, Fachschulungen für Sachbearbeiter und Archivare, laufende Doku-Updates."
]
},
changes:[
{titel:"Datendiät", text:"Eine DSGVO-Ergänzung zur Datenminimierung erzwingt die komplette Neugestaltung von Metadaten-Modell und Archivierungs-Policies."},
{titel:"Dropdown-Wunsch", text:"Das Finanzamt bittet um ein neues Metadaten-Feld „Kostenstelle“ als Auswahlliste."},
{titel:"Versionspflege", text:"Das quartalsweise Update der DMS-Software (Sicherheits- und Funktions-Patches) steht an."},
{titel:"Ransomware!", text:"Alarm: Dokumente werden verdächtig verschlüsselt — Storage sofort isolieren und aus den letzten Snapshots wiederherstellen."}
]},
{ service:"Kommunales GIS-Portal (Geoinformations-System)",
desc:"Web-basiertes Karten- und Analyse-Portal, das Fachämtern (Bau, Umwelt, Verkehr) räumliche Daten (Flurstücke, Infrastruktur, Umweltzonen) und bearbeitbare Layer bereitstellt.",
detail:{
nutzen:[
"<b>Zentraler Daten-Hub</b> — einheitlicher Zugriff auf Flurstücke, Infrastruktur- und Umwelt-Daten für alle Fachämter.",
"<b>Interaktive Karten & Analysen</b> — web-basierte Visualisierung, Layer-Management und räumliche Auswertungen.",
"<b>Transparenz für Bürger*innen</b> — öffentliche Karten-Ansichten erhöhen das Vertrauen in kommunale Entscheidungen.",
"<b>Verbesserte Planungsqualität</b> — schnellere, datenbasierte Entscheidungen in Bau-, Umwelt- und Verkehrs-Planung.",
"<b>Skalierbarkeit</b> — Unterstützung großer Datenmengen (TB-Scale) und hoher Nutzerzahlen."
],
aufgaben:[
"<b>GIS-Server-Installation</b> — Deployment von GeoServer oder ArcGIS Server, Konfiguration von OGC-Services (WMS/WFS).",
"<b>Infrastruktur-Provisionierung</b> — GPU-beschleunigte Rendering-Nodes, redundanter Storage-Pool (RAID 6/SSD), Hochdurchsatz-Netz (10 GbE).",
"<b>Daten-Ingestion-Pipelines</b> — ETL-Prozesse (CityGML → 3-D-Tiles, Raster-Import) automatisiert via Python-Scripts/Docker.",
"<b>Netz- und CDN-Einbindung</b> — optimierte Auslieferung öffentlicher Karten, Edge-Caching, DDoS-Schutz.",
"<b>Security & Identity</b> — AD-basiertes RBAC, OAuth-2.0-Token-Auth, regelmäßige Pen-Tests, Verschlüsselung in-transit."
]
},
changes:[
{titel:"Norm-Zwang", text:"Eine bundesweite Vorgabe zu EU-Standards erzwingt die komplette Migration des GIS-Stacks auf konforme Services und Datenmodelle."},
{titel:"Grün dazu!", text:"Das Umweltamt möchte im Analyse-Modul „Klimarisiko-Analyse“ eine weitere Datenquelle zu städtischen Grünflächen anbinden."},
{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",
desc:"Web-Applikation, über die Fachämter interne Beschaffungen (Material, Dienstleistungen) anlegen, prüfen und Verträge digital verwalten.",
detail:{
nutzen:[
"<b>Vollständig digitaler Beschaffungs-Workflow</b> — von Bedarfsermittlung über Ausschreibung bis Vertragsarchivierung.",
"<b>Automatisierte Genehmigungs-Ketten</b> — rollenbasierte Freigaben reduzieren Durchlaufzeiten und Fehler.",
"<b>Kosten- und Compliance-Kontrolle</b> — Echtzeit-Auswertungen, Einhaltung von Haushalts- und Vergaberegeln.",
"<b>Revisionssichere Archivierung</b> — gesetzeskonforme Aufbewahrung von Verträgen (1030 Jahre).",
"<b>Reduzierter Papierverbrauch</b> — umweltfreundlich und effizienter."
],
aufgaben:[
"<b>System-Selection & Customizing</b> — Implementierung einer e-Procurement-Lösung (SAP SRM, Odoo Purchase) inkl. Workflow-Engine und E-Signatur.",
"<b>HA-Infrastruktur-Betrieb</b> — VM-Cluster, MariaDB-Galera-Datenbank, tägliche Backups, Disaster-Recovery-Strategie.",
"<b>Identity & Access Management</b> — AD-basiertes RBAC, SAML-SSO, MFA für alle Nutzergruppen (Anforderer, Genehmiger, Finanz).",
"<b>Sicherheits- und Netzwerk-Design</b> — DMZ-Zone, HTTPS-Only, Firewall-Regeln für externe Lieferanten-Portale, IPS/IDS-Monitoring.",
"<b>Patch- und Update-Management</b> — regelmäßige Sicherheits-Patches, automatisierte CI/CD-Deployments.",
"<b>Monitoring & SLA-Erfüllung</b> — 99,5 % Uptime, 2-Stunden-Reaktionszeit, Dashboard (ELK-Stack + Grafana).",
"<b>Support & Schulung</b> — 2nd-Level-Help-Desk, Benutzerhandbuch, Fachschulungen für Einkauf- und Fachabteilungen."
]
},
changes:[
{titel:"Vergabe neu!", text:"Eine neue EU-Vergaberichtlinie zwingt zur Einführung von E-Invoicing und erweiterten Transparenz-Reports."},
{titel:"Format-Wechsel", text:"Die Verträge der Fachämter liegen wegen aktualisierter Sicherheitsrichtlinien in einem neuen Dateiformat vor — die Schnittstellen der Web-Applikation müssen entsprechend angepasst werden."},
{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."}
]}
];
const STATIONEN = [
{ id:"ds_01", phase:"design", typ:"aktivitaet",
name:"Definieren der erforderlichen Service-Eigenschaften",
beschreibung:"Grundlegende Definition des neuen oder geänderten Services aus fachlicher Perspektive. Startpunkt für die Service-Definition als zentrales Artefakt.",
umfasst:["Zweck, Nutzen, Zielgruppen","Utility & Warranty","SLA-/SLO-Anforderungen","Abhängigkeiten zu unterstützenden Services","Fachliche & technische Anforderungen"],
artefakt:"Service-Definition (Entwurf)",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["architektur","C"],["spm","C"]],
quiz:[
{frage:"Wer trägt die Ergebnisverantwortung (A) für diese Aktivität?",
optionen:["Service Owner","Projektleitung","SPM","IT-Architekt"], richtig:0,
expl:"Der (designierte) Service Owner ist Accountable; die Projektleitung führt operativ aus (R)."},
{frage:"Welches zentrale Artefakt entsteht hier?",
optionen:["Betriebshandbuch","Service-Definition","Review-Bericht","Incident Record"], richtig:1,
expl:"Die Service-Definition ist das zentrale Artefakt der Design-Phase."}
]},
{ id:"ds_02", phase:"design", typ:"aktivitaet",
name:"Designen der erforderlichen Service- und Service-Management-Komponenten",
beschreibung:"Einarbeiten der Service-Eigenschaften in ein vollständiges Designmodell.",
umfasst:["Servicearchitektur (Komponenten, Schnittstellen)","Design der Betriebs- & Supportprozesse","Sicherheit/Compliance/Datenschutz","Monitoring & Reporting","Rollenmodell"],
artefakt:"Service Design Document",
raci:[["service_owner","A"],["architektur","R"],["projektteam","R"],["spm","I"]],
quiz:[
{frage:"Wer führt das Architektur-Design operativ aus (R)?",
optionen:["SPM","Service Owner","IT-Architekt","Support Manager"], richtig:2,
expl:"IT-Architekt und Projektteam sind Responsible; der Service Owner bleibt Accountable."}
]},
{ id:"ds_03", phase:"design", typ:"aktivitaet",
name:"Beschreiben des Vorgehens zur Implementierung",
beschreibung:"Planen, WIE der Service organisatorisch eingeführt wird (nicht technisch deployt wird).",
umfasst:["Organisatorische Integration","Rollenübergaben","Anpassung Richtlinien/Prozesse/Tools","Trainings & Kommunikation","Time-to-Operate"],
artefakt:"Implementation Blueprint",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Was wird hier geplant?",
optionen:["Das technische Deployment","Wie der Service organisatorisch eingeführt wird","Die Incident-Bearbeitung","Die Portfolio-Strategie"], richtig:1,
expl:"ds_03 plant die organisatorische Einführung — bewusst getrennt vom technischen Build."}
]},
{ id:"ds_04", phase:"design", typ:"aktivitaet",
name:"Vorbereiten der Service-Implementierung",
beschreibung:"Finalisieren aller organisatorischen Voraussetzungen für die spätere Übergabe in den Betrieb.",
umfasst:["Abstimmung mit Betrieb & Support","Prozesse/Tools/Strukturen vorbereiten","ELS-Konzept (Early Life Support)","Service vollständig im Portfolio erfasst"],
artefakt:"Übergabe-Readiness (organisatorisch)",
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Wofür steht das ELS-Konzept?",
optionen:["Early Life Support","End of Life Service","Enterprise License System","Externer Lieferanten-Support"], richtig:0,
expl:"Early Life Support = erhöhte Betreuung direkt nach Go-Live (siehe op_01)."}
]},
{ id:"tr_01", phase:"transition", typ:"gate", gateNr:1,
name:"Gate 1: Darf es in die Transition?",
beschreibung:"Entry-Gate der Transition: Die SOR entscheidet zuerst, ob das Vorhaben überhaupt in die Transition darf — und wenn ja, ob die Service-Komponenten neu entwickelt oder nur konfiguriert werden. Erfordert SOR-Approval (Budget- und Ressourcenimplikationen).",
umfasst:["Aufwandsschätzung (Build vs. Configure)","Technische Risiken","Budget-Abgleich","ggf. Lieferanten-Einbindung","SOR-Vorlage für Freigabe"],
artefakt:"Gate-Entscheidung + SOR-Vorlage",
ziel:"Sicherstellen, dass nur Vorhaben in die weitere Entwicklung gehen, die ausreichend geplant sind und für die ein grundsätzliches Commitment bezüglich Ressourcen besteht (Budget, Betrieb & Support, Projektressourcen).",
// Schritt A: Freigabe-Entscheidung (Frank-Template). Schritt B: Routing (pfade).
freigabe:[
["Freigabe","Ausreichend geplant, Ressourcen-Commitment liegt vor → weiter zum Build-/Konfigurations-Routing"],
["Freigabe mit Auflagen","Weiter zum Routing; definierte Auflagen müssen nachgearbeitet werden"],
["Zurück an Design","Planung oder Commitment unzureichend → zurück in die Design-Phase"],
["Ablehnung","Vorhaben wird nicht weiterverfolgt — erfordert SOR-Eskalation"]
],
pfade:[
["Entwicklung","Neuentwicklung/wesentliche Anpassung → weiter zu tr_02"],
["Konfiguration","Bestehende Komponenten konfigurieren → springt zu tr_05"]
],
pruef:[
["Design-Vollständigkeit","Ist das Service Design Document vollständig?"],
["Budget-Verfügbarkeit","Ist Budget für die Implementierung verfügbar?"],
["Projektressourcen","Können Ressourcen mobilisiert werden?"],
["Betriebs-/Support-Bereitschaft","Grundsätzliches Commitment vorhanden?"]
],
raci:[["sor","A"],["service_owner","R"],["projektleitung","R"],["architektur","R"],["spm","C"],["lieferant","I"]],
quiz:[
{frage:"Wer entscheidet an Gate 1 (Accountable)?",
optionen:["Service Owner","SOR","SPM","Projektleitung"], richtig:1,
expl:"Gate 1 ist eine SOR-Gremienentscheidung (Budget-/Ressourcenimplikationen)."},
{frage:"Welche zwei Pfade öffnet Gate 1?",
optionen:["Go oder Stop","Intern oder Extern","Entwicklung oder Konfiguration","Test oder Release"], richtig:2,
expl:"Entwicklung (tr_02) vs. Konfiguration (tr_05) — Konfiguration überspringt tr_02tr_04."}
]},
{ id:"tr_02", phase:"transition", typ:"aktivitaet",
name:"Koordinieren der Entwicklungs- und Beschaffungsaktivitäten",
beschreibung:"Steuern der Entwicklungsaktivitäten im Projekt.",
umfasst:["Abstimmung mit Lieferanten","Ressourcenplanung","Termin- und Budgetnachführung","Sicherstellung von Change-Kontrollen","Definition von Build- und Konfigurationspaketen"],
artefakt:"Projektsteuerung / Build- & Konfigurationspakete",
raci:[["projektleitung","A/R"],["architektur","C"],["service_owner","I"],["lieferant","C/I"]],
quiz:[
{frage:"Wer trägt die Ergebnisverantwortung (A) für die Koordination der Entwicklung?",
optionen:["Projektleitung","IT-Architekt","Service Owner","Lieferant"], richtig:0,
expl:"Die Projektleitung ist Accountable; Architektur berät (C), der Service Owner ist informiert (I)."}
]},
{ id:"tr_03", phase:"transition", typ:"aktivitaet",
name:"Entwickeln von Anwendungen und Systemen",
beschreibung:"Realisierung der technischen Service-Komponenten.",
umfasst:["Softwareentwicklung","Customizing","Schnittstellenentwicklung","Infrastrukturaufbau","Versionierung & Dokumentation","Testdaten & Migrationspfade vorbereiten"],
artefakt:"Entwickelte Service-Komponenten",
raci:[["projektleitung","A"],["projektteam","R"],["lieferant","R"],["service_owner","C"],["architektur","C"]],
quiz:[
{frage:"Wer führt die Entwicklung operativ aus (R)?",
optionen:["SPM","Projektteam bzw. Lieferant","Service Owner","Testmanagement"], richtig:1,
expl:"Projektteam (intern) und Lieferant (extern) sind Responsible; die Projektleitung bleibt Accountable."}
]},
{ id:"tr_04", phase:"transition", typ:"aktivitaet",
name:"Entgegennehmen der Service-Komponenten",
beschreibung:"Qualitätssicherung beim Übergang von der Entwicklung zur Testphase.",
umfasst:["Eingangskontrolle","Prüfung von Vollständigkeit / Qualität","Sicherstellung der Dokumentationen","Übergabe an Testmanagement"],
artefakt:"Abgenommene Service-Komponenten",
raci:[["service_owner","A"],["projektleitung","R"],["testmanagement","R"],["lieferant","C"]],
quiz:[
{frage:"An wen werden die entgegengenommenen Komponenten übergeben?",
optionen:["An den Betrieb","An das Testmanagement","An die SOR","An das SPM"], richtig:1,
expl:"tr_04 ist die Qualitätssicherung beim Übergang in die Testphase — Übergabe an das Testmanagement."}
]},
{ id:"tr_05", phase:"transition", typ:"aktivitaet",
name:"Konfiguration der Service-Komponenten",
beschreibung:"Einrichtung von Konfigurationsparametern, Settings, Rollen und Zugängen. Einstiegspunkt bei Gate-1-Entscheidung Konfiguration (überspringt tr_02tr_04).",
umfasst:["Setup von Parametern, Policies, Templates","Toolkonfiguration (ITSM-Systeme, Monitoring)","Anpassung bestehender Komponenten","Verknüpfung mit Service-Katalog / Portfolio"],
artefakt:"Konfigurierte Service-Komponenten",
raci:[["service_owner","A"],["projektteam","R"],["betriebsteam","C"],["service_support_team","C"]],
quiz:[
{frage:"Bei welcher Gate-1-Entscheidung ist tr_05 der Einstiegspunkt?",
optionen:["Entwicklung","Konfiguration","Ablehnung","Go-Live"], richtig:1,
expl:"Bei der Entscheidung Konfiguration werden tr_02tr_04 übersprungen und direkt bei tr_05 eingestiegen."}
]},
{ id:"tr_06", phase:"transition", typ:"aktivitaet",
name:"Erstellen oder Aktualisieren der Betriebs-Dokumentation",
beschreibung:"Alle Betriebsunterlagen werden erstellt oder aktualisiert.",
umfasst:["Service Operation Manual","Supportanleitungen","Monitoring-Spezifikationen","Eskalationswege","Standard Changes","Konfigurations-/Betriebsrichtlinien"],
artefakt:"Betriebsdokumentation (Service Operation Manual)",
raci:[["service_owner","A"],["projektteam","R"],["betriebsteam","C"],["service_support_team","C"]],
quiz:[
{frage:"Welches Artefakt entsteht hier vor allem?",
optionen:["Service-Definition","Betriebsdokumentation (Service Operation Manual)","Gate-Vorlage","Review-Bericht"], richtig:1,
expl:"tr_06 erstellt/aktualisiert alle Betriebsunterlagen (u.a. Service Operation Manual, Supportanleitungen)."}
]},
{ id:"tr_07", phase:"transition", typ:"aktivitaet",
name:"Testen der Service-Komponenten",
beschreibung:"Verifizierung, dass alle Servicekomponenten funktionsfähig und betriebsbereit sind.",
umfasst:["Funktionstests","Integrationstests","Abnahmetests","Nachweis der Betriebsreife","Testprotokolle & Freigaben"],
artefakt:"Testprotokolle & Freigaben",
raci:[["projektleitung","A"],["testmanagement","R"],["service_owner","C"],["lieferant","C/I"]],
quiz:[
{frage:"Wer ist Responsible (R) für das Testen der Komponenten?",
optionen:["Projektleitung","Testmanagement","Service Owner","Betriebsteam"], richtig:1,
expl:"Das Testmanagement ist Responsible; die Projektleitung ist Accountable."}
]},
{ id:"tr_08", phase:"transition", typ:"aktivitaet",
name:"Formale Übergabe (Build abgeschlossen)",
beschreibung:"Der Build ist abgeschlossen. Alle Artefakte werden für die Entry-Prüfung (Gate 2) bereitgestellt.",
umfasst:["Bereitstellung aller Betriebsunterlagen","Bereitstellung der Testprotokolle","Dokumentierter Übergabe-Termin","Vorbereitung Gate-2-Antrag"],
artefakt:"Übergabepaket (Gate-2-Antrag)",
raci:[["projektleitung","A"],["service_owner","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Worauf bereitet die formale Übergabe (Build abgeschlossen) vor?",
optionen:["Gate 1","Gate 2","Gate 3","ELS-Exit"], richtig:1,
expl:"tr_08 stellt alle Artefakte für die Entry-Prüfung an Gate 2 (tr_09) bereit."}
]},
{ id:"tr_09", phase:"transition", typ:"gate", gateNr:2,
name:"Gate 2: Entry-Prüfung nach Build",
beschreibung:"Validierung der Übergabefähigkeit nach Abschluss des Builds. Dies ist eine SO-Einzelentscheidung (keine Gremienentscheidung).",
ziel:"Sicherstellen, dass die grundlegenden Voraussetzungen für das Ausrollen eines Service gegeben sind.",
umfasst:["Prüfung der Übergabe-Vollständigkeit","Prüfung des Abnahme-Status","Ressourcen-Outlook (Betrieb/Support vorbereitet)","Pilot-Entscheidung (falls erforderlich)","Dokumentation im Transition-Steckbrief"],
artefakt:"Gate-2-Entscheidung (Transition-Steckbrief)",
pfade:[
["Freigabe","Weiter zu den Deploy-Aktivitäten → tr_10"],
["Freigabe mit Auflagen","Weiter, definierte Punkte müssen nachgearbeitet werden"],
["Zurück an Build","Nachbesserung erforderlich → tr_02 (ggf. tr_05tr_07)"],
["Ablehnung","Endgültige Ablehnung — erfordert SOR-Eskalation"]
],
pruef:[
["Übergabe-Vollständigkeit","Sind alle Artefakte vorhanden?"],
["Abnahme-Status","Liegt die Auftraggeber-Abnahme vor?"],
["Ressourcen-Outlook","Sind Betrieb und Support prinzipiell vorbereitet?"],
["Pilot-Entscheidung","Ist ein Pilotbetrieb erforderlich?"]
],
raci:[["service_owner","A"],["projektleitung","R"],["betriebsteam","C"],["service_support_team","C"],["spm","I"]],
quiz:[
{frage:"Wer entscheidet an Gate 2 (Accountable)?",
optionen:["SOR-Gremium","Service Owner allein","SPM","Projektleitung"], richtig:1,
expl:"Gate 2 ist eine SO-Einzelentscheidung; bei Ablehnung erfolgt SOR-Eskalation."},
{frage:"Was prüft Gate 2 vor allem?",
optionen:["Die Budgetfreigabe für das Design","Die Übergabefähigkeit nach dem Build","Die Portfolio-Strategie","Die Incident-Quote"], richtig:1,
expl:"Gate 2 validiert die Übergabefähigkeit nach Abschluss des Builds (Vollständigkeit, Abnahme, Ressourcen)."}
]},
{ id:"tr_10", phase:"transition", typ:"aktivitaet",
name:"Ausrollen der Service-Komponenten",
beschreibung:"Technische Bereitstellung/Deployment des Services in die produktive Umgebung.",
umfasst:["Deployment von Anwendungen, Systemen, Komponenten","Konfiguration produktiver Systeme","Migration von Daten","Aktivierung von Monitoring","Zugänge, Rollen, Berechtigungen","Schnittstellen-Integration","Technische Abnahmetests"],
artefakt:"Ausgerollter Service (produktiv)",
raci:[["service_owner","A"],["betriebsteam","R"],["spm","C"],["lieferant","C/I"]],
quiz:[
{frage:"Wer rollt die Service-Komponenten aus (R)?",
optionen:["Betriebsteam","Projektteam","Testmanagement","SPM"], richtig:0,
expl:"Das Betriebsteam ist Responsible für das Deployment; der Service Owner ist Accountable."}
]},
{ id:"tr_11", phase:"transition", typ:"aktivitaet",
name:"Vorbereiten der Service-Aktivierung",
beschreibung:"Prüfung der Betriebsbereitschaft und Vorbereitung des Go-Live-Antrags für Gate 3.",
umfasst:["Review der Betriebsdokumentation","Prüfung der Überwachungsregeln","Prüfung der Rollen- und Eskalationswege","Zugriffe & Toolanbindung sicherstellen","Vorbereitung der Go-Live-Kommunikation","Validierung mit Supportteams","Vorbereitung Gate-3-Antrag (SOR-Vorlage)"],
artefakt:"Go-Live-Readiness (Gate-3-Antrag)",
raci:[["service_owner","A"],["betriebsteam","R"],["spm","C"]],
quiz:[
{frage:"Worauf bereitet tr_11 vor?",
optionen:["Den Build-Start","Den Go-Live-Antrag für Gate 3","Das ELS-Ende","Die Außerbetriebnahme"], richtig:1,
expl:"tr_11 prüft die Betriebsbereitschaft und bereitet den Gate-3-Antrag (SOR-Vorlage) vor."}
]},
{ id:"tr_12", phase:"transition", typ:"gate", gateNr:3,
name:"Gate 3: Go-Live-Freigabe",
beschreibung:"Portfolio-Freigabe und formale Aktivierung des Services. Exit-Gate der Transition — SOR-Entscheidung nach dem Konsent-Prinzip.",
ziel:"Sicherstellen, dass nur Services für den produktiven Betrieb aktiviert werden, die ausreichend geprüft und qualitätsgesichert sind und für die sowohl im Betrieb als auch im Support genügend Ressourcen zur Verfügung stehen.",
umfasst:["Prüfung der Portfolio-Konsistenz","Prüfung der Betriebsreife (SO-Bestätigung)","Prüfung der Support-Bereitschaft","Prüfung der SLA/SLO-Vereinbarung","Prüfung der Auflagen-Erfüllung aus Gate 2","Bewertung der Geschäftskritikalität","Formale Aufnahme ins Service-Portfolio"],
artefakt:"Gate-3-Entscheidung + Portfolio-Aufnahme",
pfade:[
["Go-Live","Service wird aktiviert → Operation (op_01)"],
["Go-Live mit Auflagen","Aktivierung, definierte Punkte nacharbeiten → Operation"],
["Zurück an Transition","Nachbesserung erforderlich → tr_10 (ggf. tr_09)"],
["Ablehnung","Endgültige Ablehnung des Service-Vorhabens"]
],
raci:[["sor","A"],["service_owner","R"],["spm","C"],["projektleitung","C"],["operations_manager","C"],["support_manager","C"]],
quiz:[
{frage:"Wer entscheidet an Gate 3 (Accountable)?",
optionen:["Service Owner","SOR (Konsent)","Support Manager","Betrieb"], richtig:1,
expl:"Gate 3 ist eine SOR-Entscheidung nach dem Konsent-Prinzip — das Exit-Gate der Transition."},
{frage:"Was bewirkt die Go-Live-Freigabe zusätzlich?",
optionen:["Die Aufnahme ins Service-Portfolio","Den Start des Designs","Die Schließung aller Incidents","Den ELS-Exit"], richtig:0,
expl:"Mit der Freigabe wird der Service formal ins Portfolio aufgenommen und der Katalog-Eintrag aktiviert."}
]},
{ id:"op_01", phase:"operation", typ:"aktivitaet",
name:"Early Life Support (ELS)",
beschreibung:"Phase erhöhter Aufmerksamkeit direkt nach Go-Live: produktiv, aber intensiver beobachtet und betreut als im Normalbetrieb.",
umfasst:["Erhöhte Monitoring-Bereitschaft","Direkte Eskalation zum Projektteam","Schnelles Troubleshooting","Erste Incidents aufarbeiten","ELS-Exit-Entscheidung (SO)"],
artefakt:"Stabilisierter Service (ELS-Exit)",
raci:[["service_owner","A"],["support_manager","R"],["operations_manager","R"],["projektteam","C"],["spm","I"]],
quiz:[
{frage:"Wer entscheidet über den ELS-Exit?",
optionen:["SOR","Service Owner","Support Manager","Betrieb"], richtig:1,
expl:"Der Service Owner entscheidet eigenständig (Informationspflicht an SPM)."}
]},
{ id:"op_02", phase:"operation", typ:"aktivitaet",
name:"Bereitstellen von Leitlinien für den Service-Betrieb",
beschreibung:"Strukturelle Grundlage für den täglichen Servicebetrieb.",
umfasst:["Betriebshandbuch bereitstellen/pflegen","Betriebsprozesse & Arbeitsanweisungen","Rollen, Eskalation, Kommunikation klar","Standard Changes & Known Errors"],
artefakt:"Betriebshandbuch",
raci:[["service_owner","A"],["operations_manager","R"],["spm","C/I"]],
quiz:[
{frage:"Welches Artefakt ist hier zentral?",
optionen:["Service-Definition","Betriebshandbuch","Testbericht","Gate-Vorlage"], richtig:1,
expl:"Das Betriebshandbuch ist die strukturelle Grundlage des laufenden Betriebs."}
]},
{ id:"op_03", phase:"operation", typ:"aktivitaet",
name:"Durchführen laufender Betriebsaufgaben",
beschreibung:"Täglich wiederkehrende Betriebsaktivitäten zur Erbringung des Services.",
umfasst:["Benutzerverwaltung & Berechtigungen","Backup/Restore","Technische Routineaufgaben","Konfigurationspflege","Pflege von Logs, Diensten, Überwachungsobjekten","Security- & Compliance-Anforderungen","Umsetzung freigegebener Standard-Changes"],
artefakt:"Erbrachte Betriebsleistung",
raci:[["operations_manager","A"],["betriebsteam","R"],["service_owner","C"],["lieferant","C/I"],["spm","I"]],
quiz:[
{frage:"Wer ist Responsible (R) für die laufenden Betriebsaufgaben?",
optionen:["Betriebsteam","Service Owner","SPM","Support Manager"], richtig:0,
expl:"Das Betriebsteam führt aus (R); der Betrieb (AL B&C / AL App) ist Accountable."}
]},
{ id:"op_04", phase:"operation", typ:"aktivitaet",
name:"Ressourcen, Dienstleister und Budget managen",
beschreibung:"Sicherstellen, dass der Servicebetrieb über die nötigen Mittel verfügt — stabil und wirtschaftlich.",
umfasst:["Personelle und technische Ressourcen sicherstellen","Koordination mit externen Dienstleistern","Überwachung des Betriebsbudgets","Abstimmung mit Supplier-/Finanz-/Vertragsmanagement","Sicherstellen von Wartung & Lizenzen"],
artefakt:"Ressourcen- & Budget-Status",
raci:[["operations_manager","A"],["betriebsteam","R"],["service_owner","C"],["spm","I"]],
quiz:[
{frage:"Was ist das Ziel dieser Aktivität?",
optionen:["Incidents lösen","Ressourcen, Dienstleister und Budget steuern","Den Service designen","Das Review durchführen"], richtig:1,
expl:"op_04 sichert die nötigen personellen, technischen und finanziellen Mittel für einen stabilen, wirtschaftlichen Betrieb."}
]},
{ id:"op_05", phase:"operation", typ:"aktivitaet",
name:"Überwachen der Services",
beschreibung:"Strukturierte Überwachung des Services anhand definierter KPIs und Metriken.",
umfasst:["Verfügbarkeit, Performance, Auslastung überwachen","Schnittstellen und Konfigurationsobjekte überwachen","Trends / drohende Störungen erkennen","Verknüpfung mit Events, Alerts, Dashboards","Monitoring-Regeln im Betrieb erstellen"],
artefakt:"Monitoring-Daten / Alerts",
raci:[["operations_manager","A"],["betriebsteam","R"],["service_owner","C"],["service_support_team","I"],["spm","I"]],
quiz:[
{frage:"Welche Aktivität beschreibt op_05?",
optionen:["Strukturierte Überwachung anhand von KPIs/Metriken","Erstellung der Service-Definition","Lösen von Incidents","Gate-Entscheidung"], richtig:0,
expl:"op_05 überwacht Verfügbarkeit, Performance und Auslastung und verknüpft mit Events/Alerts/Dashboards."}
]},
{ id:"op_06", phase:"operation", typ:"aktivitaet",
name:"Erstellen des Service-Qualitätsbericht",
beschreibung:"Regelmäßige formale Berichte über die erreichte Servicequalität.",
umfasst:["SLA-/SLO-Auswertung","Auswertung technischer KPIs","Abgleich gegen Qualitätsziele","Identifikation von Verbesserungspotenzialen","Zuarbeit für Service Review & SPM"],
artefakt:"Service-Qualitätsbericht",
raci:[["service_owner","A"],["betriebsteam","R"],["spm","C"]],
quiz:[
{frage:"Welches Artefakt entsteht hier?",
optionen:["Betriebshandbuch","Service-Qualitätsbericht","Problem Record","Testbericht"], richtig:1,
expl:"op_06 erstellt den Service-Qualitätsbericht (SLA-/SLO-Auswertung, KPIs) als Zuarbeit für das Review."}
]},
{ id:"op_07", phase:"operation", typ:"aktivitaet",
name:"Proaktive Problem Identifikation",
beschreibung:"Geht über Monitoring hinaus: aktive Suche nach strukturellen Problemen.",
umfasst:["Trendanalysen aus Monitoringdaten","Analyse von Engpässen, Ressourcenproblemen","Technische Analyse von Systemmustern","Erkennen potenzieller SLA-Verletzungen"],
artefakt:"Identifizierte strukturelle Probleme",
raci:[["service_owner","A"],["betriebsteam","R"],["spm","I"]],
quiz:[
{frage:"Wodurch unterscheidet sich op_07 vom reinen Monitoring?",
optionen:["Es ist rein reaktiv","Es ist die aktive Suche nach strukturellen Problemen","Es schließt Tickets","Es entscheidet über Go-Live"], richtig:1,
expl:"Proaktive Problem-Identifikation nutzt Trend-/Musteranalyse zur Früherkennung struktureller Probleme."}
]},
{ id:"sp_01", phase:"support", typ:"aktivitaet",
name:"Bereitstellen von Leitlinien für den Service-Support",
beschreibung:"Strukturelle Rahmenbedingungen, damit der Service-Support effizient arbeiten kann.",
umfasst:["Unterstützungsprozesse (Incident-/Request-Modell)","Vorgaben zu Eskalationen, Reaktionszeiten, Rollen","Tool-Konfigurationen (Ticketklassen, Templates)","Zugriffsmöglichkeiten & Security-Policies","Konsistentes Wissensmanagement"],
artefakt:"Support-Leitlinien",
raci:[["service_owner","A"],["support_manager","R"],["spm","C/I"]],
quiz:[
{frage:"Wer ist Responsible (R) für die Support-Leitlinien?",
optionen:["Support Manager","Service Owner","SPM","1st Level Agent"], richtig:0,
expl:"Der Support Manager führt aus und pflegt die Leitlinien; der Service Owner verantwortet die fachlichen Vorgaben (A)."}
]},
{ id:"sp_02", phase:"support", typ:"aktivitaet",
name:"Wissensdatenbank",
beschreibung:"Strukturierter Wissensspeicher für Support-Lösungen, Betriebsinfos, Workarounds und Standardanfragen.",
umfasst:["Lösungen zu Incidents speichern","Workarounds dokumentieren","FAQ, Standard-Requests, Anleitungen","Pflege & Aktualisierung","Zentrales Arbeitsmittel für 1st & 2nd Level"],
artefakt:"Gepflegte Wissensdatenbank",
raci:[["service_owner","A"],["service_support_team","R"],["problem_manager","C"]],
quiz:[
{frage:"Wer liefert Workarounds und Known Errors für die Wissensdatenbank (C)?",
optionen:["Problem Manager","Queue Koordinator","SPM","Testmanagement"], richtig:0,
expl:"Der Problem Manager liefert Workarounds/Known Errors; das Service-Support-Team pflegt die Inhalte (R)."}
]},
{ id:"sp_03", phase:"support", typ:"aktivitaet",
name:"Überwachen / Verteilung von Incidents und Service Requests",
beschreibung:"Koordination eingehender Störungen/Anfragen: Priorisierung, Routing, korrekte Aufnahme und Verteilung (Ticket-Queue-Koordination).",
umfasst:["Sichtung neuer Tickets","Automatisches oder manuelles Routing","Prioritäts- und Impact-Bewertung","SLA-Konformität sicherstellen","Eskalation bei Bedarf"],
artefakt:"Priorisierte/geroutete Tickets",
raci:[["support_manager","A"],["queue_koordinator","R"]],
quiz:[
{frage:"Wer übernimmt Sichtung und Routing der Tickets (R)?",
optionen:["Queue Koordinator","Support Manager","2nd Level Agent","Service Owner"], richtig:0,
expl:"Der Queue Koordinator erledigt die initiale Sichtung und das Routing; der Support Manager ist Accountable."}
]},
{ id:"sp_04", phase:"support", typ:"aktivitaet",
name:"Bearbeiten von Requests",
beschreibung:"Behandlung aller Standard-Serviceanfragen von Nutzern (z.B. Passwort, Berechtigungen, Informationen).",
umfasst:["Klassifizierung gemäß Request-Katalog","Prüfung von Berechtigungen","Erfüllung standardisierter Anfragen","Dokumentation der Erledigung"],
artefakt:"Erledigter Service Request",
raci:[["support_manager","A"],["first_level_agent","R"]],
quiz:[
{frage:"Wer bearbeitet einfache Standard-Requests (R)?",
optionen:["1st Level Agent","2nd Level Agent","Problem Manager","Betriebsteam"], richtig:0,
expl:"Der 1st Level Agent bearbeitet Standard-Requests gemäß Katalog; der Support Manager ist Accountable."}
]},
{ id:"sp_05", phase:"support", typ:"aktivitaet",
name:"Lösen von Incidents im 1st Level Support",
beschreibung:"Schnelle Lösung typischer Störungen mit bekanntem Wissen.",
umfasst:["Erstdiagnose","Nutzung der Wissensdatenbank","Anwenderunterstützung","Workarounds anwenden","Lösung dokumentieren","Entscheidung über Weiterleitung an 2nd Level"],
artefakt:"Gelöster Incident (1st Level)",
raci:[["support_manager","A"],["first_level_agent","R"]],
quiz:[
{frage:"Wer löst typische Incidents im 1st Level (R)?",
optionen:["1st Level Agent","2nd Level Agent","Problem Manager","Lieferant"], richtig:0,
expl:"Der 1st Level Agent löst mit bekanntem Wissen; reicht es nicht, erfolgt Weiterleitung an den 2nd Level."}
]},
{ id:"sp_06", phase:"support", typ:"aktivitaet",
name:"Lösen von Incidents im 2nd Level Support",
beschreibung:"Bearbeitung von Incidents, die im 1st Level nicht lösbar sind — tiefergehende Diagnostik und technische Lösung.",
umfasst:["Komplexe technische Diagnosen","Zusammenarbeit mit Betrieb, Fachverfahren, Lieferanten","Ggf. Einbezug von Spezialisten/Herstellern","Rückmeldung an 1st Level / Anwender","Lösung dokumentieren","Ungelöst → Problem Record"],
artefakt:"Gelöster Incident (2nd Level)",
raci:[["support_manager","A"],["second_level_agent","R"],["lieferant","C"],["service_owner","I"]],
quiz:[
{frage:"Wer übernimmt Incidents, die der 1st Level nicht lösen kann (R)?",
optionen:["2nd Level Agent","Queue Koordinator","SPM","Service Owner"], richtig:0,
expl:"Der 2nd Level Agent macht die tiefergehende Diagnose; bleibt es ungelöst, entsteht ein Problem Record (sp_09)."}
]},
{ id:"sp_07", phase:"support", typ:"aktivitaet",
name:"Incident Record (Gelöst) / Request Record (Gelöst)",
beschreibung:"Abschluss der Ticketbearbeitung inkl. Dokumentation und Qualitätssicherung.",
umfasst:["Validierung, ob wirklich gelöst","Kommunikation an den Endnutzer","Dokumentationen / FAQs aktualisieren","Klassifikation für Trendanalysen","Übergabe an Schließen von Incidents"],
artefakt:"Incident/Request Record (gelöst)",
raci:[["support_manager","A"],["first_level_agent","R"],["second_level_agent","R"]],
quiz:[
{frage:"Was passiert in sp_07?",
optionen:["Eröffnen eines Problem Records","Abschluss der Bearbeitung inkl. Validierung und Doku","Routing der Tickets","Root-Cause-Analyse"], richtig:1,
expl:"sp_07 validiert die Lösung, kommuniziert an den Nutzer und bereitet den finalen Abschluss (sp_08) vor."}
]},
{ id:"sp_08", phase:"support", typ:"aktivitaet",
name:"Schließen von Incidents & Service Requests",
beschreibung:"Finaler Ticketabschluss inkl. Dokumentation, Reporting und Einordnung für spätere Verbesserungen.",
umfasst:["Ticket final schließen","Prüfung vollständiger Dokumentation","Ticketklassifizierung / KPI-Zuordnung","Rückmeldung an Monitoring/Reporting","Wiederkehrende Incidents identifizieren"],
artefakt:"Geschlossenes Ticket + KPI-Zuordnung",
raci:[["support_manager","A"],["first_level_agent","R"],["second_level_agent","R"]],
quiz:[
{frage:"Wer ist Accountable für das Schließen von Incidents und Requests?",
optionen:["Support Manager","1st Level Agent","Problem Manager","SOR"], richtig:0,
expl:"Der Support Manager ist Accountable; 1st und 2nd Level sind Responsible für den finalen Abschluss."}
]},
{ id:"sp_09", phase:"support", typ:"aktivitaet",
name:"Anlegen Problem Record für nicht lösbare Incidents",
beschreibung:"Ist ein Incident auch im 2nd Level nicht lösbar, wird eine strukturelle Ursache vermutet.",
umfasst:["Nicht-lösbar-Entscheidung","Dokumentation der Symptome","Eröffnen eines Problem Records","Übergabe an Root-Cause-Analyse"],
artefakt:"Problem Record",
raci:[["problem_manager","A"],["second_level_agent","R"],["service_owner","I"]],
quiz:[
{frage:"Welches Artefakt entsteht, wenn ein Incident nicht lösbar ist?",
optionen:["Workaround","Problem Record","Service-Definition","Testprotokoll"], richtig:1,
expl:"sp_09 eröffnet einen Problem Record; der Problem Manager ist Accountable, der 2nd Level Agent Responsible."}
]},
{ id:"sp_10", phase:"support", typ:"aktivitaet",
name:"Wiederkehrende Incidents analysieren & Problem Record anlegen",
beschreibung:"Reaktiver Weg zur strukturierten Problemerkennung, wenn dieselbe Störung mehrfach auftritt.",
umfasst:["Clustering wiederkehrender Incidents","Analyse gemeinsamer Ursachen","Entscheidung: Problem Record notwendig","Übergabe an Problem Management"],
artefakt:"Problem Record (aus wiederkehrenden Incidents)",
raci:[["problem_manager","A"],["support_manager","R"],["second_level_agent","C"],["service_owner","I"]],
quiz:[
{frage:"Wann wird in sp_10 ein Problem Record angelegt?",
optionen:["Bei jedem Incident","Wenn dieselbe Störung mehrfach auftritt","Nur an Gates","Bei Go-Live"], richtig:1,
expl:"sp_10 clustert wiederkehrende Incidents und legt bei gemeinsamer Ursache einen Problem Record an."}
]},
{ id:"sp_11", phase:"support", typ:"aktivitaet",
name:"Operative Root-Cause-Analyse durchführen & Workaround bereitstellen",
beschreibung:"Start der Problembehandlung zur Ermittlung der Ursache und Schaffung eines Workarounds.",
umfasst:["Root-Cause-Analyse","Erstellen eines Workarounds","Eintrag in die Wissensdatenbank","Entscheidung über Change-Bedarf"],
artefakt:"Workaround + aktualisierter Problem Record",
raci:[["service_owner","A"],["problem_manager","R"],["second_level_agent","C"],["lieferant","C/I"]],
quiz:[
{frage:"Was wird in der operativen Root-Cause-Analyse zusätzlich bereitgestellt?",
optionen:["Ein Workaround","Ein Betriebshandbuch","Eine Gate-Vorlage","Ein Review-Bericht"], richtig:0,
expl:"sp_11 ermittelt die Ursache, erstellt einen Workaround und entscheidet über Change-Bedarf (aktualisiert den Problem Record)."}
]},
/* Review-Phase = ARBEITSSTAND (Vorschlag Frank, Change-Enablement) — noch NICHT
im Blueprint-YAML; vor Konzept-Uebernahme mit Michael abstimmen.
RACI + Quiz hier abgeleitet (Franks Entwurf nennt nur die Aktivitaeten). */
{ id:"rv_01", phase:"review", typ:"aktivitaet", reviewStation:true,
name:"Durchführen von Service-Reviews",
beschreibung:"Den Service systematisch auswerten und die Ergebnisse im Service-Review-Dokument festhalten (4 Dimensionen → Handlungsempfehlung).",
ziel:"Eine fundierte Entscheidung ermöglichen, ob der Service unverändert weiterbetrieben werden kann oder ob Änderungen erforderlich sind.",
verantwortlich:"service_owner",
pruef:[
["Leistungserbringung","Liefert der Service den erwarteten Nutzen? (Zielerreichung, Verfügbarkeit, Performance, Funktionsumfang)"],
["Betriebsstabilität","Läuft der Service störungsarm und beherrschbar? (Incident-Häufigkeit, wiederkehrende Probleme, Betriebsaufwand)"],
["Nutzerzufriedenheit","Wie bewerten die Nutzer den Service? (Support-Feedback, Beschwerden, VoC-Signale)"],
["Zukunftsfähigkeit","Ist der Service mittelfristig tragfähig? (Technische Schulden, Abhängigkeiten, strategische Passung)"]
],
empfehlung:["Weiterbetrieb (ggf. mit Monitoring-Fokus)","Änderung als Normal-Change","Änderung als Major-Change"],
umfasst:["KPIs & Monitoring auswerten","Problems & Incidents auswerten","Kundenfeedback sammeln/einholen","zugrunde liegende Infrastruktur bewerten","Bewertung über 4 Dimensionen: Leistungserbringung · Betriebsstabilität · Nutzerzufriedenheit · Zukunftsfähigkeit","Handlungsempfehlung ableiten: Weiterbetrieb · Normal-Change · Major-Change"],
artefakt:"Service-Review-Dokument",
raci:[["service_owner","A"],["betriebsteam","R"],["service_support_team","C"],["problem_manager","C"]],
quiz:[
{frage:"Welches Dokument entsteht beim Service-Review?",
optionen:["RFC","Service-Review-Dokument","Change-Steckbrief","Incident Record"], richtig:1,
expl:"KPIs, Incidents, Feedback und Infrastrukturbewertung werden im Service-Review-Dokument zusammengeführt."}
]},
{ id:"rv_02", phase:"review", typ:"aktivitaet",
name:"Bewertung der Review-Ergebnisse",
beschreibung:"Die Review-Ergebnisse bewerten und bei Änderungsbedarf einen RFC erstellen; relevante Berichte gehen an die SOR.",
umfasst:["RFC für Normal- bzw. Major-Change erstellen","Berichte bei Bedarf an die SOR weiterleiten","Ergebnisse in der SOR ganzheitlich bewerten"],
artefakt:"Bewertete Review-Ergebnisse",
raci:[["sor","A"],["service_owner","R"],["spm","C"]],
quiz:[
{frage:"Was wird erstellt, wenn die Bewertung Änderungsbedarf zeigt?",
optionen:["Ein RFC (Request for Change)","Ein Incident Record","Ein Test-Report","Eine Service-Definition"], richtig:0,
expl:"Bei Normal- oder Major-Change entsteht ein RFC; Berichte gehen an die SOR."},
{frage:"Wer bewertet die Review-Ergebnisse ganzheitlich?",
optionen:["Der 1st Level","Die SOR","Das Testmanagement","Der Lieferant"], richtig:1,
expl:"Die SOR bewertet die Ergebnisse als Gremium ganzheitlich."}
]},
{ id:"rv_03", phase:"review", typ:"aktivitaet",
name:"Definieren von Service-Änderungen",
beschreibung:"Auf Basis der Bewertung konkrete Änderungsvorschläge formulieren, konsolidieren und die ausgewählte Änderung beschreiben.",
umfasst:["passende Änderungsvorschläge formulieren","Vorschläge bewerten & konsolidieren","ausgewählte Änderung beschreiben"],
artefakt:"Beschriebene Service-Änderung",
raci:[["service_owner","A/R"],["sor","C"],["architektur","C"]],
quiz:[
{frage:"Was ist das Ziel dieser Aktivität?",
optionen:["Incidents schließen","Änderungsvorschläge formulieren, konsolidieren und beschreiben","Den Service abschalten","Das Budget planen"], richtig:1,
expl:"Aus der Bewertung werden konkrete, beschriebene Service-Änderungen abgeleitet."}
]},
{ id:"rv_04", phase:"review", typ:"aktivitaet",
name:"Starten von Service-Änderungen",
beschreibung:"Die Änderung anstoßen: bei Normal Change die Umsetzung planen; bei Major Change das Routing klären und den Change-Steckbrief ausfüllen.",
umfasst:["Normal Change: Umsetzung planen","Major Change: Routing klären (RUN / DPM / MB)","Major Change: Change-Steckbrief ausfüllen & weiterleiten"],
artefakt:"Change-Steckbrief (bei Major Change)",
raci:[["service_owner","A/R"],["sor","C"],["spm","C"],["dpm","I"]],
pfade:[["RUN","Durchführung im laufenden Betrieb (Service Owner)"],["DPM","über den Demand- & Projektprozess"],["MB","direkt in den Projektprozess oder RUN"]],
quiz:[
{frage:"Was muss beim Major Change vor der Umsetzung geklärt werden?",
optionen:["Das Routing: RUN, DPM oder MB","Die Ticket-Queue","Der Eskalationsweg","Die Lizenzkosten"], richtig:0,
expl:"Beim Major Change wird das Routing (RUN/DPM/MB) geklärt und ein Change-Steckbrief erstellt; beim Normal Change wird direkt die Umsetzung geplant."}
]},
{ id:"rv_05", phase:"review", typ:"aktivitaet",
name:"Implementieren von Service-Änderungen",
beschreibung:"Die Änderung gemäß gewähltem Weg umsetzen, dokumentieren und abschließen.",
umfasst:["Normal & Major (Weg RUN): SO führt durch, dokumentiert, schließt ab","Major (Weg DPM): Demand- & Projektprozess","Major (Weg MB): Projektprozess oder RUN"],
artefakt:"Umgesetzte Service-Änderung (konkretes Artefakt noch nicht konzipiert)",
raci:[["service_owner","A"],["projektteam","R"],["dpm","C"]],
quiz:[
{frage:"Wer führt eine Änderung auf dem Weg „RUN“ durch?",
optionen:["Der Service Owner","Der Demand Portfolio Manager","Ein externes Projektteam","Die SOR"], richtig:0,
expl:"Im Weg RUN führt der Service Owner die Änderung durch, dokumentiert und schließt sie ab."}
]}
];
/* ====================== SERVICE-AKTE (Artefakte A1-A14, App-gefuehrt) ======================
Die Akte ist rein digital: erzeugte Artefakte werden per Choice bestimmt und
gesammelt; Gates sind hart gekoppelt (oeffnen nur mit den geforderten Artefakten). */
const ARTEFAKTE = {
A1:{name:"Projektauftrag", phase:"design",
was:"Der freigegebene Auftrag aus dem Demand-Lifecycle: Ziel, Rahmen, benannte Projektleitung sowie zugesagtes Budget und Ressourcen (nach DSR/MB-Freigabe).",
warum:"Er ist das Startsignal und Mandat für die Design-Phase — ohne ihn gibt es weder einen legitimierten Start noch zugewiesene Ressourcen."},
A2:{name:"Service-Definition", phase:"design", live:true,
was:"Zweck, Nutzen und Zielgruppen des Services, Utility & Warranty, die SLA-/SLO-Anforderungen sowie Abhängigkeiten zu anderen Services.",
warum:"Sie legt fachlich fest, WAS der Service leisten soll — der Bezugspunkt für Design, Betrieb und das spätere Review."},
A3:{name:"Service Design Document", phase:"design",
was:"Servicearchitektur (Komponenten, Schnittstellen), Design der Betriebs- und Supportprozesse, Security/Datenschutz/Compliance, Monitoring/Reporting und das Rollenmodell.",
warum:"Es übersetzt das „Was“ der Service-Definition in das WIE — der technische und organisatorische Bauplan und Voraussetzung für Gate 1."},
A4:{name:"Implementation Blueprint", phase:"design",
was:"Plan zur organisatorischen Einführung: Integration, Rollenübergaben, Anpassung von Prozessen/Tools, Trainings & Kommunikation, Bewertung der Time-to-Operate.",
warum:"Er stellt sicher, dass der Service nicht nur gebaut, sondern auch wirklich in die Organisation eingeführt und übernommen werden kann."},
A5:{name:"Gate-/SOR-Vorlage", phase:"transition",
was:"Die Entscheidungsvorlage für ein Gate: Aufwand/Risiken/Budget, Empfehlung und die zu prüfenden Dimensionen. An Gate 2 als „Transition-Steckbrief“.",
warum:"Sie macht Gate-Entscheidungen nachvollziehbar und legitimiert — ohne Vorlage keine SOR-/SO-Freigabe."},
A6:{name:"Betriebsdokumentation", phase:"transition",
was:"Service Operation Manual, Betriebshandbuch, Arbeitsanweisungen, Eskalationswege, Standard Changes, Known Errors und Konfigurations-/Betriebsrichtlinien.",
warum:"Sie macht den Service betreib- und supportbar — ohne sie kein stabiler Regelbetrieb und keine schnelle, einheitliche Störungsbehebung."},
A7:{name:"Test-Report", phase:"transition",
was:"Ergebnisse von Funktions-, Integrations- und Abnahmetests, der Nachweis der Betriebsreife sowie Testprotokolle und Freigaben.",
warum:"Er belegt, dass der Service funktioniert, BEVOR er live geht — Grundlage für die Freigabe an Gate 2."},
A8:{name:"Aktivierter Service", phase:"transition",
was:"Der freigegebene, produktive Service inklusive Aufnahme ins Portfolio und aktiviertem Katalog-Eintrag (ggf. mit Support-Dokumentation).",
warum:"Es markiert den offiziellen Go-Live — ab hier ist der Service in Betrieb und im Portfolio sichtbar und steuerbar."},
A9:{name:"Service-Qualitätsbericht", phase:"operation",
was:"SLA-/SLO-Auswertung, technische KPIs (Verfügbarkeit, Response Time), Abgleich gegen Qualitätsziele und Verbesserungspotenziale — inkl. Monitoring-/Betriebsdaten.",
warum:"Er zeigt objektiv, ob der Service seine Versprechen hält, und ist die Zuarbeit fürs Review."},
A10:{name:"Incident Record", phase:"support",
was:"Aufnahme, Bearbeitung, Lösung und Abschluss einer Störung oder eines Service Requests — inklusive Klassifizierung für Auswertungen. (Trägt auch den Request Record.)",
warum:"Es dokumentiert Störungen/Anfragen nachvollziehbar, sichert SLA-Konformität und liefert Daten für Trendanalysen."},
A11:{name:"Problem Record", phase:"support", live:true,
was:"Beschreibung, Symptome und Diagnosewege, bekannte Workarounds, die Ursache (Root Cause, wenn gefunden), Change-Bedarf sowie Status und Priorität.",
warum:"Es bündelt die strukturelle Ursachenarbeit hinter wiederkehrenden oder ungelösten Incidents — das einzige im Blueprint formal definierte Artefakt."},
A12:{name:"Workaround", phase:"support",
was:"Eine vorläufige Umgehungslösung, die den Betrieb stabil hält, bis die eigentliche Ursache behoben ist — mit Eintrag in die Wissensdatenbank.",
warum:"Es hält den Service nutzbar, obwohl die Ursache noch offen ist, und reduziert den Druck auf den Support."},
A13:{name:"Wissensdatenbank-Eintrag", phase:"support", live:true,
was:"Standardlösungen, Known Errors, Workarounds, FAQ und Anleitungen — das zentrale Arbeitsmittel für 1st und 2nd Level Support.",
warum:"Es beschleunigt den Support, sichert konsistente Antworten und entlastet die höheren Support-Level."},
A14:{name:"Service-Review-Dokument", phase:"review",
was:"Verantwortlich: Service-Owner. Bewertung über 4 Dimensionen mit Leitfrage — Leistungserbringung (liefert der Service den erwarteten Nutzen?), Betriebsstabilität (läuft er störungsarm und beherrschbar?), Nutzerzufriedenheit (wie bewerten die Nutzer den Service?) und Zukunftsfähigkeit (ist er mittelfristig tragfähig?). Daraus eine Handlungsempfehlung: Weiterbetrieb (ggf. mit Monitoring-Fokus) · Änderung als Normal-Change · Änderung als Major-Change.",
warum:"Es ermöglicht eine fundierte Entscheidung, ob der Service unverändert weiterbetrieben werden kann oder ob Änderungen erforderlich sind — die Grundlage für die SOR-Bewertung."}
};
// Welche Station erzeugt welches A-Artefakt (Choice-Schritt -> Aufnahme in die Akte).
const STATION_ARTEFAKT = {
ds_01:"A2", ds_02:"A3", ds_03:"A4",
tr_06:"A6", tr_07:"A7", tr_08:"A5",
op_06:"A9",
sp_02:"A13", sp_07:"A10", sp_09:"A11", sp_11:"A12",
rv_01:"A14"
};
// Gate erzeugt Artefakt (beim Vorwaerts-Durchschreiten).
const GATE_PRODUCES = { tr_12:"A8" };
// Geforderte Artefakte je Gate (HARTE Kopplung).
const GATE_REQ = { tr_01:["A2","A3","A4"], tr_09:["A6","A7"], tr_12:["A6","A7","A2"] };
// Stationen ohne eigenes Akte-Artefakt: Ergebnis fliesst in dieses A-Artefakt ein
// (nur dokumentierte Faelle; alle uebrigen Nicht-Akte-Stationen ohne Zuordnung).
const FOLDS_INTO = {
op_02:"A6", op_05:"A9", op_07:"A11",
sp_03:"A10", sp_04:"A10", sp_05:"A10", sp_06:"A10", sp_08:"A10", sp_10:"A11"
};
/* Bearbeitungstiefe (Frank): "bearbeitet" = eigene Arbeitsergebnisse/Templates,
alles andere "entwurf" (konzeptioneller Best-Practice-Entwurf). Fuer Spieler sichtbar. */
const BEARBEITET = new Set(["ds_01","tr_01","tr_09","tr_12","rv_01"]);
function stationTiefe(id){ return BEARBEITET.has(id) ? "bearbeitet" : "entwurf"; }
function tiefeBadge(id){
const t = stationTiefe(id);
return t==="bearbeitet"
? `<span class="tiefeBadge bearbeitet" title="Ausgearbeitet — eigene Arbeitsergebnisse/Templates liegen vor; hier ist detailliertes Feedback besonders wertvoll.">● Ausgearbeitet</span>`
: `<span class="tiefeBadge entwurf" title="Konzeptioneller Entwurf nach Best Practice — noch keine substantiellen eigenen Arbeitsergebnisse.">○ Entwurf</span>`;
}
/* Dokumente mit eigenem Detail-Feedback (Frank): Gate 1/2/3 + Service Review. */
const FEEDBACK_DOCS = {
tr_01:"Gate 1", tr_09:"Gate 2", tr_12:"Gate 3", rv_01:"Service Review"
};
function addArtefakt(a){ if(a){ S.akte = S.akte || {}; S.akte[a] = true; } }
// Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station
// (bzw. vor durchschrittenen Gates) entsteht, "liegt schon vor".
function seedAkte(entryIdx){
S.akte = { A1:true };
for(const sid in STATION_ARTEFAKT){
const j = STATIONEN.findIndex(s=>s.id===sid);
if(j>=0 && j < entryIdx) addArtefakt(STATION_ARTEFAKT[sid]);
}
for(const gid in GATE_PRODUCES){
const j = STATIONEN.findIndex(s=>s.id===gid);
if(j>=0 && j < entryIdx) addArtefakt(GATE_PRODUCES[gid]);
}
}
/* ====================== STATE ====================== */
const LS_KEY = "slc-companion-proto";
function defaultState(){
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:{}, akteFlash:null, svcModalSel:null,
index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0, gateCritDone:false, gate1Approval:null,
feedback:{}, feedbackSaved:{}, feedbackQueue:[],
actStep:0, actReveal:false, actDone:false, arteWrong:null,
picks:{}, done:{}, akte:{},
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(); } }
function save(){ localStorage.setItem(LS_KEY, JSON.stringify(S)); }
/* ====================== HELPERS ====================== */
const $ = s => document.querySelector(s);
const cur = () => STATIONEN[S.index];
function pkey(sid, qi){ return sid+"#"+qi; }
function roleLabel(id){ return ROLLEN[id] || id; }
function acard(si, ci){ return USE_CASES[si].changes[ci]; } // {titel, text}
function cardHtml(si, ci){ const c = acard(si, ci);
return `<div style="font-weight:800;font-size:18px;color:var(--ink);margin-bottom:6px">${c.titel}</div>`
+ `<div>${c.text}</div>`
+ `<div style="color:var(--muted);font-style:italic;margin-top:8px">Was passiert an welchen Stellen?</div>`; }
// Finale Action-Card-Grafik (cards/s<service>-c<change>.png) — alle 24 vorhanden.
function cardImg(si, ci){ return `cards/s${si}-c${ci}.png`; }
function cardMedia(si, ci){ const f = cardImg(si, ci);
return f ? `<img class="acImg" src="${f}" alt="Action Card: ${acard(si,ci).titel}"
style="display:block;width:100%;max-width:300px;margin:6px auto;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.12)">`
: cardHtml(si, ci); }
/* ====================== RENDER: SIDEBAR ====================== */
function renderList(){
const groups = {};
STATIONEN.forEach((st,i)=>{ (groups[st.phase]=groups[st.phase]||[]).push({st,i}); });
let html = `<div class="navTop"><b>Stationen</b><button id="navClose" title="Schließen">✕</button></div>`;
for(const ph in PHASEN){
if(!groups[ph]) continue;
html += `<h3>${PHASEN[ph].label}</h3>`;
groups[ph].forEach(({st,i})=>{
const cls = [ "stationItem", i===S.index?"active":"", S.done[st.id]?"done":"" ].join(" ");
html += `<div class="${cls}" data-i="${i}">
<span class="dot" style="background:${PHASEN[ph].color}"></span>
<span class="id">${st.id}</span>
<span class="nm">${st.typ==="gate"?"⛩ ":""}${st.name.length>34?st.name.slice(0,32)+"…":st.name}</span>
</div>`;
});
}
$("#stationList").innerHTML = html;
$("#stationList").querySelectorAll(".stationItem").forEach(el=>{
el.onclick = ()=>{ document.body.classList.remove("navOpen"); enterStation(+el.dataset.i); save(); draw(); };
});
const nc = $("#navClose"); if(nc) nc.onclick = ()=> document.body.classList.remove("navOpen");
const pct = Math.round(Object.keys(S.done).length / STATIONEN.length * 100);
$("#progressBar").style.width = pct+"%";
}
/* ====================== RENDER: SERVICE-AKTE (Overlay) ====================== */
function renderAkte(){
const order = ["design","transition","operation","support","review"];
const ids = Object.keys(ARTEFAKTE);
const have = S.akte || {};
const n = ids.filter(a=>have[a]).length;
let html = `<div class="navTop"><b>📁 Service-Akte</b><button id="akteClose" title="Schließen">✕</button></div>`;
html += `<div class="akteCount">${n}/${ids.length} Artefakten gesammelt</div>`;
for(const ph of order){
const group = ids.filter(a => ARTEFAKTE[a].phase === ph);
if(!group.length) continue;
html += `<h3>${PHASEN[ph].label}</h3>`;
group.forEach(a=>{
const ok = !!have[a];
html += `<div class="akteItem ${ok?'have':''} ${a===S.akteFlash?'flash':''}">
<span class="aId" style="background:${PHASEN[ph].color}">${a}</span>
<span class="aNm">${ARTEFAKTE[a].name}${ARTEFAKTE[a].live?' · <i>lebend</i>':''}</span>
<span class="aChk">${ok?'✓':'○'}</span>
</div>`;
});
}
$("#akteList").innerHTML = html;
const c = $("#akteClose"); if(c) c.onclick = ()=> document.body.classList.remove("akteOpen");
}
/* ====================== RENDER: ROLLEN-GLOSSAR (Overlay) ====================== */
function renderRollen(){
let html = `<div class="navTop"><b>👥 Rollen</b><button id="rollenClose" title="Schließen">✕</button></div>`;
html += `<div class="akteCount">RACI-Rollen im Lebenszyklus · Farbe = Figuren-Kategorie</div>`;
for(const g of ROLLEN_GRUPPEN){
html += `<h3>${g.label}</h3>`;
g.roles.forEach(r=>{ html += rolleItemHtml(r, g.color); });
}
$("#rollenList").innerHTML = html;
const c = $("#rollenClose"); if(c) c.onclick = ()=> document.body.classList.remove("rollenOpen");
}
/* ====================== RENDER: SERVICE-BESCHREIBUNG (Modal/Popup) ====================== */
function serviceDetailHtml(si){
const u = USE_CASES[si]; const d = (u && u.detail) || {};
const nutzen = (d.nutzen||[]).map(x=>`<li>${x}</li>`).join("");
const aufgaben = (d.aufgaben||[]).map(x=>`<li>${x}</li>`).join("");
return `<p class="svcDesc">${u.desc}</p>`
+ (nutzen?`<h4 class="svcH">Kundennutzen</h4><ul class="svcList">${nutzen}</ul>`:``)
+ (aufgaben?`<h4 class="svcH">Aufgaben des IT-Providers</h4><ul class="svcList">${aufgaben}</ul>`:``);
}
function openSvcModal(si){
S.svcModalSel = (si!=null) ? si : (S.service!=null ? S.service : 0);
document.body.classList.remove("navOpen","akteOpen","rollenOpen");
document.body.classList.add("svcOpen"); save(); renderSvcModal();
}
function renderSvcModal(){
const inner = document.querySelector("#svcModal .svcModalInner");
if(!inner) return;
const sel = (S.svcModalSel!=null) ? S.svcModalSel : (S.service!=null ? S.service : 0);
const tabs = USE_CASES.map((u,si)=>
`<button class="svcTab ${si===sel?'sel':''}" data-si="${si}">${u.service}</button>`).join("");
inner.innerHTML = `
<div class="navTop"><b> Service-Beschreibung</b><button id="svcClose" title="Schließen">✕</button></div>
<div class="svcTabs">${tabs}</div>
<h3 class="svcModalTitle">${USE_CASES[sel].service}</h3>
${serviceDetailHtml(sel)}`;
const c = inner.querySelector("#svcClose"); if(c) c.onclick = ()=> document.body.classList.remove("svcOpen");
inner.querySelectorAll(".svcTab[data-si]").forEach(el=>{
el.onclick = ()=>{ S.svcModalSel=+el.dataset.si; save(); renderSvcModal(); };
});
}
/* ====================== VIEW DISPATCH ====================== */
function draw(){
document.body.classList.toggle("runMode", S.view==="run");
if(S.view!=="run"){ document.body.classList.remove("navOpen","akteOpen","rollenOpen"); }
renderCardBadge();
renderSvcModal();
if(S.view==="deck") return renderDeck();
if(S.view==="mainIntro") return renderMainIntro();
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();
}
function renderCardBadge(){
const el = $("#cardBadge");
if(S.view==="deck" || S.service==null){ el.style.display="none"; el.innerHTML=""; return; }
el.style.display="flex";
const chip = S.classifyDone ? `<span class="ctChip">${CHANGE_TYPES[S.change]}</span>` : ``;
el.innerHTML = `<span class="cb-svc">${USE_CASES[S.service].service}</span>${chip}`;
}
/* ---------- Schritt 1: Action Card ziehen (Raster aller Karten) ---------- */
function renderDeck(){
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 · 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=0; S.mode="main";
S.classifyDone=true; S.classifyWrong=null;
S.freigabeDone=true; S.freigabeWrong=null;
S.entryDone=true; S.entryWrong=null; S.bonusDone={};
S.view="mainIntro"; save(); draw(); };
});
}
/* ---------- Main-Intro: Major Change — Service wird neu eingeführt ------- */
function renderMainIntro(){
const u = USE_CASES[S.service];
const card = acard(S.service, 0);
const rec = START_EMPFEHLUNG[0];
const recIndex = STATIONEN.findIndex(s=>s.id===rec.id);
const cardBig = `<img class="classifyCard" src="cards/s${S.service}-c0.png" alt="${card.titel}">`;
$("#panel").innerHTML = `
<div class="setupHead">Main · ${u.service}</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">Major Change</div>
<h2 class="setupTitle" style="margin-top:8px">${card.titel}</h2>
<div class="recBox"><h4>Worum geht's</h4>
<p style="margin:0 0 8px">${card.text}</p>
<p style="margin:0;color:var(--muted)">Als <b>Major Change</b> wird der Service <b>neu eingeführt</b> und durchläuft den <b>kompletten Lifecycle ab Design</b>. Die Freigaben fallen unterwegs an den <b>Gates</b> (SOR bzw. Service Owner) an — hier müsst ihr nichts einordnen. Das Einordnen (Change-Art · Freigabe · Einstieg) kommt danach bei den <b>Bonus-Varianten</b>.</p></div>
<div class="recBox"><h4>Service-Beschreibung</h4>${serviceDetailHtml(S.service)}</div>
${slcOverview("design","Start — ihr durchlauft den vollen Lifecycle ab <b>Design</b>")}
</div>
</div>
<div class="actions">
<button class="ghost" id="mainBackDeck">← Service-Deck</button>
<div class="spacer"></div>
<button class="primary" id="startRun">Los geht's → voller Durchlauf</button>
</div>`;
$("#mainBackDeck").onclick=()=>{ S.view="deck"; save(); draw(); };
$("#startRun").onclick=()=>{ seedAkte(recIndex); enterStation(recIndex); S.view="run"; save(); draw(); };
}
/* ---------- Aufgabe 1 (Bonus): Change-Art-Begründung -------------------- *
* Der Typ steht auf der Bonus-Karte → kein Rate-Quiz; es wird direkt die
* Begründung gezeigt (classifyDone wird beim Kartenklick auf true gesetzt). */
function renderClassify(){
const 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.classifyDone){
const choices = CT_ORDER.map(i=>
`<button class="choice ${S.classifyWrong===i?'bad':''}" data-i="${i}">${CHANGE_TYPES[i]}</button>`).join("");
const legend = `<div class="legend"><h4>Legende: Change-Arten im DIGITOM</h4>` +
CT_ORDER.map(i=>`<div class="lgItem"><div class="lgName">${CHANGE_TYPES[i]}</div><div class="lgIdee">${CHANGE_LEGEND[i].idee}</div></div>`).join("") + `</div>`;
const hint = S.classifyWrong!=null
? `<div class="hint bad">Nicht ganz — überlegt nochmal und probiert es erneut.</div>` : ``;
$("#panel").innerHTML = `
<div class="setupHead">Aufgabe 1 · Change-Art bestimmen</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<h2 class="setupTitle" style="margin-top:0">Welche Art von Change könnte das sein?</h2>
<p class="muted">Überlegt gemeinsam und wählt die passende Change-Art. Die Legende hilft beim Einordnen.</p>
${legend}
${hint}
<div class="choiceGrid grid2">${choices}</div>
</div>
</div>
<div class="actions"><button class="ghost" id="backDeck">← Varianten</button></div>`;
$("#panel").querySelectorAll(".choice").forEach(el=>{
el.onclick=()=>{ const i=+el.dataset.i;
if(i===correct){ S.classifyWrong=null; S.classifyDone=true; } else { S.classifyWrong=i; }
save(); renderClassify(); };
});
$("#backDeck").onclick=()=>{ S.view="bonusPick"; save(); draw(); };
} else {
$("#panel").innerHTML = `
<div class="setupHead">Aufgabe 1 · Change-Art & Begründung</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">Change-Art: ${CHANGE_TYPES[correct]}</div>
<p class="muted" style="margin:0 0 4px">Die Karte ist als <b>${CHANGE_TYPES[correct]}</b> ausgewiesen — besprecht gemeinsam, <b>warum</b> das so ist.</p>
<div class="recBox"><h4>Warum ist es ein ${CHANGE_TYPES[correct]}?</h4>
<p style="margin:0 0 8px;color:var(--ink)">${CHANGE_LEGEND[correct].idee}</p>
<div class="lgBed">${CHANGE_LEGEND[correct].bed.map(b=>`<div>${b}</div>`).join("")}</div>
<p style="margin:8px 0 0;color:var(--muted)"><b>Beispiel:</b> ${CHANGE_LEGEND[correct].bsp}</p></div>
</div>
</div>
<div class="actions">
<button class="ghost" id="backDeck">← Varianten</button>
<div class="spacer"></div>
<button class="primary" id="toFreigabe">Weiter → Freigabe bestimmen</button>
</div>`;
$("#backDeck").onclick=()=>{ S.view="bonusPick"; 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(); };
}
}
/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App).
currentPh: hebt die aktuelle/nächste Phase hervor, dimmt die übrigen. */
function phaseDonut(wrongPh, clickable, currentPh){
const order=["design","transition","operation","support","review"];
const cx=100,cy=100,R=92,r=46,seg=72,start=-90-seg/2;
const P=(a,rad)=>[ (cx+rad*Math.cos(a*Math.PI/180)).toFixed(1), (cy+rad*Math.sin(a*Math.PI/180)).toFixed(1) ];
let segs="", labels="";
order.forEach((ph,i)=>{
const a0=start+i*seg, a1=a0+seg, o0=P(a0,R), o1=P(a1,R), i1=P(a1,r), i0=P(a0,r);
const cls = (clickable?"donutSeg":"") + (ph===wrongPh?" bad":"")
+ (currentPh ? (ph===currentPh?" current":" dim") : "");
segs+=`<path class="${cls}" data-ph="${ph}" d="M${o0[0]} ${o0[1]} A${R} ${R} 0 0 1 ${o1[0]} ${o1[1]} L${i1[0]} ${i1[1]} A${r} ${r} 0 0 0 ${i0[0]} ${i0[1]} Z" fill="var(--${ph})"/>`;
const mid=a0+seg/2, L=P(mid,(R+r)/2);
labels+=`<text x="${L[0]}" y="${(+L[1]+3).toFixed(1)}" text-anchor="middle" font-size="9.5" font-weight="700" fill="#fff">${PHASEN[ph].label}</text>`;
});
labels+=`<text x="100" y="96" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Service-</text>`;
labels+=`<text x="100" y="108" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Lifecycle</text>`;
return `<svg viewBox="0 0 200 200" class="slcDonut${clickable?' clickable':''}" role="img" aria-label="Service-Lifecycle: Design, Transition, Operation und Support (parallel), Review">${segs}<g pointer-events="none">${labels}</g></svg>`;
}
/* Gesamtbild-Einblendung für den Major-Walk (Frank: Start, Phasenübergänge, Schluss).
phase = hervorzuhebende Phase (oder null am Schluss). Nur im Major-Modus genutzt. */
function slcOverview(phase, caption){
const cap = caption || (phase ? `Ihr seid in der Phase <b style="color:var(--${phase})">${PHASEN[phase].label}</b>` : `Alle Phasen durchlaufen`);
return `<div class="slcOverview">
<div class="slcOvHead">Service-Lifecycle — Gesamtbild</div>
<div class="slcOrient">${phaseDonut(null, false, phase)}<div class="slcCap">${cap}</div></div>
</div>`;
}
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
function renderEntry(){
const rec = START_EMPFEHLUNG[S.change];
const recIndex = STATIONEN.findIndex(s=>s.id===rec.id);
const correctPhase = STATIONEN[recIndex].phase;
const order = ["design","transition","operation","support","review"];
const cardBig = `<img class="classifyCard" src="cards/s${S.service}-c${S.change}.png" alt="${acard(S.service,S.change).titel}">`;
if(!S.entryDone){
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">Aufgabe 3 · Einstieg finden</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]]}</div>
<h2 class="setupTitle" style="margin-top:8px">Wo würde die Umsetzung starten — nachdem der Change freigegeben ist?</h2>
<p class="muted">Klickt <b>im Ring</b> auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.</p>
${hint}
<div class="slcOrient">${phaseDonut(S.entryWrong, true)}<div class="slcCap">Operation ⇄ Support laufen parallel</div></div>
</div>
</div>
<div class="actions"><button class="ghost" id="backFreigabe">← zurück</button></div>`;
$("#panel").querySelectorAll(".donutSeg").forEach(el=>{
el.onclick=()=>{ const ph=el.dataset.ph;
if(ph===correctPhase){ S.entryWrong=null; S.entryDone=true; } else { S.entryWrong=ph; }
save(); renderEntry(); };
});
$("#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">${isBonus?'Aufgabe 3 · Einstieg ✓':"Los geht's"}</div>
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="hint ok">✓ Einstieg: ${PHASEN[correctPhase].label}</div>
<div class="recBox"><h4>Start-Station</h4>
<p style="margin:0 0 6px"><b>${rec.id}${STATIONEN[recIndex].name}</b></p>
<p style="margin:0;color:var(--muted)">${rec.grund}</p></div>
</div>
</div>
<div class="actions">
<button class="ghost" id="backFreigabe">← zurück</button>
<div class="spacer"></div>
${startBtn}
</div>`;
$("#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>Bonus-Variante</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; // Change-Art ist auf der Karte verdeckt → Gruppe bestimmt sie selbst (Quiz)
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)
tr_09: ["next","next","tr_02","end"], // Freigabe / m. Auflagen / Zurueck an Build / Ablehnung
tr_12: ["next","next","tr_10","end"] // Go-Live / m. Auflagen / Zurueck / Ablehnung
};
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; S.gateDeciderDone = false; S.gateCrit = 0; S.gateCritDone = false; S.gate1Approval = null;
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
S.akteFlash = null; document.body.classList.remove("akteOpen");
}
function gateGoto(st, i){
S.done[st.id] = true;
const t = (GATE_FLOW[st.id] || [])[i] || "next";
if(t==="end"){ S.view="end"; S.endReason="rejected"; S.endGate=st.id; save(); draw(); return; }
if(t==="next" && GATE_PRODUCES[st.id]) addArtefakt(GATE_PRODUCES[st.id]); // z. B. Gate 3 Go-Live -> A8
if(t==="next"){ enterStation(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();
}
/* Zielstation einer Gate-Entscheidung (für Phasenwechsel-Feedback). -1 = Ende/Ablehnung. */
function gateTargetIndex(st, i){
const t = (GATE_FLOW[st.id] || [])[i] || "next";
if(t==="end") return -1;
if(t==="next") return Math.min(S.index+1, STATIONEN.length-1);
const j = STATIONEN.findIndex(s=>s.id===t);
return j>=0 ? j : Math.min(S.index+1, STATIONEN.length-1);
}
function raciTable(st){
const rows = st.raci.map(([r,c])=>`<tr><td>${roleLabel(r)}</td><td><span class="raciBadge raci-${c}">${c}</span></td></tr>`).join("");
return `<table class="raci"><thead><tr><th>Rolle</th><th>RACI</th></tr></thead><tbody>${rows}</tbody></table>`;
}
/* RACI-Legende (deutsch) — wird bei der RACI-Frage immer mit angezeigt.
Frank: R + A sind die Pflicht (durchdenken!), C + I ergänzend (nice-to-have). */
function raciLegendHtml(){
const kern = [
["R","Responsible","verantwortlich für die Durchführung — erledigt die Aufgabe operativ"],
["A","Accountable","rechenschaftspflichtig — trägt die Ergebnisverantwortung (genau eine Rolle)"]
];
const erg = [
["C","Consulted","konsultiert — wird vorab um Rat/Beitrag gefragt"],
["I","Informed","informiert — wird über das Ergebnis in Kenntnis gesetzt"]
];
const row = ([l,en,de])=>`<div class="rlRow"><span class="raciBadge raci-${l}">${l}</span><span><b>${en}</b> — ${de}</span></div>`;
return `<details class="raciLegend"><summary class="rlSummary">Klicke für RACI-Legende</summary>` +
`<div class="rlBody">` +
`<div class="rlGroup"><div class="rlGroupHead kern">★ Pflicht — unbedingt durchdenken</div>${kern.map(row).join("")}</div>` +
`<div class="rlGroup"><div class="rlGroupHead">ergänzend — nice-to-have</div>${erg.map(row).join("")}</div>` +
`</div></details>`;
}
/* Ausklappbare Rollenübersicht (gleiche Inhalte wie das linke Rollen-Panel),
direkt unter der RACI-Legende im Aktivitäts-Schritt. */
function rollenLegendHtml(){
let body = "";
for(const g of ROLLEN_GRUPPEN){
const items = g.roles.filter(r=>ROLLEN[r]).map(r=>rolleItemHtml(r, g.color)).join("");
if(!items) continue;
body += `<div class="rlGroup"><div class="rlGroupHead">${g.label}</div>${items}</div>`;
}
return `<details class="raciLegend rollenLegend"><summary class="rlSummary">Klicke für Rollenübersicht</summary><div class="rlBody">${body}</div></details>`;
}
function renderRun(){
renderList();
renderAkte();
renderRollen();
const st = cur();
const ph = PHASEN[st.phase];
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>`
: ``;
const cardBig = `<div class="runCardWrap">
<img id="runCard" class="classifyCard" src="cards/s${S.service}-c${S.change}.png" alt="${acard(S.service,S.change).titel}" title="Tippen für die ausführliche Service-Beschreibung">
<div class="svcHintRow"> Tippen für Service-Beschreibung</div>
</div>`;
const stepBody = st.typ==="gate"
? (S.stage==="gateDone" ? renderGateDone(st) : renderGate(st))
: renderActivity(st);
let body = `
${loopBanner}
<div class="classifyTop">
${cardBig}
<div class="classifyMain">
<div class="sHead">${chip}<span class="sId">${st.id}</span>${tiefeBadge(st.id)}</div>
<h2 class="sTitle" style="margin-top:8px">${st.name}</h2>
${stepBody}
</div>
</div>`;
$("#panel").innerHTML = body;
wire(st);
}
/* Aktivitaet — schrittweiser Mikro-Ablauf: 4 Fragen, je einzeln + Aufloesung.
1) Was steckt hinter der Ueberschrift? 2) Beteiligte Rollen 3) RACI 4) Artefakt */
function activitySteps(st){
// Review-Station (rv_01): gate-artige Struktur — Ziel/Verantwortlich · Prüfdimensionen ·
// Handlungsempfehlung · Artefakt (Service-Review-Dokument).
if(st.reviewStation){
return [
{ label:"Ziel & Verantwortlich",
frage:`Diskutiert: Was wird im <b>Service-Review</b> geprüft — und wer verantwortet ihn?`,
auf:`<h4 class="aufH">Ziel</h4><p style="margin:0 0 12px">${st.ziel}</p>`
+ `<h4 class="aufH">Verantwortlich</h4><div class="roleChips"><span class="roleChip">${roleLabel(st.verantwortlich)}</span></div>` },
{ label:"Prüfdimensionen",
frage:`Welche <b>Dimensionen</b> bewertet der Review? Sammelt sie gemeinsam, bevor ihr auflöst.`,
auf:`<h4 class="aufH">Prüfdimensionen</h4>${st.pruef.map(([n,d])=>`<p style="margin:0 0 8px"><b>${n}</b> — ${d}</p>`).join("")}` },
{ label:"Handlungsempfehlung",
frage:`Welche <b>Handlungsempfehlung</b> kann der Review ergeben?`,
auf:`<h4 class="aufH">Mögliche Handlungsempfehlung</h4><ul>${st.empfehlung.map(e=>`<li>${e}</li>`).join("")}</ul>` },
{ label:"Artefakt", artefakt:true,
frage:`Welches <b>Artefakt</b> entsteht hier und kommt in die <b>Service-Akte</b>?`,
auf:`<h4 class="aufH">Artefakt</h4><p style="margin:0"><b>${st.artefakt}</b></p>` }
];
}
return [
{ label:"Diskussion",
frage:`Diskutiert gemeinsam: Was fällt alles unter <b>„${st.name}"</b>? Was stellt ihr euch darunter vor? Nennt Beispiele.`,
auf:`<p style="margin:0 0 8px">${st.beschreibung}</p><h4 class="aufH">Das fällt darunter</h4><ul>${st.umfasst.map(u=>`<li>${u}</li>`).join("")}</ul>` },
{ label:"Verantwortung (R + A)",
frage:`Klärt die <b>zwei Pflicht-Rollen</b> dieser Aktivität: Wer setzt sie <b>operativ</b> um (<b>R</b>esponsible) und wer ist <b>rechenschaftspflichtig</b> (<b>A</b>ccountable — genau eine Rolle)? Stellt die Figuren auf die Spielfelder <b>R</b> und <b>A</b> im RACI-Kreis.`,
legend: raciLegendHtml() + rollenLegendHtml(),
auf:`<h4 class="aufH">Operativ verantwortlich (R)</h4><div class="roleChips">${(st.raci.filter(([r,c])=>c.includes("R")).map(([r,c])=>`<span class="roleChip">${roleLabel(r)}${c.includes("A")?" (zugleich A)":""}</span>`).join("")) || '<span class="roleChip">— (keine eigene R-Rolle)</span>'}</div>`
+ `<h4 class="aufH">Rechenschaftspflichtig (A)</h4><div class="roleChips">${(st.raci.filter(([r,c])=>c.includes("A")).map(([r,c])=>`<span class="roleChip">${roleLabel(r)}</span>`).join("")) || '<span class="roleChip">—</span>'}</div>`
+ `<p class="muted" style="margin:10px 0 0;font-size:13px">R und A sind die <b>Pflicht</b>. Beratend (C) bzw. informiert (I) klärt der nächste Schritt (ergänzend).</p>` },
{ label:"Beteiligung (C + I)",
frage:`Ergänzt nun die <b>beteiligten</b> Rollen (ergänzend, nice-to-have): Wer wird <b>C</b>onsulted (vorab um Rat gefragt), wer nur <b>I</b>nformed (über das Ergebnis)? Stellt die Figuren auf die Spielfelder <b>C</b> und <b>I</b> im RACI-Kreis.`,
legend: raciLegendHtml() + rollenLegendHtml(),
auf:`<h4 class="aufH">Konsultiert (C) / Informiert (I)</h4><div class="roleChips">${(st.raci.filter(([r,c])=>c.includes("C")||c.includes("I")).map(([r,c])=>`<span class="roleChip">${roleLabel(r)} · ${c}</span>`).join("")) || '<span class="roleChip">— (keine C/I-Rolle)</span>'}</div>`
+ `<h4 class="aufH" style="margin-top:14px">RACI vollständig</h4>${raciTable(st)}` },
{ label:"Artefakt", artefakt:true,
frage:`Welches <b>Artefakt</b> entsteht hier und kommt in die <b>Service-Akte</b>?`,
auf:`<h4 class="aufH">Artefakt</h4><p style="margin:0"><b>${st.artefakt}</b></p>` }
];
}
// Antwort-Optionen fuer die Artefakt-Choice: richtiges A + 3 Distraktoren
// (bevorzugt aus derselben Phase), deterministisch nach A-Nummer sortiert.
function arteOptions(correct){
const ids = Object.keys(ARTEFAKTE);
const ph = ARTEFAKTE[correct].phase;
const same = ids.filter(a => a!==correct && ARTEFAKTE[a].phase===ph);
const other = ids.filter(a => a!==correct && ARTEFAKTE[a].phase!==ph);
const opts = [correct].concat(same.concat(other).slice(0,3));
return opts.sort((a,b)=> a.localeCompare(b, "en", {numeric:true}));
}
function renderActivity(st){
const phaseColor = PHASEN[st.phase].color;
const next = STATIONEN[S.index+1];
const lastOverall = S.index >= STATIONEN.length-1;
const phaseEnd = next && next.phase !== st.phase;
/* Eigener Abschluss-Screen nach der letzten Aufgabe */
if(S.actDone){
const phaseLine = phaseEnd
? `<p class="adPhase">🎉 Damit habt ihr die Phase <b style="color:${phaseColor}">${PHASEN[st.phase].label}</b> zu Ende gespielt — weiter mit der Phase <b style="color:${PHASEN[next.phase].color}">${PHASEN[next.phase].label}</b>.</p>`
: ``;
const btn = lastOverall ? `<button class="primary" id="finish">Durchlauf abschließen</button>`
: phaseEnd ? `<button class="primary" id="nextStation">→ Weiter zur Phase ${PHASEN[next.phase].label}</button>`
: `<button class="primary" id="nextStation">Nächste Station →</button>`;
return `<div class="actDone big">
<div class="adTitle">✓ Aktivität abgeschlossen</div>
<p style="margin:6px 0">Gut gemacht! Ihr habt <b>${st.id}${st.name}</b> durchgespielt.</p>
${phaseLine}
</div>
${phaseEnd && S.mode==="main" ? slcOverview(next.phase, `Weiter in der Phase <b style="color:${PHASEN[next.phase].color}">${PHASEN[next.phase].label}</b>`) : ``}
${FEEDBACK_DOCS[st.id] ? docFeedbackBlock(st.id) : ``}
${phaseEnd ? phaseFeedbackBlock(st.phase) : ``}
<div class="actions">
<button class="ghost" id="actBack">← zurück</button>
<div class="spacer"></div>
${btn}
</div>`;
}
/* Schrittweiser Frage-Flow */
const steps = activitySteps(st);
const i = Math.min(S.actStep||0, steps.length-1);
const step = steps[i];
const isLast = i === steps.length-1;
const arteId = STATION_ARTEFAKT[st.id]; // A-Nummer, falls diese Station eine erzeugt
const isArteChoice = step.artefakt && arteId; // Artefakt-Schritt mit echter Choice
const isNonAkte = step.artefakt && !arteId; // Station ohne eigenes Akte-Artefakt
const fold = FOLDS_INTO[st.id];
const frageHtml = isNonAkte
? `Was ist das <b>Arbeitsergebnis</b> dieser Station? <span style="color:var(--muted)">(Hier geht kein eigenes Artefakt in die Service-Akte.)</span>`
: step.frage;
let html = `<div class="tourProg">Schritt ${i+1}/${steps.length} · ${step.label}</div>
<div class="frageBox" style="border-left-color:${phaseColor}">
<div class="frageLabel">Aufgabe ${i+1}</div>
${frageHtml}
</div>`;
if(step.legend) html += step.legend;
if(isArteChoice && !S.actReveal){
// Auswahl: welches Artefakt entsteht? (richtiges A + Distraktoren)
const opts = arteOptions(arteId).map(a =>
`<button class="choice arteChoice ${S.arteWrong===a?'bad':''}" data-a="${a}">${a}${ARTEFAKTE[a].name}</button>`).join("");
html += `<div class="choiceGrid">${opts}</div>`;
if(S.arteWrong) html += `<div class="hint bad">Nicht ganz — überlegt nochmal, welches Ergebnis diese Station liefert.</div>`;
} else if(S.actReveal){
if(isArteChoice){
const A = ARTEFAKTE[arteId];
html += `<div class="aufBox">
<p style="margin:0 0 12px">✓ <b>${arteId}${A.name}</b> in die Service-Akte gelegt.</p>
<h4 class="aufH">Was umfasst es?</h4><p style="margin:0 0 10px">${A.was}</p>
<h4 class="aufH">Warum brauchen wir es?</h4><p style="margin:0">${A.warum}</p>
</div>`;
} else if(isNonAkte){
html += `<div class="aufBox">
<h4 class="aufH">Arbeitsergebnis</h4><p style="margin:0 0 8px"><b>${st.artefakt}</b></p>
<p style="margin:0;color:var(--muted)"> Kein eigenes Akte-Artefakt${fold?` — fließt in <b>${fold} ${ARTEFAKTE[fold].name}</b> ein`:``}.</p>
</div>`;
} else {
html += `<div class="aufBox">${step.auf}</div>`;
}
}
let actions = `<div class="actions">`;
if(S.actReveal || i>0) actions += `<button class="ghost" id="actBack">← zurück</button>`;
actions += `<div class="spacer"></div>`;
if(isArteChoice && !S.actReveal){ /* Antwort per Klick auf eine Option — kein Auflösen-Button */ }
else if(!S.actReveal) actions += `<button class="primary" id="actReveal">Auflösen →</button>`;
else if(!isLast) actions += `<button class="primary" id="actNext">Weiter →</button>`;
else actions += `<button class="primary" id="actToDone">Weiter →</button>`;
actions += `</div>`;
return html + actions;
}
/* Gate — Entscheidung nach Kriterien */
function renderGate(st){
const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0];
const sorNote = (keeper==="sor")
? `<p class="muted" style="margin:8px 0 0">Die <b>SOR</b> ist ein <b>Gremium</b> (SPM · Betrieb · Support-Manager · Service Owner …), das am Gate-Puck zusammenkommt — keine Einzelfigur.</p>` : ``;
const revisitNote = (S.revisit && S.revisit[st.id])
? `<div class="hint ok">↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.</div>` : ``;
// Schritt 1: Entscheider ermitteln (nur Aufforderung — Figuren auf den Gate-Puck)
if(!S.gateDeciderDone){
return `
${revisitNote}
<div class="frageBox" style="border-left-color:var(--ink)">
<div class="frageLabel">Schritt 1 · Entscheider ermitteln</div>
<b>Wer entscheidet an diesem Gate?</b> Stellt die Entscheider-Figur(en) auf den <b>Gate-Puck</b>.
</div>
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
${sorNote}
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="gateDeciderNext">Weiter → Entscheidung</button>
</div>`;
}
// Schritt 2: Prüf-Kriterien (nacheinander) + Entscheidung
// Harte Artefakt-Kopplung: Gate "oeffnet" nur mit den geforderten Artefakten in der Akte.
const req = GATE_REQ[st.id] || [];
const missing = req.filter(a => !(S.akte && S.akte[a]));
const blocked = missing.length > 0;
const opts = (st.pfade||[]).map(([n,d],i)=>
`<button class="choice" data-i="${i}" ${blocked?"disabled":""}><b>${n}</b><br><span style="color:var(--muted);font-weight:400">${d}</span></button>`).join("");
const reqLine = req.length === 0 ? `` : blocked
? `<div class="gateReq bad">🔒 Gate öffnet nicht — es fehlt in der Akte: ${missing.map(a=>a+" "+ARTEFAKTE[a].name).join(" · ")}. Erzeugt diese Artefakte zuerst.</div>`
: `<div class="gateReq ok">✓ Alle geforderten Artefakte liegen in der Akte (${req.join(" · ")}).</div>`;
const zielLine = st.ziel ? `<div class="gateZiel"><b>Ziel:</b> ${st.ziel}</div>` : ``;
// Prüf-Kriterien als nacheinander erscheinende Checkboxen (nur Anzeige, nicht blockierend)
const pruef = st.pruef || [];
const shown = Math.min(S.gateCrit||0, pruef.length); // Anzahl abgehakter Kriterien
const critItems = pruef.map(([n,d],i)=>{
if(i > shown) return ""; // noch nicht eingeblendet
const checked = i < shown;
return `<button class="critItem ${checked?'checked':''}" data-ci="${i}">
<span class="critBox">${checked?'☑':'☐'}</span>
<span><b>${n}</b> — ${d}</span>
</button>`;
}).join("");
const allChecked = pruef.length>0 && shown >= pruef.length;
const critWrap = pruef.length ? `
<div class="critHead">Worum geht's & Prüf-Kriterien</div>
<p class="muted" style="margin:0 0 10px">${st.beschreibung}</p>
<div class="gateCrit">${critItems}</div>
${allChecked?`<p class="muted" style="margin:0 0 16px">Alle Kriterien geprüft. Geforderte Artefakte in der Akte (siehe oben)? Pflicht-Figuren am Gate-Puck?</p>`:``}` : ``;
// ---- Schritt 2: Prüf-Kriterien (eigener Screen) ----
if(pruef.length && !S.gateCritDone){
return `
${revisitNote}
${zielLine}
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
${reqLine}
${critWrap}
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="gateToDecision" ${allChecked?'':'disabled'}>Weiter → Entscheidung</button>
</div>`;
}
// ---- Schritt 3: Entscheidung (eigener Screen, nach Kriterienprüfung) ----
const critRecap = pruef.length ? `<div class="hint ok">✓ Alle Prüf-Kriterien geprüft</div>` : ``;
const critBack = pruef.length ? `<button class="ghost" id="gateToCrit">← zu den Kriterien</button>` : `<div></div>`;
const deciderLine = `<p class="lead" style="margin:0 0 12px"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>`;
// Zweistufiges Gate (Gate 1): erst Freigabe-Entscheidung, dann Build-/Konfig-Routing.
if(st.freigabe){
if(S.gate1Approval==null){
const fopts = st.freigabe.map(([n,d],i)=>
`<button class="choice freigabeOpt" data-fi="${i}" ${blocked?"disabled":""}><b>${n}</b><br><span style="color:var(--muted);font-weight:400">${d}</span></button>`).join("");
return `
${revisitNote}
${critRecap}
${reqLine}
<div class="critHead">Freigabe-Entscheidung</div>
${deciderLine}
<div class="choiceGrid">${fopts}</div>
<div class="actions">${critBack}<div class="spacer"></div></div>`;
}
// Freigabe erteilt (0/1) → Routing-Schritt
const fa = st.freigabe[S.gate1Approval];
return `
${revisitNote}
<div class="hint ok">✓ Freigabe-Entscheidung: <b>${fa[0]}</b></div>
<div class="critHead">Build-/Konfigurations-Routing</div>
<p class="muted" style="margin:0 0 10px">Da freigegeben: Werden die Service-Komponenten <b>neu entwickelt</b> oder bestehende nur <b>konfiguriert</b>?</p>
<div class="choiceGrid">${opts}</div>
<div class="actions"><button class="ghost" id="gateApprovalBack">← andere Freigabe-Entscheidung</button><div class="spacer"></div></div>`;
}
// Normales Gate (2/3): Entscheidung
return `
${revisitNote}
${critRecap}
${reqLine}
<div class="critHead">Entscheidung</div>
${deciderLine}
<div class="choiceGrid">${opts}</div>
<div class="actions">${critBack}<div class="spacer"></div></div>`;
}
/* Gate — Konsequenz der Entscheidung */
function renderGateDone(st){
const i = S.gatePick || 0;
const pf = st.pfade[i] || st.pfade[0];
const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0];
const sorNote = (keeper==="sor")
? `<p class="muted">Reicht die <b>Ressourcen-/Entscheidungshoheit der SOR</b> nicht (zusätzliche Mittel nötig), wird der Change zum <b>Demand</b> → über DPM ans <b>Mission Board</b>.</p>` : ``;
const tIdx = gateTargetIndex(st, i);
const target = tIdx>=0 ? STATIONEN[tIdx] : null;
const phaseEnd = target && target.phase !== st.phase;
let feedback = `<p class="muted">Die entscheidende Rolle (<b>${roleLabel(keeper)}</b>) bleibt als Marker am Gate-Puck stehen.</p>`;
let nextLabel = `Weiter →`;
if(phaseEnd){
const curColor = PHASEN[st.phase].color, nextColor = PHASEN[target.phase].color;
nextLabel = `→ Weiter zur Phase ${PHASEN[target.phase].label}`;
feedback += `<div class="phaseDone" style="border-left-color:${nextColor}">
<div class="pdTitle">🎉 Phase abgeschlossen</div>
<p style="margin:4px 0">Gratulation — ihr habt die Phase <b style="color:${curColor}">${PHASEN[st.phase].label}</b> zu Ende gespielt!</p>
<p style="margin:4px 0">Weiter geht's mit der Phase <b style="color:${nextColor}">${PHASEN[target.phase].label}</b>.</p>
</div>`;
}
const overview = (phaseEnd && S.mode==="main")
? slcOverview(target.phase, `Weiter in der Phase <b style="color:${PHASEN[target.phase].color}">${PHASEN[target.phase].label}</b>`) : ``;
return `
<div class="step reveal">
<div class="stepHead"><span class="n">⛩</span> Entscheidung getroffen</div>
<div class="recBox"><h4>${pf[0]}</h4><p style="margin:0;color:var(--muted)">${pf[1]}</p></div>
${sorNote}
${feedback}
</div>
${overview}
${FEEDBACK_DOCS[st.id] ? docFeedbackBlock(st.id) : ``}
${phaseEnd ? phaseFeedbackBlock(st.phase) : ``}
<div class="actions">
<button class="ghost" id="gateBack">← andere Entscheidung</button>
<div class="spacer"></div>
<button class="primary" id="gateNext">${nextLabel}</button>
</div>`;
}
/* ====================== FEEDBACK (Frank: Phase + Dokument, Server-Save) ======================
Erfasst zwei Arten von Feedback und speichert sie auf dem Server (POST an
FEEDBACK_ENDPOINT) — mit Retry-Queue (offline-fest) und localStorage-Archiv als
Backup für den manuellen Export. Die Erfassung blockt den Spielfluss nie (optional). */
const FEEDBACK_ENDPOINT = ((document.querySelector('meta[name="slc-feedback-endpoint"]')||{}).content) || "feedback.php";
const FB_QUEUE = "slc-fb-queue", FB_ARCHIVE = "slc-fb-archive", FB_SESSION = "slc-fb-session";
function fbReadArr(k){ try{ return JSON.parse(localStorage.getItem(k)||"[]"); }catch(e){ return []; } }
function fbWriteArr(k,a){ localStorage.setItem(k, JSON.stringify(a)); }
function escapeHtml(s){ return (s==null?"":""+s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); }
function feedbackSessionId(){
let id = localStorage.getItem(FB_SESSION);
if(!id){ id = "s-"+Math.random().toString(36).slice(2,10)+"-"+Date.now().toString(36); localStorage.setItem(FB_SESSION, id); }
return id;
}
function fbCtx(){
return { service: S.service!=null ? USE_CASES[S.service].service : null,
serviceIdx: S.service,
change: S.change!=null ? CHANGE_TYPES[S.change] : null,
mode: S.mode };
}
/* Phasen-Feedback (2 Freitexte, am Phasenende) */
function phaseFeedbackBlock(phase){
const key = "phase:"+phase;
const fb = (S.feedback && S.feedback[key]) || {};
const saved = S.feedbackSaved && S.feedbackSaved[key];
return `<div class="fbCard" data-fbkey="${key}" data-fbtype="phase" data-target="${phase}">
<div class="fbHead">📝 Feedback zur Phase <b>${PHASEN[phase].label}</b> <span class="fbOpt">· optional</span></div>
<label class="fbQ">Welche Elemente oder Aktivitäten dieser Phase sind besonders relevant oder hilfreich?</label>
<textarea class="fbInput" data-f="relevant" rows="2" placeholder="Freitext …">${escapeHtml(fb.relevant)}</textarea>
<label class="fbQ">Welche Elemente oder Aktivitäten erscheinen überflüssig? Warum?</label>
<textarea class="fbInput" data-f="ueberfluessig" rows="2" placeholder="Freitext …">${escapeHtml(fb.ueberfluessig)}</textarea>
<div class="fbActions">${saved?`<span class="fbSaved">✓ gespeichert</span>`:`<span></span>`}<button class="ghost fbSaveBtn" type="button">💾 Feedback speichern</button></div>
</div>`;
}
/* Dokument-Feedback (Skala 15 + 2 Freitexte) für Gate 1/2/3 + Service Review */
function docFeedbackBlock(sid){
const key = "doc:"+sid;
const fb = (S.feedback && S.feedback[key]) || {};
const saved = S.feedbackSaved && S.feedbackSaved[key];
const label = FEEDBACK_DOCS[sid] || sid;
const scale = [1,2,3,4,5].map(n=>`<button type="button" class="fbScale ${(+fb.skala===n)?'sel':''}" data-n="${n}">${n}</button>`).join("");
return `<div class="fbCard" data-fbkey="${key}" data-fbtype="doc" data-target="${sid}">
<div class="fbHead">📝 Feedback zum Prüfschritt <b>„${label}"</b> <span class="fbOpt">· optional</span></div>
<label class="fbQ">Wie relevant oder hilfreich ist dieser Prüfschritt? <span class="muted">(1 = gar nicht … 5 = sehr)</span></label>
<div class="fbScaleRow">${scale}</div>
<label class="fbQ">Fehlen noch Themen oder Punkte, die auch geprüft werden sollten? Wenn ja, welche?</label>
<textarea class="fbInput" data-f="fehlt" rows="2" placeholder="Freitext …">${escapeHtml(fb.fehlt)}</textarea>
<label class="fbQ">Weiteres Feedback zu diesem Prüfschritt</label>
<textarea class="fbInput" data-f="weiteres" rows="2" placeholder="Freitext …">${escapeHtml(fb.weiteres)}</textarea>
<div class="fbActions">${saved?`<span class="fbSaved">✓ gespeichert</span>`:`<span></span>`}<button class="ghost fbSaveBtn" type="button">💾 Feedback speichern</button></div>
</div>`;
}
/* Ein Feedback-Card aus dem DOM lesen; nur committen, wenn Inhalt vorhanden. */
function commitFeedbackCard(card){
const key = card.dataset.fbkey;
const rec = (S.feedback && S.feedback[key]) || {};
card.querySelectorAll('.fbInput[data-f]').forEach(t=>{ rec[t.dataset.f] = t.value; });
const sel = card.querySelector('.fbScale.sel'); if(sel) rec.skala = +sel.dataset.n;
S.feedback = S.feedback || {}; S.feedback[key] = rec;
const hasContent = (rec.relevant||rec.ueberfluessig||rec.fehlt||rec.weiteres||rec.skala);
if(!hasContent) return false;
S.feedbackSaved = S.feedbackSaved || {}; S.feedbackSaved[key] = true;
const record = Object.assign({
id: key+"@"+feedbackSessionId(),
key, type: card.dataset.fbtype, target: card.dataset.target,
ts: new Date().toISOString(), session: feedbackSessionId()
}, fbCtx(), rec);
// Archiv + Queue (außerhalb des Spielstands, übersteht „Neu starten")
const arch = fbReadArr(FB_ARCHIVE).filter(r=>r.id!==record.id); arch.push(record); fbWriteArr(FB_ARCHIVE, arch);
const q = fbReadArr(FB_QUEUE).filter(r=>r.id!==record.id); q.push(record); fbWriteArr(FB_QUEUE, q);
return true;
}
/* Alle aktuell sichtbaren Feedback-Cards committen (z. B. beim Weiterblättern). */
function captureFeedbackInputs(){
let any=false;
document.querySelectorAll('.fbCard').forEach(card=>{ if(commitFeedbackCard(card)) any=true; });
if(any){ save(); flushFeedbackQueue(); }
return any;
}
/* Offene Server-Submissions abarbeiten (Retry). Fehlt der Endpoint, bleibt alles in der Queue. */
function flushFeedbackQueue(){
const q = fbReadArr(FB_QUEUE);
if(!q.length || !navigator.onLine) return;
q.slice().forEach(record=>{
fetch(FEEDBACK_ENDPOINT, { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(record) })
.then(r=>{ if(r.ok){ fbWriteArr(FB_QUEUE, fbReadArr(FB_QUEUE).filter(x=>x.id!==record.id)); } })
.catch(()=>{ /* offline / kein Endpoint → bleibt für späteren Retry in der Queue */ });
});
}
/* Manueller Export (Backup) — lädt das gesammelte Feedback als JSON herunter. */
function exportFeedback(){
const data = fbReadArr(FB_ARCHIVE);
if(!data.length){ alert("Noch kein Feedback erfasst."); return; }
const pending = fbReadArr(FB_QUEUE).length;
const blob = new Blob([JSON.stringify({exported:new Date().toISOString(), pendingUpload:pending, items:data}, null, 2)], {type:"application/json"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "slc-feedback-"+new Date().toISOString().slice(0,10)+".json";
document.body.appendChild(a); a.click(); a.remove();
}
function wireFeedback(){
document.querySelectorAll('.fbCard .fbScale[data-n]').forEach(btn=>{
btn.onclick = ()=>{ btn.parentNode.querySelectorAll('.fbScale').forEach(x=>x.classList.remove('sel')); btn.classList.add('sel'); };
});
document.querySelectorAll('.fbCard .fbSaveBtn').forEach(btn=>{
btn.onclick = ()=>{ const card = btn.closest('.fbCard'); commitFeedbackCard(card); save(); flushFeedbackQueue(); draw(); };
});
}
/* ====================== WIRING ====================== */
function wire(st){
const b = id => $("#"+id);
const rc = $("#runCard"); if(rc) rc.onclick = ()=> openSvcModal(S.service);
if(b("actReveal")) b("actReveal").onclick = ()=>{ S.actReveal=true; save(); draw(); };
if(b("actNext")) b("actNext").onclick = ()=>{ S.actStep=(S.actStep||0)+1; S.actReveal=false; save(); draw(); };
if(b("actToDone")) b("actToDone").onclick = ()=>{ S.actDone=true; S.akteFlash=null; document.body.classList.remove("akteOpen"); save(); draw(); };
// Artefakt-Choice
$("#panel").querySelectorAll(".arteChoice[data-a]").forEach(el=>{
el.onclick = ()=>{ const a = el.dataset.a; let correct=false;
if(a === STATION_ARTEFAKT[st.id]){ S.arteWrong=null; addArtefakt(a); S.akteFlash=a; S.actReveal=true; correct=true; }
else { S.arteWrong = a; }
save(); draw();
// Akte-Seitenleiste oeffnen, damit man sieht, dass das Artefakt nun drin ist
if(correct){ document.body.classList.remove("navOpen","rollenOpen"); document.body.classList.add("akteOpen"); }
};
});
if(b("actBack")) b("actBack").onclick = ()=>{
captureFeedbackInputs();
if(S.actDone){ S.actDone=false; }
else if(S.actReveal){ S.actReveal=false; }
else if((S.actStep||0)>0){ S.actStep--; S.actReveal=true; }
save(); draw(); };
if(b("nextStation")) b("nextStation").onclick = ()=>{ captureFeedbackInputs(); S.done[st.id]=true; enterStation(S.index+1); save(); draw(); };
if(b("finish")) b("finish").onclick = ()=>{ captureFeedbackInputs(); S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
// Gate
if(b("gateDeciderNext")) b("gateDeciderNext").onclick = ()=>{ S.gateDeciderDone=true; save(); draw(); };
if(b("gateToDecision")) b("gateToDecision").onclick = ()=>{ S.gateCritDone=true; save(); draw(); };
if(b("gateToCrit")) b("gateToCrit").onclick = ()=>{ S.gateCritDone=false; S.gate1Approval=null; save(); draw(); };
$("#panel").querySelectorAll(".critItem[data-ci]").forEach(el=>{
el.onclick = ()=>{ const i=+el.dataset.ci;
S.gateCrit = (i < (S.gateCrit||0)) ? i : i+1; // abgehaktes wieder lösen vs. nächstes aufdecken
save(); draw(); };
});
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
});
// Gate 1 — Freigabe-Entscheidung (Frank-Template): 0/1 → Routing, 2 → zurück an Design, 3 → Ablehnung.
$("#panel").querySelectorAll(".freigabeOpt[data-fi]").forEach(el=>{
el.onclick = ()=>{ const fi=+el.dataset.fi;
if(fi<=1){ S.gate1Approval=fi; save(); draw(); return; }
S.done[st.id]=true;
if(fi===2){ // Zurück an Design — echte Rückschleife, Gate wird danach erneut vorgelegt
S.loopback = { gateId: st.id, gateNr: st.gateNr, untilIdx: S.index };
enterStation(0);
} else { // Ablehnung
S.view="end"; S.endReason="rejected"; S.endGate=st.id;
}
save(); draw(); };
});
if(b("gateApprovalBack")) b("gateApprovalBack").onclick = ()=>{ S.gate1Approval=null; save(); draw(); };
if(b("gateBack")) b("gateBack").onclick = ()=>{ captureFeedbackInputs(); S.stage="gate"; save(); draw(); };
if(b("gateNext")) b("gateNext").onclick = ()=>{ captureFeedbackInputs(); gateGoto(st, S.gatePick||0); };
wireFeedback();
}
/* ====================== ABSCHLUSS-SCREEN ====================== */
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>`;
const ending = (!rejected && S.mode==="main")
? slcOverview(null, `Alle Phasen durchlaufen — der Lifecycle ist komplett`) + phaseFeedbackBlock("review")
: ``;
$("#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}
${ending}
<div class="actions">
<div class="spacer"></div>
<button class="primary" id="toBonus">Weiter → Varianten dieses Service →</button>
</div>`;
wireFeedback();
$("#toBonus").onclick = ()=>{ captureFeedbackInputs(); S.view="bonusPick"; save(); draw(); };
}
/* ====================== INIT ====================== */
(function init(){
flushFeedbackQueue(); // offene Feedback-Submissions nachreichen
window.addEventListener("online", flushFeedbackQueue); // bei Reconnect erneut versuchen
$("#fbExportBtn").onclick = exportFeedback;
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
const closeOverlays = ()=> document.body.classList.remove("navOpen","akteOpen","rollenOpen");
$("#stationsBtn").onclick = ()=>{ const o=document.body.classList.contains("navOpen"); closeOverlays(); if(!o) document.body.classList.add("navOpen"); };
$("#akteBtn").onclick = ()=>{ const o=document.body.classList.contains("akteOpen"); closeOverlays(); if(!o) document.body.classList.add("akteOpen"); };
$("#rollenBtn").onclick = ()=>{ const o=document.body.classList.contains("rollenOpen"); closeOverlays(); if(!o) document.body.classList.add("rollenOpen"); };
$("#navBackdrop").onclick = closeOverlays;
$("#svcBtn").onclick = ()=> openSvcModal(S.service);
$("#svcModal").onclick = (e)=>{ if(e.target.id==="svcModal") document.body.classList.remove("svcOpen"); };
draw();
})();
// PWA: Service Worker fuer Offline-/Kiosk-Betrieb (nur ueber http/https aktiv)
if ("serviceWorker" in navigator) {
window.addEventListener("load", ()=> navigator.serviceWorker.register("sw.js").catch(()=>{}));
}
</script>
</body>
</html>