`${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)}
`:''}`).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=>``).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=>``).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'?`RELOAD SKILLS
`:''} `).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'?`STOP RESTART `:`START `}
`).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'}
`;$('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 `${esc(label)}
`;if(type==='select')return `${esc(label)} ${opts.map(o=>`${esc(o.toUpperCase())} `).join('')}
`;if(type==='recipe-select')return `${esc(label)} — FREIES GERICHT — ${(state.recipes||[]).map(r=>`${esc(r.title)} `).join('')}
`;return `${esc(label)}
`}
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);