Add server-side rendered setup UI accessible via `heatwave web`. The dashboard is now re-rendered per request and includes a nav bar linking to the new /setup page. Setup provides full CRUD for profiles, rooms, devices, occupants, AC units (with room assignment), scenario toggles, and forecast fetching — all via POST/redirect/GET forms. - Add ShowNav field to DashboardData for conditional nav bar - Extract fetchForecastForProfile() for reuse by web handler - Create setup.html.tmpl with Tailwind-styled entity sections - Create web_handlers.go with 15 route handlers and flash cookies - Switch web.go from pre-rendered to per-request dashboard rendering - Graceful dashboard fallback when no forecast data exists
167 lines
6.5 KiB
Cheetah
167 lines
6.5 KiB
Cheetah
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Heatwave Report — {{.ProfileName}} — {{.Date}}</title>
|
|
<style>{{.CSS}}</style>
|
|
</head>
|
|
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
|
{{if .ShowNav}}
|
|
<nav class="bg-white dark:bg-gray-800 shadow mb-4">
|
|
<div class="container mx-auto flex items-center gap-6 px-4 py-3">
|
|
<span class="font-bold text-lg">Heatwave</span>
|
|
<a href="/" class="font-medium text-blue-600 dark:text-blue-400 underline">Dashboard</a>
|
|
<a href="/setup" class="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400">Setup</a>
|
|
</div>
|
|
</nav>
|
|
{{end}}
|
|
<div class="container mx-auto py-4">
|
|
|
|
<header class="mb-6">
|
|
<h1 class="text-3xl font-bold">Heatwave Report</h1>
|
|
<p class="text-gray-600 dark:text-gray-400">{{.ProfileName}} — {{.Date}}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-500">Generated {{.GeneratedAt.Format "2006-01-02 15:04"}}</p>
|
|
</header>
|
|
|
|
{{template "warnings" .}}
|
|
{{template "risk_summary" .}}
|
|
{{template "timeline" .}}
|
|
{{template "heatbudget" .}}
|
|
{{template "checklist" .}}
|
|
|
|
{{if .LLMSummary}}
|
|
<section class="mt-8 p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
|
|
<h2 class="text-xl font-semibold mb-2">AI Summary</h2>
|
|
{{if .LLMDisclaimer}}<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">{{.LLMDisclaimer}}</p>{{end}}
|
|
<div>{{.LLMSummary}}</div>
|
|
</section>
|
|
{{end}}
|
|
|
|
<footer class="mt-8 text-center text-xs text-gray-500 dark:text-gray-500 py-4">
|
|
<p>Heatwave Autopilot — This report is for planning purposes only. It does not constitute medical advice.</p>
|
|
</footer>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|
|
|
|
{{define "warnings"}}
|
|
{{if .Warnings}}
|
|
<section class="mb-6">
|
|
{{range .Warnings}}
|
|
<div class="p-4 mb-2 border-l-4 {{if eq .Severity "Extreme"}}border-red-400 bg-red-50 dark:bg-red-950{{else}}border-orange-400 bg-orange-50 dark:bg-orange-950{{end}} rounded">
|
|
<p class="font-bold {{if eq .Severity "Extreme"}}text-red-700 dark:text-red-400{{else}}text-orange-600 dark:text-orange-400{{end}}">{{.Headline}}</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{.Description}}</p>
|
|
{{if .Instruction}}<p class="text-sm font-medium mt-1">{{.Instruction}}</p>{{end}}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{.Onset}} — {{.Expires}}</p>
|
|
</div>
|
|
{{end}}
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{define "risk_summary"}}
|
|
<section class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="p-4 rounded-lg shadow dark:shadow-gray-700 {{riskBg .RiskLevel}}">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Risk Level</p>
|
|
<p class="text-2xl font-bold">{{riskBadge .RiskLevel}}</p>
|
|
</div>
|
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Peak Temperature</p>
|
|
<p class="text-2xl font-bold {{tempColor .PeakTempC}}">{{formatTemp .PeakTempC}}</p>
|
|
</div>
|
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Min Night Temp</p>
|
|
<p class="text-2xl font-bold">{{formatTemp .MinNightTempC}}</p>
|
|
{{if .PoorNightCool}}<p class="text-xs text-red-600 dark:text-red-400 font-medium">Poor night cooling</p>{{end}}
|
|
</div>
|
|
</section>
|
|
|
|
{{if .RiskWindows}}
|
|
<section class="mb-6">
|
|
<h2 class="text-xl font-semibold mb-2">Risk Windows</h2>
|
|
<div class="space-y-2">
|
|
{{range .RiskWindows}}
|
|
<div class="p-3 rounded {{riskBg .Level}} flex justify-between items-center">
|
|
<span class="font-medium">{{printf "%02d:00" .StartHour}} — {{printf "%02d:00" .EndHour}}</span>
|
|
<span>Peak {{formatTemp .PeakTempC}}</span>
|
|
<span class="px-2 py-1 rounded-full text-xs font-semibold text-white {{riskBadgeBg .Level}}">{{.Level}}</span>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{define "timeline"}}
|
|
<section class="mb-6">
|
|
<h2 class="text-xl font-semibold mb-2">Hour-by-Hour Timeline</h2>
|
|
<div class="overflow-hidden rounded-lg shadow dark:shadow-gray-700">
|
|
<table>
|
|
<thead>
|
|
<tr class="dark:bg-gray-800">
|
|
<th>Hour</th>
|
|
<th>Temp</th>
|
|
<th>Risk</th>
|
|
<th>Budget</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Timeline}}
|
|
<tr class="{{riskBg .RiskLevel}}">
|
|
<td class="font-medium">{{.HourStr}}</td>
|
|
<td class="{{tempColor .TempC}}">{{formatTemp .TempC}}</td>
|
|
<td>{{riskBadge .RiskLevel}}</td>
|
|
<td>{{statusBadge .BudgetStatus}}</td>
|
|
<td>
|
|
{{range .Actions}}
|
|
<span class="inline-block px-2 py-1 text-xs rounded bg-blue-100 dark:bg-blue-900 dark:text-blue-200 mb-1">{{.Name}}</span>
|
|
{{end}}
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{define "heatbudget"}}
|
|
{{if .RoomBudgets}}
|
|
<section class="mb-6">
|
|
<h2 class="text-xl font-semibold mb-2">Room Heat Budgets</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{{range .RoomBudgets}}
|
|
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
|
|
<h3 class="font-semibold text-lg mb-2">{{.RoomName}}</h3>
|
|
<table class="text-sm">
|
|
<tr><td class="text-gray-600 dark:text-gray-400">Internal gains</td><td class="font-medium">{{formatWatts .InternalGainsW}}</td></tr>
|
|
<tr><td class="text-gray-600 dark:text-gray-400">Solar gain</td><td class="font-medium">{{formatWatts .SolarGainW}}</td></tr>
|
|
<tr><td class="text-gray-600 dark:text-gray-400">Ventilation</td><td class="font-medium">{{formatWatts .VentGainW}}</td></tr>
|
|
<tr class="font-bold"><td>Total heat load</td><td>{{formatWatts .TotalGainW}} ({{formatBTU .TotalGainBTUH}})</td></tr>
|
|
<tr><td class="text-gray-600 dark:text-gray-400">AC capacity</td><td>{{formatBTU .ACCapacityBTUH}}</td></tr>
|
|
<tr class="font-bold"><td>Headroom</td><td class="{{statusColor .Status}}">{{formatBTU .HeadroomBTUH}}</td></tr>
|
|
</table>
|
|
<p class="mt-2 px-3 py-1 rounded-full inline-block text-xs font-semibold text-white {{statusBadgeBg .Status}}">{{.Status}}</p>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{define "checklist"}}
|
|
{{if .CareChecklist}}
|
|
<section class="mb-6">
|
|
<h2 class="text-xl font-semibold mb-2">Care Checklist</h2>
|
|
<ul class="list-disc list-inside space-y-2 p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
|
|
{{range .CareChecklist}}
|
|
<li>{{.}}</li>
|
|
{{end}}
|
|
</ul>
|
|
</section>
|
|
{{end}}
|
|
{{end}}
|