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.
209 lines
11 KiB
HTML
209 lines
11 KiB
HTML
{{define "content"}}
|
|
<div id="dashboard">
|
|
<!-- No data state -->
|
|
<div id="no-data" class="hidden">
|
|
<div class="text-center py-16">
|
|
<div class="text-6xl mb-4">🌡️</div>
|
|
<h1 class="text-2xl font-bold mb-2">{{t "dashboard.title"}}</h1>
|
|
<p class="text-gray-500 dark:text-gray-400 mb-6">{{t "dashboard.noData"}}</p>
|
|
<div class="flex gap-4 justify-center">
|
|
<a href="/guide" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "dashboard.goToGuide"}}</a>
|
|
<a href="/setup" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition">{{t "dashboard.goToSetup"}}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No forecast state -->
|
|
<div id="no-forecast" class="hidden">
|
|
<div class="text-center py-16">
|
|
<h1 class="text-2xl font-bold mb-2">{{t "dashboard.title"}}</h1>
|
|
<p class="text-gray-500 dark:text-gray-400 mb-6">{{t "dashboard.fetchForecastFirst"}}</p>
|
|
<a href="/setup#forecast" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">{{t "dashboard.goToSetup"}}</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div id="loading" class="hidden">
|
|
<div class="text-center py-16">
|
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-2 border-orange-600 border-t-transparent mb-4"></div>
|
|
<p class="text-gray-500 dark:text-gray-400">{{t "dashboard.computing"}}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div id="error-state" class="hidden">
|
|
<div class="text-center py-16">
|
|
<p class="text-red-600 dark:text-red-400 mb-4">{{t "dashboard.error"}}</p>
|
|
<button onclick="loadDashboard()" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition">↻</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data display -->
|
|
<div id="data-display" class="hidden space-y-5">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-2xl font-bold">{{t "dashboard.title"}}</h1>
|
|
<div class="flex items-center gap-3">
|
|
<button id="refresh-forecast-btn" type="button" title="{{t "dashboard.refreshForecast"}}" class="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
|
<svg id="refresh-icon" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
|
|
<span class="hidden sm:inline">{{t "dashboard.refreshForecast"}}</span>
|
|
</button>
|
|
<span id="profile-name" class="text-sm text-gray-500 dark:text-gray-400"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Settings -->
|
|
<div id="quick-settings" class="bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden">
|
|
<button id="qs-toggle" type="button" class="w-full flex items-center justify-between px-4 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
|
|
<span>{{t "dashboard.quickSettings"}}</span>
|
|
<svg id="qs-chevron" class="w-4 h-4 transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
|
</button>
|
|
<div id="qs-body" class="hidden px-4 pb-3">
|
|
<div class="flex flex-wrap items-end gap-3">
|
|
<div>
|
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">{{t "dashboard.qsIndoorTemp"}}</label>
|
|
<input id="qs-indoor-temp" type="number" step="0.5" min="15" max="35" class="w-24 px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">{{t "dashboard.qsIndoorHumidity"}}</label>
|
|
<input id="qs-indoor-humidity" type="number" step="1" min="20" max="95" placeholder="50" class="w-24 px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600">
|
|
</div>
|
|
<button id="qs-apply" type="button" class="px-3 py-1 text-sm bg-orange-600 text-white rounded hover:bg-orange-700 transition">{{t "dashboard.qsApply"}}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Warnings -->
|
|
<div id="warnings-section" class="hidden space-y-2"></div>
|
|
|
|
<!-- Two-column: Main content + Sidebar -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
|
<!-- Left column: cards, risk windows, timeline, actions -->
|
|
<div class="lg:col-span-2 space-y-5">
|
|
<!-- Summary cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
<div id="risk-card" class="rounded-xl p-4 text-center transition-all duration-200 hover:shadow-md">
|
|
<div id="risk-icon" class="mb-1"></div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">{{t "dashboard.riskLevel"}}</div>
|
|
<div id="risk-level" class="text-2xl font-bold"></div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 text-center shadow-sm border-l-4 border-orange-400 transition-all duration-200 hover:shadow-md">
|
|
<div class="mb-1"><svg class="w-5 h-5 mx-auto text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v6m0 0a6 6 0 106 6V8a6 6 0 10-6 0z"/></svg></div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">{{t "dashboard.peakTemp"}}</div>
|
|
<div id="peak-temp" class="text-2xl font-bold"></div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 text-center shadow-sm border-l-4 border-blue-400 transition-all duration-200 hover:shadow-md">
|
|
<div class="mb-1"><svg class="w-5 h-5 mx-auto text-blue-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg></div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">{{t "dashboard.minNightTemp"}}</div>
|
|
<div id="min-night-temp" class="text-2xl font-bold"></div>
|
|
<div id="poor-night-cool" class="hidden text-xs text-orange-600 dark:text-orange-400 mt-1">{{t "dashboard.poorNightCool"}}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Risk windows -->
|
|
<div id="risk-windows-section" class="hidden">
|
|
<h2 class="text-lg font-semibold mb-2">{{t "dashboard.riskWindows"}}</h2>
|
|
<div id="risk-windows" class="space-y-2"></div>
|
|
</div>
|
|
|
|
<!-- Timeline heatmap -->
|
|
<div class="relative">
|
|
<h2 class="text-lg font-semibold mb-2">{{t "dashboard.timeline"}}</h2>
|
|
<div id="timeline-chart" class="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm"></div>
|
|
<div id="cooling-legend" class="flex flex-col gap-1 text-xs text-gray-400 mt-2"></div>
|
|
<div id="timeline-tooltip" class="hidden absolute z-50 bg-gray-800 text-white text-xs rounded-lg p-3 shadow-lg max-w-xs pointer-events-none"></div>
|
|
</div>
|
|
|
|
<!-- AI Actions -->
|
|
<div id="actions-section" class="hidden">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<h2 class="text-lg font-semibold">{{t "dashboard.actions"}}</h2>
|
|
<span id="actions-badge" class="hidden text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300">AI</span>
|
|
</div>
|
|
<div id="actions-list" class="space-y-4"></div>
|
|
</div>
|
|
<!-- Actions loading skeleton (shown while AI is loading) -->
|
|
<div id="actions-loading" class="hidden">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<h2 class="text-lg font-semibold">{{t "dashboard.actions"}}</h2>
|
|
<div class="inline-block animate-spin rounded-full h-4 w-4 border-2 border-purple-500 border-t-transparent"></div>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm animate-pulse"><div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3 mb-2"></div><div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div></div>
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm animate-pulse"><div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-2"></div><div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/3"></div></div>
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 shadow-sm animate-pulse"><div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2 mb-2"></div><div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-2/5"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right column: Budgets + Summary + Care -->
|
|
<div class="space-y-5">
|
|
<!-- Room budgets -->
|
|
<div id="budgets-section" class="hidden">
|
|
<h2 class="text-lg font-semibold mb-2">{{t "dashboard.roomBudgets"}}</h2>
|
|
<div id="room-budgets" class="space-y-3"></div>
|
|
</div>
|
|
|
|
<!-- LLM Summary -->
|
|
<div id="llm-section">
|
|
<h2 class="text-lg font-semibold mb-2">{{t "dashboard.llmSummary"}}</h2>
|
|
<div id="llm-summary" class="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm border-l-4 border-purple-400 opacity-0 transition-opacity duration-500">
|
|
<div class="animate-pulse h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-2"></div>
|
|
<div class="animate-pulse h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Care checklist -->
|
|
<div id="care-section" class="hidden">
|
|
<h2 class="text-lg font-semibold mb-2">{{t "dashboard.careChecklist"}}</h2>
|
|
<ul id="care-checklist" class="space-y-1"></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script>
|
|
window.HG.t = {
|
|
warnings: "{{t "dashboard.warnings"}}",
|
|
noActions: "{{t "dashboard.noActions"}}",
|
|
effort: "{{t "dashboard.effort"}}",
|
|
impact: "{{t "dashboard.impact"}}",
|
|
aiDisclaimer: "{{t "dashboard.aiDisclaimer"}}",
|
|
actions: "{{t "dashboard.actions"}}",
|
|
internalGains: "{{t "dashboard.internalGains"}}",
|
|
solarGain: "{{t "dashboard.solarGain"}}",
|
|
ventGain: "{{t "dashboard.ventGain"}}",
|
|
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"}}",
|
|
coolOverloaded: "{{t "dashboard.coolOverloaded"}}",
|
|
coolSealed: "{{t "dashboard.coolSealed"}}",
|
|
aiActions: "{{t "dashboard.aiActions"}}",
|
|
quickSettings: "{{t "dashboard.quickSettings"}}",
|
|
qsIndoorTemp: "{{t "dashboard.qsIndoorTemp"}}",
|
|
qsIndoorHumidity: "{{t "dashboard.qsIndoorHumidity"}}",
|
|
qsApply: "{{t "dashboard.qsApply"}}",
|
|
legendTemp: "{{t "dashboard.legendTemp"}}",
|
|
legendCooling: "{{t "dashboard.legendCooling"}}",
|
|
legendAI: "{{t "dashboard.legendAI"}}",
|
|
category: {
|
|
shading: "{{t "dashboard.category.shading"}}",
|
|
ventilation: "{{t "dashboard.category.ventilation"}}",
|
|
internal_gains: "{{t "dashboard.category.internal_gains"}}",
|
|
ac_strategy: "{{t "dashboard.category.ac_strategy"}}",
|
|
hydration: "{{t "dashboard.category.hydration"}}",
|
|
care: "{{t "dashboard.category.care"}}",
|
|
},
|
|
};
|
|
</script>
|
|
<script src="/assets/js/db.js"></script>
|
|
<script src="/assets/js/dashboard.js"></script>
|
|
{{end}}
|