app update final
This commit is contained in:
parent
e06a717e4b
commit
b6500bebe1
6 changed files with 406 additions and 30 deletions
|
|
@ -130,6 +130,8 @@ formcodiert. Figuren werden **gestellt, nicht gesteckt**; es gibt **zwei** Orte:
|
||||||
**R | A** (oben) und **C | I** (unten). Die beteiligten Rollen werden zusätzlich in
|
**R | A** (oben) und **C | I** (unten). Die beteiligten Rollen werden zusätzlich in
|
||||||
die passende RACI-Zone gestellt — sichtbar wird nicht nur *wer*, sondern *in welcher
|
die passende RACI-Zone gestellt — sichtbar wird nicht nur *wer*, sondern *in welcher
|
||||||
Verantwortung*. **A** hat genau einen Platz (genau eine Rolle accountable).
|
Verantwortung*. **A** hat genau einen Platz (genau eine Rolle accountable).
|
||||||
|
Didaktische Gewichtung (Frank): **R und A** sind die Pflicht und unbedingt zu
|
||||||
|
durchdenken (obere Zeile R | A), **C und I** sind ergänzend / nice-to-have.
|
||||||
|
|
||||||
Alle Standfelder sind Ø 22 (gleich wie die Puck-Mulden — dieselben Ø-20-Figuren).
|
Alle Standfelder sind Ø 22 (gleich wie die Puck-Mulden — dieselben Ø-20-Figuren).
|
||||||
Details & Designvarianten: [`../02_Spielfiguren/`](../02_Spielfiguren/).
|
Details & Designvarianten: [`../02_Spielfiguren/`](../02_Spielfiguren/).
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
# Deployment — SLC-Workshop Companion (App)
|
# Deployment — SLC-Workshop Companion (App)
|
||||||
|
|
||||||
**Auftrag für die Server-Claude / Ops:** Diese App **statisch** ausliefern. Kein
|
**Auftrag für die Server-Claude / Ops:** Diese App **statisch** ausliefern. Kein
|
||||||
Build-Schritt, kein Backend, keine Secrets, keine Datenbank.
|
Build-Schritt, keine Secrets, keine Datenbank.
|
||||||
|
|
||||||
|
> **Neu (v0.10): kleiner Feedback-Endpoint.** Die App sammelt jetzt Workshop-Feedback
|
||||||
|
> (Phasen + Prüfschritte) und **speichert es auf dem Server** zur späteren Auswertung.
|
||||||
|
> Dafür braucht es **einen** schlanken POST-Sammelpunkt (Flat-File, **keine Datenbank**).
|
||||||
|
> Referenz liegt bei: [`feedback.php`](feedback.php). Ist der Endpoint (noch) nicht
|
||||||
|
> aktiv, geht **nichts verloren**: die App hält das Feedback lokal vor (Retry-Queue)
|
||||||
|
> und der Header-Button **„⇩ Feedback"** exportiert es als Backup-Datei.
|
||||||
|
|
||||||
## Was das ist
|
## Was das ist
|
||||||
- Eine **statische Single-Page-PWA**. Alle Inhalte (Stationen, Quiz, Use-Cases)
|
- Eine **statische Single-Page-PWA**. Alle Inhalte (Stationen, Quiz, Use-Cases)
|
||||||
|
|
@ -11,6 +18,7 @@ Build-Schritt, kein Backend, keine Secrets, keine Datenbank.
|
||||||
- `manifest.webmanifest` — PWA-Manifest
|
- `manifest.webmanifest` — PWA-Manifest
|
||||||
- `sw.js` — Service Worker (Offline-Cache der App-Shell)
|
- `sw.js` — Service Worker (Offline-Cache der App-Shell)
|
||||||
- `icon.svg` — App-Icon
|
- `icon.svg` — App-Icon
|
||||||
|
- `feedback.php` — **optionaler** Feedback-Sammelpunkt (Flat-File, JSON Lines)
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
Den Ordner `04_Tablet-Quiz/app/` als **statisches Web-Root** über den vorhandenen
|
Den Ordner `04_Tablet-Quiz/app/` als **statisches Web-Root** über den vorhandenen
|
||||||
|
|
@ -47,11 +55,44 @@ slc.example.intern {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Feedback-Endpoint (empfohlen — für die Auswertung)
|
||||||
|
|
||||||
|
Die App schickt jedes gespeicherte Feedback per `POST` als JSON an den im
|
||||||
|
`<meta name="slc-feedback-endpoint">` hinterlegten Pfad (Default: `feedback.php`,
|
||||||
|
gleiche Domain). Der Service Worker fasst POSTs nicht an — der Offline-Cache stört
|
||||||
|
also nicht.
|
||||||
|
|
||||||
|
**Variante A — PHP (Referenz, einfachste Option).** `feedback.php` liegt im App-Ordner.
|
||||||
|
Voraussetzung: `php-fpm` aktiv und das App-Verzeichnis für PHP freigegeben; das
|
||||||
|
Datenverzeichnis `feedback-data/` muss vom Webserver-User beschreibbar sein.
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# innerhalb des server{}-Blocks von oben:
|
||||||
|
location ~ \.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_pass unix:/run/php/php-fpm.sock; # Pfad anpassen
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Daten landen in `feedback-data/feedback.jsonl` (eine JSON-Zeile pro Feedback) →
|
||||||
|
später für die Auswertung einlesen/zu CSV konvertieren.
|
||||||
|
|
||||||
|
**Variante B — kein PHP verfügbar.** Endpoint auf einen beliebigen JSON-POST-Empfänger
|
||||||
|
zeigen lassen (z. B. ein kleines Node-/Worker-Skript, das den Body an eine Datei
|
||||||
|
anhängt) und im `<meta name="slc-feedback-endpoint">` dessen URL eintragen.
|
||||||
|
|
||||||
|
**Variante C — gar kein Server-Save.** Auch ohne Endpoint funktioniert alles: das
|
||||||
|
Feedback bleibt in der Retry-Queue und kann über den Header-Button **„⇩ Feedback"**
|
||||||
|
als JSON-Datei exportiert werden.
|
||||||
|
|
||||||
## Verifikation nach dem Deploy
|
## Verifikation nach dem Deploy
|
||||||
1. URL öffnen → Startbildschirm „SLC Companion · Schritt 1 · Action Card".
|
1. URL öffnen → Startbildschirm „SLC Companion".
|
||||||
2. DevTools → Application → **Service Workers**: `sw.js` ist *activated*.
|
2. DevTools → Application → **Service Workers**: `sw.js` ist *activated*.
|
||||||
3. Flugmodus/Offline → Seite neu laden → App lädt weiterhin (Offline-Cache).
|
3. Flugmodus/Offline → Seite neu laden → App lädt weiterhin (Offline-Cache).
|
||||||
4. Einen Durchlauf spielen → „Debrief" → **↓ Markdown / ↓ JSON** lädt eine Datei.
|
4. Einen Major-Durchlauf spielen, an einem Gate Feedback eintragen → **„💾 Feedback
|
||||||
|
speichern"**. Bei aktivem Endpoint erscheint kein Fehler in der Konsole; in
|
||||||
|
`feedback-data/feedback.jsonl` taucht eine neue Zeile auf. Ohne Endpoint: Header
|
||||||
|
**„⇩ Feedback"** lädt die Backup-Datei.
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
- Bei neuem Stand: `git pull`. Wenn sich App-Assets geändert haben, in `sw.js`
|
- Bei neuem Stand: `git pull`. Wenn sich App-Assets geändert haben, in `sw.js`
|
||||||
|
|
|
||||||
58
04_Tablet-Quiz/app/feedback.php
Normal file
58
04_Tablet-Quiz/app/feedback.php
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SLC-Workshop — minimaler Feedback-Sammelpunkt (Referenz-Implementierung).
|
||||||
|
*
|
||||||
|
* Nimmt einen JSON-POST der Companion-App entgegen und hängt ihn als eine Zeile
|
||||||
|
* (JSON Lines) an eine Datei an. Kein Framework, keine Datenbank, keine Secrets.
|
||||||
|
*
|
||||||
|
* Auswertung später: die Datei `feedback-data/feedback.jsonl` einlesen (jede Zeile
|
||||||
|
* ein JSON-Objekt) — z. B. nach JSON/CSV konvertieren.
|
||||||
|
*
|
||||||
|
* Voraussetzung: PHP (php-fpm) ist auf dem Webserver aktiv und dieser Pfad wird
|
||||||
|
* von der App aus erreicht (gleiche Domain). Schreibrechte aufs Datenverzeichnis
|
||||||
|
* sind nötig. Alternativen (Node/Caddy) siehe DEPLOY.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
// Nur POST erlaubt (GET nur als simpler Health-Check).
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['ok' => true, 'service' => 'slc-feedback', 'hint' => 'POST JSON to store feedback']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
if ($raw === false || strlen($raw) === 0 || strlen($raw) > 65536) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'empty or oversized body']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'invalid json']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serverseitigen Eingangszeitstempel + Roh-Metadaten ergänzen (nicht überschreibend).
|
||||||
|
$data['_received'] = gmdate('c');
|
||||||
|
$data['_ip'] = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/feedback-data';
|
||||||
|
$file = $dir . '/feedback.jsonl';
|
||||||
|
if (!is_dir($dir)) { @mkdir($dir, 0775, true); }
|
||||||
|
|
||||||
|
$line = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
$fp = @fopen($file, 'ab');
|
||||||
|
if ($fp === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'cannot open data file (Schreibrechte prüfen)']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
flock($fp, LOCK_EX);
|
||||||
|
fwrite($fp, $line);
|
||||||
|
flock($fp, LOCK_UN);
|
||||||
|
fclose($fp);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<title>SLC-Workshop — Companion-App</title>
|
<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="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" />
|
<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="manifest" href="manifest.webmanifest" />
|
||||||
<link rel="apple-touch-icon" href="icon.svg" />
|
<link rel="apple-touch-icon" href="icon.svg" />
|
||||||
<link rel="icon" href="icon.svg" type="image/svg+xml" />
|
<link rel="icon" href="icon.svg" type="image/svg+xml" />
|
||||||
|
|
@ -176,6 +178,10 @@
|
||||||
.sHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
.sHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
.sHead .phaseChip{font-size:15px;padding:8px 18px;letter-spacing:.8px}
|
.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}
|
.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}
|
.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}
|
.caseLine{color:var(--muted);font-size:13px;margin:0 0 22px}
|
||||||
.lead{font-size:16px;margin:0 0 16px}
|
.lead{font-size:16px;margin:0 0 16px}
|
||||||
|
|
@ -184,6 +190,7 @@
|
||||||
.crit{margin:8px 0 0;padding-left:20px;color:var(--muted)}
|
.crit{margin:8px 0 0;padding-left:20px;color:var(--muted)}
|
||||||
.crit li{margin:4px 0}
|
.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}
|
.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}
|
.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{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:hover{border-color:#c9d2dd}
|
||||||
|
|
@ -208,6 +215,10 @@
|
||||||
.raciLegend .rlHead{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700;margin-bottom:8px}
|
.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{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}
|
.raciLegend .rlRow .raciBadge{flex:none;margin-top:1px}
|
||||||
|
.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) */
|
/* 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{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}
|
.frageBox .frageLabel{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:700;margin-bottom:5px}
|
||||||
|
|
@ -288,6 +299,24 @@
|
||||||
.slcDonut.clickable .donutSeg{cursor:pointer;transition:opacity .12s}
|
.slcDonut.clickable .donutSeg{cursor:pointer;transition:opacity .12s}
|
||||||
.slcDonut.clickable .donutSeg:hover{opacity:.82}
|
.slcDonut.clickable .donutSeg:hover{opacity:.82}
|
||||||
.slcDonut .donutSeg.bad{stroke:var(--bad);stroke-width:4}
|
.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}
|
.akteItem.flash{animation:akteFlash 1.6s ease-out}
|
||||||
@keyframes akteFlash{0%,55%{background:#fff3bf}100%{background:transparent}}
|
@keyframes akteFlash{0%,55%{background:#fff3bf}100%{background:transparent}}
|
||||||
.slcCap{font-size:11px;color:var(--muted);margin-top:4px;text-align:center}
|
.slcCap{font-size:11px;color:var(--muted);margin-top:4px;text-align:center}
|
||||||
|
|
@ -357,13 +386,14 @@
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="brand">SLC Workshop <b>Companion</b></div>
|
<div class="brand">SLC Workshop <b>Companion</b></div>
|
||||||
<span class="tag">v0.9</span>
|
<span class="tag">v0.10</span>
|
||||||
<div id="cardBadge" class="cardBadge"></div>
|
<div id="cardBadge" class="cardBadge"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="ghost" id="svcBtn" title="Ausführliche Service-Beschreibungen">ℹ️ Service-Info</button>
|
<button class="ghost" id="svcBtn" title="Ausführliche Service-Beschreibungen">ℹ️ Service-Info</button>
|
||||||
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁 Service-Akte</button>
|
<button class="ghost" id="akteBtn" title="Service-Akte (gesammelte Artefakte)">📁 Service-Akte</button>
|
||||||
<button class="ghost" id="rollenBtn" title="Rollen-Glossar (RACI)">👥 Rollen</button>
|
<button class="ghost" id="rollenBtn" title="Rollen-Glossar (RACI)">👥 Rollen</button>
|
||||||
<button class="ghost" id="stationsBtn" title="Stationsübersicht">☰ Stationen</button>
|
<button class="ghost" id="stationsBtn" title="Stationsübersicht">☰ Stationen</button>
|
||||||
|
<button class="ghost" id="fbExportBtn" title="Gesammeltes Feedback als Backup-Datei exportieren">⇩ Feedback</button>
|
||||||
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
|
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
|
<div class="progress"><div id="progressBar" style="width:0%"></div></div>
|
||||||
|
|
@ -752,13 +782,21 @@ const STATIONEN = [
|
||||||
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).",
|
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"],
|
umfasst:["Aufwandsschätzung (Build vs. Configure)","Technische Risiken","Budget-Abgleich","ggf. Lieferanten-Einbindung","SOR-Vorlage für Freigabe"],
|
||||||
artefakt:"Gate-Entscheidung + SOR-Vorlage",
|
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:[
|
pfade:[
|
||||||
["Entwicklung","Neuentwicklung/wesentliche Anpassung → weiter zu tr_02"],
|
["Entwicklung","Neuentwicklung/wesentliche Anpassung → weiter zu tr_02"],
|
||||||
["Konfiguration","Bestehende Komponenten konfigurieren → springt zu tr_05"]
|
["Konfiguration","Bestehende Komponenten konfigurieren → springt zu tr_05"]
|
||||||
],
|
],
|
||||||
pruef:[
|
pruef:[
|
||||||
["Design-Vollständigkeit","Ist das Service Design Document vollständig?"],
|
["Design-Vollständigkeit","Ist das Service Design Document vollständig?"],
|
||||||
["Budget-Verfügbarkeit","Ist Budget für die Strategie verfügbar?"],
|
["Budget-Verfügbarkeit","Ist Budget für die Implementierung verfügbar?"],
|
||||||
["Projektressourcen","Können Ressourcen mobilisiert werden?"],
|
["Projektressourcen","Können Ressourcen mobilisiert werden?"],
|
||||||
["Betriebs-/Support-Bereitschaft","Grundsätzliches Commitment vorhanden?"]
|
["Betriebs-/Support-Bereitschaft","Grundsätzliches Commitment vorhanden?"]
|
||||||
],
|
],
|
||||||
|
|
@ -851,6 +889,7 @@ const STATIONEN = [
|
||||||
{ id:"tr_09", phase:"transition", typ:"gate", gateNr:2,
|
{ id:"tr_09", phase:"transition", typ:"gate", gateNr:2,
|
||||||
name:"Gate 2: Entry-Prüfung nach Build",
|
name:"Gate 2: Entry-Prüfung nach Build",
|
||||||
beschreibung:"Validierung der Übergabefähigkeit nach Abschluss des Builds. Dies ist eine SO-Einzelentscheidung (keine Gremienentscheidung).",
|
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"],
|
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)",
|
artefakt:"Gate-2-Entscheidung (Transition-Steckbrief)",
|
||||||
pfade:[
|
pfade:[
|
||||||
|
|
@ -899,6 +938,7 @@ const STATIONEN = [
|
||||||
{ id:"tr_12", phase:"transition", typ:"gate", gateNr:3,
|
{ id:"tr_12", phase:"transition", typ:"gate", gateNr:3,
|
||||||
name:"Gate 3: Go-Live-Freigabe",
|
name:"Gate 3: Go-Live-Freigabe",
|
||||||
beschreibung:"Portfolio-Freigabe und formale Aktivierung des Services. Exit-Gate der Transition — SOR-Entscheidung nach dem Konsent-Prinzip.",
|
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"],
|
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",
|
artefakt:"Gate-3-Entscheidung + Portfolio-Aufnahme",
|
||||||
pfade:[
|
pfade:[
|
||||||
|
|
@ -1119,8 +1159,9 @@ const STATIONEN = [
|
||||||
RACI + Quiz hier abgeleitet (Franks Entwurf nennt nur die Aktivitaeten). */
|
RACI + Quiz hier abgeleitet (Franks Entwurf nennt nur die Aktivitaeten). */
|
||||||
{ id:"rv_01", phase:"review", typ:"aktivitaet",
|
{ id:"rv_01", phase:"review", typ:"aktivitaet",
|
||||||
name:"Durchführen von Service-Reviews",
|
name:"Durchführen von Service-Reviews",
|
||||||
beschreibung:"Den Service systematisch auswerten und die Ergebnisse im Service-Review-Dokument festhalten.",
|
beschreibung:"Den Service systematisch auswerten und die Ergebnisse im Service-Review-Dokument festhalten (4 Dimensionen → Handlungsempfehlung).",
|
||||||
umfasst:["KPIs & Monitoring auswerten","Problems & Incidents auswerten","Kundenfeedback sammeln/einholen","zugrunde liegende Infrastruktur bewerten","Service-Review-Dokument ausfüllen"],
|
ziel:"Eine fundierte Entscheidung ermöglichen, ob der Service unverändert weiterbetrieben werden kann oder ob Änderungen erforderlich sind.",
|
||||||
|
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",
|
artefakt:"Service-Review-Dokument",
|
||||||
raci:[["service_owner","A"],["betriebsteam","R"],["service_support_team","C"],["problem_manager","C"]],
|
raci:[["service_owner","A"],["betriebsteam","R"],["service_support_team","C"],["problem_manager","C"]],
|
||||||
quiz:[
|
quiz:[
|
||||||
|
|
@ -1221,9 +1262,9 @@ const ARTEFAKTE = {
|
||||||
A13:{name:"Wissensdatenbank-Eintrag", phase:"support", live:true,
|
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.",
|
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."},
|
warum:"Es beschleunigt den Support, sichert konsistente Antworten und entlastet die höheren Support-Level."},
|
||||||
A14:{name:"Service-Review-Bericht", phase:"review",
|
A14:{name:"Service-Review-Dokument", phase:"review",
|
||||||
was:"Das „Service Performance & Improvement Review“: Bewertung über 4 Dimensionen (Leistung, Stabilität, Nutzerzufriedenheit, Zukunftsfähigkeit) per Ampel und eine Handlungsempfehlung (CONTINUE / IMPROVEMENT / REDESIGN / RETIRE).",
|
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 liefert die strukturierte Grundlage für die SOR-Entscheidung über die Zukunft des Service."},
|
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."},
|
||||||
A15:{name:"DPM-Rücklauf", phase:"review",
|
A15:{name:"DPM-Rücklauf", phase:"review",
|
||||||
was:"Die Übergabe an den Demand-Lifecycle — Variante A: Neuer Demand (Redesign/Erweiterung) oder Variante B: Retirement-Plan / Decommissioning-Auftrag (Stilllegung).",
|
was:"Die Übergabe an den Demand-Lifecycle — Variante A: Neuer Demand (Redesign/Erweiterung) oder Variante B: Retirement-Plan / Decommissioning-Auftrag (Stilllegung).",
|
||||||
warum:"Es schließt den Lebenszyklus: größere Änderungen oder das Ende eines Service laufen kontrolliert über den Demand-Lifecycle, nicht „nebenbei“."}
|
warum:"Es schließt den Lebenszyklus: größere Änderungen oder das Ende eines Service laufen kontrolliert über den Demand-Lifecycle, nicht „nebenbei“."}
|
||||||
|
|
@ -1246,6 +1287,20 @@ const FOLDS_INTO = {
|
||||||
op_02:"A6", op_05:"A9", op_07:"A11",
|
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"
|
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="Eigene Arbeitsergebnisse/Templates liegen vor — hier ist detailliertes Feedback besonders wertvoll.">● Bearbeitet</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; } }
|
function addArtefakt(a){ if(a){ S.akte = S.akte || {}; S.akte[a] = true; } }
|
||||||
// Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station
|
// Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station
|
||||||
|
|
@ -1270,7 +1325,8 @@ function defaultState(){
|
||||||
freigabeDone:false, freigabeWrong:null,
|
freigabeDone:false, freigabeWrong:null,
|
||||||
entryDone:false, entryWrong:null,
|
entryDone:false, entryWrong:null,
|
||||||
bonusReveal:false, bonusDone:{}, servicesDone:{}, akteFlash:null, svcModalSel:null,
|
bonusReveal:false, bonusDone:{}, servicesDone:{}, akteFlash:null, svcModalSel:null,
|
||||||
index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0,
|
index:0, stage:"discuss", quizIndex:0, gateDeciderDone:false, gateCrit:0, gate1Approval:null,
|
||||||
|
feedback:{}, feedbackSaved:{}, feedbackQueue:[],
|
||||||
actStep:0, actReveal:false, actDone:false, arteWrong:null,
|
actStep:0, actReveal:false, actDone:false, arteWrong:null,
|
||||||
picks:{}, done:{}, akte:{},
|
picks:{}, done:{}, akte:{},
|
||||||
loopback:null, revisit:{}, endReason:null, endGate:null };
|
loopback:null, revisit:{}, endReason:null, endGate:null };
|
||||||
|
|
@ -1462,6 +1518,7 @@ function renderMainIntro(){
|
||||||
<p style="margin:0 0 8px">${card.text}</p>
|
<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>
|
<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>
|
<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>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
|
@ -1580,15 +1637,17 @@ function renderFreigabe(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App) */
|
/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App).
|
||||||
function phaseDonut(wrongPh, clickable){
|
currentPh: hebt die aktuelle/nächste Phase hervor, dimmt die übrigen. */
|
||||||
|
function phaseDonut(wrongPh, clickable, currentPh){
|
||||||
const order=["design","transition","operation","support","review"];
|
const order=["design","transition","operation","support","review"];
|
||||||
const cx=100,cy=100,R=92,r=46,seg=72,start=-90-seg/2;
|
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) ];
|
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="";
|
let segs="", labels="";
|
||||||
order.forEach((ph,i)=>{
|
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 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":"");
|
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})"/>`;
|
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);
|
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="${L[0]}" y="${(+L[1]+3).toFixed(1)}" text-anchor="middle" font-size="9.5" font-weight="700" fill="#fff">${PHASEN[ph].label}</text>`;
|
||||||
|
|
@ -1597,6 +1656,15 @@ function phaseDonut(wrongPh, clickable){
|
||||||
labels+=`<text x="100" y="108" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Lifecycle</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>`;
|
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) ---------------- */
|
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
|
||||||
function renderEntry(){
|
function renderEntry(){
|
||||||
|
|
@ -1752,7 +1820,7 @@ function enterStation(idx){
|
||||||
}
|
}
|
||||||
S.index = idx;
|
S.index = idx;
|
||||||
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act";
|
||||||
S.gatePick = null; S.quizIndex = 0; S.gateDeciderDone = false; S.gateCrit = 0;
|
S.gatePick = null; S.quizIndex = 0; S.gateDeciderDone = false; S.gateCrit = 0; S.gate1Approval = null;
|
||||||
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
|
S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null;
|
||||||
S.akteFlash = null; document.body.classList.remove("akteOpen");
|
S.akteFlash = null; document.body.classList.remove("akteOpen");
|
||||||
}
|
}
|
||||||
|
|
@ -1784,16 +1852,21 @@ 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("");
|
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>`;
|
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. */
|
/* 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(){
|
function raciLegendHtml(){
|
||||||
const items = [
|
const kern = [
|
||||||
["R","Responsible","verantwortlich für die Durchführung — erledigt die Aufgabe operativ"],
|
["R","Responsible","verantwortlich für die Durchführung — erledigt die Aufgabe operativ"],
|
||||||
["A","Accountable","rechenschaftspflichtig — trägt die Ergebnisverantwortung (genau eine Rolle)"],
|
["A","Accountable","rechenschaftspflichtig — trägt die Ergebnisverantwortung (genau eine Rolle)"]
|
||||||
|
];
|
||||||
|
const erg = [
|
||||||
["C","Consulted","konsultiert — wird vorab um Rat/Beitrag gefragt"],
|
["C","Consulted","konsultiert — wird vorab um Rat/Beitrag gefragt"],
|
||||||
["I","Informed","informiert — wird über das Ergebnis in Kenntnis gesetzt"]
|
["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 `<div class="raciLegend"><div class="rlHead">Wofür RACI steht</div>` +
|
return `<div class="raciLegend"><div class="rlHead">Wofür RACI steht</div>` +
|
||||||
items.map(([l,en,de])=>`<div class="rlRow"><span class="raciBadge raci-${l}">${l}</span><span><b>${en}</b> — ${de}</span></div>`).join("") +
|
`<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>`;
|
`</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1821,7 +1894,7 @@ function renderRun(){
|
||||||
<div class="classifyTop">
|
<div class="classifyTop">
|
||||||
${cardBig}
|
${cardBig}
|
||||||
<div class="classifyMain">
|
<div class="classifyMain">
|
||||||
<div class="sHead">${chip}<span class="sId">${st.id}</span></div>
|
<div class="sHead">${chip}<span class="sId">${st.id}</span>${tiefeBadge(st.id)}</div>
|
||||||
<h2 class="sTitle" style="margin-top:8px">${st.name}</h2>
|
<h2 class="sTitle" style="margin-top:8px">${st.name}</h2>
|
||||||
${stepBody}
|
${stepBody}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1841,7 +1914,7 @@ function activitySteps(st){
|
||||||
frage:`Welche Rollen setzen diese Aktivität <b>operativ</b> um — wer sorgt für die <b>Umsetzung</b> (Responsible)? Stellt deren Figuren auf die <b>Mulden des Station-Pucks</b>.`,
|
frage:`Welche Rollen setzen diese Aktivität <b>operativ</b> um — wer sorgt für die <b>Umsetzung</b> (Responsible)? Stellt deren Figuren auf die <b>Mulden des Station-Pucks</b>.`,
|
||||||
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==="A/R"?" (zugleich A)":""}</span>`).join("")) || '<span class="roleChip">— (keine eigene R-Rolle)</span>'}</div><p class="muted" style="margin:10px 0 0;font-size:13px">Das sind die „Macher" der Aktivität. <b>Wer</b> dafür geradesteht (A) sowie beratend (C) bzw. informiert (I) ist, klärt Schritt 3.</p>` },
|
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==="A/R"?" (zugleich A)":""}</span>`).join("")) || '<span class="roleChip">— (keine eigene R-Rolle)</span>'}</div><p class="muted" style="margin:10px 0 0;font-size:13px">Das sind die „Macher" der Aktivität. <b>Wer</b> dafür geradesteht (A) sowie beratend (C) bzw. informiert (I) ist, klärt Schritt 3.</p>` },
|
||||||
{ label:"RACI",
|
{ label:"RACI",
|
||||||
frage:`Ergänzt nun die <b>vollständige RACI</b>: Wer ist <b>A</b>ccountable (trägt die Verantwortung), wer <b>C</b>onsulted, wer <b>I</b>nformed — zusätzlich zu den Responsible aus Schritt 2? Sortiert die Figuren ins <b>Aktiv-Feld (R·A·C·I)</b>.`,
|
frage:`Vervollständigt die RACI. <b>Wichtig zuerst:</b> Wer ist <b>A</b>ccountable (trägt die Ergebnisverantwortung — genau eine Rolle)? Zusammen mit den Responsible aus Schritt 2 sind <b>R + A die Pflicht</b>. <b>C</b>onsulted und <b>I</b>nformed danach ergänzen (nice-to-have). Sortiert die Figuren ins <b>Aktiv-Feld (R·A·C·I)</b>.`,
|
||||||
legend: raciLegendHtml(),
|
legend: raciLegendHtml(),
|
||||||
auf:`<h4 class="aufH">RACI (vollständig)</h4>${raciTable(st)}` },
|
auf:`<h4 class="aufH">RACI (vollständig)</h4>${raciTable(st)}` },
|
||||||
{ label:"Artefakt", artefakt:true,
|
{ label:"Artefakt", artefakt:true,
|
||||||
|
|
@ -1878,6 +1951,9 @@ function renderActivity(st){
|
||||||
<p style="margin:6px 0">Gut gemacht! Ihr habt <b>${st.id} — ${st.name}</b> durchgespielt.</p>
|
<p style="margin:6px 0">Gut gemacht! Ihr habt <b>${st.id} — ${st.name}</b> durchgespielt.</p>
|
||||||
${phaseLine}
|
${phaseLine}
|
||||||
</div>
|
</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">
|
<div class="actions">
|
||||||
<button class="ghost" id="actBack">← zurück</button>
|
<button class="ghost" id="actBack">← zurück</button>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
@ -1975,6 +2051,7 @@ function renderGate(st){
|
||||||
const reqLine = req.length === 0 ? `` : blocked
|
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 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>`;
|
: `<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)
|
// Prüf-Kriterien als nacheinander erscheinende Checkboxen (nur Anzeige, nicht blockierend)
|
||||||
const pruef = st.pruef || [];
|
const pruef = st.pruef || [];
|
||||||
|
|
@ -1994,8 +2071,34 @@ function renderGate(st){
|
||||||
<div class="gateCrit">${critItems}</div>
|
<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>`:``}` : ``;
|
${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>`:``}` : ``;
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
${zielLine}
|
||||||
|
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||||
|
${reqLine}
|
||||||
|
${critWrap}
|
||||||
|
<div class="critHead">Freigabe-Entscheidung</div>
|
||||||
|
<div class="choiceGrid">${fopts}</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>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${revisitNote}
|
${revisitNote}
|
||||||
|
${zielLine}
|
||||||
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
<p class="lead"><b>Entscheidet:</b> ${roleLabel(keeper)}</p>
|
||||||
${reqLine}
|
${reqLine}
|
||||||
${critWrap}
|
${critWrap}
|
||||||
|
|
@ -2023,6 +2126,8 @@ function renderGateDone(st){
|
||||||
<p style="margin:4px 0">Weiter geht's mit der Phase <b style="color:${nextColor}">${PHASEN[target.phase].label}</b>.</p>
|
<p style="margin:4px 0">Weiter geht's mit der Phase <b style="color:${nextColor}">${PHASEN[target.phase].label}</b>.</p>
|
||||||
</div>`;
|
</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 `
|
return `
|
||||||
<div class="step reveal">
|
<div class="step reveal">
|
||||||
<div class="stepHead"><span class="n">⛩</span> Entscheidung getroffen</div>
|
<div class="stepHead"><span class="n">⛩</span> Entscheidung getroffen</div>
|
||||||
|
|
@ -2030,6 +2135,9 @@ function renderGateDone(st){
|
||||||
${sorNote}
|
${sorNote}
|
||||||
${feedback}
|
${feedback}
|
||||||
</div>
|
</div>
|
||||||
|
${overview}
|
||||||
|
${FEEDBACK_DOCS[st.id] ? docFeedbackBlock(st.id) : ``}
|
||||||
|
${phaseEnd ? phaseFeedbackBlock(st.phase) : ``}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="ghost" id="gateBack">← andere Entscheidung</button>
|
<button class="ghost" id="gateBack">← andere Entscheidung</button>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
@ -2037,6 +2145,117 @@ function renderGateDone(st){
|
||||||
</div>`;
|
</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=>({"&":"&","<":"<",">":">",'"':"""}[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 1–5 + 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 ====================== */
|
/* ====================== WIRING ====================== */
|
||||||
function wire(st){
|
function wire(st){
|
||||||
const b = id => $("#"+id);
|
const b = id => $("#"+id);
|
||||||
|
|
@ -2055,12 +2274,13 @@ function wire(st){
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if(b("actBack")) b("actBack").onclick = ()=>{
|
if(b("actBack")) b("actBack").onclick = ()=>{
|
||||||
|
captureFeedbackInputs();
|
||||||
if(S.actDone){ S.actDone=false; }
|
if(S.actDone){ S.actDone=false; }
|
||||||
else if(S.actReveal){ S.actReveal=false; }
|
else if(S.actReveal){ S.actReveal=false; }
|
||||||
else if((S.actStep||0)>0){ S.actStep--; S.actReveal=true; }
|
else if((S.actStep||0)>0){ S.actStep--; S.actReveal=true; }
|
||||||
save(); draw(); };
|
save(); draw(); };
|
||||||
if(b("nextStation")) b("nextStation").onclick = ()=>{ S.done[st.id]=true; enterStation(S.index+1); 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 = ()=>{ S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
|
if(b("finish")) b("finish").onclick = ()=>{ captureFeedbackInputs(); S.done[st.id]=true; S.view="end"; S.endReason="done"; save(); draw(); };
|
||||||
// Gate
|
// Gate
|
||||||
if(b("gateDeciderNext")) b("gateDeciderNext").onclick = ()=>{ S.gateDeciderDone=true; save(); draw(); };
|
if(b("gateDeciderNext")) b("gateDeciderNext").onclick = ()=>{ S.gateDeciderDone=true; save(); draw(); };
|
||||||
$("#panel").querySelectorAll(".critItem[data-ci]").forEach(el=>{
|
$("#panel").querySelectorAll(".critItem[data-ci]").forEach(el=>{
|
||||||
|
|
@ -2071,8 +2291,23 @@ function wire(st){
|
||||||
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
$("#panel").querySelectorAll(".choiceGrid .choice[data-i]").forEach(el=>{
|
||||||
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
el.onclick = ()=>{ S.gatePick=+el.dataset.i; S.stage="gateDone"; save(); draw(); };
|
||||||
});
|
});
|
||||||
if(b("gateBack")) b("gateBack").onclick = ()=>{ S.stage="gate"; save(); draw(); };
|
// Gate 1 — Freigabe-Entscheidung (Frank-Template): 0/1 → Routing, 2 → zurück an Design, 3 → Ablehnung.
|
||||||
if(b("gateNext")) b("gateNext").onclick = ()=>{ gateGoto(st, S.gatePick||0); };
|
$("#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 ====================== */
|
/* ====================== ABSCHLUSS-SCREEN ====================== */
|
||||||
|
|
@ -2088,20 +2323,28 @@ function renderEnd(){
|
||||||
: `<div class="recBox">
|
: `<div class="recBox">
|
||||||
<h4>Service-Lifecycle durchlaufen</h4>
|
<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>`;
|
<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 = `
|
$("#panel").innerHTML = `
|
||||||
<div class="setupHead">Abschluss</div>
|
<div class="setupHead">Abschluss</div>
|
||||||
<h2 class="setupTitle">${icon} ${head}</h2>
|
<h2 class="setupTitle">${icon} ${head}</h2>
|
||||||
<div class="hint ${rejected?"bad":"ok"}">${USE_CASES[S.service].service} · ${CHANGE_TYPES[S.change]}</div>
|
<div class="hint ${rejected?"bad":"ok"}">${USE_CASES[S.service].service} · ${CHANGE_TYPES[S.change]}</div>
|
||||||
${box}
|
${box}
|
||||||
|
${ending}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="primary" id="toBonus">Weiter → Varianten dieses Service →</button>
|
<button class="primary" id="toBonus">Weiter → Varianten dieses Service →</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
$("#toBonus").onclick = ()=>{ S.view="bonusPick"; save(); draw(); };
|
wireFeedback();
|
||||||
|
$("#toBonus").onclick = ()=>{ captureFeedbackInputs(); S.view="bonusPick"; save(); draw(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ====================== INIT ====================== */
|
/* ====================== INIT ====================== */
|
||||||
(function 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(); } };
|
$("#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");
|
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"); };
|
$("#stationsBtn").onclick = ()=>{ const o=document.body.classList.contains("navOpen"); closeOverlays(); if(!o) document.body.classList.add("navOpen"); };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||||
const CACHE = "slc-companion-v31";
|
const CACHE = "slc-companion-v32";
|
||||||
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
||||||
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
||||||
const CARDS = [];
|
const CARDS = [];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
Chat). Hier steht, **wo wir stehen, was entschieden wurde, was offen ist** und wie man
|
Chat). Hier steht, **wo wir stehen, was entschieden wurde, was offen ist** und wie man
|
||||||
App lokal startet/deployt.
|
App lokal startet/deployt.
|
||||||
|
|
||||||
**Stand:** 2026-06-09 · **Branch:** `feat/redesign-und-companion-app` · App **v0.7**
|
**Stand:** 2026-06-10 · **Branch:** `feat/redesign-und-companion-app` · App **v0.10**
|
||||||
**Remote:** `https://git.1789.cloud/patrick/SLC_Game.git`
|
**Remote:** `https://git.1789.cloud/patrick/SLC_Game.git`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -76,8 +76,11 @@ Service gilt als bereits eingeführt) → zurück zur Bonus-Auswahl → „Servi
|
||||||
zurück, blendet einen Nacharbeits-Banner ein und legt das Gate danach **erneut**
|
zurück, blendet einen Nacharbeits-Banner ein und legt das Gate danach **erneut**
|
||||||
vor (Hinweis „Erneute Vorlage"). „Ablehnung" → eigener **End-Screen** (SOR-Eskalation).
|
vor (Hinweis „Erneute Vorlage"). „Ablehnung" → eigener **End-Screen** (SOR-Eskalation).
|
||||||
(z. B. Gate 1 „Konfiguration" → `tr_05`; SOR→DPM→Mission Board.)
|
(z. B. Gate 1 „Konfiguration" → `tr_05`; SOR→DPM→Mission Board.)
|
||||||
- **Kein App-Debrief mehr:** Der Export-Dialog (Markdown/JSON) wurde entfernt; das
|
- **Kein Lauf-Debrief mehr** (Markdown/JSON-Export des Durchlaufs raus); Workshop-**Debriefing
|
||||||
Workshop-**Debriefing am Tisch** bleibt (im Done-Screen erwähnt).
|
am Tisch** bleibt (im Done-Screen erwähnt).
|
||||||
|
- **Strukturiertes Feedback (v0.10, Frank):** Phasen-Feedback (2 Freitexte) an Phasenenden +
|
||||||
|
Dokument-Feedback (Skala 1–5 + 2 Freitexte) an Gate 1/2/3 + Service Review. **Server-Save**
|
||||||
|
via POST an `feedback.php` (Retry-Queue + localStorage-Archiv + manueller „⇩ Feedback"-Export).
|
||||||
- **4 Change-Arten** (Major/Normal/Standard/Emergency), 24 Karten. Multiple-Choice
|
- **4 Change-Arten** (Major/Normal/Standard/Emergency), 24 Karten. Multiple-Choice
|
||||||
ist aus dem Hauptfluss raus (Daten liegen noch in `index.html`, ungenutzt).
|
ist aus dem Hauptfluss raus (Daten liegen noch in `index.html`, ungenutzt).
|
||||||
- **Inhalte sind in `index.html` eingebettet** (noch keine YAML-Pipeline).
|
- **Inhalte sind in `index.html` eingebettet** (noch keine YAML-Pipeline).
|
||||||
|
|
@ -108,6 +111,35 @@ Konzept (`00_Konzept/README_konzept.md`), bis Rückkopplung mit Michael:**
|
||||||
|
|
||||||
## 5. Offene Punkte / nächste Schritte
|
## 5. Offene Punkte / nächste Schritte
|
||||||
|
|
||||||
|
**Frank-Feedback 2026-06-10 (Workshop-/Spiel-Ebene, umgesetzt in App v0.10 — Entscheidungen Frank + Patrick):**
|
||||||
|
- [x] **RACI-Betonung:** Volle RACI bleibt; **R + A** sind als „Pflicht — unbedingt durchdenken"
|
||||||
|
hervorgehoben, **C + I** als „ergänzend / nice-to-have". Legende zweigeteilt, Schritt-3-Text
|
||||||
|
umformuliert (`activitySteps`, `raciLegendHtml`).
|
||||||
|
- [x] **GRAFIK SLC (Gesamtbild) 6×** im **Major-Walk**: Start (Main-Intro), an den 4 Phasenübergängen
|
||||||
|
(`renderActivity`/`renderGateDone`) und am Schluss (`renderEnd`) — jeweils mit Highlight der
|
||||||
|
aktuellen/nächsten Phase (`slcOverview` auf Basis von `phaseDonut`). **Bonus unberührt.**
|
||||||
|
- [x] **„Bearbeitet"/„Entwurf" sichtbar:** Badge im Stations-Header (`stationTiefe`/`tiefeBadge`).
|
||||||
|
Bearbeitet = `ds_01, tr_01, tr_09, tr_12, rv_01`; alles andere Entwurf.
|
||||||
|
- [x] **Gate 1 zweistufig:** erst **Freigabe-Entscheidung** (Freigabe / mit Auflagen / **Zurück an
|
||||||
|
Design** [Rückschleife] / **Ablehnung** [End-Screen]), dann **Routing** Entwicklung/Konfiguration.
|
||||||
|
Gate-Templates: `ziel` an Gate 1/2/3 ergänzt; Gate-1-Prüfdim. „Budget für die **Implementierung**".
|
||||||
|
- [x] **Service-Review-Template (Frank):** A14 + rv_01 auf 4 Dimensionen (Leistungserbringung,
|
||||||
|
Betriebsstabilität, Nutzerzufriedenheit, Zukunftsfähigkeit) + Empfehlung **Weiterbetrieb /
|
||||||
|
Normal-Change / Major-Change** umgestellt (**ersetzt** CONTINUE/IMPROVEMENT/REDESIGN/RETIRE).
|
||||||
|
- [x] **Feedback-Erfassung + Server-Save** (re-aktiviert nach v0.7-Entfernung des Exports):
|
||||||
|
- **Phasen-Feedback** (2 Freitexte) an jedem Phasenende; **Dokument-Feedback** (Skala 1–5 +
|
||||||
|
2 Freitexte) an **Gate 1/2/3 + Service Review** (`FEEDBACK_DOCS`).
|
||||||
|
- **POST** an `<meta name="slc-feedback-endpoint">` (Default `feedback.php`), **Retry-Queue +
|
||||||
|
localStorage-Archiv** (offline-fest, übersteht „Neu starten"), Header-Button **„⇩ Feedback"**
|
||||||
|
für manuellen JSON-Export als Backup.
|
||||||
|
- **Server:** Referenz-Collector `04_Tablet-Quiz/app/feedback.php` (Flat-File `feedback-data/feedback.jsonl`,
|
||||||
|
keine DB). **DEPLOY.md** angepasst (statisch **+ ein** kleiner POST-Endpoint; PHP/Node/„ohne"-Varianten).
|
||||||
|
- **Offen / fachlich mit Frank zu bestätigen:**
|
||||||
|
- **Retirement/Außerbetriebnahme:** in Franks Review-Empfehlung bewusst nicht mehr enthalten —
|
||||||
|
bestätigen, dass das so gewollt ist (vgl. `review-phase_arbeitsstand-frank.md`).
|
||||||
|
- **Server-Endpoint provisionieren:** PHP (php-fpm) am Webserver aktivieren **oder** Endpoint-URL
|
||||||
|
im Meta-Tag umbiegen. Bis dahin greift Queue + Export (kein Datenverlust).
|
||||||
|
|
||||||
**Frank-Feedback 2026-06-08 (Workshop-Ebene, entscheiden Frank + Patrick):**
|
**Frank-Feedback 2026-06-08 (Workshop-Ebene, entscheiden Frank + Patrick):**
|
||||||
- [x] **Main/Bonus-Flow** umgesetzt (nur Major = voller Walk; Bonus = einordnen + Kurz-Auflösung).
|
- [x] **Main/Bonus-Flow** umgesetzt (nur Major = voller Walk; Bonus = einordnen + Kurz-Auflösung).
|
||||||
- [x] **Aufgabe „Freigabe-Stelle"** ergänzt (SOR/DPM/MB · SO · keine[Standard/Emergency]).
|
- [x] **Aufgabe „Freigabe-Stelle"** ergänzt (SOR/DPM/MB · SO · keine[Standard/Emergency]).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue