Files
HeatGuard/internal/report/templates/dashboard.html.tmpl
vikingowl 1c9db02334 feat: add web UI with full CRUD setup page
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
2026-02-09 10:39:00 +01:00

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}}