Companion-App: zur deploybaren PWA ausgebaut (statisch) #1
8 changed files with 196 additions and 9 deletions
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "slc-companion",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "http.server", "8099", "--bind", "127.0.0.1", "--directory", "04_Tablet-Quiz/app"],
|
||||
"port": 8099
|
||||
}
|
||||
]
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ _*.png
|
|||
*.stl
|
||||
*.bak
|
||||
*.tmp
|
||||
.claude/settings.local.json
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
# Tablet-Quiz — Begleit-App (Teilprojekt)
|
||||
|
||||
**Status:** Konzept · **Typ:** eigenständiges Software-Teilprojekt des SLC-Workshops
|
||||
**Status:** App lauffähig (PWA) · Deploy vorbereitet · **Typ:** eigenständiges Software-Teilprojekt des SLC-Workshops
|
||||
|
||||
> **Umsetzungsstand:** Die App liegt unter [`app/`](app/) als statische **PWA**
|
||||
> (offline-/kioskfähig). Sie führt den kompletten Flow durch (Action Card →
|
||||
> Startpunkt → optionale Tour → Station: Diskussion/Quiz/Auflösung → Debrief mit
|
||||
> **Markdown-/JSON-Export**). Inhalte (40 Stationen, 45 Quizfragen, 6 Use-Cases)
|
||||
> sind derzeit in `app/index.html` eingebettet. **Deployment:** statisch, siehe
|
||||
> [`app/DEPLOY.md`](app/DEPLOY.md). **Lokal testen:** `python -m http.server 8099
|
||||
> --directory 04_Tablet-Quiz/app` (oder Preview-Config `.claude/launch.json`).
|
||||
|
||||
Das Tablet-Quiz ist der **digitale Begleiter** des Tabletops — kein Ersatz fürs
|
||||
Brett. Es ist der **erklärende Gegenpart** zu den Pucks: Die Pucks tragen nur die
|
||||
|
|
@ -65,12 +73,13 @@ steht, sondern in der App liegt.
|
|||
|
||||
## 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).
|
||||
- [x] Stationsführung: linearer Durchlauf mit „Nächste Station" + Fortschritt/Phasen-Farben.
|
||||
- [x] Fragetypen 1–3 (vermittelndes Quiz).
|
||||
- [x] „Auflösen"-Mechanik (Antwort erst auf Klick) **+ ausführliche Stationsauflösung** (Erklärung/RACI/Artefakt) nach dem Quiz.
|
||||
- [x] „Unklar"-Markierung je Aktivität.
|
||||
- [x] Debrief-Export (Markdown **und** JSON, lokaler Download).
|
||||
- [x] PWA / offline lauffähig (Manifest + Service Worker).
|
||||
- [ ] `questions.json` + Stations-Inhalte aus YAMLs generieren (Build-Skript) — Inhalte aktuell in `app/index.html` eingebettet (braucht Blueprint-Repo-Zugriff).
|
||||
|
||||
### Später (Ausbau)
|
||||
- Gate-Fragen mit Rollen-Check (Typ 4–5).
|
||||
|
|
|
|||
66
04_Tablet-Quiz/app/DEPLOY.md
Normal file
66
04_Tablet-Quiz/app/DEPLOY.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Deployment — SLC-Workshop Companion (App)
|
||||
|
||||
**Auftrag für die Server-Claude / Ops:** Diese App **statisch** ausliefern. Kein
|
||||
Build-Schritt, kein Backend, keine Secrets, keine Datenbank.
|
||||
|
||||
## Was das ist
|
||||
- Eine **statische Single-Page-PWA**. Alle Inhalte (Stationen, Quiz, Use-Cases)
|
||||
sind in `index.html` eingebettet — keine Laufzeit-API nötig.
|
||||
- Dateien im Ordner `04_Tablet-Quiz/app/`:
|
||||
- `index.html` — die App
|
||||
- `manifest.webmanifest` — PWA-Manifest
|
||||
- `sw.js` — Service Worker (Offline-Cache der App-Shell)
|
||||
- `icon.svg` — App-Icon
|
||||
|
||||
## Ziel
|
||||
Den Ordner `04_Tablet-Quiz/app/` als **statisches Web-Root** über den vorhandenen
|
||||
Webserver bereitstellen, erreichbar per **HTTPS** (oder localhost).
|
||||
|
||||
> **Wichtig:** Der Service Worker (Offline/Installierbar) läuft **nur über HTTPS
|
||||
> oder localhost**. Über reines `http://` ohne TLS registriert er sich nicht —
|
||||
> die App funktioniert dann trotzdem, nur ohne Offline-Cache.
|
||||
|
||||
## Schritte
|
||||
1. Repo auf dem Server bereitstellen/aktualisieren:
|
||||
`git clone https://git.1789.cloud/patrick/SLC_Game.git` (bzw. `git pull`).
|
||||
2. Den Ordner `SLC_Game/04_Tablet-Quiz/app/` als statisches Root einbinden.
|
||||
|
||||
### Beispiel nginx
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name slc.example.intern; # anpassen
|
||||
# ssl_certificate ... ; ssl_certificate_key ... ;
|
||||
root /srv/SLC_Game/04_Tablet-Quiz/app; # Pfad anpassen
|
||||
index index.html;
|
||||
location = /sw.js { add_header Cache-Control "no-cache"; } # SW immer frisch
|
||||
location / { try_files $uri $uri/ /index.html; }
|
||||
}
|
||||
```
|
||||
|
||||
### Beispiel Caddy (TLS automatisch)
|
||||
```caddy
|
||||
slc.example.intern {
|
||||
root * /srv/SLC_Game/04_Tablet-Quiz/app
|
||||
file_server
|
||||
header /sw.js Cache-Control "no-cache"
|
||||
}
|
||||
```
|
||||
|
||||
## Verifikation nach dem Deploy
|
||||
1. URL öffnen → Startbildschirm „SLC Companion · Schritt 1 · Action Card".
|
||||
2. DevTools → Application → **Service Workers**: `sw.js` ist *activated*.
|
||||
3. Flugmodus/Offline → Seite neu laden → App lädt weiterhin (Offline-Cache).
|
||||
4. Einen Durchlauf spielen → „Debrief" → **↓ Markdown / ↓ JSON** lädt eine Datei.
|
||||
|
||||
## Updates
|
||||
- Bei neuem Stand: `git pull`. Wenn sich App-Assets geändert haben, in `sw.js`
|
||||
die Konstante `CACHE` hochzählen (z. B. `slc-companion-v1` → `-v2`), damit der
|
||||
Service Worker den Cache erneuert.
|
||||
|
||||
## Noch offen (nicht Teil dieses Deploys)
|
||||
- **YAML→Inhaltspipeline:** Inhalte sind aktuell in `index.html` eingebettet.
|
||||
Später aus den Blueprint-`service-lifecycle_*.yaml` generieren (braucht Zugriff
|
||||
aufs Blueprint-Repo). Bis dahin werden Inhalte direkt in `index.html` gepflegt.
|
||||
- **Companion-Chatbot** (optionaler Nachschlage-Bot) — siehe `../README.md` §8;
|
||||
*braucht* ein LLM-Backend und ist daher **nicht** Teil der statischen App.
|
||||
12
04_Tablet-Quiz/app/icon.svg
Normal file
12
04_Tablet-Quiz/app/icon.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<!-- maskable: vollflaechiger Hintergrund, Inhalt im sicheren Mittelbereich -->
|
||||
<rect width="512" height="512" fill="#1d2430"/>
|
||||
<!-- Bahn-Linie -->
|
||||
<line x1="150" y1="256" x2="362" y2="256" stroke="#3a4658" stroke-width="10" stroke-linecap="round"/>
|
||||
<!-- 5 Phasen-Pucks (blau/orange/gruen/teal/lila) -->
|
||||
<circle cx="160" cy="256" r="24" fill="#2F80C9"/>
|
||||
<circle cx="208" cy="256" r="24" fill="#E8893B"/>
|
||||
<circle cx="256" cy="256" r="24" fill="#5BAE5B"/>
|
||||
<circle cx="304" cy="256" r="24" fill="#3FB5B5"/>
|
||||
<circle cx="352" cy="256" r="24" fill="#8E63B5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
|
|
@ -3,7 +3,12 @@
|
|||
<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>
|
||||
<title>SLC-Workshop — Companion-App</title>
|
||||
<meta name="description" content="Begleit-App zum SLC-Workshop-Tabletop: führt durch die Stationen, vermittelndes Quiz, Auflösung und Debrief. Offline lauffähig." />
|
||||
<meta name="theme-color" content="#1d2430" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="icon.svg" />
|
||||
<link rel="icon" href="icon.svg" type="image/svg+xml" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f5f7;
|
||||
|
|
@ -160,7 +165,7 @@
|
|||
<body>
|
||||
<header>
|
||||
<div class="brand">SLC <b>Companion</b></div>
|
||||
<span class="tag">Prototyp · v0.5</span>
|
||||
<span class="tag">v0.5</span>
|
||||
<div id="cardBadge" class="cardBadge"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="ghost" id="resetBtn" title="Neue Action Card / Durchlauf zurücksetzen">Neu starten</button>
|
||||
|
|
@ -181,6 +186,8 @@
|
|||
<pre class="export" id="debriefText"></pre>
|
||||
<div class="actions">
|
||||
<button class="ghost" id="copyBtn">In Zwischenablage</button>
|
||||
<button class="ghost" id="dlMd">↓ Markdown</button>
|
||||
<button class="ghost" id="dlJson">↓ JSON</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="primary" id="closeDebrief">Schließen</button>
|
||||
</div>
|
||||
|
|
@ -1157,6 +1164,14 @@ function quizScore(){
|
|||
}));
|
||||
return {correct,total};
|
||||
}
|
||||
let DEBRIEF = { md:"", json:null };
|
||||
function download(name, text, mime){
|
||||
const blob = new Blob([text], {type:mime});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = name; document.body.appendChild(a); a.click();
|
||||
a.remove(); setTimeout(()=>URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
function openDebrief(){
|
||||
const {correct,total} = quizScore();
|
||||
const doneN = Object.keys(S.done).length;
|
||||
|
|
@ -1179,6 +1194,24 @@ function openDebrief(){
|
|||
if(p===undefined) return;
|
||||
md += `- [${p===q.richtig?"✓":"✗"}] ${st.id}: ${q.frage} → „${q.optionen[p]}"\n`;
|
||||
}));
|
||||
const stamp = new Date().toISOString().slice(0,16).replace("T"," ");
|
||||
const json = {
|
||||
erzeugt: stamp,
|
||||
service: USE_CASES[S.service].service,
|
||||
changeTyp: CHANGE_TYPES[S.change],
|
||||
ausloeser: USE_CASES[S.service].changes[S.change],
|
||||
stationenBearbeitet: doneN,
|
||||
stationenGesamt: STATIONEN.length,
|
||||
quiz: { richtig: correct, gesamt: total },
|
||||
unklar: unclearList.map(st=>({id:st.id, name:st.name})),
|
||||
quizDetail: []
|
||||
};
|
||||
STATIONEN.forEach(st=> st.quiz.forEach((q,qi)=>{
|
||||
const p = S.picks[pkey(st.id,qi)];
|
||||
if(p===undefined) return;
|
||||
json.quizDetail.push({station:st.id, frage:q.frage, gewaehlt:q.optionen[p], richtig:p===q.richtig});
|
||||
}));
|
||||
DEBRIEF = { md, json };
|
||||
$("#debriefText").textContent = md;
|
||||
$("#debriefDlg").showModal();
|
||||
}
|
||||
|
|
@ -1188,9 +1221,16 @@ function openDebrief(){
|
|||
$("#debriefBtn").onclick = openDebrief;
|
||||
$("#closeDebrief").onclick = ()=> $("#debriefDlg").close();
|
||||
$("#copyBtn").onclick = ()=> navigator.clipboard?.writeText($("#debriefText").textContent);
|
||||
$("#dlMd").onclick = ()=> download("slc-debrief.md", DEBRIEF.md, "text/markdown");
|
||||
$("#dlJson").onclick = ()=> download("slc-debrief.json", JSON.stringify(DEBRIEF.json, null, 2), "application/json");
|
||||
$("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } };
|
||||
draw();
|
||||
})();
|
||||
|
||||
// PWA: Service Worker fuer Offline-/Kiosk-Betrieb (nur ueber http/https aktiv)
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", ()=> navigator.serviceWorker.register("sw.js").catch(()=>{}));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
15
04_Tablet-Quiz/app/manifest.webmanifest
Normal file
15
04_Tablet-Quiz/app/manifest.webmanifest
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "SLC-Workshop Companion",
|
||||
"short_name": "SLC Companion",
|
||||
"description": "Begleit-App zum SLC-Workshop-Tabletop: Stationsführung, vermittelndes Quiz, Auflösung und Debrief. Offline lauffähig.",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"orientation": "landscape",
|
||||
"lang": "de",
|
||||
"background_color": "#f4f5f7",
|
||||
"theme_color": "#1d2430",
|
||||
"icons": [
|
||||
{ "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
||||
]
|
||||
}
|
||||
33
04_Tablet-Quiz/app/sw.js
Normal file
33
04_Tablet-Quiz/app/sw.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||
const CACHE = "slc-companion-v1";
|
||||
const ASSETS = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
||||
|
||||
self.addEventListener("install", (e) => {
|
||||
e.waitUntil(
|
||||
caches.open(CACHE).then((c) => c.addAll(ASSETS)).then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys()
|
||||
.then((ks) => Promise.all(ks.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (e) => {
|
||||
if (e.request.method !== "GET") return;
|
||||
e.respondWith(
|
||||
caches.match(e.request).then((hit) =>
|
||||
hit ||
|
||||
fetch(e.request)
|
||||
.then((resp) => {
|
||||
const copy = resp.clone();
|
||||
caches.open(CACHE).then((c) => c.put(e.request, copy));
|
||||
return resp;
|
||||
})
|
||||
.catch(() => caches.match("index.html"))
|
||||
)
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue