Files
HeatGuard/internal/cli/templates/setup.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

543 lines
24 KiB
Cheetah
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Heatwave Setup</title>
<style>{{.CSS}}</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<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="text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400">Dashboard</a>
<a href="/setup" class="font-medium text-blue-600 dark:text-blue-400 underline">Setup</a>
</div>
</nav>
<div class="container mx-auto py-4 px-4 max-w-4xl">
{{if .Flash}}
<div class="mb-4 p-3 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
{{.Flash}}
</div>
{{end}}
<h1 class="text-3xl font-bold mb-2">Setup</h1>
<p class="text-gray-600 dark:text-gray-400 mb-6">
Active profile: <strong>{{if .Profile}}{{.Profile.Name}}{{else}}(none){{end}}</strong>
</p>
<div class="flex flex-wrap gap-2 mb-8 text-sm">
<a href="#profiles" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Profiles</a>
<a href="#rooms" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Rooms</a>
<a href="#devices" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Devices</a>
<a href="#occupants" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Occupants</a>
<a href="#ac" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">AC Units</a>
<a href="#toggles" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Toggles</a>
<a href="#forecast" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600">Forecast</a>
</div>
{{template "profiles" .}}
{{template "rooms" .}}
{{template "devices" .}}
{{template "occupants" .}}
{{template "ac_units" .}}
{{template "toggles" .}}
{{template "forecast" .}}
<footer class="mt-8 text-center text-xs text-gray-500 dark:text-gray-500 py-4">
<p>Heatwave Autopilot</p>
</footer>
</div>
</body>
</html>
{{define "profiles"}}
<section id="profiles" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Profiles</h2>
{{if .Profiles}}
<div class="overflow-hidden rounded-lg shadow dark:shadow-gray-700 mb-4">
<table>
<thead>
<tr class="dark:bg-gray-800">
<th>Name</th>
<th>Latitude</th>
<th>Longitude</th>
<th>Timezone</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Profiles}}
<tr class="bg-white dark:bg-gray-800">
<td class="font-medium">{{.Name}}</td>
<td>{{printf "%.4f" .Latitude}}</td>
<td>{{printf "%.4f" .Longitude}}</td>
<td>{{.Timezone}}</td>
<td>
<form method="POST" action="/setup/profiles/{{.ID}}/delete" class="inline">
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline text-sm" onclick="return confirm('Delete profile?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="text-gray-500 dark:text-gray-400 mb-4">No profiles yet.</p>
{{end}}
<form method="POST" action="/setup/profiles/add" class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<h3 class="font-medium mb-3">Add Profile</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Latitude</label>
<input type="number" step="any" name="latitude" value="52.52" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Longitude</label>
<input type="number" step="any" name="longitude" value="13.405" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Timezone</label>
<input type="text" name="timezone" value="Europe/Berlin" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
</div>
<button type="submit" class="mt-3 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Add Profile</button>
</form>
</section>
{{end}}
{{define "rooms"}}
<section id="rooms" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Rooms</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
{{if .Rooms}}
<div class="overflow-hidden rounded-lg shadow dark:shadow-gray-700 mb-4">
<table>
<thead>
<tr class="dark:bg-gray-800">
<th>Name</th>
<th>Area (m²)</th>
<th>Floor</th>
<th>Orientation</th>
<th>Ceiling (m)</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Rooms}}
<tr class="bg-white dark:bg-gray-800">
<td class="font-medium">{{.Name}}</td>
<td>{{printf "%.1f" .AreaSqm}}</td>
<td>{{.Floor}}</td>
<td>{{.Orientation}}</td>
<td>{{printf "%.2f" .CeilingHeightM}}</td>
<td>
<form method="POST" action="/setup/rooms/{{.ID}}/delete" class="inline">
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline text-sm" onclick="return confirm('Delete room?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="text-gray-500 dark:text-gray-400 mb-4">No rooms yet.</p>
{{end}}
<form method="POST" action="/setup/rooms/add" class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<h3 class="font-medium mb-3">Add Room</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Area (m²)</label>
<input type="number" step="any" name="area_sqm" value="20" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Floor</label>
<input type="number" name="floor" value="0" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Orientation</label>
<select name="orientation" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="south">South</option>
<option value="west">West</option>
<option value="east">East</option>
<option value="north">North</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Ceiling Height (m)</label>
<input type="number" step="any" name="ceiling_height_m" value="2.50" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Window Fraction</label>
<input type="number" step="any" name="window_fraction" value="0.30" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">SHGC</label>
<input type="number" step="any" name="shgc" value="0.60" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Shading Type</label>
<select name="shading_type" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="none">None</option>
<option value="blinds">Blinds</option>
<option value="shutters">Shutters</option>
<option value="awning">Awning</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Shading Factor</label>
<input type="number" step="any" name="shading_factor" value="1.0" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Ventilation</label>
<select name="ventilation" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="natural">Natural</option>
<option value="mechanical">Mechanical</option>
<option value="sealed">Sealed</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Ventilation ACH</label>
<input type="number" step="any" name="ventilation_ach" value="1.5" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Insulation</label>
<select name="insulation" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="standard">Standard</option>
<option value="poor">Poor</option>
<option value="good">Good</option>
</select>
</div>
</div>
<button type="submit" class="mt-3 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Add Room</button>
</form>
{{end}}
</section>
{{end}}
{{define "devices"}}
<section id="devices" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Devices</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
{{if .Devices}}
<div class="overflow-hidden rounded-lg shadow dark:shadow-gray-700 mb-4">
<table>
<thead>
<tr class="dark:bg-gray-800">
<th>Name</th>
<th>Type</th>
<th>Room</th>
<th>Idle (W)</th>
<th>Typical (W)</th>
<th>Peak (W)</th>
<th>Duty</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Devices}}
<tr class="bg-white dark:bg-gray-800">
<td class="font-medium">{{.Name}}</td>
<td>{{.DeviceType}}</td>
<td>{{.RoomID}}</td>
<td>{{printf "%.0f" .WattsIdle}}</td>
<td>{{printf "%.0f" .WattsTypical}}</td>
<td>{{printf "%.0f" .WattsPeak}}</td>
<td>{{printf "%.0f%%" (mul .DutyCycle 100)}}</td>
<td>
<form method="POST" action="/setup/devices/{{.ID}}/delete" class="inline">
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline text-sm" onclick="return confirm('Delete device?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="text-gray-500 dark:text-gray-400 mb-4">No devices yet.</p>
{{end}}
{{if .Rooms}}
<form method="POST" action="/setup/devices/add" class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<h3 class="font-medium mb-3">Add Device</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Room</label>
<select name="room_id" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
{{range .Rooms}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Type</label>
<select name="device_type" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="computer">Computer</option>
<option value="monitor">Monitor</option>
<option value="tv">TV</option>
<option value="console">Console</option>
<option value="lighting">Lighting</option>
<option value="appliance">Appliance</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Watts Idle</label>
<input type="number" step="any" name="watts_idle" value="10" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Watts Typical</label>
<input type="number" step="any" name="watts_typical" value="80" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Watts Peak</label>
<input type="number" step="any" name="watts_peak" value="200" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Duty Cycle (01)</label>
<input type="number" step="any" name="duty_cycle" value="0.5" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
</div>
<button type="submit" class="mt-3 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Add Device</button>
</form>
{{else}}
<p class="text-sm text-gray-500 dark:text-gray-400">Add a room first to create devices.</p>
{{end}}
{{end}}
</section>
{{end}}
{{define "occupants"}}
<section id="occupants" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Occupants</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
{{if .Occupants}}
<div class="overflow-hidden rounded-lg shadow dark:shadow-gray-700 mb-4">
<table>
<thead>
<tr class="dark:bg-gray-800">
<th>Room</th>
<th>Count</th>
<th>Activity</th>
<th>Vulnerable</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Occupants}}
<tr class="bg-white dark:bg-gray-800">
<td>{{.RoomID}}</td>
<td>{{.Count}}</td>
<td>{{.ActivityLevel}}</td>
<td>{{if .Vulnerable}}Yes{{else}}No{{end}}</td>
<td>
<form method="POST" action="/setup/occupants/{{.ID}}/delete" class="inline">
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline text-sm" onclick="return confirm('Delete occupant?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="text-gray-500 dark:text-gray-400 mb-4">No occupants yet.</p>
{{end}}
{{if .Rooms}}
<form method="POST" action="/setup/occupants/add" class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<h3 class="font-medium mb-3">Add Occupant</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Room</label>
<select name="room_id" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
{{range .Rooms}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count</label>
<input type="number" name="count" value="1" min="1" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Activity Level</label>
<select name="activity_level" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="sedentary">Sedentary</option>
<option value="light">Light</option>
<option value="moderate">Moderate</option>
<option value="active">Active</option>
</select>
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 pb-2">
<input type="checkbox" name="vulnerable" value="true" class="rounded border-gray-300 dark:border-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-400">Vulnerable</span>
</label>
</div>
</div>
<button type="submit" class="mt-3 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Add Occupant</button>
</form>
{{else}}
<p class="text-sm text-gray-500 dark:text-gray-400">Add a room first to create occupants.</p>
{{end}}
{{end}}
</section>
{{end}}
{{define "ac_units"}}
<section id="ac" class="mb-8">
<h2 class="text-xl font-semibold mb-3">AC Units</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
{{if .ACUnits}}
<div class="space-y-3 mb-4">
{{range .ACUnits}}
{{$acID := .ID}}
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<div class="flex justify-between items-start mb-2">
<div>
<span class="font-medium">{{.Name}}</span>
<span class="text-sm text-gray-500 dark:text-gray-400 ml-2">{{.ACType}} — {{printf "%.0f" .CapacityBTU}} BTU/h — EER {{printf "%.1f" .EfficiencyEER}}</span>
{{if .HasDehumidify}}<span class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded ml-1">Dehumidify</span>{{end}}
</div>
<form method="POST" action="/setup/ac/{{.ID}}/delete" class="inline">
<button type="submit" class="text-red-600 dark:text-red-400 hover:underline text-sm" onclick="return confirm('Delete AC unit?')">Delete</button>
</form>
</div>
{{if $.Rooms}}
<form method="POST" action="/setup/ac/{{.ID}}/assign" class="flex flex-wrap items-center gap-2 text-sm">
<span class="text-gray-600 dark:text-gray-400">Assign to:</span>
<select name="room_id" class="px-2 py-1 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-sm">
{{range $.Rooms}}<option value="{{.ID}}">{{.Name}}</option>{{end}}
</select>
<button type="submit" class="px-2 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700">Assign</button>
</form>
{{if .AssignedRoomIDs}}
<div class="mt-2 flex flex-wrap gap-1">
{{range .AssignedRoomIDs}}
<form method="POST" action="/setup/ac/{{$acID}}/unassign" class="inline">
<input type="hidden" name="room_id" value="{{.RoomID}}">
<button type="submit" class="inline-flex items-center gap-1 px-2 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-sm">
{{.RoomName}} <span class="text-red-500">&times;</span>
</button>
</form>
{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}
</div>
{{else}}
<p class="text-gray-500 dark:text-gray-400 mb-4">No AC units yet.</p>
{{end}}
<form method="POST" action="/setup/ac/add" class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<h3 class="font-medium mb-3">Add AC Unit</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Type</label>
<select name="ac_type" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
<option value="portable">Portable</option>
<option value="split">Split</option>
<option value="window">Window</option>
<option value="central">Central</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Capacity (BTU/h)</label>
<input type="number" step="any" name="capacity_btu" value="12000" required class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Efficiency (EER)</label>
<input type="number" step="any" name="efficiency_eer" value="10.0" class="w-full px-3 py-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700">
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 pb-2">
<input type="checkbox" name="has_dehumidify" value="true" class="rounded border-gray-300 dark:border-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-400">Has Dehumidify</span>
</label>
</div>
</div>
<button type="submit" class="mt-3 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Add AC Unit</button>
</form>
{{end}}
</section>
{{end}}
{{define "toggles"}}
<section id="toggles" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Scenario Toggles</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
<div class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow dark:shadow-gray-700">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
{{range $name, $active := .Toggles}}
<form method="POST" action="/setup/toggles/set" class="flex items-center justify-between p-2 rounded border dark:border-gray-600">
<input type="hidden" name="name" value="{{$name}}">
<input type="hidden" name="active" value="{{if $active}}false{{else}}true{{end}}">
<span class="font-medium">{{$name}}</span>
<button type="submit" class="px-3 py-1 rounded text-sm text-white {{if $active}}bg-green-600 hover:bg-red-600{{else}}bg-gray-400 hover:bg-green-600{{end}}">
{{if $active}}ON{{else}}OFF{{end}}
</button>
</form>
{{end}}
<form method="POST" action="/setup/toggles/set" class="flex items-center gap-2 p-2 rounded border dark:border-gray-600">
<input type="text" name="name" placeholder="new toggle" required class="flex-1 px-2 py-1 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-sm">
<input type="hidden" name="active" value="true">
<button type="submit" class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700">Add</button>
</form>
</div>
</div>
{{end}}
</section>
{{end}}
{{define "forecast"}}
<section id="forecast" class="mb-8">
<h2 class="text-xl font-semibold mb-3">Forecast</h2>
{{if not .Profile}}
<p class="text-gray-500 dark:text-gray-400">Create a profile first.</p>
{{else}}
<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 mb-3">
Last fetched: <strong>{{if .LastFetch}}{{.LastFetch}}{{else}}never{{end}}</strong>
</p>
<form method="POST" action="/setup/forecast/fetch">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Fetch Forecast Now</button>
</form>
</div>
{{end}}
</section>
{{end}}