// Dashboard page logic (function() { "use strict"; const $ = (id) => document.getElementById(id); function show(id) { $(id).classList.remove("hidden"); } function hide(id) { $(id).classList.add("hidden"); } const riskColors = { low: { bg: "bg-green-100 dark:bg-green-900", text: "text-green-700 dark:text-green-300", border: "border-green-500" }, moderate: { bg: "bg-yellow-100 dark:bg-yellow-900", text: "text-yellow-700 dark:text-yellow-300", border: "border-yellow-500" }, high: { bg: "bg-orange-100 dark:bg-orange-900", text: "text-orange-700 dark:text-orange-300", border: "border-orange-500" }, extreme: { bg: "bg-red-100 dark:bg-red-900", text: "text-red-700 dark:text-red-300", border: "border-red-500" }, }; const budgetColors = { comfortable: "bg-green-500", marginal: "bg-yellow-500", overloaded: "bg-red-500", }; const budgetHexColors = { comfortable: "#22c55e", marginal: "#eab308", overloaded: "#ef4444", }; function tempColorHex(temp) { if (temp >= 40) return "#dc2626"; if (temp >= 35) return "#f97316"; if (temp >= 30) return "#facc15"; if (temp >= 25) return "#fde68a"; if (temp >= 20) return "#bbf7d0"; return "#bfdbfe"; } window.loadDashboard = async function() { hide("no-data"); hide("no-forecast"); hide("error-state"); hide("data-display"); show("loading"); try { const profileId = await getActiveProfileId(); if (!profileId) { hide("loading"); show("no-data"); return; } const forecasts = await dbGetByIndex("forecasts", "profileId", profileId); if (forecasts.length === 0) { hide("loading"); show("no-forecast"); return; } const today = new Date().toISOString().slice(0, 10); const payload = await getComputePayload(profileId, today); if (!payload) { hide("loading"); show("no-data"); return; } const resp = await fetch("/api/compute/dashboard", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || "Compute failed"); } const data = await resp.json(); hide("loading"); show("data-display"); renderDashboard(data); // LLM summary (async) try { const llmBody = { date: data.date, peakTempC: data.peakTempC, minNightTempC: data.minNightTempC, riskLevel: data.riskLevel, acHeadroomBTUH: data.roomBudgets && data.roomBudgets.length > 0 ? data.roomBudgets[0].headroomBtuh : 0, budgetStatus: data.roomBudgets && data.roomBudgets.length > 0 ? data.roomBudgets[0].status : "comfortable", language: window.HG && window.HG.lang === "de" ? "German" : "English", }; // Include client-side LLM settings if available const llmProvider = await getSetting("llmProvider"); const llmApiKey = await getSetting("llmApiKey"); const llmModel = await getSetting("llmModel"); if (llmProvider && llmApiKey) { llmBody.provider = llmProvider; llmBody.apiKey = llmApiKey; if (llmModel) llmBody.model = llmModel; } const llmResp = await fetch("/api/llm/summarize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(llmBody), }); if (llmResp.ok) { const llmData = await llmResp.json(); if (llmData.summary) { $("llm-summary").innerHTML = `

${esc(llmData.summary)}

AI-generated summary. Not a substitute for professional advice.

`; } else { $("llm-section").classList.add("hidden"); } } } catch (e) { $("llm-section").classList.add("hidden"); } } catch (err) { hide("loading"); show("error-state"); console.error("Dashboard error:", err); } }; function renderDashboard(data) { $("profile-name").textContent = data.profileName + " \u2014 " + data.date; // Risk card const rc = riskColors[data.riskLevel] || riskColors.low; $("risk-card").className = `rounded-xl p-4 text-center shadow-sm ${rc.bg}`; $("risk-level").className = `text-2xl font-bold capitalize ${rc.text}`; $("risk-level").textContent = data.riskLevel; // Peak temp $("peak-temp").textContent = data.peakTempC.toFixed(1) + "\u00b0C"; // Min night temp $("min-night-temp").textContent = data.minNightTempC.toFixed(1) + "\u00b0C"; if (data.poorNightCool) { $("poor-night-cool").classList.remove("hidden"); } // Warnings if (data.warnings && data.warnings.length > 0) { show("warnings-section"); $("warnings-section").innerHTML = '

Warnings

' + data.warnings.map(w => `
${esc(w.headline)}
${esc(w.description)}
${w.instruction ? `
${esc(w.instruction)}
` : ''}
${esc(w.onset)} \u2014 ${esc(w.expires)}
`).join(""); } // Risk windows if (data.riskWindows && data.riskWindows.length > 0) { show("risk-windows-section"); $("risk-windows").innerHTML = data.riskWindows.map(w => { const wc = riskColors[w.level] || riskColors.low; return `
${String(w.startHour).padStart(2,'0')}:00\u2013${String(w.endHour).padStart(2,'0')}:00 ${w.level} ${w.peakTempC.toFixed(1)}\u00b0C \u2014 ${esc(w.reason)}
`; }).join(""); } // Timeline SVG chart + budget strip if (data.timeline && data.timeline.length > 0) { renderTimelineChart(data.timeline); renderBudgetStrip(data.timeline); } // Room budgets if (data.roomBudgets && data.roomBudgets.length > 0) { show("budgets-section"); $("room-budgets").innerHTML = data.roomBudgets.map(rb => { const maxVal = Math.max(rb.totalGainBtuh, rb.acCapacityBtuh, 1); const gainPct = Math.min((rb.totalGainBtuh / maxVal) * 100, 100); const capPct = Math.min((rb.acCapacityBtuh / maxVal) * 100, 100); return `
${esc(rb.roomName)}
Internal${rb.internalGainsW.toFixed(0)} W
Solar${rb.solarGainW.toFixed(0)} W
Ventilation${rb.ventGainW.toFixed(0)} W
Total${rb.totalGainBtuh.toFixed(0)} BTU/h
AC Capacity${rb.acCapacityBtuh.toFixed(0)} BTU/h
Headroom${rb.headroomBtuh.toFixed(0)} BTU/h
GainAC
`; }).join(""); } // Care checklist if (data.careChecklist && data.careChecklist.length > 0) { show("care-section"); $("care-checklist").innerHTML = data.careChecklist.map(item => `
  • ${esc(item)}
  • `).join(""); } } // ========== SVG Timeline Chart ========== function renderTimelineChart(timeline) { const container = $("timeline-chart"); const width = 720; const height = 200; const padLeft = 40; const padRight = 10; const padTop = 15; const padBottom = 25; const chartW = width - padLeft - padRight; const chartH = height - padTop - padBottom; const temps = timeline.map(s => s.tempC); const minTemp = Math.floor(Math.min(...temps) / 5) * 5; const maxTemp = Math.ceil(Math.max(...temps) / 5) * 5; const tempRange = maxTemp - minTemp || 10; function x(i) { return padLeft + (i / (timeline.length - 1)) * chartW; } function y(t) { return padTop + (1 - (t - minTemp) / tempRange) * chartH; } // Build area path (temperature curve filled to bottom) const points = timeline.map((s, i) => `${x(i).toFixed(1)},${y(s.tempC).toFixed(1)}`); const linePath = `M${points.join(" L")}`; const areaPath = `${linePath} L${x(timeline.length - 1).toFixed(1)},${(padTop + chartH).toFixed(1)} L${padLeft},${(padTop + chartH).toFixed(1)} Z`; // Build gradient stops based on temperature const gradientStops = timeline.map((s, i) => { const pct = ((i / (timeline.length - 1)) * 100).toFixed(1); return ``; }).join(""); const gradientStopsLine = timeline.map((s, i) => { const pct = ((i / (timeline.length - 1)) * 100).toFixed(1); return ``; }).join(""); // Threshold lines const thresholds = [30, 35, 40].filter(t => t > minTemp && t < maxTemp); const thresholdLines = thresholds.map(t => ` ${t}\u00b0` ).join(""); // Y-axis labels (min, max) const yLabels = ` ${maxTemp}\u00b0 ${minTemp}\u00b0 `; // X-axis labels (every 3 hours) const xLabels = timeline .filter((s, i) => s.hour % 3 === 0) .map((s, i) => { const idx = timeline.findIndex(t => t.hour === s.hour); return `${String(s.hour).padStart(2, '0')}`; }).join(""); // Data points (circles) const circles = timeline.map((s, i) => `` ).join(""); const svg = ` ${gradientStops} ${gradientStopsLine} ${thresholdLines} ${yLabels} ${xLabels} ${circles} `; container.innerHTML = svg; // Tooltip on hover/click const tooltip = $("timeline-tooltip"); container.querySelectorAll(".timeline-dot").forEach(dot => { const handler = (e) => { const idx = parseInt(dot.dataset.idx); const slot = timeline[idx]; const actions = slot.actions && slot.actions.length > 0 ? slot.actions.map(a => `\u2022 ${esc(a.name)}`).join("
    ") : "No actions"; tooltip.innerHTML = `
    ${slot.hourStr}
    ${slot.tempC.toFixed(1)}\u00b0C ยท ${(slot.humidityPct || 0).toFixed(0)}% RH
    ${slot.budgetStatus}
    ${actions}
    `; const rect = dot.getBoundingClientRect(); tooltip.style.left = (rect.left + window.scrollX - 60) + "px"; tooltip.style.top = (rect.top + window.scrollY - tooltip.offsetHeight - 8) + "px"; tooltip.classList.remove("hidden"); }; dot.addEventListener("mouseenter", handler); dot.addEventListener("click", handler); }); container.addEventListener("mouseleave", () => tooltip.classList.add("hidden")); } // ========== Budget Status Strip ========== function renderBudgetStrip(timeline) { const strip = $("timeline-strip"); strip.innerHTML = timeline.map(slot => { const color = budgetHexColors[slot.budgetStatus] || "#d1d5db"; return `
    `; }).join(""); } function esc(s) { if (!s) return ""; const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } // Init loadDashboard(); })();