From 80466464e69a26042e193484ae06ef40c9c601a4 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Tue, 10 Feb 2026 04:36:04 +0100 Subject: [PATCH] feat: add timeline current-hour highlight and comfort risk card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass profile timezone through DashboardData so the frontend can compute the current hour and highlight it on the heatmap (white ring + orange triangle marker). Only activates when the dashboard date matches today in the profile timezone. In cold weather (peak < 22°C, risk low), the risk card now shows a teal "Comfortable" presentation with a checkmark icon instead of the generic green "Low" shield. --- internal/compute/compute.go | 1 + internal/compute/compute_test.go | 4 +++ internal/compute/types.go | 1 + web/i18n/de.json | 1 + web/i18n/en.json | 1 + web/js/dashboard.js | 48 ++++++++++++++++++++++++++------ web/templates/dashboard.html | 1 + 7 files changed, 49 insertions(+), 8 deletions(-) diff --git a/internal/compute/compute.go b/internal/compute/compute.go index c32797c..14e5e8d 100644 --- a/internal/compute/compute.go +++ b/internal/compute/compute.go @@ -82,6 +82,7 @@ func BuildDashboard(req ComputeRequest) (DashboardData, error) { GeneratedAt: time.Now(), ProfileName: req.Profile.Name, Date: req.Date, + Timezone: req.Profile.Timezone, RiskLevel: dayRisk.Level.String(), PeakTempC: dayRisk.PeakTempC, MinNightTempC: dayRisk.MinNightTempC, diff --git a/internal/compute/compute_test.go b/internal/compute/compute_test.go index 738c41c..781927d 100644 --- a/internal/compute/compute_test.go +++ b/internal/compute/compute_test.go @@ -505,6 +505,10 @@ func TestBuildDashboard_CoolModeComfort(t *testing.T) { } } + if data.Timezone != "UTC" { + t.Errorf("got Timezone %q, want %q", data.Timezone, "UTC") + } + // Room budget should be marginal (not overloaded) — no AC but ventilation can solve if len(data.RoomBudgets) == 0 { t.Fatal("expected room budgets") diff --git a/internal/compute/types.go b/internal/compute/types.go index a274433..8837dd4 100644 --- a/internal/compute/types.go +++ b/internal/compute/types.go @@ -109,6 +109,7 @@ type DashboardData struct { GeneratedAt time.Time `json:"generatedAt"` ProfileName string `json:"profileName"` Date string `json:"date"` + Timezone string `json:"timezone"` RiskLevel string `json:"riskLevel"` PeakTempC float64 `json:"peakTempC"` MinNightTempC float64 `json:"minNightTempC"` diff --git a/web/i18n/de.json b/web/i18n/de.json index 23c69ee..a453445 100644 --- a/web/i18n/de.json +++ b/web/i18n/de.json @@ -183,6 +183,7 @@ "effort": "Aufwand", "impact": "Wirkung", "aiDisclaimer": "KI-generierte Zusammenfassung. Kein Ersatz für professionelle Beratung.", + "riskComfort": "Komfortabel", "coolComfort": "Keine Kühlung nötig", "coolVentilate": "Fenster öffnen", "coolAC": "Klimaanlage", diff --git a/web/i18n/en.json b/web/i18n/en.json index 4c416f8..f72b0dc 100644 --- a/web/i18n/en.json +++ b/web/i18n/en.json @@ -183,6 +183,7 @@ "effort": "Effort", "impact": "Impact", "aiDisclaimer": "AI-generated summary. Not a substitute for professional advice.", + "riskComfort": "Comfortable", "coolComfort": "No cooling needed", "coolVentilate": "Open windows", "coolAC": "AC cooling", diff --git a/web/js/dashboard.js b/web/js/dashboard.js index 26adc00..ea3a52e 100644 --- a/web/js/dashboard.js +++ b/web/js/dashboard.js @@ -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 = ''; - $("risk-icon").innerHTML = `${shieldSvg}`; + const checkSvg = ''; + $("risk-icon").innerHTML = `${isComfort ? checkSvg : shieldSvg}`; $("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 `
`; if (s.hour % 3 !== 0) return `
`; return `
${String(s.hour).padStart(2, "0")}
`; }).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 `
` + 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(""); @@ -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); diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html index e4064a5..08974e2 100644 --- a/web/templates/dashboard.html +++ b/web/templates/dashboard.html @@ -179,6 +179,7 @@ totalGain: "{{t "dashboard.totalGain"}}", acCapacity: "{{t "dashboard.acCapacity"}}", headroom: "{{t "dashboard.headroom"}}", + riskComfort: "{{t "dashboard.riskComfort"}}", coolComfort: "{{t "dashboard.coolComfort"}}", coolVentilate: "{{t "dashboard.coolVentilate"}}", coolAC: "{{t "dashboard.coolAC"}}",