|
|
|
|
@@ -7,12 +7,15 @@
|
|
|
|
|
|
|
|
|
|
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" },
|
|
|
|
|
@@ -20,6 +23,7 @@
|
|
|
|
|
|
|
|
|
|
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" },
|
|
|
|
|
@@ -262,9 +266,13 @@
|
|
|
|
|
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[data.riskLevel] || riskColors.low;
|
|
|
|
|
const rg = riskGradients[data.riskLevel] || riskGradients.low;
|
|
|
|
|
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;
|
|
|
|
|
@@ -273,9 +281,10 @@
|
|
|
|
|
riskCard.style.background = `linear-gradient(135deg, ${gradFrom}, ${gradTo})`;
|
|
|
|
|
|
|
|
|
|
const shieldSvg = '<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
|
|
|
|
|
$("risk-icon").innerHTML = `<span class="${rc.text}">${shieldSvg}</span>`;
|
|
|
|
|
const checkSvg = '<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><path d="M22 4L12 14.01l-3-3"/></svg>';
|
|
|
|
|
$("risk-icon").innerHTML = `<span class="${rc.text}">${isComfort ? checkSvg : shieldSvg}</span>`;
|
|
|
|
|
$("risk-level").className = `text-2xl font-bold capitalize ${rc.text}`;
|
|
|
|
|
$("risk-level").textContent = data.riskLevel;
|
|
|
|
|
$("risk-level").textContent = isComfort ? (t().riskComfort || "Comfortable") : data.riskLevel;
|
|
|
|
|
|
|
|
|
|
// Peak temp
|
|
|
|
|
$("peak-temp").textContent = data.peakTempC.toFixed(1) + "\u00b0C";
|
|
|
|
|
@@ -323,7 +332,7 @@
|
|
|
|
|
|
|
|
|
|
// Timeline heatmap
|
|
|
|
|
if (data.timeline && data.timeline.length > 0) {
|
|
|
|
|
renderTimelineHeatmap(data.timeline);
|
|
|
|
|
renderTimelineHeatmap(data.timeline, data.timezone, data.date);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Room budgets
|
|
|
|
|
@@ -491,14 +500,31 @@
|
|
|
|
|
sealed: t().coolSealed || "Keep sealed",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function renderTimelineHeatmap(timeline) {
|
|
|
|
|
function renderTimelineHeatmap(timeline, timezone, dashDate) {
|
|
|
|
|
_currentTimeline = timeline;
|
|
|
|
|
_currentTimezone = timezone || null;
|
|
|
|
|
_currentDashDate = dashDate || null;
|
|
|
|
|
const container = $("timeline-chart");
|
|
|
|
|
const tooltip = $("timeline-tooltip");
|
|
|
|
|
const labels = coolModeLabels();
|
|
|
|
|
|
|
|
|
|
// Hour labels (every 3h)
|
|
|
|
|
// 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 `<div class="text-center text-xs font-bold text-orange-500">▼</div>`;
|
|
|
|
|
if (s.hour % 3 !== 0) return `<div></div>`;
|
|
|
|
|
return `<div class="text-center text-xs text-gray-400">${String(s.hour).padStart(2, "0")}</div>`;
|
|
|
|
|
}).join("");
|
|
|
|
|
@@ -507,7 +533,9 @@
|
|
|
|
|
const tempCellsHtml = timeline.map((s, i) => {
|
|
|
|
|
const color = tempColorHex(s.tempC);
|
|
|
|
|
const textColor = (s.tempC >= 35 || s.tempC < 0) ? "white" : "#1f2937";
|
|
|
|
|
return `<div class="hm-temp-cell flex items-center justify-center rounded-sm cursor-pointer" data-idx="${i}" style="background:${color};color:${textColor};height:48px">`
|
|
|
|
|
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 `<div class="hm-temp-cell flex items-center justify-center rounded-sm cursor-pointer" data-idx="${i}" style="background:${color};color:${textColor};height:48px;${ringStyle}">`
|
|
|
|
|
+ `<span class="hm-temp-label hidden sm:inline text-xs font-medium">${Math.round(s.tempC)}</span></div>`;
|
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
|
|
@@ -696,6 +724,8 @@
|
|
|
|
|
// Reset LLM/AI state so loadDashboard triggers fresh calls
|
|
|
|
|
_hourActionMap = null;
|
|
|
|
|
_currentTimeline = null;
|
|
|
|
|
_currentTimezone = null;
|
|
|
|
|
_currentDashDate = null;
|
|
|
|
|
_qsInitialized = false;
|
|
|
|
|
|
|
|
|
|
await loadDashboard();
|
|
|
|
|
@@ -759,6 +789,8 @@
|
|
|
|
|
_qsInitialized = false;
|
|
|
|
|
_hourActionMap = null;
|
|
|
|
|
_currentTimeline = null;
|
|
|
|
|
_currentTimezone = null;
|
|
|
|
|
_currentDashDate = null;
|
|
|
|
|
loadDashboard();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Quick settings apply error:", e);
|
|
|
|
|
|