@@ -971,3 +971,10 @@ Anschließend erkennt `GET /api/docker/skill-services` auch die gestoppten, gela
|
||||
v9.1 ergänzt das Skill Mesh um dedizierte Remote-Integrationen für Philips Hue, UniFi Network, UniFi Protect, Proxmox VE, Dockge/Compose, Netzwerkdiagnose/Wake-on-LAN und ntfy. Außerdem unterstützt das Skill-Protokoll jetzt `runtime.env_from`, sodass Secrets aus Worker-ENVs nur explizit an den jeweiligen Skill-Prozess weitergegeben werden.
|
||||
|
||||
Details, ENV-Beispiele und Workflow-Ideen stehen in [`HOME-CONTROL-SKILLS.md`](HOME-CONTROL-SKILLS.md).
|
||||
|
||||
## v9.1.1 – Clean UI State Render Fix
|
||||
|
||||
- Fixes a Clean-UI JavaScript scope regression where `renderCleanHome()` referenced the `renderState()`-local `sttLabel`. The resulting `ReferenceError` stopped the render chain before groceries, meals, recipes and later modules were refreshed.
|
||||
- Voice status labels are now derived through shared helpers (`sttStatusLabel` / `ttsStatusLabel`).
|
||||
- Dashboard module renderers are isolated with `safeRender`, so an optional card can no longer prevent unrelated modules such as the grocery list from updating.
|
||||
- Adds a regression test for the exact failure.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCleanHomeVoiceLabelDoesNotBreakModuleRendering(t *testing.T) {
|
||||
b, err := os.ReadFile("../../internal/ui/static/app.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := string(b)
|
||||
start := strings.Index(s, "function renderCleanHome()")
|
||||
end := strings.Index(s[start:], "function renderBrief()")
|
||||
if start < 0 || end < 0 {
|
||||
t.Fatal("renderCleanHome/renderBrief not found")
|
||||
}
|
||||
body := s[start : start+end]
|
||||
if strings.Contains(body, "sttLabel") {
|
||||
t.Fatal("renderCleanHome references renderState-local sttLabel and can abort downstream rendering")
|
||||
}
|
||||
if !strings.Contains(body, "sttStatusLabel(voice)") {
|
||||
t.Fatal("renderCleanHome must derive its own STT label")
|
||||
}
|
||||
if !strings.Contains(s, "safeRender('groceries',renderGroceries)") {
|
||||
t.Fatal("grocery renderer must be isolated from failures in other dashboard modules")
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+46
-5
@@ -48,7 +48,7 @@
|
||||
"source": "mealplan:auto:nudeln|gramm",
|
||||
"aiManaged": true,
|
||||
"createdAt": "2026-08-28T11:11:06+02:00",
|
||||
"updatedAt": "2026-08-29T17:02:11+02:00"
|
||||
"updatedAt": "2026-08-29T19:40:32+02:00"
|
||||
},
|
||||
{
|
||||
"id": "grocery_ai_dl0gl2ul9jmg",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source": "mealplan:auto:thomatensauce|ml",
|
||||
"aiManaged": true,
|
||||
"createdAt": "2026-08-28T11:11:06+02:00",
|
||||
"updatedAt": "2026-08-29T17:02:11+02:00"
|
||||
"updatedAt": "2026-08-29T19:40:32+02:00"
|
||||
}
|
||||
],
|
||||
"recipes": [
|
||||
@@ -178,15 +178,14 @@
|
||||
{
|
||||
"id": "card_ai_dl0dbme6x2a0",
|
||||
"title": "Hausflur putzen",
|
||||
"notes": "Offene Aufgabe ohne Fälligkeit; zunächst in der Inbox sammeln und bei Bedarf in den nächsten Arbeitsschritt verschieben.",
|
||||
"column": "inbox",
|
||||
"priority": "normal",
|
||||
"taskId": "task_tool_dl0dbme5o3ls",
|
||||
"source": "task-sync",
|
||||
"aiManaged": true,
|
||||
"confidence": 0.72,
|
||||
"confidence": 1,
|
||||
"createdAt": "2026-08-28T08:37:42+02:00",
|
||||
"updatedAt": "2026-08-29T16:12:32+02:00"
|
||||
"updatedAt": "2026-08-29T18:57:22+02:00"
|
||||
}
|
||||
],
|
||||
"documents": [],
|
||||
@@ -394,9 +393,51 @@
|
||||
"role": "assistant",
|
||||
"content": "Essensplan: 7 Abendessen geplant.\n• Freitag, 28.08. · Nudeln mit Thomatensauce\n• Samstag, 29.08. · Nudeln mit Thomatensauce\n• Sonntag, 30.08. · Nudeln mit Thomatensauce\n• Montag, 31.08. · Nudeln mit Thomatensauce\n• Dienstag, 01.09. · Nudeln mit Thomatensauce\n• Mittwoch, 02.09. · Nudeln mit Thomatensauce\n• Donnerstag, 03.09. · Nudeln mit Thomatensauce",
|
||||
"createdAt": "2026-08-28T11:11:25+02:00"
|
||||
},
|
||||
{
|
||||
"id": "chat_dl1l0y2k7420",
|
||||
"role": "user",
|
||||
"content": "Setze Spaghetti auf die Einkaufsliste",
|
||||
"createdAt": "2026-08-29T18:52:34+02:00"
|
||||
},
|
||||
{
|
||||
"id": "chat_dl1l1vh069nc",
|
||||
"role": "assistant",
|
||||
"content": "Der lokale JARVIS-Agent ist momentan nicht vollständig verfügbar. Es wurde keine unbestätigte Home-Änderung ausgeführt.",
|
||||
"createdAt": "2026-08-29T18:53:47+02:00"
|
||||
},
|
||||
{
|
||||
"id": "chat_dl1l2e7je3q4",
|
||||
"role": "user",
|
||||
"content": "Setze Spaghetti auf die Einkaufsliste",
|
||||
"createdAt": "2026-08-29T18:54:28+02:00"
|
||||
},
|
||||
{
|
||||
"id": "chat_dl1l3xc70g08",
|
||||
"role": "assistant",
|
||||
"content": "Der lokale JARVIS-Agent ist momentan nicht vollständig verfügbar. Es wurde keine unbestätigte Home-Änderung ausgeführt.",
|
||||
"createdAt": "2026-08-29T18:56:28+02:00"
|
||||
}
|
||||
],
|
||||
"activity": [
|
||||
{
|
||||
"id": "act_dl1l4mfzxeis",
|
||||
"kind": "kanban",
|
||||
"message": "KI-Kanban (interval/deterministic-fallback): 1 Karten gepflegt",
|
||||
"createdAt": "2026-08-29T18:57:22+02:00"
|
||||
},
|
||||
{
|
||||
"id": "act_dl1l3xc6e5x8",
|
||||
"kind": "warning",
|
||||
"message": "Tool-Agent: ollama nicht erreichbar: Post \"http://localhost:11434/api/chat\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)",
|
||||
"createdAt": "2026-08-29T18:56:28+02:00"
|
||||
},
|
||||
{
|
||||
"id": "act_dl1l1vgzu0ag",
|
||||
"kind": "warning",
|
||||
"message": "Tool-Agent: ollama nicht erreichbar: Post \"http://localhost:11434/api/chat\": context canceled",
|
||||
"createdAt": "2026-08-29T18:53:47+02:00"
|
||||
},
|
||||
{
|
||||
"id": "act_dl1hmf4ihncw",
|
||||
"kind": "kanban",
|
||||
|
||||
@@ -11,6 +11,9 @@ const mealName=m=>({breakfast:'FRÜH',lunch:'MITTAG',dinner:'ABEND',snack:'SNACK
|
||||
const taskOrigin=t=>t.source==='jarvis-command'?'JARVIS':t.aiManaged?'AI SCOUT':'MANUAL';
|
||||
const messageHTML=text=>esc(text).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
|
||||
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.'},
|
||||
@@ -28,7 +31,7 @@ function toast(msg){const el=$('#toast');el.textContent=msg;el.classList.add('sh
|
||||
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 renderState(){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'));$('#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=voice.sttReady?'LOCAL':(!voice.whisperBinReady?'BIN':(!voice.whisperModelReady?'MODEL':(!voice.ffmpegReady?'FFMPEG':'SETUP'))),ttsLabel=voice.ttsReady?'LOCAL':(!voice.piperBinReady?'BIN':(!voice.piperModelReady?'MODEL':'SETUP'));setChip('#stt',!!voice.sttReady,sttLabel);setChip('#tts',!!voice.ttsReady,ttsLabel);$('#stt').title=voice.sttReady?`Whisper: ${voice.whisperBin||'whisper-cli'} · Modell: ${voice.whisperModel||''}`:voiceSetupMessage(voice,'stt');$('#tts').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'}$('#chunkCount').textContent=chunks;$('#memoryBar').style.width=`${Math.min(100,5+Math.log2(chunks+1)*12)}%`;$('#wakeWord').textContent=String(voice.wakeWord||'jarvis').toUpperCase();$('#whisperDiag').textContent=voice.sttReady?'ONLINE':sttLabel;$('#whisperDiag').title=voice.sttReady?'Whisper bereit':voiceSetupMessage(voice,'stt');$('#whisperDiag').className=voice.sttReady?'good':'bad';$('#piperDiag').textContent=voice.ttsReady?'ONLINE':ttsLabel;$('#piperDiag').title=voice.ttsReady?'Piper bereit':voiceSetupMessage(voice,'tts');$('#piperDiag').className=voice.ttsReady?'good':'bad';renderCleanHome();renderBrief();renderCalendar();renderTasks();renderKanban();renderGroceries();renderMeals();renderRecipes();renderReminders();renderDocuments();renderSkills();renderSkillMesh();renderActivity();const dbg=sys.debug||{};const dstat=$('#debugTraceStatus');if(dstat){dstat.classList.toggle('off',!dbg.enabled);dstat.innerHTML=`<i></i> ${dbg.enabled?'DEBUG TRACE':'TRACE OFF'}`};if($('#debugExportBtn'))$('#debugExportBtn').disabled=!dbg.enabled;if($('#debugClearBtn'))$('#debugClearBtn').disabled=!dbg.enabled;if(!bootstrappedChat){renderInitialChat();bootstrappedChat=true}}
|
||||
function renderState(){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'));$('#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);$('#stt').title=voice.sttReady?`Whisper: ${voice.whisperBin||'whisper-cli'} · Modell: ${voice.whisperModel||''}`:voiceSetupMessage(voice,'stt');$('#tts').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'}$('#chunkCount').textContent=chunks;$('#memoryBar').style.width=`${Math.min(100,5+Math.log2(chunks+1)*12)}%`;$('#wakeWord').textContent=String(voice.wakeWord||'jarvis').toUpperCase();$('#whisperDiag').textContent=voice.sttReady?'ONLINE':sttLabel;$('#whisperDiag').title=voice.sttReady?'Whisper bereit':voiceSetupMessage(voice,'stt');$('#whisperDiag').className=voice.sttReady?'good':'bad';$('#piperDiag').textContent=voice.ttsReady?'ONLINE':ttsLabel;$('#piperDiag').title=voice.ttsReady?'Piper bereit':voiceSetupMessage(voice,'tts');$('#piperDiag').className=voice.ttsReady?'good':'bad';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 dbg=sys.debug||{};const dstat=$('#debugTraceStatus');if(dstat){dstat.classList.toggle('off',!dbg.enabled);dstat.innerHTML=`<i></i> ${dbg.enabled?'DEBUG TRACE':'TRACE OFF'}`};if($('#debugExportBtn'))$('#debugExportBtn').disabled=!dbg.enabled;if($('#debugClearBtn'))$('#debugClearBtn').disabled=!dbg.enabled;if(!bootstrappedChat){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)));
|
||||
@@ -43,7 +46,7 @@ function renderCleanHome(){
|
||||
const ev=events[0],evEl=$('#cleanNextEvent');if(evEl)evEl.innerHTML=ev?`<strong>${esc(ev.title)}</strong><p>${esc(fmtDay(parseDate(ev.start)))} · ${esc(ev.allDay?'Ganztägig':fmtClock(ev.start))}${ev.location?' · '+esc(ev.location):''}</p>`:'<strong>Keine Termine</strong><p>Dein Kalender ist frei.</p>';
|
||||
const task=tasks[0],taskEl=$('#cleanNextTask');if(taskEl)taskEl.innerHTML=task?`<strong>${esc(task.title)}</strong><p>${task.priority==='high'?'Hohe Priorität':'Offen'}${task.due?' · '+esc(fmtTime(task.due)):''}</p>`:'<strong>Keine offenen Aufgaben</strong><p>Du bist auf dem aktuellen Stand.</p>';
|
||||
const meal=nextMeal,mealEl=$('#cleanNextMeal');if(mealEl)mealEl.innerHTML=meal?`<strong>${esc(meal.title||meal.recipeTitle||'Geplante Mahlzeit')}</strong><p>${esc(fmtDay(parseDate(meal.date)))} · ${esc(mealName(meal.meal))}${meal.servings?` · ${meal.servings} Portionen`:''}</p>`:'<strong>Noch nichts geplant</strong><p>Plane eine Mahlzeit für heute oder morgen.</p>';
|
||||
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');cv.textContent=voice.sttReady?'Bereit':sttLabel;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`;
|
||||
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&&d<until}).length;$('#briefTasks').textContent=(state.tasks||[]).filter(t=>t.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(`<div class="day-cell ${i===0?'today':''}"><span>${d.toLocaleDateString('de-DE',{weekday:'short'}).toUpperCase()}</span><strong>${d.getDate()}</strong>${count?`<b>${count}</b>`:''}</div>`)}$('#weekRail').innerHTML=days.join('');const visible=events.filter(e=>{const d=parseDate(e.start);return d&&d>=today&&d<end}).slice(0,18);$('#agenda').innerHTML=visible.length?visible.map(e=>`<div class="agenda-item"><div class="agenda-time">${esc(e.allDay?'GANZTÄGIG':fmtClock(e.start))}</div><div><strong>${esc(e.title)}</strong><span>${esc(fmtDay(parseDate(e.start)))}${e.location?' · '+esc(e.location):''} · ${e.aiManaged?'JARVIS':'MANUAL'}</span></div><button class="icon-delete" data-delete-event="${esc(e.id)}">×</button></div>`).join(''):'<div class="empty">KEINE TERMINE IN DEN NÄCHSTEN 7 TAGEN</div>';$$('[data-delete-event]').forEach(b=>b.onclick=()=>deleteEntity('/api/events/',b.dataset.deleteEvent,'Termin entfernt'))}
|
||||
|
||||
Reference in New Issue
Block a user