diff --git a/01_3D-Druck/openscad/artefakt-token.scad b/01_3D-Druck/openscad/artefakt-token.scad new file mode 100644 index 0000000..cc75290 --- /dev/null +++ b/01_3D-Druck/openscad/artefakt-token.scad @@ -0,0 +1,56 @@ +// Artefakt-Token (Service-Akte) — gedruckte "Karte" statt Pappkarte +// SLC-Workshop Tabletop · Einheiten: mm +// Kleine Tile mit gravierter A-Nummer; wird in einen Slot des Artefakt-Trays +// (artefakt-tray.scad) gelegt. Eingefaerbt in der PHASENFARBE der erzeugenden +// Phase (Design/Transition/Operation/Support/Review) -> Farbe via Filament. +// +// "Lebende" Artefakte (A2 Service-Definition, A11 Problem Record, +// A13 Wissensdatenbank) wachsen sichtbar: Basis-Token + je Aktualisierung +// eine duenne Status-Platte oben drauf (Entwurf -> Final -> Aktualisiert). +// +// part = "token" -> ein Artefakt-Token (Nummer via tok_label) +// "plate" -> eine Status-Platte zum Aufstapeln (lebende Artefakte) + +part = "token"; // "token" | "plate" + +/* [Token] */ +tok_w = 30; // Breite +tok_d = 20; // Tiefe +tok_h = 4; // Hoehe (Basis-Token) +tok_r = 2.5; // Eckenradius +tok_label = "A2"; // gravierte A-Nummer +lab_size = 11; // Schriftgroesse A-Nummer +lab_depth = 0.8; // Gravurtiefe + +/* [Status-Platte] (lebende Artefakte) */ +plate_h = 2.5; // Hoehe je Aktualisierungs-Platte + +$fn = 48; + +module rrect(l, w, h, r) { + linear_extrude(h) offset(r) offset(-r) square([l, w], center = true); +} + +module token(label) { + difference() { + rrect(tok_w, tok_d, tok_h, tok_r); + translate([0, 0, tok_h - lab_depth]) + linear_extrude(lab_depth + 0.1) + text(label, size = lab_size, halign = "center", valign = "center"); + } +} + +// duenne Platte zum Aufstapeln; kleine Mittenrille als Status-Markierung +module status_plate() { + difference() { + rrect(tok_w, tok_d, plate_h, tok_r); + translate([0, 0, plate_h - 0.5]) + linear_extrude(0.6) + square([tok_w - 10, 1.2], center = true); + } +} + +if (part == "plate") status_plate(); +else token(tok_label); + +echo(part = part, tok = [tok_w, tok_d, tok_h], plate_h = plate_h); diff --git a/01_3D-Druck/openscad/artefakt-tray.scad b/01_3D-Druck/openscad/artefakt-tray.scad new file mode 100644 index 0000000..741013d --- /dev/null +++ b/01_3D-Druck/openscad/artefakt-tray.scad @@ -0,0 +1,107 @@ +// Artefakt-Tray (Service-Akte) — 3D-Aufnahme fuer die 15 Artefakt-Token +// SLC-Workshop Tabletop · Einheiten: mm +// Flaches Tableau mit 15 beschrifteten Steck-Slots (A1-A15), in 5 PHASEN-Reihen +// gruppiert (Design -> Transition -> Operation -> Support -> Review). Liegt neben +// der aktuellen Station und wandert mit. Die Token (artefakt-token.scad) tragen +// die Phasenfarbe; der Tray selbst ist EINFARBIG (Beschriftung via Gravur). +// +// Mechanik: +// - Artefakt erzeugt -> Token in seinen Slot legen. +// - "Lebende" Artefakte (A2/A11/A13, hier mit eingraviertem Zusatz-Rahmen +// markiert): Status-Platten oben aufstapeln -> Stapel waechst sichtbar. +// - Gate-Kopplung bleibt REGEL: ein Gate "oeffnet" nur, wenn die geforderten +// Token im Tray liegen (keine Mechanik am Gate-Puck). + +/* [Platte] */ +plate_thick = 6; // Dicke +margin = 8; // Aussenrand +corner_r = 5; + +/* [Slots] (Token Ø 30x20 wird REINGELEGT) */ +slot_w = 31; // Slot-Innenbreite (Token 30 + Spiel) +slot_d = 21; // Slot-Innentiefe (Token 20 + Spiel) +pocket_dep = 3.0; // Vertiefung (Token 4 hoch -> steht ~1 mm vor = greifbar) +pocket_r = 2.5; +gap_x = 7; // Abstand zwischen Slots in einer Reihe +row_gap = 8; // Abstand zwischen den Phasen-Reihen +header_h = 9; // Hoehe der Phasen-Kopfzeile ueber den Slots +finger_r = 6; // Greifkerbe an der Slot-Unterkante + +/* [Gravur] */ +num_size = 7; // A-Nummer im Slot-Boden +num_depth = 0.8; +hdr_size = 6; // Phasen-Name +hdr_depth = 0.8; +lf_inset = 3; // "lebend"-Rahmen: Abstand zum Slot-Rand +lf_w = 1.2; // Strichstaerke +lf_depth = 0.6; + +$fn = 48; + +// [Phasen-Reihe, [[A-Nummer, lebend?], ...]] — Reihenfolge = Lebenszyklus +ROWS = [ + ["DESIGN", [["A1", false], ["A2", true], ["A3", false], ["A4", false]]], + ["TRANSITION", [["A5", false], ["A6", false], ["A7", false], ["A8", false]]], + ["OPERATION", [["A9", false]]], + ["SUPPORT", [["A10", false], ["A11", true], ["A12", false], ["A13", true]]], + ["REVIEW", [["A14", false], ["A15", false]]] +]; +cols_max = 4; +n_rows = len(ROWS); + +// abgeleitete Maße +content_w = cols_max * slot_w + (cols_max - 1) * gap_x; // 145 +plate_w = content_w + 2 * margin; // 161 +block_h = header_h + slot_d; // 30 +content_h = n_rows * block_h + (n_rows - 1) * row_gap; // 182 +plate_h = content_h + 2 * margin; // 198 + +// Platz von Ecke (0,0) aus; y oben = plate_h +function blockTopY(r) = plate_h - margin - r * (block_h + row_gap); +function slotCenterY(r) = blockTopY(r) - header_h - slot_d/2; +function slotCenterX(c) = margin + slot_w/2 + c * (slot_w + gap_x); + +module rrect(l, w, h, r) { + linear_extrude(h) offset(r) offset(-r) square([l, w], center = true); +} + +module pocket(cx, cy, label, live) { + // Vertiefung + translate([cx, cy, plate_thick - pocket_dep]) + rrect(slot_w, slot_d, pocket_dep + 0.1, pocket_r); + // Greifkerbe an der Unterkante + translate([cx, cy - slot_d/2, plate_thick - pocket_dep]) + cylinder(r = finger_r, h = pocket_dep + 0.1); + // A-Nummer im Boden (sichtbar bei leerem Slot) + translate([cx, cy, plate_thick - pocket_dep - num_depth]) + linear_extrude(num_depth + 0.1) + text(label, size = num_size, halign = "center", valign = "center"); + // "lebendes" Artefakt: zusaetzlicher Rahmen als Hinweis (stapeln/aktualisieren) + if (live) + translate([cx, cy, plate_thick - pocket_dep - lf_depth]) + linear_extrude(lf_depth + 0.1) + difference() { + square([slot_w - 2*lf_inset, slot_d - 2*lf_inset], center = true); + square([slot_w - 2*lf_inset - 2*lf_w, slot_d - 2*lf_inset - 2*lf_w], center = true); + } +} + +module tray() { + difference() { + translate([plate_w/2, plate_h/2, 0]) rrect(plate_w, plate_h, plate_thick, corner_r); + for (r = [0 : n_rows - 1]) { + hdr = ROWS[r][0]; + slots = ROWS[r][1]; + // Phasen-Kopfzeile (links ueber der Reihe) + translate([margin + 1, blockTopY(r) - header_h/2, plate_thick - hdr_depth]) + linear_extrude(hdr_depth + 0.1) + text(hdr, size = hdr_size, halign = "left", valign = "center"); + for (c = [0 : len(slots) - 1]) + pocket(slotCenterX(c), slotCenterY(r), slots[c][0], slots[c][1]); + } + } +} + +tray(); + +echo(plate_w = plate_w, plate_h = plate_h, plate_thick = plate_thick, slots = 15); diff --git a/04_Tablet-Quiz/app/index.html b/04_Tablet-Quiz/app/index.html index 67c6313..3ba0b80 100644 --- a/04_Tablet-Quiz/app/index.html +++ b/04_Tablet-Quiz/app/index.html @@ -68,6 +68,31 @@ body.navOpen aside { transform: translateX(0); } .navBackdrop { position:fixed; inset:0; background:rgba(15,22,35,.42); z-index:29; opacity:0; pointer-events:none; transition:opacity .2s; } body.navOpen .navBackdrop { opacity:1; pointer-events:auto; } + body.akteOpen .navBackdrop { opacity:1; pointer-events:auto; } + /* Service-Akte als Overlay von rechts (Header-Button 📁 Akte) */ + #akteList { + position: fixed; top:0; right:0; bottom:0; width: 340px; max-width: 88vw; + background: var(--panel); border-left:1px solid var(--line); padding: 14px; + overflow:auto; z-index: 30; + transform: translateX(100%); transition: transform .22s ease; + box-shadow: 0 0 40px rgba(20,30,50,.18); + } + body.akteOpen #akteList { transform: translateX(0); } + body:not(.runMode) #akteList { display:none; } + body:not(.runMode) #akteBtn { display:none; } + #akteList h3 { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:14px 0 4px; } + .akteCount { font-size:13px; color:var(--muted); margin:2px 0 6px; font-variant-numeric:tabular-nums; } + .akteItem { display:flex; align-items:center; gap:10px; padding:7px 4px; border-radius:8px; font-size:13px; opacity:.5; } + .akteItem.have { opacity:1; } + .akteItem .aId { color:#fff; font-size:11px; font-weight:700; border-radius:6px; padding:2px 7px; min-width:30px; text-align:center; flex:none; } + .akteItem .aNm { flex:1; } + .akteItem .aNm i { color:var(--muted); font-style:italic; } + .akteItem .aChk { color:var(--ok); font-weight:700; } + /* Gate: Artefakt-Anforderung (harte Kopplung) */ + .gateReq { border-radius:8px; padding:9px 12px; margin:8px 0 12px; font-size:14px; font-weight:600; line-height:1.4; } + .gateReq.ok { background:#f1faf4; color:#177a44; border:1px solid #cde6d6; } + .gateReq.bad { background:#fdf3f3; color:#b5202a; border:1px solid #f0c9c9; } + .choice[disabled] { opacity:.45; cursor:default; } .navTop { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } .navTop b { font-size:13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); } .navTop button { border:none; background:none; font-size:20px; line-height:1; color:var(--muted); cursor:pointer; padding:4px 8px; } @@ -244,6 +269,7 @@ v0.6
+ @@ -252,6 +278,7 @@
+
@@ -881,6 +908,54 @@ const STATIONEN = [ ]} ]; +/* ====================== SERVICE-AKTE (Artefakte A1-A15, App-gefuehrt) ====================== + Die Akte ist rein digital: erzeugte Artefakte werden per Choice bestimmt und + gesammelt; Gates sind hart gekoppelt (oeffnen nur mit den geforderten Artefakten). */ +const ARTEFAKTE = { + A1:{name:"Projektauftrag", phase:"design"}, + A2:{name:"Service-Definition", phase:"design", live:true}, + A3:{name:"Service Design Document", phase:"design"}, + A4:{name:"Implementation Blueprint", phase:"design"}, + A5:{name:"Gate-/SOR-Vorlage", phase:"transition"}, + A6:{name:"Betriebsdokumentation", phase:"transition"}, + A7:{name:"Test-Report", phase:"transition"}, + A8:{name:"Aktivierter Service", phase:"transition"}, + A9:{name:"Service-Qualitätsbericht", phase:"operation"}, + A10:{name:"Incident Record", phase:"support"}, + A11:{name:"Problem Record", phase:"support", live:true}, + A12:{name:"Workaround", phase:"support"}, + A13:{name:"Wissensdatenbank-Eintrag", phase:"support", live:true}, + A14:{name:"Service-Review-Bericht", phase:"review"}, + A15:{name:"DPM-Rücklauf", phase:"review"} +}; +// Welche Station erzeugt welches A-Artefakt (Choice-Schritt -> Aufnahme in die Akte). +const STATION_ARTEFAKT = { + ds_01:"A2", ds_02:"A3", ds_03:"A4", + tr_06:"A6", tr_07:"A7", tr_08:"A5", + op_06:"A9", + sp_02:"A13", sp_07:"A10", sp_09:"A11", sp_11:"A12", + rv_02:"A14", rv_05:"A15" +}; +// Gate erzeugt Artefakt (beim Vorwaerts-Durchschreiten). +const GATE_PRODUCES = { tr_12:"A8" }; +// Geforderte Artefakte je Gate (HARTE Kopplung). +const GATE_REQ = { tr_01:["A2","A3","A4"], tr_09:["A6","A7"], tr_12:["A6","A7","A2"] }; + +function addArtefakt(a){ if(a){ S.akte = S.akte || {}; S.akte[a] = true; } } +// Beim Start nach Einstiegspunkt vorbefuellen: alles, was VOR der Einstiegs-Station +// (bzw. vor durchschrittenen Gates) entsteht, "liegt schon vor". +function seedAkte(entryIdx){ + S.akte = { A1:true }; + for(const sid in STATION_ARTEFAKT){ + const j = STATIONEN.findIndex(s=>s.id===sid); + if(j>=0 && j < entryIdx) addArtefakt(STATION_ARTEFAKT[sid]); + } + for(const gid in GATE_PRODUCES){ + const j = STATIONEN.findIndex(s=>s.id===gid); + if(j>=0 && j < entryIdx) addArtefakt(GATE_PRODUCES[gid]); + } +} + /* ====================== STATE ====================== */ const LS_KEY = "slc-companion-proto"; function defaultState(){ @@ -888,8 +963,8 @@ function defaultState(){ classifyDone:false, classifyWrong:null, entryDone:false, entryWrong:null, index:0, stage:"discuss", quizIndex:0, - actStep:0, actReveal:false, actDone:false, - picks:{}, done:{}, + actStep:0, actReveal:false, actDone:false, arteWrong:null, + picks:{}, done:{}, akte:{}, loopback:null, revisit:{}, endReason:null, endGate:null }; } let S = load(); @@ -939,10 +1014,35 @@ function renderList(){ $("#progressBar").style.width = pct+"%"; } +/* ====================== RENDER: SERVICE-AKTE (Overlay) ====================== */ +function renderAkte(){ + const order = ["design","transition","operation","support","review"]; + const ids = Object.keys(ARTEFAKTE); + const have = S.akte || {}; + const n = ids.filter(a=>have[a]).length; + let html = ``; + html += `
${n}/15 Artefakten gesammelt
`; + for(const ph of order){ + const group = ids.filter(a => ARTEFAKTE[a].phase === ph); + if(!group.length) continue; + html += `

${PHASEN[ph].label}

`; + group.forEach(a=>{ + const ok = !!have[a]; + html += `
+ ${a} + ${ARTEFAKTE[a].name}${ARTEFAKTE[a].live?' · lebend':''} + ${ok?'✓':'○'} +
`; + }); + } + $("#akteList").innerHTML = html; + const c = $("#akteClose"); if(c) c.onclick = ()=> document.body.classList.remove("akteOpen"); +} + /* ====================== VIEW DISPATCH ====================== */ function draw(){ document.body.classList.toggle("runMode", S.view==="run"); - if(S.view!=="run") document.body.classList.remove("navOpen"); + if(S.view!=="run"){ document.body.classList.remove("navOpen"); document.body.classList.remove("akteOpen"); } renderCardBadge(); if(S.view==="deck") return renderDeck(); if(S.view==="classify") return renderClassify(); @@ -1084,7 +1184,7 @@ function renderEntry(){ `; $("#backClassify").onclick=()=>{ S.view="classify"; save(); draw(); }; - $("#startRun").onclick=()=>{ enterStation(recIndex); S.view="run"; save(); draw(); }; + $("#startRun").onclick=()=>{ seedAkte(recIndex); enterStation(recIndex); S.view="run"; save(); draw(); }; } } @@ -1104,12 +1204,13 @@ function enterStation(idx){ S.index = idx; S.stage = STATIONEN[idx].typ==="gate" ? "gate" : "act"; S.gatePick = null; S.quizIndex = 0; - S.actStep = 0; S.actReveal = false; S.actDone = false; + S.actStep = 0; S.actReveal = false; S.actDone = false; S.arteWrong = null; } function gateGoto(st, i){ S.done[st.id] = true; const t = (GATE_FLOW[st.id] || [])[i] || "next"; if(t==="end"){ S.view="end"; S.endReason="rejected"; S.endGate=st.id; save(); draw(); return; } + if(t==="next" && GATE_PRODUCES[st.id]) addArtefakt(GATE_PRODUCES[st.id]); // z. B. Gate 3 Go-Live -> A8 if(t==="next"){ enterStation(S.index+1); } else { const j = STATIONEN.findIndex(s=>s.id===t); @@ -1148,6 +1249,7 @@ function raciLegendHtml(){ function renderRun(){ renderList(); + renderAkte(); const st = cur(); const ph = PHASEN[st.phase]; const chip = st.typ==="gate" @@ -1188,11 +1290,21 @@ function activitySteps(st){ frage:`Klärt die RACI: Wer ist R, A, C, I? Sortiert die Figuren ins Aktiv-Feld (R·A·C·I).`, legend: raciLegendHtml(), auf:`

RACI

${raciTable(st)}` }, - { label:"Artefakt", + { label:"Artefakt", artefakt:true, frage:`Welche Artefaktkarte entsteht hier und gehört in die Service-Akte?`, auf:`

Artefakt

${st.artefakt}

` } ]; } +// Antwort-Optionen fuer die Artefakt-Choice: richtiges A + 3 Distraktoren +// (bevorzugt aus derselben Phase), deterministisch nach A-Nummer sortiert. +function arteOptions(correct){ + const ids = Object.keys(ARTEFAKTE); + const ph = ARTEFAKTE[correct].phase; + const same = ids.filter(a => a!==correct && ARTEFAKTE[a].phase===ph); + const other = ids.filter(a => a!==correct && ARTEFAKTE[a].phase!==ph); + const opts = [correct].concat(same.concat(other).slice(0,3)); + return opts.sort((a,b)=> a.localeCompare(b, "en", {numeric:true})); +} function renderActivity(st){ const phaseColor = PHASEN[st.phase].color; const next = STATIONEN[S.index+1]; @@ -1224,6 +1336,8 @@ function renderActivity(st){ const i = Math.min(S.actStep||0, steps.length-1); const step = steps[i]; const isLast = i === steps.length-1; + const arteId = STATION_ARTEFAKT[st.id]; // A-Nummer, falls diese Station eine erzeugt + const isArteChoice = step.artefakt && arteId; // Artefakt-Schritt mit echter Choice let html = `
Schritt ${i+1}/${steps.length} · ${step.label}
@@ -1231,14 +1345,27 @@ function renderActivity(st){ ${step.frage}
`; if(step.legend) html += step.legend; - if(S.actReveal) html += `
${step.auf}
`; + + if(isArteChoice && !S.actReveal){ + // Auswahl: welches Artefakt entsteht? (richtiges A + Distraktoren) + const opts = arteOptions(arteId).map(a => + ``).join(""); + html += `
${opts}
`; + if(S.arteWrong) html += `
Nicht ganz — überlegt nochmal, welches Ergebnis diese Station liefert.
`; + } else if(S.actReveal){ + if(isArteChoice) + html += `

Artefakt

${arteId} — ${ARTEFAKTE[arteId].name} in die Service-Akte gelegt.

`; + else + html += `
${step.auf}
`; + } let actions = `
`; if(S.actReveal || i>0) actions += ``; actions += `
`; - if(!S.actReveal) actions += ``; - else if(!isLast) actions += ``; - else actions += ``; + if(isArteChoice && !S.actReveal){ /* Antwort per Klick auf eine Option — kein Auflösen-Button */ } + else if(!S.actReveal) actions += ``; + else if(!isLast) actions += ``; + else actions += ``; actions += `
`; return html + actions; } @@ -1247,13 +1374,21 @@ function renderActivity(st){ function renderGate(st){ const keeper = (st.raci.find(([r,c])=>c==="A")||[])[0]; const pruef = (st.pruef||[]).map(([n,d])=>`
  • ${n} — ${d}
  • `).join(""); + // Harte Artefakt-Kopplung: Gate "oeffnet" nur mit den geforderten Artefakten in der Akte. + const req = GATE_REQ[st.id] || []; + const missing = req.filter(a => !(S.akte && S.akte[a])); + const blocked = missing.length > 0; const opts = (st.pfade||[]).map(([n,d],i)=> - ``).join(""); + ``).join(""); + const reqLine = req.length === 0 ? `` : blocked + ? `
    🔒 Gate öffnet nicht — es fehlt in der Akte: ${missing.map(a=>a+" "+ARTEFAKTE[a].name).join(" · ")}. Erzeugt diese Artefakte zuerst.
    ` + : `
    ✓ Alle geforderten Artefakte liegen in der Akte (${req.join(" · ")}).
    `; const revisitNote = (S.revisit && S.revisit[st.id]) ? `
    ↩ Erneute Vorlage nach Nacharbeit — prüft erneut und entscheidet neu.
    ` : ``; return ` ${revisitNote}

    Entscheidet: ${roleLabel(keeper)}

    + ${reqLine}
    ${opts}
    Worum geht's & Prüf-Kriterien @@ -1306,6 +1441,13 @@ function wire(st){ if(b("actReveal")) b("actReveal").onclick = ()=>{ S.actReveal=true; save(); draw(); }; if(b("actNext")) b("actNext").onclick = ()=>{ S.actStep=(S.actStep||0)+1; S.actReveal=false; save(); draw(); }; if(b("actToDone")) b("actToDone").onclick = ()=>{ S.actDone=true; save(); draw(); }; + // Artefakt-Choice + $("#panel").querySelectorAll(".arteChoice[data-a]").forEach(el=>{ + el.onclick = ()=>{ const a = el.dataset.a; + if(a === STATION_ARTEFAKT[st.id]){ S.arteWrong=null; addArtefakt(a); S.actReveal=true; } + else { S.arteWrong = a; } + save(); draw(); }; + }); if(b("actBack")) b("actBack").onclick = ()=>{ if(S.actDone){ S.actDone=false; } else if(S.actReveal){ S.actReveal=false; } @@ -1349,8 +1491,9 @@ function renderEnd(){ /* ====================== INIT ====================== */ (function init(){ $("#resetBtn").onclick = ()=>{ if(confirm("Neue Action Card ziehen und Durchlauf zurücksetzen?")){ S=defaultState(); save(); draw(); } }; - $("#stationsBtn").onclick = ()=> document.body.classList.toggle("navOpen"); - $("#navBackdrop").onclick = ()=> document.body.classList.remove("navOpen"); + $("#stationsBtn").onclick = ()=>{ document.body.classList.remove("akteOpen"); document.body.classList.toggle("navOpen"); }; + $("#akteBtn").onclick = ()=>{ document.body.classList.remove("navOpen"); document.body.classList.toggle("akteOpen"); }; + $("#navBackdrop").onclick = ()=>{ document.body.classList.remove("navOpen"); document.body.classList.remove("akteOpen"); }; draw(); })(); diff --git a/04_Tablet-Quiz/app/sw.js b/04_Tablet-Quiz/app/sw.js index 199f025..a62b9de 100644 --- a/04_Tablet-Quiz/app/sw.js +++ b/04_Tablet-Quiz/app/sw.js @@ -1,5 +1,5 @@ /* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */ -const CACHE = "slc-companion-v8"; +const CACHE = "slc-companion-v9"; const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"]; // Action-Card-Grafiken (cards/s-c.png) fuer Offline vorab cachen (alle 30). const CARDS = [];