first
This commit is contained in:
commit
c87b0b1775
23 changed files with 2658 additions and 0 deletions
101
04_Tablet-Quiz/README.md
Normal file
101
04_Tablet-Quiz/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Tablet-Quiz — Begleit-App (Teilprojekt)
|
||||
|
||||
**Status:** Konzept · **Typ:** eigenständiges Software-Teilprojekt des SLC-Workshops
|
||||
|
||||
Das Tablet-Quiz ist der **digitale Begleiter** des Tabletops — kein Ersatz fürs
|
||||
Brett. Es ist der **erklärende Gegenpart** zu den Plättchen und **ersetzt deren
|
||||
Rückseite**: Die Plättchen tragen nur noch die Kurzbezeichnung, die ausführliche
|
||||
Erklärung liefert die App. Sie **führt die Stationsreihenfolge** (linearer
|
||||
Lifecycle), stellt pro Station ein **vermittelndes Quiz**, gibt danach die
|
||||
**ausführliche Auflösung** und protokolliert Verständnislücken fürs Debrief.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel & Rolle im Spiel
|
||||
|
||||
- **Stationsführung:** schaltet Station für Station automatisch weiter („Nächste Station") — die Plättchen brauchen keinen Code.
|
||||
- **Active Recall verstärken:** erst Diskussion am Board, dann vermittelndes Quiz, dann Auflösung — Gruppe rät, App bestätigt/korrigiert.
|
||||
- **Vollständige Erklärung:** liefert nach dem Quiz die ausführliche Auflösung (ersetzt die Plättchenrückseite) aus dem Blueprint (Single Source of Truth).
|
||||
- **Dokumentation:** erfasst automatisch, welche Aktivitäten unklar waren (→ `../05_Workshop-Dokumentation/`).
|
||||
|
||||
Bewusst **nicht** das Ziel: das Spiel digital ersetzen, Echtzeit-Multiplayer,
|
||||
Accounts/Login, Cloud-Pflicht.
|
||||
|
||||
## 2. Datengrundlage (keine Doppelpflege)
|
||||
|
||||
Die App liest ausschließlich die bestehenden Blueprint-Dateien und leitet
|
||||
Fragen daraus ab:
|
||||
|
||||
| Quelle | liefert |
|
||||
|--------|---------|
|
||||
| `service-lifecycle_*.yaml` | Aktivitäten, Beschreibungen, Reihenfolge, Gates |
|
||||
| `spm_rollen.yaml` | Rollen, RACI, Gate-Keeper |
|
||||
|
||||
Ein Build-Schritt konvertiert die YAMLs in ein statisches `questions.json`.
|
||||
Damit bleibt der Blueprint die einzige Wahrheit; Inhalte werden nie im App-Code
|
||||
dupliziert.
|
||||
|
||||
## 3. Fragetypen
|
||||
|
||||
1. **Reihenfolge:** „Was kommt nach `tr_08`?"
|
||||
2. **Rolle / RACI:** „Wer ist *Accountable* für `op_06`?"
|
||||
3. **Artefakt:** „Welches Artefakt entsteht bei `tr_07`?"
|
||||
4. **Gate-Logik:** „Wer muss an Gate 1 zustimmen?" / „Welche Pfade gibt es?"
|
||||
5. **Zuordnung:** „In welcher Phase liegt `sp_09`?"
|
||||
|
||||
Jede Frage: Gruppentipp → *Auflösen*-Button → Modellantwort. Im Anschluss an das
|
||||
Quiz folgt die **ausführliche Auflösung** der Station (vollständige Beschreibung +
|
||||
Rollen/RACI + Artefakt aus der YAML) — das ist der Inhalt, der früher auf der
|
||||
Plättchenrückseite stand.
|
||||
|
||||
## 4. Ablauf (UI-Flow)
|
||||
|
||||
```
|
||||
[Start] → Szenario wählen (= Action Card)
|
||||
→ App führt zur aktuellen Station (linearer Lifecycle, Fortschritt sichtbar)
|
||||
→ Station:
|
||||
→ Gruppe diskutiert am Board anhand der Kurzbezeichnung (App noch zu)
|
||||
→ Quiz (vermittelnd): Frage(n) → Gruppentipp → "Auflösen" → richtig/falsch
|
||||
→ ausführliche Auflösung der Station (Erklärung + RACI + Artefakt)
|
||||
→ Gruppe reflektiert; optional "war unklar" markieren
|
||||
→ "Nächste Station"
|
||||
→ an Gates: Gate-Frage + Rollen-Check
|
||||
→ [Ende] → Debrief-Export (unklare Aktivitäten, Quote, Pfad)
|
||||
```
|
||||
|
||||
## 5. Funktionsumfang (MVP)
|
||||
|
||||
- [ ] `questions.json` + Stations-Inhalte aus YAMLs generieren (Build-Skript).
|
||||
- [ ] Stationsführung: linearer Durchlauf mit „Nächste Station" + Fortschritt/Phasen-Farben.
|
||||
- [ ] Fragetypen 1–3 (vermittelndes Quiz).
|
||||
- [ ] „Auflösen"-Mechanik (Antwort erst auf Klick) **+ ausführliche Stationsauflösung** (Erklärung/RACI/Artefakt) nach dem Quiz.
|
||||
- [ ] „Unklar"-Markierung je Aktivität.
|
||||
- [ ] Debrief-Export (Markdown/JSON, lokal).
|
||||
|
||||
### Später (Ausbau)
|
||||
- Gate-Fragen mit Rollen-Check (Typ 4–5).
|
||||
- Mehrere Szenarien mit unterschiedlichen Fragesets.
|
||||
- Punktestand / Team-Modus.
|
||||
- Mehrsprachigkeit.
|
||||
|
||||
## 6. Technik-Empfehlung
|
||||
|
||||
- **Single-Page-Web-App**, offline lauffähig (PWA), passt zum bestehenden
|
||||
HTML-first-Stil im Repo (vgl. MB-Retro-HTMLs).
|
||||
- Kein Backend nötig: statisches `questions.json` + LocalStorage für das Logbuch.
|
||||
- Tablet im Kiosk-/Vollbildmodus; keine Konten, keine Cloud.
|
||||
- Stack-Vorschlag: Vanilla JS oder leichtes Framework, ein Build-Skript (Node/Python)
|
||||
für die YAML→JSON-Konvertierung.
|
||||
|
||||
## 7. Schnittstellen zum restlichen Spiel
|
||||
|
||||
- **Eingang:** Szenarioauswahl = gezogene Action Card (`../03_Karten/`).
|
||||
- **Inhalt:** Aktivitäten/Gates/Rollen = Brett-Elemente (`../00_Konzept/`).
|
||||
- **Ausgang:** Debrief-Daten → Workshop-Dokumentation (`../05_Workshop-Dokumentation/`).
|
||||
|
||||
## 8. Offene Punkte
|
||||
|
||||
- [ ] Format `questions.json` spezifizieren.
|
||||
- [ ] Entscheidung Framework vs. Vanilla.
|
||||
- [ ] Wer pflegt/baut? (intern DIGIT vs. extern)
|
||||
- [ ] Datenschutz: rein lokal, keine personenbezogenen Daten — bestätigen.
|
||||
491
04_Tablet-Quiz/prototype/index.html
Normal file
491
04_Tablet-Quiz/prototype/index.html
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
<!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 (Prototyp)</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f5f7;
|
||||
--panel: #ffffff;
|
||||
--ink: #1d2430;
|
||||
--muted: #6b7686;
|
||||
--line: #e2e6ec;
|
||||
--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(20,30,50,.08), 0 6px 24px rgba(20,30,50,.06);
|
||||
}
|
||||
* { 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 select, button {
|
||||
font: inherit; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: #fff; color: var(--ink); padding: 8px 12px; cursor: pointer;
|
||||
}
|
||||
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: 300px 1fr; gap: 0; min-height: calc(100% - 55px); }
|
||||
@media (max-width: 820px) { .layout { grid-template-columns: 1fr; } aside { display:none; } }
|
||||
|
||||
aside { border-right: 1px solid var(--line); background: var(--panel); padding: 14px; overflow:auto; }
|
||||
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 .flag { margin-left:auto; color: var(--accent); }
|
||||
.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(--ink); }
|
||||
.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); }
|
||||
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
.actions { display:flex; gap:10px; align-items:center; margin-top:24px; flex-wrap:wrap; }
|
||||
.actions .spacer { flex:1; }
|
||||
.unclear { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:14px; }
|
||||
|
||||
dialog { border:none; border-radius:16px; padding:0; box-shadow: var(--shadow); max-width: 640px; width: 92%; }
|
||||
dialog::backdrop { background: rgba(15,22,35,.45); }
|
||||
.modal { padding: 24px; }
|
||||
.modal h2 { margin:0 0 6px; }
|
||||
pre.export { background:#0f1623; color:#d6e2f0; padding:14px; border-radius:10px; overflow:auto; max-height:320px; font-size:13px; }
|
||||
.stat { display:flex; gap:20px; margin:14px 0; }
|
||||
.stat div b { display:block; font-size:24px; }
|
||||
.stat div span { color:var(--muted); font-size:12px; }
|
||||
.note { font-size:12px; color:var(--muted); margin-top:18px; text-align:center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="brand">SLC <b>Companion</b></div>
|
||||
<span class="tag">Prototyp · v0.4</span>
|
||||
<div class="scenario">
|
||||
<select id="scenarioSel" title="Action Card / Szenario"></select>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="ghost" id="resetBtn" title="Durchlauf zurücksetzen">Zurücksetzen</button>
|
||||
<button class="primary" id="debriefBtn">Debrief</button>
|
||||
</header>
|
||||
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
|
||||
|
||||
<div class="layout">
|
||||
<aside id="stationList"></aside>
|
||||
<main><div class="card" id="panel"></div></main>
|
||||
</div>
|
||||
|
||||
<dialog id="debriefDlg">
|
||||
<div class="modal">
|
||||
<h2>Debrief-Export</h2>
|
||||
<p style="color:var(--muted);margin-top:0">Lokale Auswertung dieses Durchlaufs — nichts wird hochgeladen.</p>
|
||||
<div class="stat" id="debriefStat"></div>
|
||||
<pre class="export" id="debriefText"></pre>
|
||||
<div class="actions">
|
||||
<button class="ghost" id="copyBtn">In Zwischenablage</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="closeDebrief">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
/* =========================================================================
|
||||
BEISPIELDATEN — Auszug aus den Lifecycle-YAMLs (Design, Gate 1, Operation).
|
||||
Im Echtbetrieb generiert ein Build-Skript questions.json aus
|
||||
service-lifecycle_*.yaml + spm_rollen.yaml (keine Doppelpflege).
|
||||
========================================================================= */
|
||||
const ROLLEN = {
|
||||
spm:"Service-Portfolio-Manager", sor:"Service Operations Runde (SOR)",
|
||||
service_owner:"Service Owner", al_basis_cloud:"AL Basis & Cloud",
|
||||
al_applikationen:"AL Applikationen", 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:"Architektur",
|
||||
lieferant:"Lieferant", operations_manager:"Betrieb (AL B&C / AL App)"
|
||||
};
|
||||
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)"}
|
||||
};
|
||||
const SZENARIEN = [
|
||||
"Neues Fachverfahren wird eingeführt",
|
||||
"Cloud-Migration eines Bestandsservices",
|
||||
"Strategiewechsel: Service soll abgelöst werden"
|
||||
];
|
||||
|
||||
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","Architektur"], 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 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","Architektur","Support Manager"], richtig:2,
|
||||
expl:"Architektur 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: Entwicklung oder Konfiguration?",
|
||||
beschreibung:"Entry-Gate der Transition: Entscheidung, ob die Service-Komponenten 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",
|
||||
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 Strategie 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_02–tr_04."}
|
||||
]},
|
||||
{ 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"]],
|
||||
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."}
|
||||
]}
|
||||
];
|
||||
|
||||
/* ====================== STATE ====================== */
|
||||
const LS_KEY = "slc-companion-proto";
|
||||
function defaultState(){
|
||||
return { scenario:0, index:0, stage:"discuss", quizIndex:0,
|
||||
picks:{}, done:{}, unclear:{} };
|
||||
}
|
||||
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; }
|
||||
|
||||
/* ====================== RENDER: SIDEBAR ====================== */
|
||||
function renderList(){
|
||||
const groups = {};
|
||||
STATIONEN.forEach((st,i)=>{ (groups[st.phase]=groups[st.phase]||[]).push({st,i}); });
|
||||
let html = "";
|
||||
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>
|
||||
${S.unclear[st.id]?'<span class="flag">●</span>':''}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
$("#stationList").innerHTML = html;
|
||||
$("#stationList").querySelectorAll(".stationItem").forEach(el=>{
|
||||
el.onclick = ()=>{ S.index = +el.dataset.i; S.stage="discuss"; S.quizIndex=0; save(); render(); };
|
||||
});
|
||||
const pct = Math.round(Object.keys(S.done).length / STATIONEN.length * 100);
|
||||
$("#progressBar").style.width = pct+"%";
|
||||
}
|
||||
|
||||
/* ====================== RENDER: PANEL ====================== */
|
||||
function render(){
|
||||
renderList();
|
||||
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>`;
|
||||
|
||||
let body = `
|
||||
${chip}
|
||||
<div class="stationName">${st.name}</div>
|
||||
<div class="stationId">${st.id}</div>
|
||||
<div class="token">Action-Stein: <b>${SZENARIEN[S.scenario]}</b></div>
|
||||
`;
|
||||
|
||||
if(S.stage==="discuss") body += renderDiscuss(st);
|
||||
else if(S.stage==="quiz") body += renderQuiz(st);
|
||||
else body += renderReveal(st);
|
||||
|
||||
$("#panel").innerHTML = body;
|
||||
wire(st);
|
||||
}
|
||||
|
||||
function renderDiscuss(st){
|
||||
return `
|
||||
<div class="step">
|
||||
<div class="stepHead"><span class="n">1</span> Diskussion · App noch zu</div>
|
||||
<div class="discuss">
|
||||
<strong>Besprecht in der Gruppe — anhand der Kurzbezeichnung:</strong>
|
||||
<ul>
|
||||
<li>Was passiert hier konkret für euer Szenario?</li>
|
||||
<li>Wer macht es? Steckt die Rollen-Figuren ins <b>Aktiv-Feld (R/A/C/I)</b>.</li>
|
||||
<li>Welches Artefakt entsteht?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="toQuiz">Quiz starten →</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderQuiz(st){
|
||||
const qi = S.quizIndex, q = st.quiz[qi];
|
||||
const picked = S.picks[pkey(st.id,qi)];
|
||||
const answered = picked !== undefined;
|
||||
const opts = q.optionen.map((o,i)=>{
|
||||
let cls = "opt";
|
||||
if(answered){
|
||||
if(i===q.richtig) cls+=" correct";
|
||||
else if(i===picked) cls+=" wrong";
|
||||
} else if(i===picked) cls+=" sel";
|
||||
const mark = answered ? (i===q.richtig?'<span class="mark" style="color:var(--ok)">✓</span>': (i===picked?'<span class="mark" style="color:var(--bad)">✗</span>':'')) : '';
|
||||
return `<button class="${cls}" data-opt="${i}" ${answered?'disabled':''}>${o}${mark}</button>`;
|
||||
}).join("");
|
||||
return `
|
||||
<div class="step">
|
||||
<div class="stepHead"><span class="n">2</span> Quiz · Frage ${qi+1} / ${st.quiz.length}</div>
|
||||
<div class="q">
|
||||
<div class="frage">${q.frage}</div>
|
||||
<div class="opts">${opts}</div>
|
||||
${answered ? `<div class="qExpl">${q.expl}</div>` : ``}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ghost" id="backDiscuss">← Diskussion</button>
|
||||
<div class="spacer"></div>
|
||||
${answered
|
||||
? (qi < st.quiz.length-1
|
||||
? `<button class="primary" id="nextQ">Nächste Frage →</button>`
|
||||
: `<button class="primary" id="toReveal">Auflösung anzeigen →</button>`)
|
||||
: `<button class="primary" disabled>Antwort wählen…</button>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderReveal(st){
|
||||
const raci = st.raci.map(([r,c])=>`<tr><td>${roleLabel(r)}</td><td><span class="raciBadge raci-${c}">${c}</span></td></tr>`).join("");
|
||||
let extra = "";
|
||||
if(st.typ==="gate"){
|
||||
extra += `<h4>Entscheidungspfade</h4><div class="pfade">` +
|
||||
st.pfade.map(([n,d])=>`<div class="pfad"><b>${n}</b><span style="color:var(--muted)">${d}</span></div>`).join("") + `</div>`;
|
||||
extra += `<h4>Prüf-Dimensionen</h4><ul>` + st.pruef.map(([n,d])=>`<li><b>${n}</b> — ${d}</li>`).join("") + `</ul>`;
|
||||
}
|
||||
return `
|
||||
<div class="step reveal">
|
||||
<div class="stepHead"><span class="n">3</span> Auflösung & Reflexion</div>
|
||||
<p>${st.beschreibung}</p>
|
||||
<h4>Umfasst</h4>
|
||||
<ul>${st.umfasst.map(u=>`<li>${u}</li>`).join("")}</ul>
|
||||
<h4>Rollen / RACI</h4>
|
||||
<table class="raci"><thead><tr><th>Rolle</th><th>RACI</th></tr></thead><tbody>${raci}</tbody></table>
|
||||
<h4>Artefakt</h4><p>${st.artefakt}</p>
|
||||
${extra}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<label class="unclear"><input type="checkbox" id="unclear" ${S.unclear[st.id]?'checked':''}/> War unklar</label>
|
||||
<div class="spacer"></div>
|
||||
${S.index < STATIONEN.length-1
|
||||
? `<button class="primary" id="nextStation">Nächste Station →</button>`
|
||||
: `<button class="primary" id="finish">Durchlauf abschließen</button>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ====================== WIRING ====================== */
|
||||
function wire(st){
|
||||
const b = id => $("#"+id);
|
||||
if(b("toQuiz")) b("toQuiz").onclick = ()=>{ S.stage="quiz"; S.quizIndex=0; save(); render(); };
|
||||
if(b("backDiscuss")) b("backDiscuss").onclick = ()=>{ S.stage="discuss"; save(); render(); };
|
||||
$("#panel").querySelectorAll(".opt[data-opt]").forEach(el=>{
|
||||
el.onclick = ()=>{ S.picks[pkey(st.id,S.quizIndex)] = +el.dataset.opt; save(); render(); };
|
||||
});
|
||||
if(b("nextQ")) b("nextQ").onclick = ()=>{ S.quizIndex++; save(); render(); };
|
||||
if(b("toReveal")) b("toReveal").onclick = ()=>{ S.stage="reveal"; save(); render(); };
|
||||
if(b("unclear")) b("unclear").onchange = e=>{ if(e.target.checked) S.unclear[st.id]=true; else delete S.unclear[st.id]; save(); renderList(); };
|
||||
if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; S.index++; S.stage="discuss"; S.quizIndex=0; save(); render(); };
|
||||
if(b("finish")) b("finish").onclick = ()=>{ S.done[st.id]=true; save(); openDebrief(); };
|
||||
}
|
||||
|
||||
/* ====================== DEBRIEF ====================== */
|
||||
function quizScore(){
|
||||
let correct=0, total=0;
|
||||
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
|
||||
const p = S.picks[pkey(st.id,qi)];
|
||||
if(p!==undefined){ total++; if(p===q.richtig) correct++; }
|
||||
}));
|
||||
return {correct,total};
|
||||
}
|
||||
function openDebrief(){
|
||||
const {correct,total} = quizScore();
|
||||
const doneN = Object.keys(S.done).length;
|
||||
const unclearList = STATIONEN.filter(st=>S.unclear[st.id]);
|
||||
$("#debriefStat").innerHTML = `
|
||||
<div><b>${doneN}/${STATIONEN.length}</b><span>Stationen</span></div>
|
||||
<div><b>${total?Math.round(correct/total*100):0}%</b><span>Quiz richtig (${correct}/${total})</span></div>
|
||||
<div><b>${unclearList.length}</b><span>als unklar markiert</span></div>`;
|
||||
let md = `# SLC-Workshop — Debrief\n\n`;
|
||||
md += `**Szenario:** ${SZENARIEN[S.scenario]}\n`;
|
||||
md += `**Stationen bearbeitet:** ${doneN}/${STATIONEN.length}\n`;
|
||||
md += `**Quiz:** ${correct}/${total} richtig\n\n`;
|
||||
md += `## Als unklar markiert\n`;
|
||||
md += unclearList.length ? unclearList.map(st=>`- ${st.id} — ${st.name}`).join("\n") : "_keine_";
|
||||
md += `\n\n## Quiz-Detail\n`;
|
||||
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
|
||||
const p = S.picks[pkey(st.id,qi)];
|
||||
if(p===undefined) return;
|
||||
md += `- [${p===q.richtig?"✓":"✗"}] ${st.id}: ${q.frage} → „${q.optionen[p]}"\n`;
|
||||
}));
|
||||
$("#debriefText").textContent = md;
|
||||
$("#debriefDlg").showModal();
|
||||
}
|
||||
|
||||
/* ====================== INIT ====================== */
|
||||
(function init(){
|
||||
const sel = $("#scenarioSel");
|
||||
sel.innerHTML = SZENARIEN.map((s,i)=>`<option value="${i}">${s}</option>`).join("");
|
||||
sel.value = S.scenario;
|
||||
sel.onchange = ()=>{ S.scenario = +sel.value; save(); render(); };
|
||||
$("#debriefBtn").onclick = openDebrief;
|
||||
$("#closeDebrief").onclick = ()=> $("#debriefDlg").close();
|
||||
$("#copyBtn").onclick = ()=> navigator.clipboard?.writeText($("#debriefText").textContent);
|
||||
$("#resetBtn").onclick = ()=>{ if(confirm("Durchlauf zurücksetzen?")){ S=defaultState(); save(); render(); } };
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue