const $=(q,root=document)=>root.querySelector(q);const $$=(q,root=document)=>[...root.querySelectorAll(q)]; let lastTraceId='';let state={tasks:[],events:[],reminders:[],groceries:[],recipes:[],mealPlans:[],kanban:[],documents:[],chat:[],activity:[],system:{}}; let bootstrappedChat=false,pttRecorder=null,pttStream=null,pttChunks=[],pttContext='',wakeEnabled=false,wakeStream=null,wakeToken=0,wakeArmedUntil=0,wakeBusy=false,speaking=false,commandBusy=false,toastTimer,dialogType='',dialogEditing=null; const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); const localDateKey=d=>`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const parseDate=v=>{if(!v)return null;if(/^\d{4}-\d{2}-\d{2}$/.test(v)){const[a,b,c]=v.split('-').map(Number);return new Date(a,b-1,c)}const d=new Date(v);return Number.isNaN(d.getTime())?null:d}; const fmtTime=iso=>{const d=parseDate(iso);if(!d)return iso||'—';return new Intl.DateTimeFormat('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}).format(d)}; const fmtClock=iso=>{const d=parseDate(iso);if(!d)return'—';return /^\d{4}-\d{2}-\d{2}$/.test(iso)?'GANZTÄGIG':d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}; const fmtDay=d=>d.toLocaleDateString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit'}); const mealName=m=>({breakfast:'FRÜH',lunch:'MITTAG',dinner:'ABEND',snack:'SNACK'}[m]||String(m||'ABEND').toUpperCase()); const taskOrigin=t=>t.source==='jarvis-command'?'JARVIS':t.aiManaged?'AI SCOUT':'MANUAL'; const messageHTML=text=>esc(text).replace(/\*\*(.+?)\*\*/g,'$1').replace(/`([^`]+)`/g,'$1').replace(/\n/g,'
'); function voiceSetupMessage(voice={},kind='stt'){if(kind==='stt'){if(!voice.ffmpegReady)return 'FFmpeg fehlt im Container';if(voice.whisperBinReady===false)return `Whisper-Binary fehlt: ${voice.whisperBin||'nicht gefunden'}`;if(voice.whisperModelReady===false)return `Whisper-Modell fehlt: ${voice.whisperModel||'/models/ggml-small.bin'}`;return voice.whisperError||voice.whisperModelError||'Lokales Whisper ist noch nicht konfiguriert'}if(voice.piperBinReady===false)return `Piper-Binary fehlt: ${voice.piperBin||'/tools/piper'}`;if(voice.piperModelReady===false)return `Piper-Modell fehlt: ${voice.piperModel||'/models/de_DE-thorsten-medium.onnx'}`;return voice.piperError||voice.piperModelError||'Lokales Piper ist noch nicht konfiguriert'} function sttStatusLabel(voice={}){return voice.sttReady?'LOCAL':(!voice.whisperBinReady?'BIN':(!voice.whisperModelReady?'MODEL':(!voice.ffmpegReady?'FFMPEG':'SETUP')))} function ttsStatusLabel(voice={}){return voice.ttsReady?'LOCAL':(!voice.piperBinReady?'BIN':(!voice.piperModelReady?'MODEL':'SETUP'))} function safeRender(name,fn){try{fn()}catch(e){console.error(`[JARVIS UI] ${name} render failed`,e)}} const THEME_KEY='jarvis-ui-theme',VIEW_KEY='jarvis-clean-view'; const cleanViews={ home:{eyebrow:'Übersicht',title:'Home',subtitle:'Dein Zuhause, Termine und Aufgaben auf einen Blick.'}, calendar:{eyebrow:'Planung',title:'Kalender',subtitle:'Termine und Erinnerungen in einer ruhigen Zeitansicht.'}, tasks:{eyebrow:'Fokus',title:'Aufgaben',subtitle:'Offene Punkte priorisieren, abhaken und per Sprache verwalten.'}, kanban:{eyebrow:'Workflow',title:'Kanban',subtitle:'Arbeit sichtbar machen und den KI-Organizer gezielt einsetzen.'}, food:{eyebrow:'Haushalt',title:'Essen & Einkauf',subtitle:'Essensplan, Einkaufsliste und Rezepte an einem Ort.'}, knowledge:{eyebrow:'System',title:'Wissen & System',subtitle:'Dokumentwissen, Sprache, Modelle und Aktivität des lokalen Systems.'} }; function applyCleanView(view,persist=true){const v=cleanViews[view]?view:'home';document.body.dataset.view=v;$$('[data-clean-view]').forEach(btn=>btn.classList.toggle('active',btn.dataset.cleanView===v));const m=cleanViews[v];if($('#cleanViewEyebrow'))$('#cleanViewEyebrow').textContent=m.eyebrow;if($('#cleanViewTitle'))$('#cleanViewTitle').textContent=m.title;if($('#cleanViewSubtitle'))$('#cleanViewSubtitle').textContent=m.subtitle;if(persist)try{localStorage.setItem(VIEW_KEY,v)}catch(_){}window.scrollTo({top:0,behavior:'smooth'});} function applyTheme(theme,persist=false){const t=theme==='apple'?'apple':'jarvis';document.body.dataset.theme=t;document.documentElement.style.colorScheme=t==='apple'?'light':'dark';const meta=document.querySelector('meta[name="theme-color"]');if(meta)meta.setAttribute('content',t==='apple'?'#f4f5f7':'#02060a');$$('.theme-option').forEach(btn=>btn.classList.toggle('active',btn.dataset.theme===t));if(t==='apple'&&!document.body.dataset.view){let view='home';try{view=localStorage.getItem(VIEW_KEY)||view}catch(_){}applyCleanView(view,false)}if(persist)try{localStorage.setItem(THEME_KEY,t)}catch(_){} } function initTheme(){let theme='jarvis',view='home';try{theme=localStorage.getItem(THEME_KEY)||theme;view=localStorage.getItem(VIEW_KEY)||view}catch(_){}document.body.dataset.view=cleanViews[view]?view:'home';applyTheme(theme,false);applyCleanView(document.body.dataset.view,false);$$('.theme-option').forEach(btn=>btn.onclick=()=>applyTheme(btn.dataset.theme,true));$$('[data-clean-view]').forEach(btn=>btn.onclick=()=>applyCleanView(btn.dataset.cleanView,true));$$('[data-clean-jump]').forEach(btn=>btn.onclick=()=>applyCleanView(btn.dataset.cleanJump,true));if($('#cleanOpenHome'))$('#cleanOpenHome').onclick=()=>applyCleanView('home',true);if($('#cleanQuickMic'))$('#cleanQuickMic').onclick=()=>startPTT('');} async function api(url,options={}){const res=await fetch(url,options);if(!res.ok){throw new Error((await res.text()).trim()||`HTTP ${res.status}`)}const type=res.headers.get('content-type')||'';return type.includes('application/json')?res.json():res} function toast(msg){const el=$('#toast');el.textContent=msg;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('show'),2800)} function tickClock(){const n=new Date();$('#clock').textContent=n.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit',second:'2-digit'});$('#date').textContent=n.toLocaleDateString('de-DE',{weekday:'short',day:'2-digit',month:'short',year:'numeric'}).toUpperCase();if($('#cleanTopDate'))$('#cleanTopDate').textContent=n.toLocaleDateString('de-DE',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}setInterval(tickClock,1000);tickClock(); function setChip(id,ok,label){const dot=$(id+'Dot');if(!dot)return;const chip=dot.closest('.status-chip');chip.classList.toggle('online',!!ok);chip.classList.toggle('offline',ok===false);$(id+'Status').textContent=label} async function refresh(){try{state=await api('/api/state');renderState();setChip('#core',true,'ONLINE')}catch(e){setChip('#core',false,'OFFLINE')}} function renderSystemStatus(){ const sys=state.system||{},voice=sys.voice||{},ollama=sys.ollama||{},chunks=Number(sys.ragChunks||0); const chatReady=ollama.chatReady!==false&&!!(ollama.model||sys.model),embedReady=ollama.embeddingReady!==false&&!!(ollama.embeddingModel||sys.embeddingModel); setChip('#core',ollama.reachable===false?false:chatReady,chatReady?'LOCAL':(ollama.reachable===false?'OLLAMA OFF':'NO MODEL')); const coreStatus=$('#coreStatus');if(coreStatus)coreStatus.title=chatReady?`ACTIVE MODEL: ${ollama.model||sys.model}`:(ollama.lastError||'Kein Chat-Modell erkannt'); setChip('#rag',embedReady?(chunks>0?true:null):false,embedReady?(chunks?`${chunks} CHUNKS`:'EMPTY'):'NO EMBED'); const sttLabel=sttStatusLabel(voice),ttsLabel=ttsStatusLabel(voice); setChip('#stt',!!voice.sttReady,sttLabel);setChip('#tts',!!voice.ttsReady,ttsLabel); const sttStatus=$('#sttStatus'),ttsStatus=$('#ttsStatus'); const sttChip=sttStatus?.closest('.status-chip'),ttsChip=ttsStatus?.closest('.status-chip'); if(sttChip)sttChip.title=voice.sttReady?`Whisper: ${voice.whisperBin||'whisper-cli'} · Modell: ${voice.whisperModel||''}`:voiceSetupMessage(voice,'stt'); if(ttsChip)ttsChip.title=voice.ttsReady?`Piper: ${voice.piperBin||'piper'} · Modell: ${voice.piperModel||''}`:voiceSetupMessage(voice,'tts'); const mt=$('#modelTag'),ctxN=Number(sys.context?.numCtx||0),ctxLabel=ctxN>=1024?`${Math.round(ctxN/1024)}K`:(ctxN||'—'); if(mt){mt.textContent=chatReady?`LOCAL · ${String(ollama.model||sys.model).toUpperCase()} · CTX ${ctxLabel}`:'LOCAL · DEGRADED';mt.title=`Skill Agent: ${sys.agent?.mode||'unknown'} · ${sys.skills?.action_count||sys.agent?.tools?.length||0} actions · ${sys.skills?.plugin_count||0} plugins · max ${sys.agent?.maxSteps||0} steps · Conversation ${sys.context?.historyTokens||0} tokens`;} const scout=$('#scoutBtn'),kai=$('#kanbanAI'),reindex=$('#reindexBtn'); if(scout){scout.disabled=!chatReady;scout.title=chatReady?'Lokalen Task-Scout starten':'Benötigt ein lokales Ollama-Chatmodell'} if(kai){kai.disabled=!chatReady;kai.title=chatReady?'Kanban mit lokalem Modell organisieren':'Benötigt ein lokales Ollama-Chatmodell'} if(reindex){reindex.disabled=!embedReady;reindex.title=embedReady?'RAG neu indexieren':'Benötigt ein lokales Embedding-Modell'} const chunkCount=$('#chunkCount'),memoryBar=$('#memoryBar'),wakeWord=$('#wakeWord'),whisperDiag=$('#whisperDiag'),piperDiag=$('#piperDiag'); if(chunkCount)chunkCount.textContent=chunks;if(memoryBar)memoryBar.style.width=`${Math.min(100,5+Math.log2(chunks+1)*12)}%`;if(wakeWord)wakeWord.textContent=String(voice.wakeWord||'jarvis').toUpperCase(); if(whisperDiag){whisperDiag.textContent=voice.sttReady?'ONLINE':sttLabel;whisperDiag.title=voice.sttReady?'Whisper bereit':voiceSetupMessage(voice,'stt');whisperDiag.className=voice.sttReady?'good':'bad'} if(piperDiag){piperDiag.textContent=voice.ttsReady?'ONLINE':ttsLabel;piperDiag.title=voice.ttsReady?'Piper bereit':voiceSetupMessage(voice,'tts');piperDiag.className=voice.ttsReady?'good':'bad'} } function renderState(){ safeRender('system-status',renderSystemStatus); safeRender('clean-home',renderCleanHome);safeRender('brief',renderBrief);safeRender('calendar',renderCalendar);safeRender('tasks',renderTasks);safeRender('kanban',renderKanban);safeRender('groceries',renderGroceries);safeRender('meals',renderMeals);safeRender('recipes',renderRecipes);safeRender('reminders',renderReminders);safeRender('documents',renderDocuments);safeRender('skills',renderSkills);safeRender('skill-mesh',renderSkillMesh);safeRender('activity',renderActivity); const sys=state.system||{},dbg=sys.debug||{},dstat=$('#debugTraceStatus');if(dstat){dstat.classList.toggle('off',!dbg.enabled);dstat.innerHTML=` ${dbg.enabled?'DEBUG TRACE':'TRACE OFF'}`};if($('#debugExportBtn'))$('#debugExportBtn').disabled=!dbg.enabled;if($('#debugClearBtn'))$('#debugClearBtn').disabled=!dbg.enabled; if(!bootstrappedChat){safeRender('initial-chat',renderInitialChat);bootstrappedChat=true} } function renderCleanHome(){ const now=new Date(),today=new Date();today.setHours(0,0,0,0);const tomorrow=new Date(today);tomorrow.setDate(tomorrow.getDate()+1);const weekEnd=new Date(today);weekEnd.setDate(weekEnd.getDate()+7); const events=[...(state.events||[])].filter(e=>{const d=parseDate(e.start);return d&&d>=today}).sort((a,b)=>String(a.start).localeCompare(String(b.start))); const tasks=[...(state.tasks||[])].filter(t=>t.status!=='done').sort((a,b)=>(b.priority==='high')-(a.priority==='high')||String(a.due||'z').localeCompare(String(b.due||'z'))); const mealOrder={breakfast:0,lunch:1,dinner:2,snack:3};const meals=[...(state.mealPlans||[])].filter(m=>{const d=parseDate(m.date);return d&&d>=today}).sort((a,b)=>String(a.date).localeCompare(String(b.date))+(mealOrder[a.meal]||0)-(mealOrder[b.meal]||0)); const hour=now.getHours(),greet=hour<11?'Guten Morgen':hour<17?'Guten Tag':'Guten Abend';if($('#cleanGreetingLabel'))$('#cleanGreetingLabel').textContent=greet; const eventWeek=events.filter(e=>{const d=parseDate(e.start);return d&&d!g.checked).length; if($('#cleanGreeting'))$('#cleanGreeting').textContent=openTasks||eventWeek?'Das Wichtigste ist vorbereitet.':'Alles ruhig. Du bist auf dem aktuellen Stand.'; if($('#cleanHomeSummary'))$('#cleanHomeSummary').textContent=`${eventWeek} Termin${eventWeek===1?'':'e'} in den nächsten 7 Tagen · ${openTasks} offene Aufgabe${openTasks===1?'':'n'} · ${groceryOpen} Einkauf${groceryOpen===1?'':'seinträge'}.`; if($('#homeCalendarQuick'))$('#homeCalendarQuick').textContent=`${eventWeek} ${eventWeek===1?'Termin':'Termine'}`;if($('#homeTasksQuick'))$('#homeTasksQuick').textContent=`${openTasks} offen`;if($('#homeKanbanQuick'))$('#homeKanbanQuick').textContent=`${(state.kanban||[]).length} Karten`; const nextMeal=meals[0];if($('#homeFoodQuick'))$('#homeFoodQuick').textContent=nextMeal?`${fmtDay(parseDate(nextMeal.date))} · ${mealName(nextMeal.meal)}`:'Nichts geplant'; const ev=events[0],evEl=$('#cleanNextEvent');if(evEl)evEl.innerHTML=ev?`${esc(ev.title)}

${esc(fmtDay(parseDate(ev.start)))} · ${esc(ev.allDay?'Ganztägig':fmtClock(ev.start))}${ev.location?' · '+esc(ev.location):''}

`:'Keine Termine

Dein Kalender ist frei.

'; const task=tasks[0],taskEl=$('#cleanNextTask');if(taskEl)taskEl.innerHTML=task?`${esc(task.title)}

${task.priority==='high'?'Hohe Priorität':'Offen'}${task.due?' · '+esc(fmtTime(task.due)):''}

`:'Keine offenen Aufgaben

Du bist auf dem aktuellen Stand.

'; const meal=nextMeal,mealEl=$('#cleanNextMeal');if(mealEl)mealEl.innerHTML=meal?`${esc(meal.title||meal.recipeTitle||'Geplante Mahlzeit')}

${esc(fmtDay(parseDate(meal.date)))} · ${esc(mealName(meal.meal))}${meal.servings?` · ${meal.servings} Portionen`:''}

`:'Noch nichts geplant

Plane eine Mahlzeit für heute oder morgen.

'; const sys=state.system||{},ollama=sys.ollama||{},voice=sys.voice||{};if($('#cleanCoreQuick'))$('#cleanCoreQuick').textContent=ollama.chatReady===false?'Nicht bereit':String(ollama.model||sys.model||'Lokal');if($('#cleanRagQuick'))$('#cleanRagQuick').textContent=Number(sys.ragChunks||0)?`${sys.ragChunks} Chunks`:'Leer';if($('#cleanVoiceQuick')){const cv=$('#cleanVoiceQuick'),label=sttStatusLabel(voice);cv.textContent=voice.sttReady?'Bereit':label;cv.title=voice.sttReady?'Lokales Whisper bereit':voiceSetupMessage(voice,'stt')}if($('#cleanModelStatus'))$('#cleanModelStatus').textContent=ollama.chatReady===false?'Modell fehlt':String(ollama.model||sys.model||'Online');if($('#cleanSkillsQuick'))$('#cleanSkillsQuick').textContent=`${Number(sys.mesh?.online_count||0)} Worker · ${Number(sys.skills?.action_count||0)} Actions`; } function renderBrief(){const start=new Date();start.setHours(0,0,0,0);const until=new Date(start);until.setDate(until.getDate()+7);$('#briefEvents').textContent=(state.events||[]).filter(e=>{const d=parseDate(e.start);return d&&d>=start&&dt.status!=='done').length;$('#briefGroceries').textContent=(state.groceries||[]).filter(g=>!g.checked).length;$('#briefReminders').textContent=(state.reminders||[]).filter(r=>!r.done).length} function renderCalendar(){const today=new Date();today.setHours(0,0,0,0);const end=new Date(today);end.setDate(end.getDate()+7);const events=[...(state.events||[])].sort((a,b)=>String(a.start).localeCompare(String(b.start)));const days=[];for(let i=0;i<7;i++){const d=new Date(today);d.setDate(d.getDate()+i);const key=localDateKey(d),count=events.filter(e=>{const ed=parseDate(e.start);return ed&&localDateKey(ed)===key}).length;days.push(`
${d.toLocaleDateString('de-DE',{weekday:'short'}).toUpperCase()}${d.getDate()}${count?`${count}`:''}
`)}$('#weekRail').innerHTML=days.join('');const visible=events.filter(e=>{const d=parseDate(e.start);return d&&d>=today&&d`
${esc(e.allDay?'GANZTÄGIG':fmtClock(e.start))}
${esc(e.title)}${esc(fmtDay(parseDate(e.start)))}${e.location?' · '+esc(e.location):''} · ${e.aiManaged?'JARVIS':'MANUAL'}
`).join(''):'
KEINE TERMINE IN DEN NÄCHSTEN 7 TAGEN
';$$('[data-delete-event]').forEach(b=>b.onclick=()=>deleteEntity('/api/events/',b.dataset.deleteEvent,'Termin entfernt'))} function renderTasks(){const tasks=state.tasks||[],open=tasks.filter(t=>t.status!=='done');$('#taskOpen').textContent=open.length;$('#taskAI').textContent=open.filter(t=>t.aiManaged).length;$('#taskHigh').textContent=open.filter(t=>t.priority==='high').length;const ordered=[...tasks].sort((a,b)=>(a.status==='done')-(b.status==='done')||(b.priority==='high')-(a.priority==='high')||String(a.due||'z').localeCompare(String(b.due||'z')));$('#taskList').innerHTML=ordered.length?ordered.slice(0,24).map(t=>`
${esc(t.title)}
${taskOrigin(t)}${esc(t.priority||'normal')}${t.due?`DUE ${esc(fmtTime(t.due))}`:''}
${t.aiManaged?`
CONFIDENCE ${Math.round((t.confidence||0)*100)}%${t.source?' · '+esc(t.source):''}
`:''}
`).join(''):'
ACTION QUEUE CLEAR
';$$('[data-task-check]').forEach(el=>el.onchange=async()=>{const t=state.tasks.find(x=>x.id===el.dataset.taskCheck);if(!t)return;try{await post('/api/tasks',{...t,status:el.checked?'done':'open'});await refresh()}catch(e){toast(e.message)}});$$('[data-task-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/tasks/',b.dataset.taskDelete,'Aufgabe entfernt'))} const columns=[['inbox','INBOX'],['next','NEXT'],['doing','DOING'],['waiting','WAITING'],['done','DONE']]; function renderKanban(){const cards=state.kanban||[];$('#kanbanBoard').innerHTML=columns.map(([key,label])=>{const list=cards.filter(c=>c.column===key);return `
${label}${list.length}
${list.map(c=>`
${esc(c.title)}${c.notes?`

${esc(c.notes)}

`:''}
${c.aiManaged?'AI '+Math.round((c.confidence||0)*100)+'%':'MANUAL'} · ${esc(c.priority||'normal')}${c.due?' · '+esc(fmtTime(c.due)):''}
`).join('')}
`}).join('');$$('.kanban-card').forEach(card=>card.ondragstart=e=>{e.dataTransfer.setData('text/plain',card.dataset.cardId);e.dataTransfer.effectAllowed='move'});$$('[data-kanban-col]').forEach(col=>{col.ondragover=e=>{e.preventDefault();col.classList.add('drag-over')};col.ondragleave=()=>col.classList.remove('drag-over');col.ondrop=async e=>{e.preventDefault();col.classList.remove('drag-over');const id=e.dataTransfer.getData('text/plain'),card=state.kanban.find(c=>c.id===id);if(!card)return;try{await post('/api/kanban',{...card,column:col.dataset.kanbanCol,aiManaged:false,source:'manual-override'});toast('Karte manuell fixiert');await refresh()}catch(err){toast(err.message)}}});$$('[data-card-delete]').forEach(b=>b.onclick=e=>{e.stopPropagation();deleteEntity('/api/kanban/',b.dataset.cardDelete,'Karte entfernt')})} function renderGroceries(){const xs=state.groceries||[],open=xs.filter(x=>!x.checked);$('#groceryOpen').textContent=open.length;$('#groceryBar').style.width=`${xs.length?Math.round(open.length/xs.length*100):0}%`;const sorted=[...xs].sort((a,b)=>a.checked-b.checked||String(a.category).localeCompare(String(b.category)));$('#groceryList').innerHTML=sorted.length?sorted.map(g=>`
${esc([g.quantity,g.unit,g.name].filter(Boolean).join(' '))}${esc(g.category||'Allgemein')} · ${g.aiManaged?'AI/AUTO':'MANUAL'}${g.source?.startsWith('mealplan:auto:')?' · ESSENSPLAN':''}
`).join(''):'
EINKAUFSLISTE LEER
';$$('[data-grocery-check]').forEach(el=>el.onchange=async()=>{const g=state.groceries.find(x=>x.id===el.dataset.groceryCheck);if(!g)return;try{await post('/api/groceries',{...g,checked:el.checked});await refresh()}catch(e){toast(e.message)}});$$('[data-grocery-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/groceries/',b.dataset.groceryDelete,'Eintrag entfernt'))} function renderMeals(){const today=new Date();today.setHours(0,0,0,0);const plans=state.mealPlans||[];const out=[];for(let i=0;i<7;i++){const d=new Date(today);d.setDate(d.getDate()+i);const key=localDateKey(d),list=plans.filter(m=>m.date===key).sort((a,b)=>String(a.meal).localeCompare(String(b.meal)));out.push(`
${d.toLocaleDateString('de-DE',{weekday:'short'}).toUpperCase()}${d.getDate()}.${d.getMonth()+1}.
${list.length?list.map(m=>`
${mealName(m.meal)}${esc(m.title)}${m.servings||2} PORT. ${m.aiManaged?'· AI':''}
`).join(''):'
OPEN
'}
`)}$('#mealTimeline').innerHTML=out.join('');$$('[data-meal-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/meal-plans/',b.dataset.mealDelete,'Plan entfernt'))} function renderRecipes(){const rs=state.recipes||[];$('#recipeList').innerHTML=rs.length?rs.slice(0,40).map(r=>`
${esc(r.title)}

${esc(r.description||`${(r.ingredients||[]).length} Zutaten · ${(r.steps||[]).length} Schritte`)}

${r.prepMinutes?`${r.prepMinutes} MIN · `:''}${r.servings||2} PORT. ${r.aiManaged?'· AI':''}
`).join(''):'
NO CULINARY MEMORY
';$$('[data-recipe-edit]').forEach(b=>b.onclick=()=>{const r=state.recipes.find(x=>x.id===b.dataset.recipeEdit);if(r)openDialog('recipe',r)});$$('[data-recipe-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/recipes/',b.dataset.recipeDelete,'Rezept entfernt'))} function renderReminders(){const rs=[...(state.reminders||[])].sort((a,b)=>a.done-b.done||String(a.at).localeCompare(String(b.at)));$('#reminderList').innerHTML=rs.length?rs.slice(0,24).map(r=>`
${esc(r.title)}${esc(fmtTime(r.at))} · ${r.aiManaged?'AI/AUTO':'MANUAL'}${r.linkedType==='event'?' · KALENDER':''}
`).join(''):'
ALERT GRID CLEAR
';$$('[data-reminder-check]').forEach(el=>el.onchange=async()=>{const r=state.reminders.find(x=>x.id===el.dataset.reminderCheck);if(!r)return;try{await post('/api/reminders',{...r,done:el.checked});await refresh()}catch(e){toast(e.message)}});$$('[data-reminder-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/reminders/',b.dataset.reminderDelete,'Erinnerung entfernt'))} function renderDocuments(){const docs=state.documents||[];$('#documentList').innerHTML=docs.length?docs.slice(0,14).map(d=>`
${esc((d.name.split('.').pop()||'DOC').slice(0,4).toUpperCase())}
${esc(d.name)}${d.chunkCount||0} chunks · ${d.indexedAt?'INDEXED':'PENDING'}
`).join(''):'
KNOWLEDGE VAULT EMPTY
';$$('[data-doc-delete]').forEach(b=>b.onclick=()=>deleteEntity('/api/documents/',b.dataset.docDelete,'Dokument entfernt'))} function renderSkills(){const skills=state.system?.skills||{},mods=skills.modules||[],el=$('#skillList'),tag=$('#skillCountTag');if(tag)tag.innerHTML=` ${Number(skills.action_count||0)} ACTIONS`;if(!el)return;if(!mods.length){el.innerHTML='
KEINE SKILLS GELADEN
';return}const sorted=[...mods].sort((a,b)=>(a.source==='plugin'?0:1)-(b.source==='plugin'?0:1)||String(a.name||a.id).localeCompare(String(b.name||b.id)));el.innerHTML=sorted.map(m=>{const actions=(m.actions||[]).map(a=>`${esc(a.name||a.tool_name||'action')}`).join('');const perms=m.source==='plugin'?`Runtime ${esc(m.runtime||'process')} · Network ${m.permissions?.network?'ja':'nein'} · System Exec ${m.permissions?.system_exec?'ja':'nein'}`:m.source==='remote'?`Remote HTTP · ${(m.actions||[]).length} Actions`:`${(m.actions||[]).length} Core Actions`;return `
${esc(m.name||m.id)}${esc(m.source||'plugin')} · ${esc(m.version||'')}
${m.description?`

${esc(m.description)}

`:''}${m.error?`

${esc(m.error)}

`:''}
${actions}
${perms}
`}).join('')} function renderSkillMesh(){const mesh=state.system?.mesh||{},docker=state.system?.dockerSkills||{},workers=mesh.workers||[],services=docker.services||[],tag=$('#meshCountTag'),wel=$('#meshWorkerList'),del=$('#dockerSkillList');if(tag)tag.innerHTML=` ${Number(mesh.online_count||0)} / ${Number(mesh.worker_count||0)} WORKERS`;if(wel)wel.innerHTML=workers.length?workers.map(w=>`
${esc(w.name||w.id)}${esc(w.runtime||'runtime')}${w.runtime_version?' · '+esc(w.runtime_version):''}
${esc(w.state||'offline').toUpperCase()}
${esc(w.state||'offline')}${Number(w.skill_count||0)} Skills${Number(w.action_count||0)} Actions${esc((w.endpoint||'').replace(/^https?:\/\//,''))}
${w.state==='online'?`
`:''}
`).join(''):'
KEINE REMOTE WORKER ENROLLED
';$$('[data-worker-reload]').forEach(b=>b.onclick=async()=>{b.disabled=true;try{const out=await api(`/api/mesh/workers/${encodeURIComponent(b.dataset.workerReload)}/reload`,{method:'POST'});toast(out.ok===false?(out.error||'Worker-Reload fehlgeschlagen'):'Worker-Skills neu geladen');setTimeout(refresh,500)}catch(e){toast('Worker: '+e.message)}finally{b.disabled=false}});if(del){if(!docker.enabled){del.innerHTML='
DOCKER CONTROLLER DEAKTIVIERT
'}else if(!docker.available){del.innerHTML=`
DOCKER SOCK NICHT VERFÜGBAR${docker.error?' · '+esc(docker.error):''}
`}else{del.innerHTML=services.length?services.map(c=>`
${esc(c.worker_name||c.name||c.id)}${esc(c.runtime||'skill')} · ${esc(c.image||'')}
${esc((c.state||'unknown').toUpperCase())}
${esc(c.status||c.state||'')}
${c.state==='running'?``:``}
`).join(''):'
KEINE GELABELTEN SKILL-CONTAINER
';$$('[data-docker-action]').forEach(b=>b.onclick=async()=>{b.disabled=true;try{const out=await api(`/api/docker/skill-services/${encodeURIComponent(b.dataset.dockerId)}/${encodeURIComponent(b.dataset.dockerAction)}`,{method:'POST'});toast(out.ok?`Container ${b.dataset.dockerAction}`:(out.error||'Docker-Aktion fehlgeschlagen'));setTimeout(refresh,700)}catch(e){toast('Docker: '+e.message)}finally{b.disabled=false}})}}} function renderActivity(){const xs=state.activity||[];$('#activityList').innerHTML=xs.length?xs.slice(0,20).map(a=>`
${esc(a.message)}${esc(fmtTime(a.createdAt))} · ${esc(String(a.kind||'event').toUpperCase())}
`).join(''):'
NO TELEMETRY EVENTS
'} function renderInitialChat(){const conv=$('#conversation'),turns=(state.chat||[]).slice(-12);if(!turns.length)return;conv.innerHTML='';turns.forEach(t=>addMessage(t.content,t.role==='user'?'user':'assistant',false));conv.scrollTop=conv.scrollHeight} function addMessage(text,role='assistant',animate=true,mode=''){const conv=$('#conversation'),el=document.createElement('div');el.className=`message ${role} ${mode||''}`.trim();const label=role==='user'?'OPERATOR':mode==='action'?'JARVIS // COMMIT':mode==='clarification'?'JARVIS // CLARIFY':'JARVIS';el.innerHTML=`
${role==='user'?'U':'J'}
${label}

`;$('p',el).innerHTML=messageHTML(text);if(animate)el.style.opacity='0';conv.appendChild(el);conv.scrollTop=conv.scrollHeight;if(animate)requestAnimationFrame(()=>{el.style.transition='.22s';el.style.opacity='1'});return el} function setCoreMode(mode,caption){$('#voiceModeLabel').textContent=mode;const active=!['STANDBY','READY'].includes(mode);$('#coreVisual').classList.toggle('active',active);if(caption)$('#overlayTranscript').dataset.caption=caption} async function post(url,obj){return api(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(obj)})} async function deleteEntity(prefix,id,msg){try{await api(prefix+encodeURIComponent(id),{method:'DELETE'});toast(msg);await refresh()}catch(e){toast(e.message)}} async function sendCommand(message,fromVoice=false,contextName=''){const raw=String(message||'').trim();if(!raw)return;if(commandBusy){toast('JARVIS verarbeitet bereits einen Befehl');return}commandBusy=true;const exec=$('#commandForm .execute');exec.disabled=true;exec.textContent='PROCESSING';$('#commandInput').value='';addMessage(contextName?`[${contextName}] ${raw}`:raw,'user');setCoreMode('THINKING');const pending=addMessage('Agent → Tool wählen → JSON validieren → ausführen …','assistant');pending.classList.add('pending');const routed=contextName?`Im Kontext des Moduls ${contextName}: ${raw}`:raw;try{const out=await post('/api/ai/chat',{message:routed});lastTraceId=out.traceId||'';pending.remove();addMessage(out.reply,'assistant',true,out.mode||'');renderSources(out.sources||[],out.automations||[],out.mode||'chat',lastTraceId,out.toolResults||[],out.plan||null);if(out.automations?.length)toast(out.automations.map(a=>a.message).join(' · '));setCoreMode(out.mode==='clarification'?'AWAITING INPUT':'READY');if(fromVoice)await speakLocal(out.reply);await refresh()}catch(e){pending.remove();addMessage(`Systemfehler: ${e.message}`,'assistant');setCoreMode('STANDBY')}finally{commandBusy=false;exec.disabled=false;exec.innerHTML='EXECUTE ↗'}setTimeout(()=>setCoreMode(wakeEnabled?'WAKE ARMED':'STANDBY'),1400)} function renderSources(sources,actions=[],mode='chat',traceId='',toolResults=[],plan=null){const trace=traceId?`TRACE ${esc(traceId.slice(-12).toUpperCase())}`:'';const planTitle=plan?.steps?.length?plan.steps.map((s,i)=>`${i+1}. ${s.task||s.tool}`).join('\n'):'';const planChip=plan?.steps?.length?`PLAN · ${plan.steps.length} SCHRITTE`:'';const toolChips=(toolResults||[]).slice(0,8).map(t=>`${esc(String(t.tool||'TOOL').toUpperCase())}${t.success===false?' · ERROR':''}`).join('');if(actions.length){$('#sourceRail').innerHTML=`TOOL COMMIT · ${actions.length} MUTATION${actions.length===1?'':'S'}`+planChip+toolChips+trace;return}if(toolChips||planChip){$('#sourceRail').innerHTML=`TOOL AGENT`+planChip+toolChips+trace;return}$('#sourceRail').innerHTML=(sources.length?sources.slice(0,7).map(s=>`${esc(s.name)} · ${Math.round((s.score||0)*100)}%`).join(''):`${mode==='clarification'?'AGENT AWAITS INPUT':'LOCAL TOOL CORE'}`)+trace} $('#commandForm').onsubmit=e=>{e.preventDefault();sendCommand($('#commandInput').value)};$('#commandInput').onkeydown=e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();$('#commandForm').requestSubmit()}}; $('#clearChatBtn').onclick=async()=>{try{await api('/api/chat',{method:'DELETE'});state.chat=[];const conv=$('#conversation');conv.innerHTML='';addMessage('Command channel bereinigt. Home-Daten, Kalender, Aufgaben und RAG bleiben unverändert.','assistant',false,'action');renderSources([],[],'chat');toast('Chatverlauf geleert')}catch(e){toast(e.message)}}; $('#debugExportBtn').onclick=()=>{const a=document.createElement('a');a.href='/api/debug/export?limit=10000';a.download='jarvis-debug.json';document.body.appendChild(a);a.click();a.remove();toast(lastTraceId?`Debug-Export · letzter Trace ${lastTraceId.slice(-12)}`:'Debug-Export wird erstellt')}; $('#debugClearBtn').onclick=async()=>{if(!confirm('Nur den Debug-Trace löschen? Home-Daten und Chat bleiben erhalten.'))return;try{await api('/api/debug',{method:'DELETE'});toast('Debug-Trace geleert')}catch(e){toast(e.message)}}; $('#automationBtn').onclick=async()=>{const b=$('#automationBtn');b.disabled=true;b.textContent='SYNCHRONIZING';try{const o=await api('/api/automation/run',{method:'POST'});toast(`Einkauf ${o.groceries} · Reminder ${o.reminders} · Kanban ${o.kanban}`);await refresh()}catch(e){toast(e.message)}finally{b.disabled=false;b.textContent='RUN HOME AUTOMATION'}}; $('#scoutBtn').onclick=async()=>{const b=$('#scoutBtn');b.disabled=true;b.textContent='SCANNING';setCoreMode('SCANNING');try{const out=await api('/api/ai/scout',{method:'POST'});toast(`${out.created} neue Aufgabe(n)`);await refresh()}catch(e){toast(e.message)}finally{b.disabled=false;b.textContent='SCAN FOR TASKS';setCoreMode('STANDBY')}}; $('#kanbanAI').onclick=async()=>{const b=$('#kanbanAI');b.disabled=true;b.textContent='ORGANIZING';try{const out=await api('/api/ai/kanban',{method:'POST'});toast(`${out.updated} KI-Karten gepflegt`);await refresh()}catch(e){toast(e.message)}finally{b.disabled=false;b.textContent='AI ORGANIZE'}}; const drop=$('#dropzone'),fileInput=$('#fileInput');['dragenter','dragover'].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.add('drag')}));['dragleave','drop'].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.remove('drag')}));drop.ondrop=e=>{const f=e.dataTransfer.files?.[0];if(f)uploadDocument(f)};fileInput.onchange=()=>{if(fileInput.files?.[0])uploadDocument(fileInput.files[0]);fileInput.value=''}; async function uploadDocument(file){const fd=new FormData();fd.append('file',file);toast('Dokument wird lokal indexiert …');setCoreMode('INDEXING');try{const out=await api('/api/documents',{method:'POST',body:fd});toast(out.indexed?'Dokument im RAG':'Text gespeichert, Indexierung fehlgeschlagen');await refresh()}catch(e){toast(e.message)}finally{setCoreMode('STANDBY')}} if($('#reloadSkillsBtn'))$('#reloadSkillsBtn').onclick=async()=>{try{const out=await api('/api/skills/reload',{method:'POST'});toast(out.ok?'Skills neu geladen':(out.warning||'Skills mit Warnungen geladen'));await refresh()}catch(e){toast('Skills: '+e.message)}}; if($('#refreshMeshBtn'))$('#refreshMeshBtn').onclick=async()=>{try{await refresh();toast('Skill Mesh aktualisiert')}catch(e){toast('Mesh: '+e.message)}}; $('#reindexBtn').onclick=async()=>{const b=$('#reindexBtn');b.disabled=true;b.textContent='INDEXING';try{const o=await api('/api/rag/reindex',{method:'POST'});toast(`${o.indexed} Dokument(e), ${o.chunks} Chunks`);await refresh()}catch(e){toast(e.message)}finally{b.disabled=false;b.textContent='REINDEX'}}; const dialogDefs={ task:{title:'Neue Aufgabe',code:'ACTION QUEUE',endpoint:'/api/tasks',fields:[['title','Titel','text',true],['priority','Priorität','select',false,['normal','high','low']],['due','Fällig','datetime-local'],['notes','Notizen','textarea']]}, event:{title:'Neuer Termin',code:'TEMPORAL MATRIX',endpoint:'/api/events',fields:[['title','Titel','text',true],['start','Start','datetime-local',true],['end','Ende','datetime-local'],['location','Ort','text'],['notes','Notizen','textarea']]}, reminder:{title:'Neue Erinnerung',code:'ALERT GRID',endpoint:'/api/reminders',fields:[['title','Titel','text',true],['at','Zeitpunkt','datetime-local',true],['notes','Notizen','textarea']]}, grocery:{title:'Einkauf ergänzen',code:'SUPPLY MATRIX',endpoint:'/api/groceries',fields:[['name','Artikel','text',true],['quantity','Menge','text'],['unit','Einheit','text'],['category','Kategorie','text']]}, meal:{title:'Essen planen',code:'NUTRITION SCHEDULE',endpoint:'/api/meal-plans',fields:[['date','Datum','date',true],['meal','Mahlzeit','select',false,['breakfast','lunch','dinner','snack']],['recipeId','Rezept','recipe-select'],['title','Freies Gericht','text'],['servings','Portionen','number'],['notes','Notizen','textarea']]}, recipe:{title:'Rezept speichern',code:'CULINARY MEMORY',endpoint:'/api/recipes',fields:[['title','Titel','text',true],['servings','Portionen','number'],['prepMinutes','Zubereitung (Min.)','number'],['description','Beschreibung','textarea'],['ingredientsText','Zutaten · je Zeile: Menge | Einheit | Name','textarea'],['stepsText','Schritte · einer je Zeile','textarea'],['tagsText','Tags · kommagetrennt','text']]}, kanban:{title:'Manuelle Kanban-Karte',code:'FLOW ENGINE',endpoint:'/api/kanban',fields:[['title','Titel','text',true],['column','Spalte','select',false,['inbox','next','doing','waiting','done']],['priority','Priorität','select',false,['normal','high','low']],['due','Fällig','datetime-local'],['notes','Notizen','textarea']]} }; function fieldHTML(f){const[name,label,type,required,opts]=f,req=required?'required':'';if(type==='textarea')return `
`;if(type==='select')return `
`;if(type==='recipe-select')return `
`;return `
`} function openDialog(type,existing=null){const d=dialogDefs[type];if(!d)return;dialogType=type;dialogEditing=existing?structuredClone(existing):null;$('#dialogCode').textContent=d.code;$('#dialogTitle').textContent=existing&&type==='recipe'?'Rezept bearbeiten':d.title;$('#dialogFields').innerHTML=d.fields.map(fieldHTML).join('');const form=$('#entityForm');form.reset();if(type==='meal')form.elements.date.value=localDateKey(new Date());if(type==='task'||type==='kanban')form.elements.priority.value='normal';if(type==='kanban')form.elements.column.value='inbox';if(type==='meal'){form.elements.meal.value='dinner';form.elements.servings.value='2'}if(type==='recipe')form.elements.servings.value='2';if(existing){for(const [k,val] of Object.entries(existing)){if(form.elements[k]&&val!=null)form.elements[k].value=val}if(type==='recipe'){form.elements.ingredientsText.value=(existing.ingredients||[]).map(i=>[i.quantity||'',i.unit||'',i.name||''].join(' | ')).join('\n');form.elements.stepsText.value=(existing.steps||[]).join('\n');form.elements.tagsText.value=(existing.tags||[]).join(', ')}}$('#entityDialog').showModal()} $$('[data-add]').forEach(b=>b.onclick=()=>openDialog(b.dataset.add));const syncMealGroceriesBtn=$('#syncMealGroceriesBtn');if(syncMealGroceriesBtn)syncMealGroceriesBtn.onclick=async()=>{try{const out=await post('/api/automation/run',{});toast(`Einkauf aus Essensplan aktualisiert${out?.groceries!=null?` · ${out.groceries} Änderungen`:''}`);await refresh()}catch(e){toast(e.message)}};$('#dialogClose').onclick=$('#dialogCancel').onclick=()=>{dialogEditing=null;$('#entityDialog').close()}; $('#entityForm').onsubmit=async e=>{e.preventDefault();const def=dialogDefs[dialogType],fd=new FormData(e.currentTarget),raw=Object.fromEntries(fd.entries()),v=dialogEditing?{...dialogEditing,...raw}:raw;['due','start','end','at'].forEach(k=>{if(v[k])v[k]=new Date(v[k]).toISOString();else if(k in v)delete v[k]});['servings','prepMinutes'].forEach(k=>{if(v[k])v[k]=Number(v[k])});if(dialogType==='recipe'){v.ingredients=String(v.ingredientsText||'').split('\n').map(x=>x.trim()).filter(Boolean).map(line=>{const p=line.split('|').map(x=>x.trim());return p.length>=3?{quantity:p[0],unit:p[1],name:p.slice(2).join(' | ')}:{name:line}});v.steps=String(v.stepsText||'').split('\n').map(x=>x.trim()).filter(Boolean);v.tags=String(v.tagsText||'').split(',').map(x=>x.trim()).filter(Boolean);delete v.ingredientsText;delete v.stepsText;delete v.tagsText}if(dialogType==='meal'&&v.recipeId){const r=state.recipes.find(x=>x.id===v.recipeId);if(r&&!v.title)v.title=r.title}if(dialogType==='task'){v.status='open';v.aiManaged=false}if(dialogType==='kanban'){v.aiManaged=false;v.source='manual'}try{await post(def.endpoint,v);dialogEditing=null;$('#entityDialog').close();toast('Gespeichert');await refresh()}catch(err){toast(err.message)}}; function mediaOptions(){for(const t of['audio/webm;codecs=opus','audio/webm','audio/ogg;codecs=opus','audio/mp4'])if(window.MediaRecorder?.isTypeSupported?.(t))return{mimeType:t};return{}}function extFor(type){if(type.includes('ogg'))return'.ogg';if(type.includes('mp4'))return'.mp4';return'.webm'}async function ensureMic(){if(!navigator.mediaDevices?.getUserMedia)throw new Error('Browser unterstützt keinen Mikrofonzugriff');return navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}})}async function transcribeBlob(blob){const fd=new FormData();fd.append('audio',blob,'voice'+extFor(blob.type||'audio/webm'));const out=await api('/api/voice/transcribe',{method:'POST',body:fd});return String(out.text||'').trim()} async function startPTT(contextName=''){if(pttRecorder){stopPTT();return}if(!(state.system?.voice?.sttReady)){toast(voiceSetupMessage(state.system?.voice||{},'stt'));return}pttContext=contextName;const resumeWake=wakeEnabled;if(resumeWake)await stopWake(false);try{pttStream=await ensureMic();pttChunks=[];pttRecorder=new MediaRecorder(pttStream,mediaOptions());pttRecorder.ondataavailable=e=>{if(e.data?.size)pttChunks.push(e.data)};pttRecorder.onstop=async()=>{const rec=pttRecorder;pttRecorder=null;const blob=new Blob(pttChunks,{type:rec?.mimeType||'audio/webm'});pttChunks=[];pttStream?.getTracks().forEach(t=>t.stop());pttStream=null;$('#globalMic').classList.remove('recording');$('#voiceOverlay').classList.remove('show');setCoreMode('TRANSCRIBE');try{const text=await transcribeBlob(blob);$('#overlayTranscript').textContent=text||'Kein Sprachsignal erkannt';if(text)await sendCommand(text,true,pttContext)}catch(e){toast(e.message);setCoreMode('STANDBY')}pttContext='';if(resumeWake&&$('#wakeToggle').checked)startWake()};pttRecorder.start();$('#globalMic').classList.add('recording');$('#voiceOverlay').classList.add('show');$('#voiceContextLabel').textContent=(contextName||'GLOBAL').toUpperCase();$('#overlayStatus').textContent='LISTENING';$('#overlayTranscript').textContent='Sprich jetzt · erneut drücken zum Senden';setCoreMode('LISTENING')}catch(e){toast(e.message);pttContext='';if(resumeWake&&$('#wakeToggle').checked)startWake()}} function stopPTT(){if(pttRecorder&&pttRecorder.state!=='inactive'){$('#overlayStatus').textContent='PROCESSING';pttRecorder.stop()}}$('#globalMic').onclick=()=>startPTT('');$('#inlineMic').onclick=()=>startPTT('');$$('.module-mic').forEach(b=>b.onclick=()=>startPTT(b.dataset.voiceContext||'')); function recordFixedChunk(stream,ms){return new Promise((resolve,reject)=>{let chunks=[],rec;try{rec=new MediaRecorder(stream,mediaOptions())}catch(e){reject(e);return}rec.ondataavailable=e=>{if(e.data?.size)chunks.push(e.data)};rec.onerror=e=>reject(e.error||new Error('Recorder error'));rec.onstop=()=>resolve(new Blob(chunks,{type:rec.mimeType||'audio/webm'}));rec.start();setTimeout(()=>{if(rec.state!=='inactive')rec.stop()},ms)})} async function startWake(){if(wakeEnabled)return;if(!(state.system?.voice?.sttReady)){$('#wakeToggle').checked=false;toast(voiceSetupMessage(state.system?.voice||{},'stt'));return}wakeEnabled=true;const token=++wakeToken;try{wakeStream=await ensureMic();$('#micDiag').textContent='HOT';$('#micDiag').className='good';$('#wakeHelp').textContent=`Lokaler Wake-Scan aktiv. Sage „${String(state.system.voice.wakeWord||'jarvis').toUpperCase()}“ und danach deinen Befehl.`;setCoreMode('WAKE ARMED')}catch(e){wakeEnabled=false;$('#wakeToggle').checked=false;toast(e.message);return}while(wakeEnabled&&token===wakeToken){try{const blob=await recordFixedChunk(wakeStream,3000);if(!wakeEnabled||token!==wakeToken||speaking||blob.size<1000)continue;if(wakeBusy)continue;wakeBusy=true;let text='';try{text=await transcribeBlob(blob)}catch(e){if(wakeEnabled)console.debug(e.message)}finally{wakeBusy=false}if(text)processWakeTranscript(text)}catch(e){if(wakeEnabled)toast('Wake-Word: '+e.message);break}}} async function stopWake(updateToggle=true){wakeEnabled=false;++wakeToken;wakeStream?.getTracks().forEach(t=>t.stop());wakeStream=null;wakeArmedUntil=0;$('#micDiag').textContent='READY';$('#micDiag').className='';if(updateToggle)$('#wakeToggle').checked=false;$('#wakeHelp').textContent='Wake-Word aus. Jeder Modul-Button und der globale Voice-Core starten Push-to-talk.';setCoreMode('STANDBY')} function processWakeTranscript(raw){const text=raw.trim(),wake=String(state.system?.voice?.wakeWord||'jarvis').toLowerCase(),lower=text.toLowerCase(),idx=lower.indexOf(wake);if(idx>=0){const after=text.slice(idx+wake.length).replace(/^[\s,.:;!\-]+/,'').trim();wakeArmedUntil=Date.now()+8500;$('#voiceOverlay').classList.add('show');$('#voiceContextLabel').textContent='GLOBAL';$('#overlayStatus').textContent='WAKE CONFIRMED';$('#overlayTranscript').textContent=after||'Ich höre …';setCoreMode('COMMAND');if(after.length>1){wakeArmedUntil=0;$('#voiceOverlay').classList.remove('show');sendCommand(after,true)}else setTimeout(()=>{if(Date.now()>wakeArmedUntil)$('#voiceOverlay').classList.remove('show')},9000);return}if(Date.now()1){wakeArmedUntil=0;$('#voiceOverlay').classList.remove('show');sendCommand(text,true)}}$('#wakeToggle').onchange=e=>{if(e.target.checked)startWake();else stopWake()}; async function speakLocal(text){if(!(state.system?.voice?.ttsReady))return;speaking=true;setCoreMode('SPEAKING');try{const res=await fetch('/api/voice/speak',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})});if(!res.ok)throw new Error((await res.text()).trim()||`TTS HTTP ${res.status}`);const blob=await res.blob(),url=URL.createObjectURL(blob),audio=new Audio(url);await new Promise((resolve,reject)=>{audio.onended=resolve;audio.onerror=reject;audio.play().catch(reject)});URL.revokeObjectURL(url)}catch(e){toast('TTS: '+e.message)}finally{speaking=false;setCoreMode(wakeEnabled?'WAKE ARMED':'STANDBY')}} window.addEventListener('beforeunload',()=>{pttStream?.getTracks().forEach(t=>t.stop());wakeStream?.getTracks().forEach(t=>t.stop())});initTheme();refresh();setInterval(refresh,9000);