// Dashboard page logic (function() { "use strict"; const $ = (id) => document.getElementById(id); const t = () => window.HG.t || {}; let _hourActionMap = null; let _currentTimeline = null; let _currentTimezone = null; let _currentDashDate = null; 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" }, comfort: { bg: "bg-teal-100 dark:bg-teal-900", text: "text-teal-700 dark:text-teal-300", border: "border-teal-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 riskGradients = { low: { from: "#f0fdf4", to: "#dcfce7", darkFrom: "#052e16", darkTo: "#064e3b" }, comfort: { from: "#f0fdfa", to: "#ccfbf1", darkFrom: "#042f2e", darkTo: "#134e4a" }, moderate: { from: "#fefce8", to: "#fef9c3", darkFrom: "#422006", darkTo: "#3f3f00" }, high: { from: "#fff7ed", to: "#ffedd5", darkFrom: "#431407", darkTo: "#7c2d12" }, extreme: { from: "#fef2f2", to: "#fecaca", darkFrom: "#450a0a", darkTo: "#7f1d1d" }, }; const warningSeverityColors = { Minor: { bg: "bg-yellow-50 dark:bg-yellow-900/30", border: "border-yellow-300 dark:border-yellow-700", text: "text-yellow-700 dark:text-yellow-300", pill: "bg-yellow-200 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-200" }, Moderate: { bg: "bg-orange-50 dark:bg-orange-900/30", border: "border-orange-300 dark:border-orange-700", text: "text-orange-700 dark:text-orange-300", pill: "bg-orange-200 text-orange-800 dark:bg-orange-800 dark:text-orange-200" }, Severe: { bg: "bg-red-50 dark:bg-red-900/30", border: "border-red-300 dark:border-red-800", text: "text-red-700 dark:text-red-300", pill: "bg-red-200 text-red-800 dark:bg-red-800 dark:text-red-200" }, Extreme: { bg: "bg-red-100 dark:bg-red-900/50", border: "border-red-500 dark:border-red-600", text: "text-red-800 dark:text-red-200", pill: "bg-red-300 text-red-900 dark:bg-red-700 dark:text-red-100" }, }; const budgetBorderColors = { comfortable: "border-green-400", marginal: "border-yellow-400", overloaded: "border-red-400", }; const effortColors = { none: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300", low: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300", medium: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300", high: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300", }; const impactColors = { low: "bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300", medium: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300", high: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300", }; const categoryIcons = { shading: '', ventilation: '', internal_gains: '', ac_strategy: '', hydration: '', care: '', }; 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"; if (temp >= 10) return "#bfdbfe"; if (temp >= 0) return "#93c5fd"; return "#6366f1"; } function isDark() { return document.documentElement.classList.contains("dark") || window.matchMedia("(prefers-color-scheme: dark)").matches; } 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); initQuickSettings(); // LLM credentials (shared between summary and actions) const llmProvider = await getSetting("llmProvider"); const llmApiKey = await getSetting("llmApiKey"); const llmModel = await getSetting("llmModel"); const language = window.HG && window.HG.lang === "de" ? "German" : "English"; // 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: language, }; 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) { renderLlmSummary(llmData.summary); } else { $("llm-section").classList.add("hidden"); } } } catch (e) { $("llm-section").classList.add("hidden"); } // AI actions (async) — skip when no heat risk const needsHeatActions = data.peakTempC >= 22 || data.riskLevel !== "low"; if (llmProvider && llmApiKey && needsHeatActions) { show("actions-loading"); try { const rooms = (payload.rooms || []).map(r => ({ name: r.name, orientation: r.orientation || "", shadingType: r.shadingType || "none", hasAC: (payload.acAssignments || []).some(a => a.roomId === r.id), })); const actionsBody = { date: data.date, indoorTempC: data.indoorTempC, peakTempC: data.peakTempC, minNightTempC: data.minNightTempC, poorNightCool: data.poorNightCool, riskLevel: data.riskLevel, riskWindows: data.riskWindows || [], timeline: (data.timeline || []).map(s => ({ hour: s.hour, tempC: s.tempC, humidityPct: s.humidityPct, budgetStatus: s.budgetStatus, coolMode: s.coolMode, })), roomBudgets: (data.roomBudgets || []).map(rb => ({ totalGainW: rb.totalGainW, })), rooms: rooms, provider: llmProvider, apiKey: llmApiKey, language: language, }; if (llmModel) actionsBody.model = llmModel; const actResp = await fetch("/api/llm/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(actionsBody), }); if (actResp.ok) { const actData = await actResp.json(); if (actData.actions && actData.actions.length > 0) { renderAIActions(actData.actions); _hourActionMap = buildHourActionMap(actData.actions); if (_currentTimeline) { renderTimelineAnnotations(_currentTimeline, _hourActionMap); renderTimelineLegend(_currentTimeline, _hourActionMap); } } else { hide("actions-loading"); } } else { hide("actions-loading"); } } catch (e) { hide("actions-loading"); } } } catch (err) { hide("loading"); show("error-state"); console.error("Dashboard error:", err); } }; // ========== Mini Markdown ========== function miniMarkdown(text) { return text .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/^- (.+)$/gm, "
  • $1
  • "); } // ========== LLM Summary ========== function renderLlmSummary(summary) { const paragraphs = summary.split(/\n\n+/).filter(Boolean); const html = paragraphs.map(p => { const rendered = miniMarkdown(esc(p)); if (rendered.includes("${rendered}`; } return `

    ${rendered}

    `; }).join(""); const el = $("llm-summary"); el.innerHTML = `
    ${html}

    ${esc(t().aiDisclaimer)}

    `; // Fade in requestAnimationFrame(() => el.classList.remove("opacity-0")); } // ========== Main Render ========== function renderDashboard(data) { $("profile-name").textContent = data.profileName + " \u2014 " + data.date; // Detect comfort day: cold weather, low risk const isComfort = data.peakTempC < 22 && data.riskLevel === "low"; const riskKey = isComfort ? "comfort" : data.riskLevel; // Risk card with gradient const rc = riskColors[riskKey] || riskColors.low; const rg = riskGradients[riskKey] || riskGradients.low; const dark = isDark(); const gradFrom = dark ? rg.darkFrom : rg.from; const gradTo = dark ? rg.darkTo : rg.to; const riskCard = $("risk-card"); riskCard.className = "rounded-xl p-4 text-center shadow-sm transition-all duration-200 hover:shadow-md"; riskCard.style.background = `linear-gradient(135deg, ${gradFrom}, ${gradTo})`; const shieldSvg = ''; const checkSvg = ''; $("risk-icon").innerHTML = `${isComfort ? checkSvg : shieldSvg}`; $("risk-level").className = `text-2xl font-bold capitalize ${rc.text}`; $("risk-level").textContent = isComfort ? (t().riskComfort || "Comfortable") : 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 — severity-based coloring if (data.warnings && data.warnings.length > 0) { show("warnings-section"); $("warnings-section").innerHTML = '

    ' + esc(t().warnings) + '

    ' + data.warnings.map(w => { const sc = warningSeverityColors[w.severity] || warningSeverityColors.Moderate; return `
    ${esc(w.headline)} ${esc(w.severity)}
    ${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 heatmap if (data.timeline && data.timeline.length > 0) { renderTimelineHeatmap(data.timeline, data.timezone, data.date); } // 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 internalPct = (rb.internalGainsW / maxVal) * 100; const solarPct = (rb.solarGainW / maxVal) * 100; const ventPct = (rb.ventGainW / maxVal) * 100; const capPct = Math.min((rb.acCapacityBtuh / maxVal) * 100, 100); const bc = budgetBorderColors[rb.status] || budgetBorderColors.comfortable; return `
    ${esc(rb.roomName)}
    ${esc(t().internalGains)}${rb.internalGainsW.toFixed(0)} W
    ${esc(t().solarGain)}${rb.solarGainW.toFixed(0)} W
    ${esc(t().ventGain)}${rb.ventGainW.toFixed(0)} W
    ${esc(t().totalGain)}${rb.totalGainBtuh.toFixed(0)} BTU/h
    ${esc(t().acCapacity)}${rb.acCapacityBtuh.toFixed(0)} BTU/h
    ${esc(t().headroom)}${rb.headroomBtuh.toFixed(0)} BTU/h
    ${esc(t().internalGains)} ${esc(t().solarGain)} ${esc(t().ventGain)} ${esc(t().acCapacity)}
    `; }).join(""); } // Care checklist if (data.careChecklist && data.careChecklist.length > 0) { show("care-section"); $("care-checklist").innerHTML = data.careChecklist.map(item => `
  • ${esc(item)}
  • `).join(""); } // Fade in LLM skeleton requestAnimationFrame(() => $("llm-summary").classList.remove("opacity-0")); } function formatHourRange(hours) { if (hours.length === 0) return ""; if (hours.length === 1) return String(hours[0]).padStart(2, '0') + ":00"; const ranges = []; let start = hours[0], end = hours[0]; for (let i = 1; i < hours.length; i++) { if (hours[i] === end + 1) { end = hours[i]; } else { ranges.push(start === end ? String(start).padStart(2, '0') + ":00" : String(start).padStart(2, '0') + ":00\u2013" + String(end).padStart(2, '0') + ":00"); start = end = hours[i]; } } ranges.push(start === end ? String(start).padStart(2, '0') + ":00" : String(start).padStart(2, '0') + ":00\u2013" + String(end).padStart(2, '0') + ":00"); return ranges.join(", "); } // ========== AI Actions ========== function renderAIActions(actions) { const groups = {}; actions.forEach(a => { const cat = a.category || "other"; if (!groups[cat]) groups[cat] = []; groups[cat].push(a); }); const categoryOrder = ["shading", "ventilation", "internal_gains", "ac_strategy", "hydration", "care"]; const sortedCats = Object.keys(groups).sort((a, b) => { const ia = categoryOrder.indexOf(a); const ib = categoryOrder.indexOf(b); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib); }); const html = sortedCats.map(cat => { const icon = categoryIcons[cat] || ''; const label = (t().category && t().category[cat]) || cat; const cards = groups[cat].map(a => { const hours = (a.hours || []).sort((x, y) => x - y); const hourRange = formatHourRange(hours); const ec = effortColors[a.effort] || effortColors.none; const ic = impactColors[a.impact] || impactColors.low; return `
    ${esc(a.name)}
    ${hourRange}
    ${a.description ? `
    ${esc(a.description)}
    ` : ''}
    ${esc(t().effort)}: ${esc(a.effort || 'none')} ${esc(t().impact)}: ${esc(a.impact || 'low')}
    `; }).join(""); return `
    ${icon} ${esc(label)}
    ${cards}
    `; }).join(""); hide("actions-loading"); show("actions-section"); const badge = $("actions-badge"); if (badge) { badge.textContent = t().aiActions || "AI"; badge.classList.remove("hidden"); } $("actions-list").innerHTML = html; } // ========== Heatmap Timeline ========== const coolModeColors = { comfort: "#6ee7b7", ventilate: "#38bdf8", ac: "#4ade80", overloaded: "#f87171", sealed: "#a78bfa", }; const categoryColors = { shading: "#f59e0b", ventilation: "#38bdf8", internal_gains: "#a78bfa", ac_strategy: "#4ade80", hydration: "#60a5fa", care: "#fb7185", }; const coolModeLabels = () => ({ comfort: t().coolComfort || "No cooling needed", ventilate: t().coolVentilate || "Open windows", ac: t().coolAC || "AC cooling", overloaded: t().coolOverloaded || "AC overloaded", sealed: t().coolSealed || "Keep sealed", }); function renderTimelineHeatmap(timeline, timezone, dashDate) { _currentTimeline = timeline; _currentTimezone = timezone || null; _currentDashDate = dashDate || null; const container = $("timeline-chart"); const tooltip = $("timeline-tooltip"); const labels = coolModeLabels(); // Determine current hour in profile timezone (only if dashboard date is today) let currentHour = -1; if (timezone && dashDate) { try { const now = new Date(); const todayInTz = now.toLocaleDateString("en-CA", { timeZone: timezone }); if (todayInTz === dashDate) { const hourStr = now.toLocaleString("en-US", { timeZone: timezone, hour12: false, hour: "2-digit" }); currentHour = parseInt(hourStr, 10); } } catch (_) { /* invalid timezone, skip */ } } // Hour labels (every 3h) + current hour marker const hourLabelsHtml = timeline.map(s => { const isCurrent = s.hour === currentHour; if (isCurrent) return `
    `; if (s.hour % 3 !== 0) return `
    `; return `
    ${String(s.hour).padStart(2, "0")}
    `; }).join(""); // Temp cells const tempCellsHtml = timeline.map((s, i) => { const color = tempColorHex(s.tempC); const textColor = (s.tempC >= 35 || s.tempC < 0) ? "white" : "#1f2937"; const isCurrent = s.hour === currentHour; const ringStyle = isCurrent ? "outline:2px solid white;outline-offset:-2px;box-shadow:0 0 0 3px rgba(249,115,22,0.6);" : ""; return `
    ` + `
    `; }).join(""); // Cooling mode cells const coolCellsHtml = timeline.map(s => { const mode = s.coolMode || "ac"; const color = coolModeColors[mode] || "#d1d5db"; return `
    `; }).join(""); container.innerHTML = `
    ${hourLabelsHtml}
    ${tempCellsHtml}
    ${coolCellsHtml}
    `; // Tooltip handlers container.querySelectorAll(".hm-temp-cell").forEach(cell => { const handler = () => { const idx = parseInt(cell.dataset.idx); const slot = timeline[idx]; const modeLabel = labels[slot.coolMode] || slot.coolMode || ""; let tooltipHtml = `
    ${slot.hourStr}
    ${slot.tempC.toFixed(1)}\u00b0C \u00b7 ${(slot.humidityPct || 0).toFixed(0)}% RH${slot.pressureHpa ? ` \u00b7 ${slot.pressureHpa.toFixed(0)} hPa` : ""}
    ${slot.budgetStatus} \u00b7 ${esc(modeLabel)}
    `; const hourActions = _hourActionMap && _hourActionMap[slot.hour]; if (hourActions && hourActions.length > 0) { const catLabels = (t().category) || {}; const maxShow = 4; const shown = hourActions.slice(0, maxShow); const remaining = hourActions.length - maxShow; tooltipHtml += `
    `; shown.forEach(a => { const color = categoryColors[a.category] || "#9ca3af"; tooltipHtml += `
    ${esc(a.name)}
    `; }); if (remaining > 0) { tooltipHtml += `
    +${remaining} more
    `; } tooltipHtml += `
    `; } tooltip.innerHTML = tooltipHtml; const rect = cell.getBoundingClientRect(); const parentRect = tooltip.parentElement.getBoundingClientRect(); tooltip.style.left = (rect.left - parentRect.left + rect.width / 2 - 60) + "px"; tooltip.style.top = (rect.top - parentRect.top - tooltip.offsetHeight - 8) + "px"; tooltip.classList.remove("hidden"); }; cell.addEventListener("mouseenter", handler); cell.addEventListener("click", handler); }); container.addEventListener("mouseleave", () => tooltip.classList.add("hidden")); // Legend (initial render without AI data) renderTimelineLegend(timeline, _hourActionMap); } // ========== AI Timeline Annotations ========== function buildHourActionMap(actions) { const map = {}; (actions || []).forEach(a => { (a.hours || []).forEach(h => { if (!map[h]) map[h] = []; map[h].push(a); }); }); return Object.keys(map).length > 0 ? map : null; } function renderTimelineLegend(timeline, hourActionMap) { const legend = $("cooling-legend"); if (!legend) return; const labels = coolModeLabels(); const ts = t(); // Temperature scale const tempSteps = [ { label: "<0", color: "#6366f1" }, { label: "0", color: "#93c5fd" }, { label: "10", color: "#bfdbfe" }, { label: "20", color: "#bbf7d0" }, { label: "25", color: "#fde68a" }, { label: "30", color: "#facc15" }, { label: "35", color: "#f97316" }, { label: "40+", color: "#dc2626" }, ]; let html = `
    `; html += `${esc(ts.legendTemp || "Temperature")}`; html += tempSteps.map(s => `${s.label}` ).join(""); html += `
    `; // Cooling modes const usedModes = [...new Set((timeline || []).map(s => s.coolMode || "ac"))]; html += `
    `; html += `${esc(ts.legendCooling || "Cooling")}`; html += usedModes.map(mode => { const color = coolModeColors[mode] || "#d1d5db"; const lbl = labels[mode] || mode; return `${esc(lbl)}`; }).join(""); html += `
    `; // AI categories (only when hourActionMap has entries) if (hourActionMap) { const usedCats = new Set(); Object.values(hourActionMap).forEach(actions => { actions.forEach(a => { if (a.category) usedCats.add(a.category); }); }); if (usedCats.size > 0) { const catLabels = ts.category || {}; html += `
    `; html += `${esc(ts.legendAI || "AI Actions")}`; const categoryOrder = ["shading", "ventilation", "internal_gains", "ac_strategy", "hydration", "care"]; const sorted = [...usedCats].sort((a, b) => { const ia = categoryOrder.indexOf(a); const ib = categoryOrder.indexOf(b); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib); }); html += sorted.map(cat => { const color = categoryColors[cat] || "#9ca3af"; const lbl = catLabels[cat] || cat; return `${esc(lbl)}`; }).join(""); html += `
    `; } } legend.innerHTML = html; } function renderTimelineAnnotations(timeline, hourActionMap) { if (!hourActionMap || !timeline) return; const container = $("timeline-chart"); if (!container) return; // Remove previous annotation row if any const prev = container.querySelector(".hm-ai-row"); if (prev) prev.remove(); const cellsHtml = timeline.map(s => { const actions = hourActionMap[s.hour]; if (!actions || actions.length === 0) { return `
    `; } const cats = [...new Set(actions.map(a => a.category).filter(Boolean))]; const dots = cats.map(cat => { const color = categoryColors[cat] || "#9ca3af"; return ``; }).join(""); return `
    ${dots}
    `; }).join(""); const row = document.createElement("div"); row.className = "hm-ai-row grid gap-px mt-px"; row.style.gridTemplateColumns = `repeat(${timeline.length},minmax(0,1fr))`; row.innerHTML = cellsHtml; container.appendChild(row); } // ========== Forecast Refresh ========== async function refreshForecast() { const btn = $("refresh-forecast-btn"); const icon = $("refresh-icon"); if (!btn) return; btn.disabled = true; icon.classList.add("animate-spin"); try { const profileId = await getActiveProfileId(); if (!profileId) return; await fetchForecastForProfile(profileId); // Reset LLM/AI state so loadDashboard triggers fresh calls _hourActionMap = null; _currentTimeline = null; _currentTimezone = null; _currentDashDate = null; _qsInitialized = false; await loadDashboard(); } catch (err) { console.error("Forecast refresh error:", err); } finally { btn.disabled = false; icon.classList.remove("animate-spin"); } } // Attach handler after DOM is ready const refreshBtn = $("refresh-forecast-btn"); if (refreshBtn) refreshBtn.addEventListener("click", refreshForecast); // ========== Quick Settings ========== let _qsInitialized = false; async function initQuickSettings() { if (_qsInitialized) return; _qsInitialized = true; const toggle = $("qs-toggle"); const body = $("qs-body"); const chevron = $("qs-chevron"); if (!toggle || !body) return; toggle.addEventListener("click", () => { body.classList.toggle("hidden"); chevron.style.transform = body.classList.contains("hidden") ? "" : "rotate(180deg)"; }); // Load current values from first room try { const profileId = await getActiveProfileId(); if (profileId) { const rooms = await dbGetByIndex("rooms", "profileId", profileId); if (rooms.length > 0) { const r = rooms[0]; if (r.indoorTempC) $("qs-indoor-temp").value = r.indoorTempC; if (r.indoorHumidityPct) $("qs-indoor-humidity").value = r.indoorHumidityPct; } } } catch (_) { /* ignore */ } $("qs-apply").addEventListener("click", async () => { const tempVal = parseFloat($("qs-indoor-temp").value); const humVal = parseFloat($("qs-indoor-humidity").value); try { const profileId = await getActiveProfileId(); if (!profileId) return; const rooms = await dbGetByIndex("rooms", "profileId", profileId); for (const room of rooms) { if (!isNaN(tempVal) && tempVal >= 15 && tempVal <= 35) room.indoorTempC = tempVal; if (!isNaN(humVal) && humVal >= 20 && humVal <= 95) { room.indoorHumidityPct = humVal; } else { delete room.indoorHumidityPct; } await dbPut("rooms", room); } _qsInitialized = false; _hourActionMap = null; _currentTimeline = null; _currentTimezone = null; _currentDashDate = null; loadDashboard(); } catch (e) { console.error("Quick settings apply error:", e); } }); } function esc(s) { if (!s) return ""; const div = document.createElement("div"); div.textContent = s; return div.innerHTML; } // Init loadDashboard(); })();