This commit is contained in:
@@ -7,6 +7,9 @@ Versionierung [semantisch](https://semver.org/lang/de/).
|
||||
## [Unreleased] – 2026-07-13 · BugHunter, NetWorker & echter Lernfortschritt
|
||||
|
||||
### ServiceClash
|
||||
- **Auswertung repariert:** Nach dem letzten Ticket wurde die Abschlussansicht nicht
|
||||
geöffnet, weil der Renderer noch auf ein nicht vorhandenes Folgeticket zugriff. Die
|
||||
Abschlussprüfung erfolgt nun vor jeder Ticketzustands-Abfrage.
|
||||
- **Letztes geplantes Spiel ausgeliefert:** drei Service-Desk-Schichten mit insgesamt
|
||||
**24 kuratierten Tickets** aus Incident-, Request-, Change-, Monitoring-, Backup- und
|
||||
Security-Szenarien. Jede Aufgabe führt verständlich durch Art, Priorität und passende
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(()=>{'use strict';const view=document.getElementById('scView');let catalog=[],session=null,index=0,started=0,states={};const esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const api=(url,opt={})=>fetch('/api/serviceclash'+url,{headers:{'Content-Type':'application/json'},...opt}).then(async r=>{const data=await r.json();if(!r.ok)throw new Error(data.message||'Serverfehler');return data});const post=(url,data)=>api(url,{method:'POST',body:JSON.stringify(data)});const seconds=()=>Math.max(1,Math.round((Date.now()-started)/1000));const current=()=>session.tickets[index];
|
||||
function renderCatalog(){view.innerHTML=`<div class="sc-eyebrow">Service Desk Simulation</div><h1 class="sc-title">Welche Schicht übernimmst du?</h1><p class="sc-intro">ServiceClash erklärt die Triage direkt im Spiel: Ordne jedes Ticket zuerst als <b>Störung</b> oder <b>Anfrage</b> ein, schätze seine Dringlichkeit ein und wähle dann die beste erste Maßnahme. Du darfst Fehler machen – bei Bedarf zeigt dir „Aufdecken“ die Begründung.</p><div class="sc-shifts">${catalog.map(s=>`<article class="sc-card"><div class="sc-eyebrow">${esc(s.difficulty)} · ${esc(s.examScope)}</div><h2>${esc(s.title)}</h2><p>${esc(s.description)}</p><div class="sc-meta"><span>${s.ticketCount} Tickets</span><span>${s.waves} Wellen</span></div><button class="sc-btn primary" data-shift="${esc(s.slug)}">SCHICHT BRIEFEN</button></article>`).join('')}</div>`;view.querySelectorAll('[data-shift]').forEach(b=>b.onclick=()=>brief(b.dataset.shift));}
|
||||
async function brief(slug){try{session=await api('/session/'+encodeURIComponent(slug));index=0;states={};view.innerHTML=`<article class="sc-panel sc-brief"><div class="sc-eyebrow">${esc(session.difficulty)} · ${session.tickets.length} Tickets</div><h1>${esc(session.title)}</h1><p class="sc-intro">${esc(session.description)}</p><p class="sc-tip"><b>So spielst du:</b> Eine <b>Störung</b> bedeutet, dass etwas bereits nicht funktioniert. Eine <b>Anfrage</b> wünscht eine geplante Leistung. Priorität <b>hoch</b> wählst du bei größerer Auswirkung, Sicherheitsrisiko oder Zeitkritik. Die Punkte berücksichtigen alle drei Entscheidungen.</p><button id="scStart" class="sc-btn primary">LEITSTAND ÖFFNEN →</button></article>`;document.getElementById('scStart').onclick=()=>{started=Date.now();renderRun()}}catch(error){fail(error)}}
|
||||
function ticketState(ticket){return states[ticket.ticketId]||{step:'classify',done:[]}}function renderRun(){const ticket=current(),state=ticketState(ticket),done=Object.values(states).filter(s=>s.done?.length===3).length;const wave=Math.max(...session.tickets.map(t=>t.wave));if(!ticket){finish();return}const choices=choiceMarkup(ticket,state);view.innerHTML=`<div class="sc-command"><aside class="sc-panel sc-queue"><div class="sc-eyebrow">${esc(session.title)}</div><div class="sc-stat">Gelöst ${done}/${session.tickets.length}</div><div class="sc-meter"><i style="width:${done/session.tickets.length*100}%"></i></div><div class="sc-ticket-list">${session.tickets.map((t,i)=>`<button class="${i===index?'active':''} ${ticketState(t).done?.length===3?'done':''}" data-ticket="${i}">W${t.wave} · ${esc(t.title)}</button>`).join('')}</div></aside><article class="sc-panel sc-ticket"><div class="sc-ticket-head"><span>WELLE ${ticket.wave}/${wave} · ${esc(ticket.requester)}</span><span>${ticket.points} PTS</span></div><h1>${esc(ticket.title)}</h1><p>${esc(ticket.text)}</p><div class="sc-steps"><span class="sc-step ${state.step==='classify'?'active':''} ${state.done.includes('classify')?'done':''}">1 ART</span><span class="sc-step ${state.step==='priority'?'active':''} ${state.done.includes('priority')?'done':''}">2 PRIORITÄT</span><span class="sc-step ${state.step==='solve'?'active':''} ${state.done.includes('solve')?'done':''}">3 MASSNAHME</span></div>${choices}${state.feedback?`<div class="sc-feedback ${state.bad?'bad':''}">${state.feedback}</div>`:''}${state.done.length===3?`<div class="sc-debrief"><b>Debrief:</b> ${esc(state.explanation||'Ticket dokumentiert.')}<br><button id="scNext" class="sc-btn primary">${index+1===session.tickets.length?'SCHICHT AUSWERTEN':'NÄCHSTES TICKET →'}</button></div>`:''}</article></div>`;view.querySelectorAll('[data-ticket]').forEach(b=>b.onclick=()=>{const target=Number(b.dataset.ticket);if(target<=index||ticketState(session.tickets[target]).done?.length===3){index=target;renderRun()}});view.querySelectorAll('[data-value]').forEach(b=>b.onclick=()=>check(state.step,b.dataset.value));const reveal=document.getElementById('scReveal');if(reveal)reveal.onclick=()=>check(state.step,'',true);const next=document.getElementById('scNext');if(next)next.onclick=()=>{index++;renderRun()}}
|
||||
function ticketState(ticket){return states[ticket.ticketId]||{step:'classify',done:[]}}function renderRun(){const ticket=current();if(!ticket){finish();return}const state=ticketState(ticket),done=Object.values(states).filter(s=>s.done?.length===3).length;const wave=Math.max(...session.tickets.map(t=>t.wave));const choices=choiceMarkup(ticket,state);view.innerHTML=`<div class="sc-command"><aside class="sc-panel sc-queue"><div class="sc-eyebrow">${esc(session.title)}</div><div class="sc-stat">Gelöst ${done}/${session.tickets.length}</div><div class="sc-meter"><i style="width:${done/session.tickets.length*100}%"></i></div><div class="sc-ticket-list">${session.tickets.map((t,i)=>`<button class="${i===index?'active':''} ${ticketState(t).done?.length===3?'done':''}" data-ticket="${i}">W${t.wave} · ${esc(t.title)}</button>`).join('')}</div></aside><article class="sc-panel sc-ticket"><div class="sc-ticket-head"><span>WELLE ${ticket.wave}/${wave} · ${esc(ticket.requester)}</span><span>${ticket.points} PTS</span></div><h1>${esc(ticket.title)}</h1><p>${esc(ticket.text)}</p><div class="sc-steps"><span class="sc-step ${state.step==='classify'?'active':''} ${state.done.includes('classify')?'done':''}">1 ART</span><span class="sc-step ${state.step==='priority'?'active':''} ${state.done.includes('priority')?'done':''}">2 PRIORITÄT</span><span class="sc-step ${state.step==='solve'?'active':''} ${state.done.includes('solve')?'done':''}">3 MASSNAHME</span></div>${choices}${state.feedback?`<div class="sc-feedback ${state.bad?'bad':''}">${state.feedback}</div>`:''}${state.done.length===3?`<div class="sc-debrief"><b>Debrief:</b> ${esc(state.explanation||'Ticket dokumentiert.')}<br><button id="scNext" class="sc-btn primary">${index+1===session.tickets.length?'SCHICHT AUSWERTEN':'NÄCHSTES TICKET →'}</button></div>`:''}</article></div>`;view.querySelectorAll('[data-ticket]').forEach(b=>b.onclick=()=>{const target=Number(b.dataset.ticket);if(target<=index||ticketState(session.tickets[target]).done?.length===3){index=target;renderRun()}});view.querySelectorAll('[data-value]').forEach(b=>b.onclick=()=>check(state.step,b.dataset.value));const reveal=document.getElementById('scReveal');if(reveal)reveal.onclick=()=>check(state.step,'',true);const next=document.getElementById('scNext');if(next)next.onclick=()=>{index++;renderRun()}}
|
||||
function choiceMarkup(ticket,state){if(state.done.length===3)return '';if(state.step==='classify')return `<div class="sc-choices"><button class="sc-choice" data-value="incident"><b>Störung</b><br><small>Etwas funktioniert bereits nicht.</small></button><button class="sc-choice" data-value="request"><b>Anfrage</b><br><small>Eine Leistung soll geplant erbracht werden.</small></button></div>${revealButton()}`;if(state.step==='priority')return `<div class="sc-choices"><button class="sc-choice" data-value="high"><b>Hoch</b><br><small>Sicherheits-, Geschäfts- oder Zeitrisiko.</small></button><button class="sc-choice" data-value="medium"><b>Mittel</b><br><small>Spürbar, aber mit begrenzter Auswirkung.</small></button><button class="sc-choice" data-value="low"><b>Niedrig</b><br><small>Planbar, lokal oder ohne akute Auswirkung.</small></button></div>${revealButton()}`;return `<div class="sc-choices">${ticket.options.map(o=>`<button class="sc-choice" data-value="${esc(o.id)}">${esc(o.text)}</button>`).join('')}</div>${revealButton()}`}
|
||||
const revealButton=()=>`<button id="scReveal" class="sc-reveal">Antwort aufdecken (keine Punkte für diesen Schritt)</button>`;
|
||||
async function check(step,value,reveal=false){const ticket=current();try{const result=await post('/check',{ticketId:ticket.ticketId,step,value,reveal});const state=ticketState(ticket);states[ticket.ticketId]=state;if(result.resolved){state.done=[...state.done,step];state.step=step==='classify'?'priority':step==='priority'?'solve':'done'}state.bad=!result.correct;state.feedback=result.correct?'Richtig eingeordnet.':`${reveal?'Aufgedeckt':'Noch nicht passend.'} ${result.answer?`Richtig wäre: <b>${esc(label(result.answer,ticket))}</b>.`:''}`;if(result.ticketDone){state.explanation=result.explanation;state.feedback='Ticket abgeschlossen. Prüfe kurz das Debrief und entscheide dann selbst, wann du weitergehst.'}renderRun()}catch(error){fail(error)}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ServiceClash · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/serviceclash.css?v=1"><script src="/js/theme.js"></script></head><body><main class="sc-app"><header class="sc-top"><div class="sc-nav"><a href="/dashboard">← Dashboard</a><a href="/serviceclash" class="sc-nav-button">SCHICHTEN</a></div><div class="sc-brand"><b>ServiceClash</b><span>LEITSTAND · TRIAGE</span></div><span class="mf-a11y-host"></span></header><section id="scView" class="sc-view"><p class="sc-loading">LEITSTAND WIRD HOCHGEFAHREN …</p></section></main><script src="/js/serviceclash.js?v=1"></script></body></html>
|
||||
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ServiceClash · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/serviceclash.css?v=1"><script src="/js/theme.js"></script></head><body><main class="sc-app"><header class="sc-top"><div class="sc-nav"><a href="/dashboard">← Dashboard</a><a href="/serviceclash" class="sc-nav-button">SCHICHTEN</a></div><div class="sc-brand"><b>ServiceClash</b><span>LEITSTAND · TRIAGE</span></div><span class="mf-a11y-host"></span></header><section id="scView" class="sc-view"><p class="sc-loading">LEITSTAND WIRD HOCHGEFAHREN …</p></section></main><script src="/js/serviceclash.js?v=2"></script></body></html>
|
||||
|
||||
Reference in New Issue
Block a user