f9db29f709820eb6d8379df62e735085e24b953d
projects/hot-tub/.gitignore
| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | 1 | __pycache__/ |
| 2 | 2 | *.py[cod] |
| 3 | 3 | *.pyo |
| 4 | +.venv-web/ |
projects/hot-tub/deploy/web/hot-tub-web.logrotate
| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | +/var/log/hot-tub/web.log { |
|
| 2 | + weekly |
|
| 3 | + rotate 8 |
|
| 4 | + compress |
|
| 5 | + delaycompress |
|
| 6 | + missingok |
|
| 7 | + notifempty |
|
| 8 | + copytruncate |
|
| 9 | +} |
projects/hot-tub/deploy/web/hot-tub-web.service
| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | +[Unit] |
|
| 2 | +Description=Hot Tub Web Service |
|
| 3 | +After=network-online.target redis-server.service |
|
| 4 | +Wants=network-online.target |
|
| 5 | +Documentation=file://src/web/INSTALL.md |
|
| 6 | + |
|
| 7 | +[Service] |
|
| 8 | +Type=simple |
|
| 9 | +User=hot-tub |
|
| 10 | +Group=hot-tub |
|
| 11 | +Environment=PYTHONPATH=__INSTALL_ROOT__/src |
|
| 12 | +ExecStart=/usr/bin/python3 -m web --config /etc/hot-tub/web.json |
|
| 13 | +Restart=on-failure |
|
| 14 | +RestartSec=5 |
|
| 15 | + |
|
| 16 | +[Install] |
|
| 17 | +WantedBy=multi-user.target |
projects/hot-tub/deploy/web/install.sh
| ... | ... | @@ -0,0 +1,71 @@ |
| 1 | +#!/usr/bin/env bash |
|
| 2 | +# Install hot-tub web service on Raspberry Pi OS (run with sudo). |
|
| 3 | +set -euo pipefail |
|
| 4 | + |
|
| 5 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
| 6 | +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" |
|
| 7 | +INSTALL_ROOT="${INSTALL_ROOT:-${REPO_ROOT}}" |
|
| 8 | + |
|
| 9 | +if [[ "$(id -u)" -ne 0 ]]; then |
|
| 10 | + echo "Run as root: sudo $0" >&2 |
|
| 11 | + exit 1 |
|
| 12 | +fi |
|
| 13 | + |
|
| 14 | +DIST="${INSTALL_ROOT}/src/web/frontend/dist/index.html" |
|
| 15 | +if [[ ! -f "${DIST}" ]]; then |
|
| 16 | + echo "Missing built frontend: ${DIST}" >&2 |
|
| 17 | + echo "Build on a dev machine: cd src/web/frontend && npm install && npm run build" >&2 |
|
| 18 | + exit 1 |
|
| 19 | +fi |
|
| 20 | + |
|
| 21 | +echo "==> Installing packages (python3-redis, python3-flask)..." |
|
| 22 | +export DEBIAN_FRONTEND=noninteractive |
|
| 23 | +apt-get update -qq |
|
| 24 | +apt-get install -y -qq python3-redis python3-flask |
|
| 25 | + |
|
| 26 | +if ! systemctl is-active --quiet redis-server 2>/dev/null && ! systemctl is-active --quiet redis 2>/dev/null; then |
|
| 27 | + echo "==> Redis not running; installing redis-server..." |
|
| 28 | + apt-get install -y -qq redis-server |
|
| 29 | +fi |
|
| 30 | + |
|
| 31 | +echo "==> Ensuring hot-tub system user..." |
|
| 32 | +if ! id hot-tub &>/dev/null; then |
|
| 33 | + useradd --system --home /var/lib/hot-tub --shell /usr/sbin/nologin hot-tub |
|
| 34 | +fi |
|
| 35 | + |
|
| 36 | +echo "==> Preparing log and config paths..." |
|
| 37 | +mkdir -p /var/log/hot-tub /etc/hot-tub /var/lib/hot-tub |
|
| 38 | +chown hot-tub:hot-tub /var/log/hot-tub /var/lib/hot-tub |
|
| 39 | + |
|
| 40 | +if [[ ! -f /etc/hot-tub/web.json ]]; then |
|
| 41 | + install -m 0644 "${INSTALL_ROOT}/src/web/config.example.json" /etc/hot-tub/web.json |
|
| 42 | + sed -i "s|\"static_dir\": \"frontend/dist\"|\"static_dir\": \"${INSTALL_ROOT}/src/web/frontend/dist\"|" /etc/hot-tub/web.json |
|
| 43 | + echo " Created /etc/hot-tub/web.json from example" |
|
| 44 | +else |
|
| 45 | + echo " Keeping existing /etc/hot-tub/web.json" |
|
| 46 | +fi |
|
| 47 | + |
|
| 48 | +echo "==> Installing systemd unit..." |
|
| 49 | +sed "s|__INSTALL_ROOT__|${INSTALL_ROOT}|g" "${SCRIPT_DIR}/hot-tub-web.service" \ |
|
| 50 | + > /etc/systemd/system/hot-tub-web.service |
|
| 51 | + |
|
| 52 | +echo "==> Installing logrotate..." |
|
| 53 | +install -m 0644 "${SCRIPT_DIR}/hot-tub-web.logrotate" /etc/logrotate.d/hot-tub-web |
|
| 54 | + |
|
| 55 | +echo "==> Enabling services..." |
|
| 56 | +systemctl enable redis-server 2>/dev/null || systemctl enable redis 2>/dev/null || true |
|
| 57 | +systemctl start redis-server 2>/dev/null || systemctl start redis 2>/dev/null || true |
|
| 58 | +systemctl daemon-reload |
|
| 59 | +systemctl enable hot-tub-web.service |
|
| 60 | +systemctl restart hot-tub-web.service || systemctl start hot-tub-web.service |
|
| 61 | + |
|
| 62 | +echo "" |
|
| 63 | +echo "Installation complete." |
|
| 64 | +echo " Repo / code root: ${INSTALL_ROOT}" |
|
| 65 | +echo " Config: /etc/hot-tub/web.json" |
|
| 66 | +echo " Log: /var/log/hot-tub/web.log" |
|
| 67 | +echo " UI: http://<pi-ip>:8080/" |
|
| 68 | +echo "" |
|
| 69 | +echo "Status: systemctl status hot-tub-web" |
|
| 70 | +echo "Logs: tail -f /var/log/hot-tub/web.log" |
|
| 71 | +echo "SSE: curl -N http://localhost:8080/api/events" |
projects/hot-tub/diagrams/hot-tub-electrical-v2.drawio
| ... | ... | @@ -0,0 +1,238 @@ |
| 1 | +<mxfile host="app.diagrams.net" modified="2026-10-22T17:18:00.000Z" agent="Claude Sonnet via Cursor" version="22.1.0" type="device"> |
|
| 2 | + <diagram id="hot-tub-electrical-v2" name="Hot tub electrical (v2)"> |
|
| 3 | + <mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="970" math="0" shadow="0"> |
|
| 4 | + <root> |
|
| 5 | + <mxCell id="0" /> |
|
| 6 | + <mxCell id="1" parent="0" /> |
|
| 7 | + |
|
| 8 | + <mxCell id="v2title" value="Hot tub — electrical wiring (v2)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=20;fontStyle=1" vertex="1" parent="1"> |
|
| 9 | + <mxGeometry x="200" y="18" width="1000" height="38" as="geometry" /> |
|
| 10 | + </mxCell> |
|
| 11 | + <mxCell id="v2subtitle" value="Rendered by Claude Sonnet · same facts as v1: GPIO22 heat relay, GPIO23 filter relay, GPIO4 1-Wire bus with all 5 DS18B20 probes. Low-water float is a FUTURE addition — not installed, no GPIO wired today." style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;fontColor=#666666;" vertex="1" parent="1"> |
|
| 12 | + <mxGeometry x="120" y="56" width="1160" height="26" as="geometry" /> |
|
| 13 | + </mxCell> |
|
| 14 | + |
|
| 15 | + <!-- Legend --> |
|
| 16 | + <mxCell id="lg1sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1"> |
|
| 17 | + <mxGeometry x="40" y="92" width="16" height="16" as="geometry" /> |
|
| 18 | + </mxCell> |
|
| 19 | + <mxCell id="lg1tx" value="Controller (Pi)" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 20 | + <mxGeometry x="62" y="88" width="150" height="24" as="geometry" /> |
|
| 21 | + </mxCell> |
|
| 22 | + |
|
| 23 | + <mxCell id="lg2sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1"> |
|
| 24 | + <mxGeometry x="260" y="92" width="16" height="16" as="geometry" /> |
|
| 25 | + </mxCell> |
|
| 26 | + <mxCell id="lg2tx" value="Sensor / 1-Wire" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 27 | + <mxGeometry x="282" y="88" width="150" height="24" as="geometry" /> |
|
| 28 | + </mxCell> |
|
| 29 | + |
|
| 30 | + <mxCell id="lg3sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;" vertex="1" parent="1"> |
|
| 31 | + <mxGeometry x="480" y="92" width="16" height="16" as="geometry" /> |
|
| 32 | + </mxCell> |
|
| 33 | + <mxCell id="lg3tx" value="Optional / planned" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 34 | + <mxGeometry x="502" y="88" width="160" height="24" as="geometry" /> |
|
| 35 | + </mxCell> |
|
| 36 | + |
|
| 37 | + <mxCell id="lg4sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1"> |
|
| 38 | + <mxGeometry x="700" y="92" width="16" height="16" as="geometry" /> |
|
| 39 | + </mxCell> |
|
| 40 | + <mxCell id="lg4tx" value="GPIO control signal" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 41 | + <mxGeometry x="722" y="88" width="170" height="24" as="geometry" /> |
|
| 42 | + </mxCell> |
|
| 43 | + |
|
| 44 | + <mxCell id="lg5sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1"> |
|
| 45 | + <mxGeometry x="920" y="92" width="16" height="16" as="geometry" /> |
|
| 46 | + </mxCell> |
|
| 47 | + <mxCell id="lg5tx" value="120 VAC power" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 48 | + <mxGeometry x="942" y="88" width="150" height="24" as="geometry" /> |
|
| 49 | + </mxCell> |
|
| 50 | + |
|
| 51 | + <mxCell id="lg6sw" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#666666;dashed=1;" vertex="1" parent="1"> |
|
| 52 | + <mxGeometry x="1140" y="92" width="16" height="16" as="geometry" /> |
|
| 53 | + </mxCell> |
|
| 54 | + <mxCell id="lg6tx" value="Plumbing / mechanical" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=11;" vertex="1" parent="1"> |
|
| 55 | + <mxGeometry x="1162" y="88" width="180" height="24" as="geometry" /> |
|
| 56 | + </mxCell> |
|
| 57 | + |
|
| 58 | + <!-- Lane: low-voltage control --> |
|
| 59 | + <mxCell id="laneControl" value="Low-voltage control & sensors" style="swimlane;html=1;startSize=34;fillColor=#f5f5f5;strokeColor=#666666;fontStyle=1;fontSize=13;rounded=0;" vertex="1" parent="1"> |
|
| 60 | + <mxGeometry x="40" y="130" width="620" height="580" as="geometry" /> |
|
| 61 | + </mxCell> |
|
| 62 | + |
|
| 63 | + <mxCell id="pi" value="<b>Raspberry Pi 3</b><br>(controller)<br><br>• hot-tub-control<br>• hot-tub-temps<br>• hot-tub-web<br>• Redis (localhost)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;" vertex="1" parent="laneControl"> |
|
| 64 | + <mxGeometry x="20" y="44" width="220" height="130" as="geometry" /> |
|
| 65 | + </mxCell> |
|
| 66 | + |
|
| 67 | + <mxCell id="busbar" value="1-Wire bus — GPIO4 (BCM), 4.7 kΩ pull-up to 3.3V, shared GND" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;fontSize=11;verticalAlign=middle;" vertex="1" parent="laneControl"> |
|
| 68 | + <mxGeometry x="20" y="190" width="560" height="30" as="geometry" /> |
|
| 69 | + </mxCell> |
|
| 70 | + |
|
| 71 | + <mxCell id="lbl_water" value="Water bundle — 4 probes (tub_avg_f)" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontStyle=1;fontColor=#8a6d1a;" vertex="1" parent="laneControl"> |
|
| 72 | + <mxGeometry x="20" y="224" width="330" height="14" as="geometry" /> |
|
| 73 | + </mxCell> |
|
| 74 | + <mxCell id="lbl_hotin" value="Hot-in bundle (hot_in_f)" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontStyle=1;fontColor=#8a6d1a;" vertex="1" parent="laneControl"> |
|
| 75 | + <mxGeometry x="460" y="224" width="130" height="14" as="geometry" /> |
|
| 76 | + </mxCell> |
|
| 77 | + |
|
| 78 | + <mxCell id="tub1" value="Tub 1<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 79 | + <mxGeometry x="20" y="242" width="84" height="56" as="geometry" /> |
|
| 80 | + </mxCell> |
|
| 81 | + <mxCell id="tub2" value="Tub 2<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 82 | + <mxGeometry x="112" y="242" width="84" height="56" as="geometry" /> |
|
| 83 | + </mxCell> |
|
| 84 | + <mxCell id="tub3" value="Tub 3<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 85 | + <mxGeometry x="204" y="242" width="84" height="56" as="geometry" /> |
|
| 86 | + </mxCell> |
|
| 87 | + <mxCell id="tub4" value="Tub 4<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 88 | + <mxGeometry x="296" y="242" width="84" height="56" as="geometry" /> |
|
| 89 | + </mxCell> |
|
| 90 | + <mxCell id="hotin" value="Hot-in<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6b3;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 91 | + <mxGeometry x="460" y="242" width="100" height="56" as="geometry" /> |
|
| 92 | + </mxCell> |
|
| 93 | + |
|
| 94 | + <mxCell id="note_probes" value="5 physical DS18B20 probes share one 1-Wire bus (two pluggable M8/M12 bundles). Identity learned at install time — src/temp/README.md." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;dashed=1;align=left;spacingLeft=10;verticalAlign=top;spacingTop=6;fontSize=10;fontColor=#555555;" vertex="1" parent="laneControl"> |
|
| 95 | + <mxGeometry x="20" y="308" width="560" height="46" as="geometry" /> |
|
| 96 | + </mxCell> |
|
| 97 | + |
|
| 98 | + <mxCell id="future_label" value="FUTURE CONCERN — not yet installed" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=11;fontStyle=1;fontColor=#9a5b00;" vertex="1" parent="laneControl"> |
|
| 99 | + <mxGeometry x="20" y="364" width="400" height="16" as="geometry" /> |
|
| 100 | + </mxCell> |
|
| 101 | + |
|
| 102 | + <mxCell id="float" value="<b>Low-water float switch</b><br>Planned: GPIO17 → switch, other side → GND<br><i>No hardware installed. Water level is presumed adequate in software (src/control/safety.py TODO).</i>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;dashPattern=4 4;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 103 | + <mxGeometry x="20" y="384" width="270" height="110" as="geometry" /> |
|
| 104 | + </mxCell> |
|
| 105 | + |
|
| 106 | + <mxCell id="flow" value="<b>Flow switch</b> (optional)<br>Planned: GPIO27 → GND<br><i>No hardware installed; not implemented.</i>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;dashPattern=4 4;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;fontSize=10;" vertex="1" parent="laneControl"> |
|
| 107 | + <mxGeometry x="310" y="384" width="270" height="80" as="geometry" /> |
|
| 108 | + </mxCell> |
|
| 109 | + |
|
| 110 | + <mxCell id="gpio_out" value="<b>GPIO outputs</b> — BCM22 → heat relay IN+ · BCM23 → filter relay IN+ · common GND to relay modules · active_high: true (config)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=10;verticalAlign=middle;" vertex="1" parent="laneControl"> |
|
| 111 | + <mxGeometry x="20" y="504" width="560" height="50" as="geometry" /> |
|
| 112 | + </mxCell> |
|
| 113 | + |
|
| 114 | + <!-- Lane: AC power --> |
|
| 115 | + <mxCell id="laneAC" value="AC power & pumps" style="swimlane;html=1;startSize=34;fillColor=#fff7f0;strokeColor=#d79b00;fontStyle=1;fontSize=13;rounded=0;" vertex="1" parent="1"> |
|
| 116 | + <mxGeometry x="700" y="130" width="620" height="580" as="geometry" /> |
|
| 117 | + </mxCell> |
|
| 118 | + |
|
| 119 | + <mxCell id="gfci" value="<b>Outdoor outlet</b><br>+ GFCI protection" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 120 | + <mxGeometry x="20" y="44" width="160" height="60" as="geometry" /> |
|
| 121 | + </mxCell> |
|
| 122 | + |
|
| 123 | + <mxCell id="cord" value="Outdoor-rated<br>extension cord" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 124 | + <mxGeometry x="210" y="44" width="150" height="60" as="geometry" /> |
|
| 125 | + </mxCell> |
|
| 126 | + |
|
| 127 | + <mxCell id="relay1" value="<b>Enclosed AC relay #1</b><br>HEAT PUMP outlet<br>Low-voltage IN ← GPIO22" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 128 | + <mxGeometry x="20" y="124" width="260" height="80" as="geometry" /> |
|
| 129 | + </mxCell> |
|
| 130 | + |
|
| 131 | + <mxCell id="relay2" value="<b>Enclosed AC relay #2</b><br>FILTER PUMP outlet<br>Low-voltage IN ← GPIO23" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 132 | + <mxGeometry x="310" y="124" width="260" height="80" as="geometry" /> |
|
| 133 | + </mxCell> |
|
| 134 | + |
|
| 135 | + <mxCell id="pump_heat" value="<b>Heat circulation pump</b><br>120 VAC plug → relay #1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#b85450;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 136 | + <mxGeometry x="20" y="224" width="260" height="90" as="geometry" /> |
|
| 137 | + </mxCell> |
|
| 138 | + |
|
| 139 | + <mxCell id="pump_filt" value="<b>Filter pump</b><br>120 VAC plug → relay #2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#b85450;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 140 | + <mxGeometry x="310" y="224" width="260" height="70" as="geometry" /> |
|
| 141 | + </mxCell> |
|
| 142 | + |
|
| 143 | + <mxCell id="heater" value="<b>Propane camp heater</b><br>Gas & controls: manual<br>Ignites on water flow" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#666666;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 144 | + <mxGeometry x="20" y="334" width="260" height="70" as="geometry" /> |
|
| 145 | + </mxCell> |
|
| 146 | + |
|
| 147 | + <mxCell id="tub" value="<b>Stock-tank tub</b><br>(water reservoir)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#666666;align=left;spacingLeft=8;verticalAlign=middle;" vertex="1" parent="laneAC"> |
|
| 148 | + <mxGeometry x="310" y="334" width="260" height="70" as="geometry" /> |
|
| 149 | + </mxCell> |
|
| 150 | + |
|
| 151 | + <mxCell id="note_plumb" value="<i>Plumbing is a closed loop:</i> tub → pump intake → pump → (heater for the heat loop) → return to tub. AC relays switch pump power only — they never touch propane gas." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;dashed=1;align=left;spacingLeft=10;verticalAlign=top;spacingTop=6;fontSize=11;fontColor=#555555;" vertex="1" parent="laneAC"> |
|
| 152 | + <mxGeometry x="20" y="424" width="550" height="90" as="geometry" /> |
|
| 153 | + </mxCell> |
|
| 154 | + |
|
| 155 | + <!-- Edges: control/sensor side --> |
|
| 156 | + <mxCell id="e_pi_busbar" value="3.3V / GND" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;endArrow=classic;endFill=1;fontSize=10;" edge="1" parent="1" source="pi" target="busbar"> |
|
| 157 | + <mxGeometry relative="1" as="geometry" /> |
|
| 158 | + </mxCell> |
|
| 159 | + <mxCell id="e_tub1_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub1" target="busbar"> |
|
| 160 | + <mxGeometry relative="1" as="geometry" /> |
|
| 161 | + </mxCell> |
|
| 162 | + <mxCell id="e_tub2_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub2" target="busbar"> |
|
| 163 | + <mxGeometry relative="1" as="geometry" /> |
|
| 164 | + </mxCell> |
|
| 165 | + <mxCell id="e_tub3_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub3" target="busbar"> |
|
| 166 | + <mxGeometry relative="1" as="geometry" /> |
|
| 167 | + </mxCell> |
|
| 168 | + <mxCell id="e_tub4_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub4" target="busbar"> |
|
| 169 | + <mxGeometry relative="1" as="geometry" /> |
|
| 170 | + </mxCell> |
|
| 171 | + <mxCell id="e_hotin_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="hotin" target="busbar"> |
|
| 172 | + <mxGeometry relative="1" as="geometry" /> |
|
| 173 | + </mxCell> |
|
| 174 | + <mxCell id="e_float_pi" value="GPIO17 (future)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#9673a6;dashed=1;dashPattern=4 4;endArrow=classic;fontSize=10;" edge="1" parent="1" source="float" target="pi"> |
|
| 175 | + <mxGeometry relative="1" as="geometry" /> |
|
| 176 | + </mxCell> |
|
| 177 | + <mxCell id="e_flow_pi" value="GPIO27 (optional)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#9673a6;dashed=1;dashPattern=4 4;endArrow=classic;fontSize=10;" edge="1" parent="1" source="flow" target="pi"> |
|
| 178 | + <mxGeometry relative="1" as="geometry" /> |
|
| 179 | + </mxCell> |
|
| 180 | + <mxCell id="e_pi_gpioout" value="GPIO22 / GPIO23" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;endArrow=classic;strokeWidth=2;fontSize=10;" edge="1" parent="1" source="pi" target="gpio_out"> |
|
| 181 | + <mxGeometry relative="1" as="geometry" /> |
|
| 182 | + </mxCell> |
|
| 183 | + |
|
| 184 | + <!-- Edges: GPIO crossing to AC lane --> |
|
| 185 | + <mxCell id="e_gpio_relay1" value="heat control" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;endArrow=classic;strokeWidth=2;fontSize=10;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=0;entryY=0.3;entryDx=0;entryDy=0;" edge="1" parent="1" source="gpio_out" target="relay1"> |
|
| 186 | + <mxGeometry relative="1" as="geometry" /> |
|
| 187 | + </mxCell> |
|
| 188 | + <mxCell id="e_gpio_relay2" value="filter control" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;endArrow=classic;strokeWidth=2;fontSize=10;exitX=1;exitY=0.7;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="gpio_out" target="relay2"> |
|
| 189 | + <mxGeometry relative="1" as="geometry" /> |
|
| 190 | + </mxCell> |
|
| 191 | + |
|
| 192 | + <!-- Edges: AC power chain --> |
|
| 193 | + <mxCell id="e_gfci_cord" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="gfci" target="cord"> |
|
| 194 | + <mxGeometry relative="1" as="geometry" /> |
|
| 195 | + </mxCell> |
|
| 196 | + <mxCell id="e_cord_relay1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="cord" target="relay1"> |
|
| 197 | + <mxGeometry relative="1" as="geometry" /> |
|
| 198 | + </mxCell> |
|
| 199 | + <mxCell id="e_cord_relay2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="cord" target="relay2"> |
|
| 200 | + <mxGeometry relative="1" as="geometry" /> |
|
| 201 | + </mxCell> |
|
| 202 | + <mxCell id="e_relay1_pump" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="relay1" target="pump_heat"> |
|
| 203 | + <mxGeometry relative="1" as="geometry" /> |
|
| 204 | + </mxCell> |
|
| 205 | + <mxCell id="e_relay2_pump" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="relay2" target="pump_filt"> |
|
| 206 | + <mxGeometry relative="1" as="geometry" /> |
|
| 207 | + </mxCell> |
|
| 208 | + |
|
| 209 | + <!-- Edges: plumbing loop --> |
|
| 210 | + <mxCell id="e_pumpheat_heater" value="hot water" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;fontSize=10;" edge="1" parent="1" source="pump_heat" target="heater"> |
|
| 211 | + <mxGeometry relative="1" as="geometry" /> |
|
| 212 | + </mxCell> |
|
| 213 | + <mxCell id="e_heater_tub" value="return" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;fontSize=10;" edge="1" parent="1" source="heater" target="tub"> |
|
| 214 | + <mxGeometry relative="1" as="geometry" /> |
|
| 215 | + </mxCell> |
|
| 216 | + <mxCell id="e_tub_pumpheat" value="intake" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;fontSize=10;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="tub" target="pump_heat"> |
|
| 217 | + <mxGeometry relative="1" as="geometry" /> |
|
| 218 | + </mxCell> |
|
| 219 | + <mxCell id="e_tub_pumpfilt" value="intake" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;fontSize=10;" edge="1" parent="1" source="tub" target="pump_filt"> |
|
| 220 | + <mxGeometry relative="1" as="geometry" /> |
|
| 221 | + </mxCell> |
|
| 222 | + <mxCell id="e_pumpfilt_tub" value="filtered return" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;fontSize=10;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;" edge="1" parent="1" source="pump_filt" target="tub"> |
|
| 223 | + <mxGeometry relative="1" as="geometry" /> |
|
| 224 | + </mxCell> |
|
| 225 | + |
|
| 226 | + <!-- GPIO summary table --> |
|
| 227 | + <mxCell id="table" value="<b>GPIO summary (BCM numbering)</b><br><table cellpadding="4" style="font-size:11px"><tr><td><b>Function</b></td><td><b>GPIO</b></td><td><b>Notes</b></td></tr><tr><td>Heat relay</td><td>22</td><td>control daemon (config.json: gpio.heat_pump)</td></tr><tr><td>Filter relay</td><td>23</td><td>control daemon (config.json: gpio.filter_pump)</td></tr><tr><td>1-Wire data (×5 probes)</td><td>4</td><td>w1-gpio overlay; 4.7kΩ pull-up to 3.3V; 4 tub + 1 hot-in</td></tr><tr><td>Low-water float</td><td>17</td><td><b>FUTURE — no hardware installed</b></td></tr><tr><td>Flow switch</td><td>27</td><td>optional, not implemented</td></tr></table><br><b>Note:</b> the shipped <code>hot-tub-temps</code> daemon reads the Pi's own 1-Wire sysfs directly — no separate USB/UART sensor hub is used." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;" vertex="1" parent="1"> |
|
| 228 | + <mxGeometry x="40" y="730" width="1280" height="150" as="geometry" /> |
|
| 229 | + </mxCell> |
|
| 230 | + |
|
| 231 | + <mxCell id="warn" value="⚠ Pi does not power pumps. Use GFCI protection. Drip loops on every plug. Never exceed the amp rating of the relay module, cord, or outlet. Do not automate the propane gas valve. Water level is currently software-presumed adequate — add the float switch before leaving the tub unattended." style="text;html=1;strokeColor=none;fillColor=#fff2cc;align=center;verticalAlign=middle;fontSize=11;fontStyle=1" vertex="1" parent="1"> |
|
| 232 | + <mxGeometry x="40" y="900" width="1280" height="30" as="geometry" /> |
|
| 233 | + </mxCell> |
|
| 234 | + |
|
| 235 | + </root> |
|
| 236 | + </mxGraphModel> |
|
| 237 | + </diagram> |
|
| 238 | +</mxfile> |
projects/hot-tub/diagrams/hot-tub-electrical-v3.drawio
| ... | ... | @@ -0,0 +1,260 @@ |
| 1 | +<mxfile host="app.diagrams.net" modified="2026-10-22T17:48:00.000Z" agent="cursor-agent" version="24.0.0" type="device"> |
|
| 2 | + <diagram id="hotTubElectricalV3" name="Hot Tub Electrical v3"> |
|
| 3 | + <mxGraphModel dx="2200" dy="1400" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1350" math="0" shadow="0"> |
|
| 4 | + <root> |
|
| 5 | + <mxCell id="0" /> |
|
| 6 | + <mxCell id="1" parent="0" /> |
|
| 7 | + |
|
| 8 | + <!-- ===== Title ===== --> |
|
| 9 | + <mxCell id="title1" value="Hot Tub Electrical Circuit Diagram — v3 (fresh design)" style="text;html=1;align=center;verticalAlign=middle;fontSize=22;fontStyle=1;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 10 | + <mxGeometry x="40" y="10" width="1710" height="40" as="geometry" /> |
|
| 11 | + </mxCell> |
|
| 12 | + |
|
| 13 | + <!-- ===== Section A: AC Power Path ===== --> |
|
| 14 | + <mxCell id="secA_bg" value="AC Power Path — 120V AC, GFCI Protected" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;fontSize=14;fontStyle=1;fillColor=#FFE6CC;strokeColor=#D79B00;spacingLeft=14;spacingTop=10;" vertex="1" parent="1"> |
|
| 15 | + <mxGeometry x="40" y="60" width="1710" height="260" as="geometry" /> |
|
| 16 | + </mxCell> |
|
| 17 | + |
|
| 18 | + <mxCell id="houseOutlet" value="House / Outdoor Outlet (120V AC)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 19 | + <mxGeometry x="80" y="160" width="160" height="70" as="geometry" /> |
|
| 20 | + </mxCell> |
|
| 21 | + |
|
| 22 | + <mxCell id="gfci" value="GFCI Protection (outlet, breaker, or inline GFCI adapter)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 23 | + <mxGeometry x="300" y="160" width="160" height="70" as="geometry" /> |
|
| 24 | + </mxCell> |
|
| 25 | + |
|
| 26 | + <mxCell id="extCord" value="Outdoor-Rated Extension Cord" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 27 | + <mxGeometry x="520" y="160" width="180" height="70" as="geometry" /> |
|
| 28 | + </mxCell> |
|
| 29 | + |
|
| 30 | + <mxCell id="relay1" value="Enclosed Relay Module 1 (low-voltage control input) "HEAT PUMP OUTLET"" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 31 | + <mxGeometry x="760" y="110" width="200" height="70" as="geometry" /> |
|
| 32 | + </mxCell> |
|
| 33 | + |
|
| 34 | + <mxCell id="relay2" value="Enclosed Relay Module 2 (low-voltage control input) "FILTER PUMP OUTLET"" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 35 | + <mxGeometry x="760" y="220" width="200" height="70" as="geometry" /> |
|
| 36 | + </mxCell> |
|
| 37 | + |
|
| 38 | + <mxCell id="heatPump" value="Heat Circulation Pump (2.64 gpm)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 39 | + <mxGeometry x="1020" y="110" width="180" height="70" as="geometry" /> |
|
| 40 | + </mxCell> |
|
| 41 | + |
|
| 42 | + <mxCell id="filterPump" value="Filter Pump (1000 gph)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#D79B00;" vertex="1" parent="1"> |
|
| 43 | + <mxGeometry x="1020" y="220" width="180" height="70" as="geometry" /> |
|
| 44 | + </mxCell> |
|
| 45 | + |
|
| 46 | + <mxCell id="propaneHeater" value="Propane Heater (manual gas & water controls)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#F5F5F5;strokeColor=#666666;" vertex="1" parent="1"> |
|
| 47 | + <mxGeometry x="1260" y="110" width="180" height="70" as="geometry" /> |
|
| 48 | + </mxCell> |
|
| 49 | + |
|
| 50 | + <mxCell id="tub" value="Stock Tank (6' x 2' x 2')" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#F5F5F5;strokeColor=#666666;" vertex="1" parent="1"> |
|
| 51 | + <mxGeometry x="1500" y="165" width="180" height="70" as="geometry" /> |
|
| 52 | + </mxCell> |
|
| 53 | + |
|
| 54 | + <!-- AC path edges --> |
|
| 55 | + <mxCell id="e_house_gfci" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;" edge="1" parent="1" source="houseOutlet" target="gfci"> |
|
| 56 | + <mxGeometry relative="1" as="geometry" /> |
|
| 57 | + </mxCell> |
|
| 58 | + <mxCell id="e_gfci_cord" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;" edge="1" parent="1" source="gfci" target="extCord"> |
|
| 59 | + <mxGeometry relative="1" as="geometry" /> |
|
| 60 | + </mxCell> |
|
| 61 | + <mxCell id="e_cord_relay1" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;exitX=1;exitY=0.25;entryX=0;entryY=0.5;" edge="1" parent="1" source="extCord" target="relay1"> |
|
| 62 | + <mxGeometry relative="1" as="geometry" /> |
|
| 63 | + </mxCell> |
|
| 64 | + <mxCell id="e_cord_relay2" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;exitX=1;exitY=0.75;entryX=0;entryY=0.5;" edge="1" parent="1" source="extCord" target="relay2"> |
|
| 65 | + <mxGeometry relative="1" as="geometry" /> |
|
| 66 | + </mxCell> |
|
| 67 | + <mxCell id="e_relay1_heatpump" value="switched 120V" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;fontSize=10;" edge="1" parent="1" source="relay1" target="heatPump"> |
|
| 68 | + <mxGeometry relative="1" as="geometry" /> |
|
| 69 | + </mxCell> |
|
| 70 | + <mxCell id="e_relay2_filterpump" value="switched 120V" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#D79B00;strokeWidth=2;rounded=0;fontSize=10;" edge="1" parent="1" source="relay2" target="filterPump"> |
|
| 71 | + <mxGeometry relative="1" as="geometry" /> |
|
| 72 | + </mxCell> |
|
| 73 | + |
|
| 74 | + <!-- Plumbing context edges (non-electrical, for reference only) --> |
|
| 75 | + <mxCell id="e_heatpump_heater" value="water flow" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#999999;dashed=1;dashPattern=1 3;endArrow=open;fontSize=9;fontColor=#666666;" edge="1" parent="1" source="heatPump" target="propaneHeater"> |
|
| 76 | + <mxGeometry relative="1" as="geometry" /> |
|
| 77 | + </mxCell> |
|
| 78 | + <mxCell id="e_heater_tub" value="hot return" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#999999;dashed=1;dashPattern=1 3;endArrow=open;fontSize=9;fontColor=#666666;exitX=1;exitY=0.5;entryX=0.5;entryY=0;" edge="1" parent="1" source="propaneHeater" target="tub"> |
|
| 79 | + <mxGeometry relative="1" as="geometry" /> |
|
| 80 | + </mxCell> |
|
| 81 | + <mxCell id="e_filterpump_tub" value="filter loop (intake & return)" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#999999;dashed=1;dashPattern=1 3;startArrow=open;endArrow=open;fontSize=9;fontColor=#666666;exitX=1;exitY=0.5;entryX=0.5;entryY=1;" edge="1" parent="1" source="filterPump" target="tub"> |
|
| 82 | + <mxGeometry relative="1" as="geometry" /> |
|
| 83 | + </mxCell> |
|
| 84 | + |
|
| 85 | + <!-- ===== Section B: Low-Voltage Control (Raspberry Pi) ===== --> |
|
| 86 | + <mxCell id="secB_bg" value="Low-Voltage Control — Raspberry Pi GPIO" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;fontSize=14;fontStyle=1;fillColor=#DAE8FC;strokeColor=#6C8EBF;spacingLeft=14;spacingTop=10;" vertex="1" parent="1"> |
|
| 87 | + <mxGeometry x="40" y="340" width="1710" height="220" as="geometry" /> |
|
| 88 | + </mxCell> |
|
| 89 | + |
|
| 90 | + <mxCell id="piPower" value="5V USB Power Supply (separate wall outlet, not through relay)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fillColor=#FFFFFF;strokeColor=#1F77B4;" vertex="1" parent="1"> |
|
| 91 | + <mxGeometry x="80" y="420" width="180" height="70" as="geometry" /> |
|
| 92 | + </mxCell> |
|
| 93 | + |
|
| 94 | + <mxCell id="pi" value="Raspberry Pi GPIO4 = 1-Wire data bus GPIO22 = Heat relay control GPIO23 = Filter relay control GPIO17 = Float switch (future) GPIO27 = Flow switch (optional) 3.3V / GND = sensor power" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;align=left;spacingLeft=8;fillColor=#FFFFFF;strokeColor=#1F77B4;strokeWidth=2;" vertex="1" parent="1"> |
|
| 95 | + <mxGeometry x="360" y="390" width="260" height="140" as="geometry" /> |
|
| 96 | + </mxCell> |
|
| 97 | + |
|
| 98 | + <mxCell id="noteB" value="Blue lines above connect GPIO22 / GPIO23 to the relay modules' low-voltage control inputs in the AC Power Path section. The Pi never switches 120V AC directly — it only triggers the enclosed relay modules." style="text;html=1;align=left;verticalAlign=middle;fontSize=11;fontStyle=2;fillColor=none;strokeColor=none;whiteSpace=wrap;" vertex="1" parent="1"> |
|
| 99 | + <mxGeometry x="680" y="420" width="1000" height="60" as="geometry" /> |
|
| 100 | + </mxCell> |
|
| 101 | + |
|
| 102 | + <mxCell id="e_pipower_pi" value="5V" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#1F77B4;strokeWidth=2;rounded=0;fontSize=10;" edge="1" parent="1" source="piPower" target="pi"> |
|
| 103 | + <mxGeometry relative="1" as="geometry" /> |
|
| 104 | + </mxCell> |
|
| 105 | + |
|
| 106 | + <mxCell id="e_pi_relay1_ctrl" value="GPIO22 control (low-voltage)" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#1F77B4;strokeWidth=2;rounded=0;fontSize=10;exitX=0.65;exitY=0;entryX=0.15;entryY=1;" edge="1" parent="1" source="pi" target="relay1"> |
|
| 107 | + <mxGeometry relative="1" as="geometry" /> |
|
| 108 | + </mxCell> |
|
| 109 | + |
|
| 110 | + <mxCell id="e_pi_relay2_ctrl" value="GPIO23 control (low-voltage)" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#1F77B4;strokeWidth=2;rounded=0;fontSize=10;exitX=0.85;exitY=0;entryX=0.15;entryY=1;" edge="1" parent="1" source="pi" target="relay2"> |
|
| 111 | + <mxGeometry relative="1" as="geometry" /> |
|
| 112 | + </mxCell> |
|
| 113 | + |
|
| 114 | + <!-- ===== Section C: 1-Wire Temperature Sensor Bus ===== --> |
|
| 115 | + <mxCell id="secC_bg" value="1-Wire Temperature Sensor Bus — GPIO4" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;fontSize=14;fontStyle=1;fillColor=#D5E8D4;strokeColor=#82B366;spacingLeft=14;spacingTop=10;" vertex="1" parent="1"> |
|
| 116 | + <mxGeometry x="40" y="580" width="1710" height="380" as="geometry" /> |
|
| 117 | + </mxCell> |
|
| 118 | + |
|
| 119 | + <mxCell id="resistor" value="4.7kΩ Pull-up Resistor" style="rounded=0;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 120 | + <mxGeometry x="660" y="630" width="160" height="50" as="geometry" /> |
|
| 121 | + </mxCell> |
|
| 122 | + |
|
| 123 | + <mxCell id="junction" value="" style="ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#000000;resizable=0;" vertex="1" parent="1"> |
|
| 124 | + <mxGeometry x="732" y="700" width="16" height="16" as="geometry" /> |
|
| 125 | + </mxCell> |
|
| 126 | + |
|
| 127 | + <mxCell id="bus" value="Shared 1-Wire Bus (GPIO4 Data + 3.3V + GND)" style="rounded=0;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;strokeWidth=2;" vertex="1" parent="1"> |
|
| 128 | + <mxGeometry x="700" y="750" width="960" height="40" as="geometry" /> |
|
| 129 | + </mxCell> |
|
| 130 | + |
|
| 131 | + <mxCell id="labelHotInNote" value="Not included in tub average" style="text;html=1;align=center;verticalAlign=middle;fontSize=10;fontStyle=2;fillColor=none;strokeColor=none;whiteSpace=wrap;" vertex="1" parent="1"> |
|
| 132 | + <mxGeometry x="700" y="805" width="170" height="20" as="geometry" /> |
|
| 133 | + </mxCell> |
|
| 134 | + |
|
| 135 | + <mxCell id="labelTubAvg" value="4 Tub Probes — averaged for tub temperature (software flags disagreement between probes)" style="text;html=1;align=center;verticalAlign=middle;fontSize=10;fontStyle=2;fillColor=none;strokeColor=none;whiteSpace=wrap;" vertex="1" parent="1"> |
|
| 136 | + <mxGeometry x="900" y="805" width="760" height="20" as="geometry" /> |
|
| 137 | + </mxCell> |
|
| 138 | + |
|
| 139 | + <mxCell id="probeHotIn" value="DS18B20 Hot-Water-Input Probe (hot_in_f)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 140 | + <mxGeometry x="700" y="830" width="170" height="80" as="geometry" /> |
|
| 141 | + </mxCell> |
|
| 142 | + |
|
| 143 | + <mxCell id="probeTub1" value="DS18B20 Tub Probe 1 (hot-return side)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 144 | + <mxGeometry x="900" y="830" width="170" height="80" as="geometry" /> |
|
| 145 | + </mxCell> |
|
| 146 | + |
|
| 147 | + <mxCell id="probeTub2" value="DS18B20 Tub Probe 2 (opposite side)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 148 | + <mxGeometry x="1100" y="830" width="170" height="80" as="geometry" /> |
|
| 149 | + </mxCell> |
|
| 150 | + |
|
| 151 | + <mxCell id="probeTub3" value="DS18B20 Tub Probe 3 (near end)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 152 | + <mxGeometry x="1300" y="830" width="170" height="80" as="geometry" /> |
|
| 153 | + </mxCell> |
|
| 154 | + |
|
| 155 | + <mxCell id="probeTub4" value="DS18B20 Tub Probe 4 (far end)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#FFFFFF;strokeColor=#2CA02C;" vertex="1" parent="1"> |
|
| 156 | + <mxGeometry x="1500" y="830" width="170" height="80" as="geometry" /> |
|
| 157 | + </mxCell> |
|
| 158 | + |
|
| 159 | + <!-- Sensor bus edges --> |
|
| 160 | + <mxCell id="e_pi_junction" value="GPIO4 (1-Wire Data)" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=2;rounded=0;fontSize=10;exitX=0.3;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="pi" target="junction"> |
|
| 161 | + <mxGeometry relative="1" as="geometry" /> |
|
| 162 | + </mxCell> |
|
| 163 | + |
|
| 164 | + <mxCell id="e_junction_resistor" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=2;rounded=0;exitX=0.5;exitY=0;entryX=0.3;entryY=1;" edge="1" parent="1" source="junction" target="resistor"> |
|
| 165 | + <mxGeometry relative="1" as="geometry" /> |
|
| 166 | + </mxCell> |
|
| 167 | + |
|
| 168 | + <mxCell id="e_resistor_pi3v3" value="3.3V" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=2;rounded=0;fontSize=10;exitX=0.3;exitY=0;entryX=0.45;entryY=1;" edge="1" parent="1" source="resistor" target="pi"> |
|
| 169 | + <mxGeometry relative="1" as="geometry" /> |
|
| 170 | + </mxCell> |
|
| 171 | + |
|
| 172 | + <mxCell id="e_junction_bus" value="data" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=2;rounded=0;fontSize=10;exitX=1;exitY=0.5;entryX=0.05;entryY=0;" edge="1" parent="1" source="junction" target="bus"> |
|
| 173 | + <mxGeometry relative="1" as="geometry" /> |
|
| 174 | + </mxCell> |
|
| 175 | + |
|
| 176 | + <mxCell id="e_pi_gnd_bus" value="GND" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=2;rounded=0;fontSize=10;exitX=0.55;exitY=1;entryX=0.95;entryY=0;" edge="1" parent="1" source="pi" target="bus"> |
|
| 177 | + <mxGeometry relative="1" as="geometry" /> |
|
| 178 | + </mxCell> |
|
| 179 | + |
|
| 180 | + <mxCell id="e_bus_probeHotIn" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=1;rounded=0;endArrow=none;startArrow=none;" edge="1" parent="1" source="bus" target="probeHotIn"> |
|
| 181 | + <mxGeometry relative="1" as="geometry" /> |
|
| 182 | + </mxCell> |
|
| 183 | + <mxCell id="e_bus_probeTub1" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=1;rounded=0;endArrow=none;startArrow=none;" edge="1" parent="1" source="bus" target="probeTub1"> |
|
| 184 | + <mxGeometry relative="1" as="geometry" /> |
|
| 185 | + </mxCell> |
|
| 186 | + <mxCell id="e_bus_probeTub2" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=1;rounded=0;endArrow=none;startArrow=none;" edge="1" parent="1" source="bus" target="probeTub2"> |
|
| 187 | + <mxGeometry relative="1" as="geometry" /> |
|
| 188 | + </mxCell> |
|
| 189 | + <mxCell id="e_bus_probeTub3" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=1;rounded=0;endArrow=none;startArrow=none;" edge="1" parent="1" source="bus" target="probeTub3"> |
|
| 190 | + <mxGeometry relative="1" as="geometry" /> |
|
| 191 | + </mxCell> |
|
| 192 | + <mxCell id="e_bus_probeTub4" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#2CA02C;strokeWidth=1;rounded=0;endArrow=none;startArrow=none;" edge="1" parent="1" source="bus" target="probeTub4"> |
|
| 193 | + <mxGeometry relative="1" as="geometry" /> |
|
| 194 | + </mxCell> |
|
| 195 | + |
|
| 196 | + <!-- ===== Section D: Future / Optional additions ===== --> |
|
| 197 | + <mxCell id="secD_bg" value="Future / Optional — Not Currently Installed" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;fontSize=14;fontStyle=1;fillColor=#F5F5F5;strokeColor=#666666;dashed=1;spacingLeft=14;spacingTop=10;" vertex="1" parent="1"> |
|
| 198 | + <mxGeometry x="40" y="980" width="1710" height="160" as="geometry" /> |
|
| 199 | + </mxCell> |
|
| 200 | + |
|
| 201 | + <mxCell id="floatSwitch" value="Low-Water Float Switch GPIO17 — FUTURE (not installed; water level presumed adequate today)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#F5F5F5;strokeColor=#666666;dashed=1;" vertex="1" parent="1"> |
|
| 202 | + <mxGeometry x="300" y="1030" width="240" height="80" as="geometry" /> |
|
| 203 | + </mxCell> |
|
| 204 | + |
|
| 205 | + <mxCell id="flowSwitch" value="Inline Flow Switch GPIO27 — OPTIONAL (not installed)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#F5F5F5;strokeColor=#666666;dashed=1;" vertex="1" parent="1"> |
|
| 206 | + <mxGeometry x="620" y="1030" width="240" height="80" as="geometry" /> |
|
| 207 | + </mxCell> |
|
| 208 | + |
|
| 209 | + <mxCell id="outsideTemp" value="Outside Air DS18B20 Probe OPTIONAL — would share the 1-Wire bus above (not installed; for future anti-freeze logic)" style="rounded=1;whiteSpace=wrap;html=1;fontSize=11;fillColor=#F5F5F5;strokeColor=#666666;dashed=1;" vertex="1" parent="1"> |
|
| 210 | + <mxGeometry x="940" y="1030" width="260" height="80" as="geometry" /> |
|
| 211 | + </mxCell> |
|
| 212 | + |
|
| 213 | + <mxCell id="e_bus_outsideTemp" value="optional bus tap" style="edgeStyle=orthogonalEdgeStyle;html=1;strokeColor=#666666;dashed=1;dashPattern=2 2;rounded=0;fontSize=9;exitX=0.9;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="bus" target="outsideTemp"> |
|
| 214 | + <mxGeometry relative="1" as="geometry" /> |
|
| 215 | + </mxCell> |
|
| 216 | + |
|
| 217 | + <!-- ===== Legend ===== --> |
|
| 218 | + <mxCell id="legend_bg" value="Legend" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=left;fontSize=14;fontStyle=1;fillColor=#FFFFFF;strokeColor=#000000;spacingLeft=14;spacingTop=10;" vertex="1" parent="1"> |
|
| 219 | + <mxGeometry x="40" y="1160" width="620" height="170" as="geometry" /> |
|
| 220 | + </mxCell> |
|
| 221 | + |
|
| 222 | + <mxCell id="legendSwatch1" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#D79B00;strokeColor=none;" vertex="1" parent="1"> |
|
| 223 | + <mxGeometry x="60" y="1198" width="40" height="10" as="geometry" /> |
|
| 224 | + </mxCell> |
|
| 225 | + <mxCell id="legendText1" value="120V AC mains (GFCI protected)" style="text;html=1;align=left;verticalAlign=middle;fontSize=12;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 226 | + <mxGeometry x="115" y="1190" width="500" height="26" as="geometry" /> |
|
| 227 | + </mxCell> |
|
| 228 | + |
|
| 229 | + <mxCell id="legendSwatch2" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1F77B4;strokeColor=none;" vertex="1" parent="1"> |
|
| 230 | + <mxGeometry x="60" y="1228" width="40" height="10" as="geometry" /> |
|
| 231 | + </mxCell> |
|
| 232 | + <mxCell id="legendText2" value="Low-voltage DC — Pi power & relay control signals" style="text;html=1;align=left;verticalAlign=middle;fontSize=12;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 233 | + <mxGeometry x="115" y="1220" width="500" height="26" as="geometry" /> |
|
| 234 | + </mxCell> |
|
| 235 | + |
|
| 236 | + <mxCell id="legendSwatch3" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#2CA02C;strokeColor=none;" vertex="1" parent="1"> |
|
| 237 | + <mxGeometry x="60" y="1258" width="40" height="10" as="geometry" /> |
|
| 238 | + </mxCell> |
|
| 239 | + <mxCell id="legendText3" value="1-Wire sensor bus (3.3V / GND / GPIO4 data)" style="text;html=1;align=left;verticalAlign=middle;fontSize=12;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 240 | + <mxGeometry x="115" y="1250" width="500" height="26" as="geometry" /> |
|
| 241 | + </mxCell> |
|
| 242 | + |
|
| 243 | + <mxCell id="legendSwatch4" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#999999;dashed=1;" vertex="1" parent="1"> |
|
| 244 | + <mxGeometry x="60" y="1288" width="40" height="10" as="geometry" /> |
|
| 245 | + </mxCell> |
|
| 246 | + <mxCell id="legendText4" value="Plumbing / water flow (non-electrical, shown for context)" style="text;html=1;align=left;verticalAlign=middle;fontSize=12;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 247 | + <mxGeometry x="115" y="1280" width="500" height="26" as="geometry" /> |
|
| 248 | + </mxCell> |
|
| 249 | + |
|
| 250 | + <mxCell id="legendSwatch5" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#666666;dashed=1;" vertex="1" parent="1"> |
|
| 251 | + <mxGeometry x="60" y="1313" width="40" height="10" as="geometry" /> |
|
| 252 | + </mxCell> |
|
| 253 | + <mxCell id="legendText5" value="Future / optional — not currently installed" style="text;html=1;align=left;verticalAlign=middle;fontSize=12;fillColor=none;strokeColor=none;" vertex="1" parent="1"> |
|
| 254 | + <mxGeometry x="115" y="1305" width="500" height="26" as="geometry" /> |
|
| 255 | + </mxCell> |
|
| 256 | + |
|
| 257 | + </root> |
|
| 258 | + </mxGraphModel> |
|
| 259 | + </diagram> |
|
| 260 | +</mxfile> |
projects/hot-tub/diagrams/hot-tub-electrical.drawio
| ... | ... | @@ -0,0 +1,180 @@ |
| 1 | +<mxfile host="app.diagrams.net" modified="2026-10-22T18:00:00.000Z" agent="Cursor" version="22.1.0" type="device"> |
|
| 2 | + <diagram id="hot-tub-electrical" name="Hot tub electrical"> |
|
| 3 | + <mxGraphModel dx="1200" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="940" math="0" shadow="0"> |
|
| 4 | + <root> |
|
| 5 | + <mxCell id="0" /> |
|
| 6 | + <mxCell id="1" parent="0" /> |
|
| 7 | + |
|
| 8 | + <mxCell id="title" value="Hot tub — electrical wiring (control side)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=18;fontStyle=1" vertex="1" parent="1"> |
|
| 9 | + <mxGeometry x="250" y="20" width="700" height="40" as="geometry" /> |
|
| 10 | + </mxCell> |
|
| 11 | + <mxCell id="subtitle" value="Matches hardware-setup.md and control.json (GPIO22 heat, GPIO23 filter, GPIO4 1-Wire). All 5 DS18B20 probes read directly on the Pi's own 1-Wire bus. Low-water float is a FUTURE addition — not installed, no GPIO wired today." style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;fontSize=11;fontColor=#666666;" vertex="1" parent="1"> |
|
| 12 | + <mxGeometry x="100" y="56" width="1000" height="34" as="geometry" /> |
|
| 13 | + </mxCell> |
|
| 14 | + |
|
| 15 | + <mxCell id="enc" value="Waterproof enclosure — keep low-voltage wiring separate from AC pump cords" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;dashPattern=8 8;verticalAlign=top;fontStyle=1;spacingTop=8;" vertex="1" parent="1"> |
|
| 16 | + <mxGeometry x="40" y="100" width="580" height="600" as="geometry" /> |
|
| 17 | + </mxCell> |
|
| 18 | + |
|
| 19 | + <mxCell id="pi" value="<b>Raspberry Pi 3</b><br>(controller)<br><br>• hot-tub-control<br>• hot-tub-temps<br>• hot-tub-web<br>• Redis (localhost)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;" vertex="1" parent="1"> |
|
| 20 | + <mxGeometry x="70" y="150" width="200" height="130" as="geometry" /> |
|
| 21 | + </mxCell> |
|
| 22 | + |
|
| 23 | + <mxCell id="busbar" value="1-Wire bus — GPIO4 (BCM), 4.7 kΩ pull-up to 3.3V, shared GND" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontStyle=1;fontSize=11;" vertex="1" parent="1"> |
|
| 24 | + <mxGeometry x="70" y="300" width="510" height="34" as="geometry" /> |
|
| 25 | + </mxCell> |
|
| 26 | + |
|
| 27 | + <mxCell id="lbl_water" value="Water bundle — 4 probes (tub_avg_f)" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontStyle=1;fontColor=#8a6d1a;" vertex="1" parent="1"> |
|
| 28 | + <mxGeometry x="70" y="348" width="320" height="16" as="geometry" /> |
|
| 29 | + </mxCell> |
|
| 30 | + <mxCell id="lbl_hotin" value="Hot-in bundle — 1 probe (hot_in_f)" style="text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontStyle=1;fontColor=#8a6d1a;" vertex="1" parent="1"> |
|
| 31 | + <mxGeometry x="400" y="348" width="180" height="16" as="geometry" /> |
|
| 32 | + </mxCell> |
|
| 33 | + |
|
| 34 | + <mxCell id="tub1" value="Tub probe 1<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1"> |
|
| 35 | + <mxGeometry x="70" y="368" width="90" height="56" as="geometry" /> |
|
| 36 | + </mxCell> |
|
| 37 | + <mxCell id="tub2" value="Tub probe 2<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1"> |
|
| 38 | + <mxGeometry x="170" y="368" width="90" height="56" as="geometry" /> |
|
| 39 | + </mxCell> |
|
| 40 | + <mxCell id="tub3" value="Tub probe 3<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1"> |
|
| 41 | + <mxGeometry x="270" y="368" width="90" height="56" as="geometry" /> |
|
| 42 | + </mxCell> |
|
| 43 | + <mxCell id="tub4" value="Tub probe 4<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1"> |
|
| 44 | + <mxGeometry x="370" y="368" width="90" height="56" as="geometry" /> |
|
| 45 | + </mxCell> |
|
| 46 | + <mxCell id="hotin" value="Hot-in probe<br>DS18B20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6b3;strokeColor=#d6b656;fontSize=10;" vertex="1" parent="1"> |
|
| 47 | + <mxGeometry x="490" y="368" width="90" height="56" as="geometry" /> |
|
| 48 | + </mxCell> |
|
| 49 | + |
|
| 50 | + <mxCell id="note_probes" value="5 physical DS18B20 probes share one 1-Wire bus, wired as two pluggable bundles (M8/M12 drops). Identity (which ROM id is which role) is learned at install time — see src/temp/README.md." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;dashed=1;align=left;spacingLeft=10;verticalAlign=top;spacingTop=6;fontSize=10;fontColor=#555555;" vertex="1" parent="1"> |
|
| 51 | + <mxGeometry x="70" y="434" width="510" height="56" as="geometry" /> |
|
| 52 | + </mxCell> |
|
| 53 | + |
|
| 54 | + <mxCell id="future_box" value="FUTURE CONCERN — not yet installed" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fdf0e6;strokeColor=#d79b00;dashed=1;dashPattern=6 6;verticalAlign=top;fontStyle=1;fontSize=12;fontColor=#9a5b00;spacingTop=6;align=center;" vertex="1" parent="1"> |
|
| 55 | + <mxGeometry x="70" y="500" width="510" height="130" as="geometry" /> |
|
| 56 | + </mxCell> |
|
| 57 | + |
|
| 58 | + <mxCell id="float" value="<b>Low-water float switch</b><br>Planned: GPIO17 → switch, other side → GND<br><i>No hardware installed. Water level is presumed adequate in software (src/control/safety.py TODO). Not wired into the control daemon.</i>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;dashPattern=4 4;align=left;spacingLeft=10;verticalAlign=top;spacingTop=6;fontSize=10;" vertex="1" parent="1"> |
|
| 59 | + <mxGeometry x="90" y="530" width="240" height="90" as="geometry" /> |
|
| 60 | + </mxCell> |
|
| 61 | + |
|
| 62 | + <mxCell id="flow" value="<b>Flow switch</b> (optional)<br>Planned: GPIO27 → GND<br><i>No hardware installed; not implemented.</i>" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;dashed=1;dashPattern=4 4;align=left;spacingLeft=10;verticalAlign=top;spacingTop=6;fontSize=10;" vertex="1" parent="1"> |
|
| 63 | + <mxGeometry x="350" y="530" width="210" height="90" as="geometry" /> |
|
| 64 | + </mxCell> |
|
| 65 | + |
|
| 66 | + <mxCell id="gpio_out" value="<b>GPIO outputs</b> — BCM22 → heat relay IN+ · BCM23 → filter relay IN+ · common GND to relay modules · active_high: true (config)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;spacingLeft=10;verticalAlign=middle;" vertex="1" parent="1"> |
|
| 67 | + <mxGeometry x="70" y="650" width="510" height="34" as="geometry" /> |
|
| 68 | + </mxCell> |
|
| 69 | + |
|
| 70 | + <mxCell id="e_pi_bus" value="3.3V + GND" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;endArrow=classic;endFill=1;fontSize=10;" edge="1" parent="1" source="pi" target="busbar"> |
|
| 71 | + <mxGeometry relative="1" as="geometry" /> |
|
| 72 | + </mxCell> |
|
| 73 | + <mxCell id="e_tub1_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub1" target="busbar"> |
|
| 74 | + <mxGeometry relative="1" as="geometry" /> |
|
| 75 | + </mxCell> |
|
| 76 | + <mxCell id="e_tub2_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub2" target="busbar"> |
|
| 77 | + <mxGeometry relative="1" as="geometry" /> |
|
| 78 | + </mxCell> |
|
| 79 | + <mxCell id="e_tub3_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub3" target="busbar"> |
|
| 80 | + <mxGeometry relative="1" as="geometry" /> |
|
| 81 | + </mxCell> |
|
| 82 | + <mxCell id="e_tub4_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="tub4" target="busbar"> |
|
| 83 | + <mxGeometry relative="1" as="geometry" /> |
|
| 84 | + </mxCell> |
|
| 85 | + <mxCell id="e_hotin_bus" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#d6b656;startArrow=none;endArrow=none;" edge="1" parent="1" source="hotin" target="busbar"> |
|
| 86 | + <mxGeometry relative="1" as="geometry" /> |
|
| 87 | + </mxCell> |
|
| 88 | + |
|
| 89 | + <mxCell id="e_float_pi" value="GPIO17 (future)" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#9673a6;dashed=1;dashPattern=4 4;endArrow=classic;fontSize=9;" edge="1" parent="1" source="float" target="pi"> |
|
| 90 | + <mxGeometry relative="1" as="geometry"> |
|
| 91 | + <Array as="points"> |
|
| 92 | + <mxPoint x="30" y="575" /> |
|
| 93 | + <mxPoint x="30" y="215" /> |
|
| 94 | + </Array> |
|
| 95 | + </mxGeometry> |
|
| 96 | + </mxCell> |
|
| 97 | + |
|
| 98 | + <mxCell id="ac_box" value="AC side (enclosed relay modules or outdoor-rated location)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;dashed=1;dashPattern=8 8;verticalAlign=top;fontStyle=1;spacingTop=8;" vertex="1" parent="1"> |
|
| 99 | + <mxGeometry x="650" y="100" width="460" height="420" as="geometry" /> |
|
| 100 | + </mxCell> |
|
| 101 | + |
|
| 102 | + <mxCell id="gfci" value="<b>Outdoor outlet</b><br>+ GFCI protection" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1"> |
|
| 103 | + <mxGeometry x="690" y="140" width="160" height="60" as="geometry" /> |
|
| 104 | + </mxCell> |
|
| 105 | + |
|
| 106 | + <mxCell id="cord" value="Outdoor-rated<br>extension cord" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1"> |
|
| 107 | + <mxGeometry x="890" y="140" width="120" height="60" as="geometry" /> |
|
| 108 | + </mxCell> |
|
| 109 | + |
|
| 110 | + <mxCell id="relay1" value="<b>Enclosed AC relay #1</b><br>HEAT PUMP outlet<br>Low-voltage IN ← GPIO22" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;align=left;spacingLeft=8;" vertex="1" parent="1"> |
|
| 111 | + <mxGeometry x="690" y="240" width="200" height="80" as="geometry" /> |
|
| 112 | + </mxCell> |
|
| 113 | + |
|
| 114 | + <mxCell id="relay2" value="<b>Enclosed AC relay #2</b><br>FILTER PUMP outlet<br>Low-voltage IN ← GPIO23" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;align=left;spacingLeft=8;" vertex="1" parent="1"> |
|
| 115 | + <mxGeometry x="690" y="350" width="200" height="80" as="geometry" /> |
|
| 116 | + </mxCell> |
|
| 117 | + |
|
| 118 | + <mxCell id="pump_heat" value="<b>Heat circulation pump</b><br>120 VAC plug → relay #1<br>Tub → pump → propane heater → tub" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#b85450;align=left;spacingLeft=8;" vertex="1" parent="1"> |
|
| 119 | + <mxGeometry x="910" y="230" width="180" height="90" as="geometry" /> |
|
| 120 | + </mxCell> |
|
| 121 | + |
|
| 122 | + <mxCell id="pump_filt" value="<b>Filter pump</b><br>120 VAC plug → relay #2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#b85450;align=left;spacingLeft=8;" vertex="1" parent="1"> |
|
| 123 | + <mxGeometry x="910" y="345" width="180" height="70" as="geometry" /> |
|
| 124 | + </mxCell> |
|
| 125 | + |
|
| 126 | + <mxCell id="heater" value="<b>Propane camp heater</b><br>Gas & controls: <b>manual</b><br>Ignites on water flow" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#666666;align=left;spacingLeft=8;" vertex="1" parent="1"> |
|
| 127 | + <mxGeometry x="910" y="430" width="180" height="70" as="geometry" /> |
|
| 128 | + </mxCell> |
|
| 129 | + |
|
| 130 | + <mxCell id="e_gfci_cord" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="gfci" target="cord"> |
|
| 131 | + <mxGeometry relative="1" as="geometry" /> |
|
| 132 | + </mxCell> |
|
| 133 | + <mxCell id="e_cord_r1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="cord" target="relay1"> |
|
| 134 | + <mxGeometry relative="1" as="geometry" /> |
|
| 135 | + </mxCell> |
|
| 136 | + <mxCell id="e_cord_r2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="cord" target="relay2"> |
|
| 137 | + <mxGeometry relative="1" as="geometry" /> |
|
| 138 | + </mxCell> |
|
| 139 | + <mxCell id="e_r1_p" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="relay1" target="pump_heat"> |
|
| 140 | + <mxGeometry relative="1" as="geometry" /> |
|
| 141 | + </mxCell> |
|
| 142 | + <mxCell id="e_r2_p" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#b85450;endArrow=classic;" edge="1" parent="1" source="relay2" target="pump_filt"> |
|
| 143 | + <mxGeometry relative="1" as="geometry" /> |
|
| 144 | + </mxCell> |
|
| 145 | + <mxCell id="e_pump_h" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#666666;dashed=1;endArrow=classic;" edge="1" parent="1" source="pump_heat" target="heater"> |
|
| 146 | + <mxGeometry relative="1" as="geometry" /> |
|
| 147 | + </mxCell> |
|
| 148 | + |
|
| 149 | + <mxCell id="e_gpio_r1" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;endArrow=classic;strokeWidth=2;" edge="1" parent="1" source="gpio_out" target="relay1"> |
|
| 150 | + <mxGeometry relative="1" as="geometry"> |
|
| 151 | + <Array as="points"> |
|
| 152 | + <mxPoint x="630" y="667" /> |
|
| 153 | + <mxPoint x="630" y="280" /> |
|
| 154 | + </Array> |
|
| 155 | + </mxGeometry> |
|
| 156 | + </mxCell> |
|
| 157 | + <mxCell id="e_gpio_r2" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#82b366;endArrow=classic;strokeWidth=2;" edge="1" parent="1" source="gpio_out" target="relay2"> |
|
| 158 | + <mxGeometry relative="1" as="geometry"> |
|
| 159 | + <Array as="points"> |
|
| 160 | + <mxPoint x="615" y="667" /> |
|
| 161 | + <mxPoint x="615" y="390" /> |
|
| 162 | + </Array> |
|
| 163 | + </mxGeometry> |
|
| 164 | + </mxCell> |
|
| 165 | + <mxCell id="e_gpio_l" value="Low-voltage control wires only (follow relay module manual)" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=10;fontColor=#006600;" vertex="1" parent="1"> |
|
| 166 | + <mxGeometry x="595" y="560" width="105" height="90" as="geometry" /> |
|
| 167 | + </mxCell> |
|
| 168 | + |
|
| 169 | + <mxCell id="table" value="<b>GPIO summary (BCM)</b><br><table cellpadding="4" style="font-size:11px"><tr><td><b>Function</b></td><td><b>GPIO</b></td><td><b>Notes</b></td></tr><tr><td>Heat relay</td><td>22</td><td>control daemon</td></tr><tr><td>Filter relay</td><td>23</td><td>control daemon</td></tr><tr><td>1-Wire data (×5 probes)</td><td>4</td><td>w1-gpio overlay; 4.7k pull-up; 4 tub + 1 hot-in</td></tr><tr><td>Low-water float</td><td>17</td><td><b>FUTURE — no hardware installed</b></td></tr><tr><td>Flow switch</td><td>27</td><td>optional, not implemented</td></tr></table><br><b>Note:</b> the shipped <code>hot-tub-temps</code> daemon reads the Pi's own 1-Wire sysfs directly — no separate USB/UART sensor hub is used." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;align=left;spacingLeft=10;verticalAlign=top;spacingTop=8;" vertex="1" parent="1"> |
|
| 170 | + <mxGeometry x="40" y="730" width="1120" height="160" as="geometry" /> |
|
| 171 | + </mxCell> |
|
| 172 | + |
|
| 173 | + <mxCell id="warn" value="⚠ Pi does not power pumps. Use GFCI. Drip loops on all plugs. Do not exceed relay/cord/outlet amp ratings. Water level is currently software-presumed adequate — add the float switch before leaving the tub unattended." style="text;html=1;strokeColor=none;fillColor=#fff2cc;align=center;verticalAlign=middle;fontSize=11;fontStyle=1" vertex="1" parent="1"> |
|
| 174 | + <mxGeometry x="40" y="900" width="1120" height="30" as="geometry" /> |
|
| 175 | + </mxCell> |
|
| 176 | + |
|
| 177 | + </root> |
|
| 178 | + </mxGraphModel> |
|
| 179 | + </diagram> |
|
| 180 | +</mxfile> |
projects/hot-tub/hardware-setup.md
| ... | ... | @@ -8,7 +8,11 @@ The design goal is to keep the system beginner-friendly: use plug-in AC switchin |
| 8 | 8 | |
| 9 | 9 | ## System Overview |
| 10 | 10 | |
| 11 | -The Raspberry Pi receives temperature data from a small sensor hub, reads safety sensors, then turns two AC loads on and off: |
|
| 11 | +**Editable wiring diagrams:** [diagrams/hot-tub-electrical.drawio](diagrams/hot-tub-electrical.drawio) (v1) and [diagrams/hot-tub-electrical-v2.drawio](diagrams/hot-tub-electrical-v2.drawio) (v2, same facts, swimlane layout with legend) — open either in [diagrams.net](https://app.diagrams.net/) or the Draw.io desktop app. |
|
| 12 | + |
|
| 13 | +**Current understanding (2026-10-22):** the Raspberry Pi reads **five DS18B20 probes directly on its own 1-Wire bus** (4 tub-water probes + 1 hot-water-input probe) — there is no separate microcontroller sensor hub in the shipped design (that approach is kept below only as a legacy alternative). A **low-water float switch is a FUTURE addition**: no hardware is installed yet, and the control daemon does not read one today. Water level is presumed adequate in software until that hardware exists. |
|
| 14 | + |
|
| 15 | +The Raspberry Pi reads the five temperature probes, then turns two AC loads on and off: |
|
| 12 | 16 | |
| 13 | 17 | - Heat circulation pump: moves tub water through the propane heater. |
| 14 | 18 | - Filter pump: runs the 1000 gph filtration pump on demand or on a schedule. |
| ... | ... | @@ -27,20 +31,20 @@ When the heat pump runs, water flow causes the propane heater to ignite using it |
| 27 | 31 | +-------------+ |
| 28 | 32 | | Raspberry Pi | |
| 29 | 33 | +-------------+ |
| 30 | - | | | |
|
| 31 | - USB / UART| | |GPIO / USB control |
|
| 32 | - | | | |
|
| 33 | - +-----+ | +------------------+ |
|
| 34 | - | | | |
|
| 35 | - +-----------+ +-----------+ +-----------+ |
|
| 36 | - | Temp | | Low Water | | AC Switch | |
|
| 37 | - | Sensor Hub| | Sensor | | Modules | |
|
| 38 | - +-----+-----+ +-----------+ +-----+-----+ |
|
| 39 | - | | |
|
| 40 | - +-----------+ +--------------+--------------+ |
|
| 41 | - | 4 Tub | | | |
|
| 42 | - | Probes | +---------------+ +---------------+ |
|
| 43 | - +-----------+ | Heat Circ Pump| | Filter Pump | |
|
| 34 | + | | |
|
| 35 | + 1-Wire GPIO4| |GPIO22 / GPIO23 |
|
| 36 | + | | |
|
| 37 | + +-----------+ +------------------+ |
|
| 38 | + | 5 DS18B20 | | |
|
| 39 | + | probes: | +-----------+ |
|
| 40 | + | 4 tub + | | AC Switch | |
|
| 41 | + | 1 hot-in | | Modules | |
|
| 42 | + +-----------+ +-----+-----+ |
|
| 43 | + | |
|
| 44 | + +--------------+--------------+ |
|
| 45 | + | | |
|
| 46 | + +---------------+ +---------------+ |
|
| 47 | + | Heat Circ Pump| | Filter Pump | |
|
| 44 | 48 | +-------+-------+ +---------------+ |
| 45 | 49 | | |
| 46 | 50 | v |
| ... | ... | @@ -50,6 +54,8 @@ When the heat pump runs, water flow causes the propane heater to ignite using it |
| 50 | 54 | | |
| 51 | 55 | v |
| 52 | 56 | Back to Tub |
| 57 | + |
|
| 58 | + [FUTURE, not installed] Low-water float switch -----> GPIO17 |
|
| 53 | 59 | ``` |
| 54 | 60 | |
| 55 | 61 | ## Recommended Shopping List |
| ... | ... | @@ -98,18 +104,50 @@ Avoid: |
| 98 | 104 | - Non-GFCI AC power. |
| 99 | 105 | - Running pump current through breadboards, jumper wires, or small PCB traces. |
| 100 | 106 | |
| 101 | -### Temperature Sensor Hub |
|
| 107 | +### Temperature Probes — Direct Pi 1-Wire (shipped design) |
|
| 102 | 108 | |
| 103 | 109 | Buy: |
| 104 | 110 | |
| 111 | +- **5** waterproof DS18B20 temperature probes with stainless tips and 6 ft to 10 ft cables — 4 for the tub, 1 for the hot-water input. |
|
| 112 | +- 1 4.7 kOhm resistor for the DS18B20 pull-up (one resistor serves the whole bus). |
|
| 113 | +- 5 waterproof 3-pin connectors, such as M8/M12 aviation connectors, or 3-pin waterproof pigtail connectors, wired as **two pluggable bundles**: a 4-probe "water" bundle and a 1-probe "hot-in" bundle. |
|
| 114 | + |
|
| 115 | +All five probes connect directly to the Raspberry Pi's own 1-Wire bus — **no separate microcontroller sensor hub is used.** The shipped `hot-tub-temps` daemon (`src/temp/`) reads `/sys/bus/w1/devices/28-*` via the Pi's `w1-gpio` overlay, learns which ROM ids belong to which bundle during a guided install (see [src/temp/README.md](src/temp/README.md)), and publishes readings to Redis. This replaces the Pico/hub concept described in the legacy alternative below. |
|
| 116 | + |
|
| 117 | +**Four tub probes** — calculate the tub temperature from their average: |
|
| 118 | + |
|
| 119 | +- Hot-return side probe: measures the side where heated water enters the tub. |
|
| 120 | +- Opposite-side probe: measures the side farthest from the hot return. |
|
| 121 | +- Near-end probe: measures one end of the tank. |
|
| 122 | +- Far-end probe: measures the other end of the tank. |
|
| 123 | + |
|
| 124 | +A 4-point average is a better fit for this tub than a single probe because the heater return can create temporary hot spots. The controller also watches the spread between probes — if one probe reads far hotter than the others, the software can flag disagreement instead of trusting a bad reading. |
|
| 125 | + |
|
| 126 | +**One hot-water-input probe** — measures the water entering from the propane heater (the `hot_in_f` reading), not tub water. It is mounted on the heater-side return line, close to where hot water re-enters the tub plumbing, so the control daemon can confirm the heater is actually producing hot water during startup (see "startup check" in [src/control/README.md](src/control/README.md)). It is **not** included in the tub average. |
|
| 127 | + |
|
| 128 | +The DS18B20 probes use three wires, and all five probes share one bus: |
|
| 129 | + |
|
| 130 | +```text |
|
| 131 | +DS18B20 wire Pi connection |
|
| 132 | +------------------------------------------------ |
|
| 133 | +Red / VCC 3.3V |
|
| 134 | +Black / GND Ground |
|
| 135 | +Yellow / Data GPIO4 (BCM), with 4.7k pull-up to 3.3V |
|
| 136 | +``` |
|
| 137 | + |
|
| 138 | +Many DS18B20 probes use red, black, and yellow wires, but verify the datasheet for the exact probes you buy. Each probe has a unique digital address (ROM id) that the temperature daemon labels during a one-time teach-in: connect one bundle at a time so the daemon can learn which ids are "hot-in" vs "tub". |
|
| 139 | + |
|
| 140 | +For reliability, run all five probe cables back to a small terminal block near the Pi instead of splicing them permanently. This makes it easier to replace a bad probe later. |
|
| 141 | + |
|
| 142 | +#### Legacy alternative: separate microcontroller sensor hub (not used in the shipped build) |
|
| 143 | + |
|
| 144 | +An earlier design considered offloading temperature reading to a small microcontroller (e.g. Raspberry Pi Pico) that would read the DS18B20 bus and send one compact package to the Pi over USB/UART. This is **not** what the shipped `hot-tub-temps` daemon does, and is kept here only for historical reference in case a future revision wants to move sensing off the main Pi. |
|
| 145 | + |
|
| 146 | +Would have needed: |
|
| 147 | + |
|
| 105 | 148 | - 1 Raspberry Pi Pico, Pico W, Arduino Nano / Uno, Adafruit QT Py RP2040, or similar small microcontroller. |
| 106 | 149 | - 1 USB cable from the sensor hub to the Raspberry Pi, or a 3-wire UART connection if the hub will be mounted separately. |
| 107 | 150 | - 1 small screw-terminal breakout board or terminal strip for the sensor hub. |
| 108 | -- Optional: 1 small IP65 junction box if the sensor hub will sit near the tub instead of inside the main control enclosure. |
|
| 109 | - |
|
| 110 | -The recommended approach is to connect all tub temperature probes to this small sensor hub instead of connecting the probes directly to the Raspberry Pi. A Raspberry Pi Pico is a good fit because it is inexpensive, easy to power from the Pi over USB, and can run simple firmware that reads the DS18B20 bus, averages the readings, detects missing probes, and sends one compact data package to the Pi. |
|
| 111 | - |
|
| 112 | -The main Raspberry Pi remains the system controller. It runs the web interface, target-temperature logic, pump control, logging, and alerts. The sensor hub only owns temperature measurement. |
|
| 113 | 151 | |
| 114 | 152 | ```text |
| 115 | 153 | 4 DS18B20 tub probes |
| ... | ... | @@ -128,7 +166,7 @@ The main Raspberry Pi remains the system controller. It runs the web interface, |
| 128 | 166 | +----------------------+ |
| 129 | 167 | ``` |
| 130 | 168 | |
| 131 | -The sensor hub should report data in a simple JSON or JSON-like line format, one message per reading interval. For example: |
|
| 169 | +Illustrative hub-to-Pi message format (not the actual `temps:latest` schema the shipped daemon publishes — see [src/temp/README.md](src/temp/README.md) for that): |
|
| 132 | 170 | |
| 133 | 171 | ```json |
| 134 | 172 | { |
| ... | ... | @@ -143,42 +181,11 @@ The sensor hub should report data in a simple JSON or JSON-like line format, one |
| 143 | 181 | } |
| 144 | 182 | ``` |
| 145 | 183 | |
| 146 | -The average should only include sensors with `status: "ok"`. If too many sensors are missing, or if the working sensors disagree by an extreme amount, the Raspberry Pi should treat the temperature package as unsafe and refuse to heat. |
|
| 147 | - |
|
| 148 | -### Temperature Probes |
|
| 149 | - |
|
| 150 | -Buy: |
|
| 151 | - |
|
| 152 | -- 4 waterproof DS18B20 temperature probes with stainless tips and 6 ft to 10 ft cables. |
|
| 153 | -- 1 4.7 kOhm resistor, if your sensor hub breakout does not include a DS18B20 pull-up resistor. |
|
| 154 | -- 4 waterproof 3-pin connectors, such as M8/M12 aviation connectors, or 3-pin waterproof pigtail connectors. |
|
| 155 | - |
|
| 156 | -Use four tub temperature probes and calculate the tub temperature from their average: |
|
| 157 | - |
|
| 158 | -- Hot-return side probe: measures the side where heated water enters. |
|
| 159 | -- Opposite-side probe: measures the side farthest from the hot return. |
|
| 160 | -- Near-end probe: measures one end of the tank. |
|
| 161 | -- Far-end probe: measures the other end of the tank. |
|
| 162 | - |
|
| 163 | -A 4-point average is a better fit for this tub than a single probe because the heater return can create temporary hot spots. The controller can also watch the spread between probes. For example, if one probe reads 10 degrees hotter than the others, the software can ignore that probe temporarily or keep circulating water until the readings converge. |
|
| 164 | - |
|
| 165 | -The DS18B20 probes use three wires: |
|
| 166 | - |
|
| 167 | -```text |
|
| 168 | -DS18B20 wire Sensor hub connection |
|
| 169 | ------------------------------------------------- |
|
| 170 | -Red / VCC 3.3V |
|
| 171 | -Black / GND Ground |
|
| 172 | -Yellow / Data One digital input, with 4.7k pull-up to 3.3V |
|
| 173 | -``` |
|
| 174 | - |
|
| 175 | -Many DS18B20 probes use red, black, and yellow wires, but verify the datasheet for the exact probes you buy. DS18B20 probes can share the same 3-wire bus, so all four probes can connect to the same 3.3V, ground, and sensor-hub data terminals. Each probe has a unique digital address that the sensor-hub firmware can label during setup. |
|
| 184 | +### Water Level / Dry-Run Protection — FUTURE CONCERN, not installed |
|
| 176 | 185 | |
| 177 | -For reliability, run the four probe cables back to a small terminal block near the sensor hub instead of splicing them permanently. This makes it easier to replace a bad probe later. |
|
| 186 | +**Current state: no float switch is installed, and the control daemon does not read one.** Water level is presumed adequate in software (`src/control/safety.py` has a `# TODO: GPIO17 low-water float when hardware is added` marker, but nothing wires to it yet). This section describes the planned future addition, not a required part of the current build. |
|
| 178 | 187 | |
| 179 | -### Water Level / Dry-Run Protection |
|
| 180 | - |
|
| 181 | -Buy: |
|
| 188 | +Would need, when added: |
|
| 182 | 189 | |
| 183 | 190 | - 1 vertical float switch or horizontal side-mount float switch rated for water exposure. |
| 184 | 191 | - 1 waterproof 2-pin connector. |
| ... | ... | @@ -186,7 +193,7 @@ Buy: |
| 186 | 193 | |
| 187 | 194 | The water-level sensor should tell the Pi whether the tub has enough water to safely run the heating pump. A float switch is more beginner-friendly than ultrasonic sensing because steam, splashing, and outdoor light can confuse distance sensors. |
| 188 | 195 | |
| 189 | -Recommended mounting: |
|
| 196 | +Recommended mounting (once installed): |
|
| 190 | 197 | |
| 191 | 198 | - Place the float switch near the side wall of the tank. |
| 192 | 199 | - Set it slightly above the minimum safe intake level for the heat circulation pump. |
| ... | ... | @@ -250,8 +257,9 @@ Recommended enclosure layout: |
| 250 | 257 | | Waterproof Control Box | |
| 251 | 258 | | | |
| 252 | 259 | | +------------------+ +-----------------+ | |
| 253 | -| | Raspberry Pi | USB | Temp Sensor Hub | | |
|
| 254 | -| | on standoffs +------>| plus terminals | | |
|
| 260 | +| | Raspberry Pi | 1-Wire| Probe terminal | | |
|
| 261 | +| | on standoffs +------>| block (5x DS18B20| | |
|
| 262 | +| | (GPIO4 + pull-up)| | 4 tub + 1 hot-in)| | |
|
| 255 | 263 | | +------------------+ +-----------------+ | |
| 256 | 264 | | | |
| 257 | 265 | | Low-voltage side AC relay side | |
| ... | ... | @@ -337,9 +345,9 @@ Probe 4: other long end of the tank |
| 337 | 345 | |
| 338 | 346 | During heating, display both the average temperature and the temperature spread. The average is useful for control. The spread tells the user whether the tank is evenly mixed yet. |
| 339 | 347 | |
| 340 | -### Low-Water Float Switch |
|
| 348 | +### Low-Water Float Switch — FUTURE CONCERN, not installed |
|
| 341 | 349 | |
| 342 | -Mount the float switch near the pump intake height. The controller should refuse to run the heat pump if this switch indicates low water. |
|
| 350 | +Not built yet; the mounting below is the plan for when this hardware is added. Today the controller does not check water level at all. Mount the float switch near the pump intake height, once installed. The controller should refuse to run the heat pump if this switch indicates low water. |
|
| 343 | 351 | |
| 344 | 352 | ```text |
| 345 | 353 | Side view of tub wall |
| ... | ... | @@ -348,7 +356,7 @@ Side view of tub wall |
| 348 | 356 | |
| 349 | 357 | [ temp probe ] |
| 350 | 358 | |
| 351 | - Minimum safe line -----[ float switch ]---- |
|
| 359 | + Minimum safe line -----[ float switch ]---- <-- FUTURE, not installed |
|
| 352 | 360 | |
| 353 | 361 | [ pump intake / strainer ] |
| 354 | 362 | ``` |
| ... | ... | @@ -362,32 +370,29 @@ Bring each sensor into the enclosure through a waterproof connector or cable gla |
| 362 | 370 | Suggested Pi GPIO assignments: |
| 363 | 371 | |
| 364 | 372 | ```text |
| 365 | -Function Pi Pin / GPIO |
|
| 366 | ------------------------------------------------- |
|
| 367 | -Temperature sensor hub USB serial, or UART GPIO pins |
|
| 368 | -Low-water float switch GPIO17 |
|
| 369 | -Optional flow switch GPIO27 |
|
| 370 | -Heat relay control GPIO22 |
|
| 371 | -Filter relay control GPIO23 |
|
| 373 | +Function Pi Pin / GPIO Status |
|
| 374 | +---------------------------------------------------------------- |
|
| 375 | +1-Wire probe bus (x5) GPIO4 Shipped (hot-tub-temps) |
|
| 376 | +Heat relay control GPIO22 Shipped (hot-tub-control) |
|
| 377 | +Filter relay control GPIO23 Shipped (hot-tub-control) |
|
| 378 | +Low-water float switch GPIO17 FUTURE — not installed |
|
| 379 | +Optional flow switch GPIO27 Optional — not implemented |
|
| 372 | 380 | Ground Any Pi GND pin |
| 373 | 381 | 3.3V sensor power Pi 3.3V pin |
| 374 | 382 | ``` |
| 375 | 383 | |
| 376 | -Suggested sensor hub wiring: |
|
| 384 | +Suggested DS18B20 bus wiring (direct to the Pi, no sensor hub): |
|
| 377 | 385 | |
| 378 | 386 | ```text |
| 379 | -Function Sensor hub connection |
|
| 387 | +Function Pi connection |
|
| 380 | 388 | ------------------------------------------------ |
| 381 | -DS18B20 data bus Any firmware-supported digital input |
|
| 389 | +DS18B20 data bus (x5) GPIO4 (BCM) |
|
| 382 | 390 | DS18B20 pull-up 4.7 kOhm from data to 3.3V |
| 383 | 391 | DS18B20 power 3.3V |
| 384 | 392 | DS18B20 ground Ground |
| 385 | -Pi communication USB serial, or UART TX/RX plus shared ground |
|
| 386 | 393 | ``` |
| 387 | 394 | |
| 388 | -Use USB serial if the sensor hub sits inside the same enclosure as the Pi. It is simple, powers the hub, and appears to the Pi as a serial device. Use UART only if you have a reason to avoid USB, and keep the Pi and hub grounds tied together. |
|
| 389 | - |
|
| 390 | -For switches such as float and flow sensors, wire one side to GPIO and the other to ground, then configure the Pi input with an internal pull-up resistor in software. |
|
| 395 | +For switches such as the future float and optional flow sensors, wire one side to GPIO and the other to ground, then configure the Pi input with an internal pull-up resistor in software. |
|
| 391 | 396 | |
| 392 | 397 | ### AC Power Path |
| 393 | 398 | |
| ... | ... | @@ -429,31 +434,28 @@ Important rules: |
| 429 | 434 | 3. Install cable glands or bulkhead connectors. |
| 430 | 435 | Put sensor connectors on one side or the bottom of the enclosure. Put AC/control cable entries separately. |
| 431 | 436 | |
| 432 | -4. Mount the Raspberry Pi, temperature sensor hub, and terminal blocks. |
|
| 437 | +4. Mount the Raspberry Pi and terminal blocks. |
|
| 433 | 438 | Use standoffs, DIN rail adapters, or a mounting plate. Do not let electronics float loose in the box. |
| 434 | 439 | |
| 435 | -5. Connect the DS18B20 temperature probes to the sensor hub. |
|
| 436 | - Wire VCC to 3.3V, ground to ground, and data to the hub's DS18B20 data input with a 4.7 kOhm pull-up resistor. |
|
| 437 | - |
|
| 438 | -6. Connect the sensor hub to the Raspberry Pi. |
|
| 439 | - Use USB serial if the hub is in the same enclosure. Confirm the Pi receives JSON-style readings with average temperature, individual readings, and failed-sensor status. |
|
| 440 | +5. Connect the DS18B20 temperature probes directly to the Pi's 1-Wire bus. |
|
| 441 | + Wire VCC to 3.3V, ground to ground, and data to GPIO4 with a 4.7 kOhm pull-up resistor. Connect one bundle at a time (hot-in, then water, or vice versa) so `hot-tub-temps` can learn each probe's role — see [src/temp/INSTALL.md](src/temp/INSTALL.md). |
|
| 440 | 442 | |
| 441 | -7. Connect the low-water float switch. |
|
| 442 | - Wire it to a GPIO input and ground through a waterproof connector. |
|
| 443 | +6. *(Future — skip for now.)* Connect the low-water float switch. |
|
| 444 | + Not part of the current build. When this hardware is added, wire it to a GPIO input and ground through a waterproof connector. |
|
| 443 | 445 | |
| 444 | -8. Connect relay control inputs. |
|
| 446 | +7. Connect relay control inputs. |
|
| 445 | 447 | Wire the Pi GPIO relay outputs to the low-voltage control inputs of the enclosed relay modules. Follow the relay module's manual for polarity and trigger voltage. |
| 446 | 448 | |
| 447 | -9. Plug pumps into the relay-controlled outlets. |
|
| 449 | +8. Plug pumps into the relay-controlled outlets. |
|
| 448 | 450 | Label one outlet "HEAT PUMP" and the other "FILTER PUMP". |
| 449 | 451 | |
| 450 | -10. Test each part manually. |
|
| 451 | - Confirm the sensor hub can read all four temperature probes, the Pi can receive the temperature package, the Pi can see the float switch change state, and the Pi can turn the heat pump and filter outlets on/off. |
|
| 452 | +9. Test each part manually. |
|
| 453 | + Confirm the Pi can read all five temperature probes (`redis-cli GET temps:latest`), and can turn the heat pump and filter outlets on/off. |
|
| 452 | 454 | |
| 453 | -11. Test with water before using heat. |
|
| 455 | +10. Test with water before using heat. |
|
| 454 | 456 | Run the heat circulation pump with propane closed first. Confirm water flow and no leaks. |
| 455 | 457 | |
| 456 | -12. Test heating under supervision. |
|
| 458 | +11. Test heating under supervision. |
|
| 457 | 459 | Open propane, start heating, and watch the first full heat-up cycle in person. Confirm the controller stops heating near the target temperature. |
| 458 | 460 | |
| 459 | 461 | ## Recommended Labels |
| ... | ... | @@ -466,9 +468,8 @@ Label everything before closing the box: |
| 466 | 468 | - TUB TEMP 2 |
| 467 | 469 | - TUB TEMP 3 |
| 468 | 470 | - TUB TEMP 4 |
| 469 | -- TEMP SENSOR HUB |
|
| 470 | -- PI USB TEMP LINK |
|
| 471 | -- LOW WATER SWITCH |
|
| 471 | +- HOT WATER INPUT TEMP |
|
| 472 | +- LOW WATER SWITCH, once installed (FUTURE — not currently part of the build) |
|
| 472 | 473 | - OUTSIDE TEMP, if installed |
| 473 | 474 | - PI POWER |
| 474 | 475 | - GFCI REQUIRED |
| ... | ... | @@ -479,12 +480,12 @@ Labels matter because this system will be used outdoors, serviced later, and pos |
| 479 | 480 | |
| 480 | 481 | The hardware should make it possible for the software to enforce these rules: |
| 481 | 482 | |
| 482 | -- Do not run heat if the low-water switch says water is too low. |
|
| 483 | -- Do not run heat if the temperature sensor hub reports missing probes beyond the allowed limit. |
|
| 483 | +- *(Future — not enforced today; no float switch installed.)* Do not run heat if the low-water switch says water is too low. |
|
| 484 | +- Do not run heat if too many temperature probes are missing or failing beyond the allowed limit. |
|
| 484 | 485 | - Do not run heat if the working temperature probes disagree by an extreme amount. |
| 485 | 486 | - Do not run heat above a hard maximum temperature. |
| 486 | 487 | - Do not run heat indefinitely if temperature is not rising. |
| 487 | -- Optionally stop heat if the flow switch says water is not moving. |
|
| 488 | +- Optionally stop heat if the flow switch says water is not moving (also not installed today). |
|
| 488 | 489 | - Keep filter control independent from heat control. |
| 489 | 490 | |
| 490 | 491 | Hardware alone will not enforce all of this, but the sensors and relay layout make those software checks possible. |
| ... | ... | @@ -506,47 +507,45 @@ Result: remote on/off for heating circulation and filtration. |
| 506 | 507 | |
| 507 | 508 | Add: |
| 508 | 509 | |
| 509 | -- Four waterproof DS18B20 tub temperature probes. |
|
| 510 | -- Temperature sensor hub. |
|
| 511 | -- Sensor-hub firmware that reports average temperature, individual probe readings, and failed probes to the Pi. |
|
| 510 | +- Five waterproof DS18B20 probes (4 tub + 1 hot-water-input), wired directly to the Pi's own 1-Wire bus on GPIO4 — no separate sensor hub. |
|
| 511 | +- The `hot-tub-temps` daemon (shipped in `src/temp/`), which reads the probes, learns probe roles during install, and publishes average temperature, individual readings, and failed probes to Redis. |
|
| 512 | 512 | - Web interface target temperature setting. |
| 513 | -- Software hysteresis, such as heating until the 4-point average reaches target temperature and restarting when the average drops 3 degrees. |
|
| 513 | +- Software hysteresis, such as heating until the 4-point tub average reaches target temperature and restarting when the average drops 3 degrees. |
|
| 514 | 514 | |
| 515 | 515 | Result: the system can maintain a target temperature range. |
| 516 | 516 | |
| 517 | -### Phase 3: Safety Sensors |
|
| 517 | +### Phase 3: Safety Sensors — FUTURE, not yet built |
|
| 518 | 518 | |
| 519 | -Add: |
|
| 519 | +Add, when this hardware is built: |
|
| 520 | 520 | |
| 521 | -- Low-water float switch. |
|
| 522 | -- Optional flow switch. |
|
| 521 | +- Low-water float switch (not installed today; `src/control/safety.py` has a TODO hook for it). |
|
| 522 | +- Optional flow switch (not installed today). |
|
| 523 | 523 | - Optional outside temperature sensor. |
| 524 | 524 | |
| 525 | -Result: the system can refuse unsafe heating and support future freeze-protection behavior. |
|
| 525 | +Result (once built): the system can refuse unsafe heating and support future freeze-protection behavior. **Today, none of this phase exists** — water level is presumed adequate in software. |
|
| 526 | 526 | |
| 527 | 527 | ## Final Recommended Parts Summary |
| 528 | 528 | |
| 529 | 529 | For the first complete version, buy: |
| 530 | 530 | |
| 531 | 531 | - Raspberry Pi Zero 2 W, Pi 3, or Pi 4. |
| 532 | -- Raspberry Pi Pico, Arduino Nano / Uno, or similar microcontroller for the temperature sensor hub. |
|
| 533 | -- USB cable for the sensor hub, or UART wiring if mounted separately. |
|
| 534 | -- Small screw-terminal breakout board or terminal strip for the sensor hub. |
|
| 535 | -- Optional small IP65 junction box for the sensor hub if it will sit near the tub. |
|
| 536 | 532 | - Official Pi power supply. |
| 537 | 533 | - 32 GB or larger high-endurance microSD card. |
| 538 | 534 | - IP65 / NEMA 4X waterproof enclosure. |
| 539 | 535 | - Pi screw-terminal breakout or HAT. |
| 540 | 536 | - Waterproof cable glands or M8/M12 connectors. |
| 541 | 537 | - 2 enclosed AC relay outlet modules, or 2 outdoor smart plugs with local control. |
| 542 | -- 4 waterproof DS18B20 tub temperature probes. |
|
| 543 | -- 1 4.7 kOhm resistor for the DS18B20 pull-up, unless included on the sensor hub breakout. |
|
| 544 | -- 4 waterproof 3-pin connectors for the temperature probes. |
|
| 545 | -- 1 waterproof float switch. |
|
| 546 | -- Optional inline flow switch. |
|
| 547 | -- Optional outside temperature probe. |
|
| 538 | +- **5** waterproof DS18B20 temperature probes — 4 for the tub, 1 for the hot-water input — wired directly to the Pi's 1-Wire bus (GPIO4). No sensor-hub microcontroller is needed. |
|
| 539 | +- 1 4.7 kOhm resistor for the DS18B20 pull-up. |
|
| 540 | +- 5 waterproof 3-pin connectors for the temperature probes, bundled as a 4-probe "water" connector and a 1-probe "hot-in" connector. |
|
| 548 | 541 | - Wago connectors or screw terminal blocks. |
| 549 | 542 | - Outdoor-rated extension cord and GFCI protection. |
| 550 | 543 | - Hot-water-rated hose, hose clamps, valves, unions, and strainers as needed. |
| 551 | 544 | |
| 552 | -This gives the system enough hardware to measure a 4-point average water temperature, control heating circulation, control filtration, and detect the most important unsafe condition: low water. |
|
| 545 | +**Not required for the current build, future concern only:** |
|
| 546 | + |
|
| 547 | +- 1 waterproof float switch (low-water protection — planned for GPIO17, not installed, not read by the control daemon today). |
|
| 548 | +- Optional inline flow switch. |
|
| 549 | +- Optional outside temperature probe. |
|
| 550 | + |
|
| 551 | +This gives the system enough hardware to measure a 4-point average tub temperature plus a hot-water-input temperature, control heating circulation, and control filtration. **Low-water detection is not yet implemented** — water level is presumed adequate in software until the float switch above is added. |
projects/hot-tub/software-architecture.md
| ... | ... | @@ -5,7 +5,7 @@ This document describes the software running on the Raspberry Pi 3 controller: f |
| 5 | 5 | ## Design Principles |
| 6 | 6 | |
| 7 | 7 | 1. **Single decision-maker.** Only the Control Daemon may drive the heat and filter relays (GPIO). Every other process or remote device sends *intents* (mode, target temperature, enable/disable). The Control Daemon validates those intents against safety rules before changing outputs. |
| 8 | -2. **Sensors are read-only.** The Temperature Daemon owns the 1-Wire (or sensor-hub) interface. Nothing else bit-bangs the DS18B20 bus. |
|
| 8 | +2. **Sensors are read-only.** The Temperature Daemon owns the Pi's own 1-Wire interface (direct GPIO4, no separate sensor-hub microcontroller — see [hardware-setup.md](hardware-setup.md)). Nothing else bit-bangs the DS18B20 bus. |
|
| 9 | 9 | 3. **Redis is the local bus.** Services do not call each other over HTTP for core control. They publish events and subscribe to what they need. Redis also holds the latest snapshot so a newly started subscriber can catch up without waiting for the next publish. |
| 10 | 10 | 4. **Clients are interchangeable.** Web UI, HTTP API, tub-side console, and indoor monitor all express the same kinds of intents. They never talk to GPIO or sensors directly. |
| 11 | 11 | 5. **Fail safe.** On Control Daemon crash, restart, or loss of valid temperature data, heat turns off (or stays off). Filter behavior is mode-dependent but must never leave heat running unsupervised after a fault. |
| ... | ... | @@ -39,7 +39,8 @@ This document describes the software running on the Raspberry Pi 3 controller: f |
| 39 | 39 | | | GPIO | | | |
| 40 | 40 | | v | | | |
| 41 | 41 | | Heat / filter relays | | | |
| 42 | -| Float / flow inputs | | | |
|
| 42 | +| Float / flow inputs (FUTURE, | | | |
|
| 43 | +| not installed) | | | |
|
| 43 | 44 | | | | | |
| 44 | 45 | | +----------------+ health | | | |
| 45 | 46 | | | Web / API | -----------> | | | |
| ... | ... | @@ -70,7 +71,7 @@ GPIO numbers for relays and safety inputs match [hardware-setup.md](hardware-set |
| 70 | 71 | |
| 71 | 72 | **Inputs (hardware):** |
| 72 | 73 | |
| 73 | -- DS18B20 probes on the Pi 1-Wire bus (`w1-gpio`), or a USB/UART sensor hub if that path is used instead. |
|
| 74 | +- Five DS18B20 probes directly on the Pi's own 1-Wire bus (`w1-gpio` overlay, GPIO4) — 4 tub probes + 1 hot-water-input probe. No separate microcontroller sensor hub is used in the shipped design. |
|
| 74 | 75 | - Optional: outside-air probe on the same bus (labeled distinctly in config). |
| 75 | 76 | |
| 76 | 77 | **Outputs (Redis):** |
| ... | ... | @@ -115,7 +116,7 @@ GPIO numbers for relays and safety inputs match [hardware-setup.md](hardware-set |
| 115 | 116 | |---|---| |
| 116 | 117 | | Redis `temps.reading` | Latest probe package from Temperature Daemon | |
| 117 | 118 | | Redis command channel (e.g. `control.command`) | User intents: set mode, set target °F, acknowledge fault, etc. | |
| 118 | -| GPIO inputs | Low-water float (GPIO17), optional flow switch (GPIO27) | |
|
| 119 | +| GPIO inputs | **FUTURE, not installed today:** low-water float (GPIO17), optional flow switch (GPIO27) — see [hardware-setup.md](hardware-setup.md#water-level--dry-run-protection--future-concern-not-installed). `src/control/safety.py` has a TODO hook only; water level is presumed adequate for now. | |
|
| 119 | 120 | | Local config / Redis keys | Hard max temp, hysteresis band, probe disagreement limit, no-rise timeout | |
| 120 | 121 | |
| 121 | 122 | **Outputs:** |
| ... | ... | @@ -156,12 +157,12 @@ Heat hysteresis example (compatible with the current PoC thinking): turn heat ** |
| 156 | 157 | |
| 157 | 158 | **Safety rules (enforced here, not in the Web UI):** |
| 158 | 159 | |
| 159 | -- No heat if low-water float indicates dry/low. |
|
| 160 | +- *(Future — not implemented; no float hardware installed.)* No heat if low-water float indicates dry/low. Water level is presumed adequate today. |
|
| 160 | 161 | - No heat if too few tub probes are valid (configurable minimum). |
| 161 | 162 | - No heat if probe spread exceeds a hard limit. |
| 162 | 163 | - No heat above a hard maximum °F (absolute ceiling, independent of user target). |
| 163 | 164 | - No heat if temperature is not rising for N minutes while heat has been commanded on (stuck/no-fuel/no-flow). |
| 164 | -- Optional: no heat if flow switch says no flow while heat pump should be running. |
|
| 165 | +- *(Future — not implemented.)* Optional: no heat if flow switch says no flow while heat pump should be running. |
|
| 165 | 166 | - User target is clamped to a safe range before use. |
| 166 | 167 | - On daemon start: relays initialized **off** until the first valid sample and mode evaluation. |
| 167 | 168 | |
| ... | ... | @@ -170,7 +171,7 @@ Heat hysteresis example (compatible with the current PoC thinking): turn heat ** |
| 170 | 171 | - Relays are low-voltage control inputs to enclosed AC modules — the Pi never switches mains directly. |
| 171 | 172 | - Confirm active-high vs active-low for the chosen IoT Relay modules; invert in one place in this daemon. |
| 172 | 173 | - GPIO access should be exclusive to this process (do not also run `control_poc.py` or other experiments against the same pins). |
| 173 | -- Float/flow inputs use internal pull-ups; treat open/closed polarity in config. |
|
| 174 | +- *(Future — not installed today.)* Float/flow inputs, once added, would use internal pull-ups; treat open/closed polarity in config. |
|
| 174 | 175 | |
| 175 | 176 | **Persistence:** |
| 176 | 177 | |
| ... | ... | @@ -383,10 +384,10 @@ Architectural rule reused everywhere: **intent in, Control decides, state out.** |
| 383 | 384 | |
| 384 | 385 | | Hardware | Owning software | Notes | |
| 385 | 386 | |---|---|---| |
| 386 | -| DS18B20 bus / sensor hub | Temperature Daemon | Exclusive | |
|
| 387 | +| DS18B20 bus (5 probes, direct Pi 1-Wire, GPIO4) | Temperature Daemon | Exclusive; no sensor hub in shipped design | |
|
| 387 | 388 | | Heat relay GPIO | Control Daemon | Exclusive | |
| 388 | 389 | | Filter relay GPIO | Control Daemon | Exclusive | |
| 389 | -| Float / flow GPIO | Control Daemon | Read in safety path | |
|
| 390 | +| Float / flow GPIO | Control Daemon | **FUTURE** — not installed; would be read in safety path once added | |
|
| 390 | 391 | | Status LED — Temperature | Health Daemon | Exclusive; reflects `health:temps` | |
| 391 | 392 | | Status LED — Control | Health Daemon | Exclusive; reflects `health:control` | |
| 392 | 393 | | Status LED — Web | Health Daemon | Exclusive; reflects `health:web` | |
| ... | ... | @@ -418,7 +419,9 @@ Temperature Daemon publishes tub_avg_f=104.5 |
| 418 | 419 | → Control: avg above band → heat GPIO OFF, publish state |
| 419 | 420 | ``` |
| 420 | 421 | |
| 421 | -### Low water while heating |
|
| 422 | +### Low water while heating — FUTURE, not implemented today |
|
| 423 | + |
|
| 424 | +No float switch hardware is installed, and `src/control/safety.py` only has a TODO hook for it — this scenario describes the planned behavior once that hardware exists, not current behavior. Today, water level is presumed adequate in software. |
|
| 422 | 425 | |
| 423 | 426 | ```text |
| 424 | 427 | Float opens (low water) |
projects/hot-tub/src/control/INSTALL.md
| ... | ... | @@ -67,6 +67,7 @@ Expect heat to stay off until `temps:latest` is valid and fresh (default stale t |
| 67 | 67 | ```bash |
| 68 | 68 | cd src |
| 69 | 69 | PYTHONPATH=. python3 -m control --dry-run --once |
| 70 | +PYTHONPATH=. python3 -m control --no-gpio # Redis live, GPIO faked (stack test with temp_sim + web) |
|
| 70 | 71 | PYTHONPATH=. python3 -m unittest control.tests.test_commands control.tests.test_safety \ |
| 71 | 72 | control.tests.test_filter_policy control.tests.test_heat_fsm -v |
| 72 | 73 | ``` |
projects/hot-tub/src/control/__main__.py
| ... | ... | @@ -42,6 +42,11 @@ def main() -> None: |
| 42 | 42 | help="No Redis; log GPIO decisions only", |
| 43 | 43 | ) |
| 44 | 44 | parser.add_argument( |
| 45 | + "--no-gpio", |
|
| 46 | + action="store_true", |
|
| 47 | + help="Do not drive relays; still connect to Redis (for stack testing)", |
|
| 48 | + ) |
|
| 49 | + parser.add_argument( |
|
| 45 | 50 | "-v", |
| 46 | 51 | "--verbose", |
| 47 | 52 | action="store_true", |
| ... | ... | @@ -63,12 +68,13 @@ def main() -> None: |
| 63 | 68 | |
| 64 | 69 | setup_logging(cfg.log_path, verbose=args.verbose) |
| 65 | 70 | |
| 71 | + gpio_dry_run = args.no_gpio |
|
| 66 | 72 | if args.once: |
| 67 | - snapshot = run_once(cfg, dry_run=args.dry_run) |
|
| 73 | + snapshot = run_once(cfg, dry_run=args.dry_run, gpio_dry_run=gpio_dry_run) |
|
| 68 | 74 | print(json.dumps(snapshot, indent=2)) |
| 69 | 75 | return |
| 70 | 76 | |
| 71 | - run_loop(cfg, dry_run=args.dry_run) |
|
| 77 | + run_loop(cfg, dry_run=args.dry_run, gpio_dry_run=gpio_dry_run) |
|
| 72 | 78 | |
| 73 | 79 | |
| 74 | 80 | if __name__ == "__main__": |
projects/hot-tub/src/control/daemon.py
| ... | ... | @@ -51,13 +51,19 @@ def _apply_gpio(relays: RelayOutputs, state: ControlState) -> None: |
| 51 | 51 | |
| 52 | 52 | |
| 53 | 53 | class ControlEngine: |
| 54 | - def __init__(self, cfg: ControlConfig, *, dry_run: bool = False) -> None: |
|
| 54 | + def __init__( |
|
| 55 | + self, |
|
| 56 | + cfg: ControlConfig, |
|
| 57 | + *, |
|
| 58 | + dry_run: bool = False, |
|
| 59 | + gpio_dry_run: bool = False, |
|
| 60 | + ) -> None: |
|
| 55 | 61 | self.cfg = cfg |
| 56 | 62 | self.dry_run = dry_run |
| 57 | 63 | self.state = load_persisted_state(cfg.state_path, default_target=cfg.target_f_default) |
| 58 | 64 | self.ctx = FsmContext() |
| 59 | 65 | self.reading: dict[str, Any] | None = None |
| 60 | - self.relays = RelayOutputs(cfg.gpio, dry_run=dry_run) |
|
| 66 | + self.relays = RelayOutputs(cfg.gpio, dry_run=dry_run or gpio_dry_run) |
|
| 61 | 67 | self.relays.all_off() |
| 62 | 68 | self._last_persist_hash: str | None = None |
| 63 | 69 | self._last_publish_mono = 0.0 |
| ... | ... | @@ -111,10 +117,11 @@ def run_once( |
| 111 | 117 | cfg: ControlConfig, |
| 112 | 118 | *, |
| 113 | 119 | dry_run: bool = False, |
| 120 | + gpio_dry_run: bool = False, |
|
| 114 | 121 | engine: ControlEngine | None = None, |
| 115 | 122 | redis_io: ControlRedis | None = None, |
| 116 | 123 | ) -> dict[str, Any]: |
| 117 | - eng = engine or ControlEngine(cfg, dry_run=dry_run) |
|
| 124 | + eng = engine or ControlEngine(cfg, dry_run=dry_run, gpio_dry_run=gpio_dry_run) |
|
| 118 | 125 | if eng.reading is None and not dry_run: |
| 119 | 126 | r = redis_io or ControlRedis(cfg.redis_url) |
| 120 | 127 | try: |
| ... | ... | @@ -128,14 +135,19 @@ def run_once( |
| 128 | 135 | return snapshot |
| 129 | 136 | |
| 130 | 137 | |
| 131 | -def run_loop(cfg: ControlConfig, *, dry_run: bool = False) -> None: |
|
| 138 | +def run_loop( |
|
| 139 | + cfg: ControlConfig, |
|
| 140 | + *, |
|
| 141 | + dry_run: bool = False, |
|
| 142 | + gpio_dry_run: bool = False, |
|
| 143 | +) -> None: |
|
| 132 | 144 | global _stop |
| 133 | 145 | _stop = False |
| 134 | 146 | signal.signal(signal.SIGTERM, _handle_signal) |
| 135 | 147 | signal.signal(signal.SIGINT, _handle_signal) |
| 136 | 148 | |
| 137 | - logger.info("Control daemon starting (dry_run=%s)", dry_run) |
|
| 138 | - engine = ControlEngine(cfg, dry_run=dry_run) |
|
| 149 | + logger.info("Control daemon starting (dry_run=%s gpio_dry_run=%s)", dry_run, gpio_dry_run) |
|
| 150 | + engine = ControlEngine(cfg, dry_run=dry_run, gpio_dry_run=gpio_dry_run) |
|
| 139 | 151 | redis_io: ControlRedis | None = None |
| 140 | 152 | if not dry_run: |
| 141 | 153 | redis_io = ControlRedis(cfg.redis_url) |
projects/hot-tub/src/control/heat_fsm.py
| ... | ... | @@ -110,11 +110,14 @@ def tick_heat( |
| 110 | 110 | cfg, |
| 111 | 111 | reading, |
| 112 | 112 | require_hot_in=False, |
| 113 | + ignore_target=True, |
|
| 113 | 114 | ) |
| 114 | 115 | if not safety.allow_heat and state.heat_phase == "backflush": |
| 116 | + state.heat_pump = "off" |
|
| 115 | 117 | if safety.trip: |
| 116 | 118 | apply_safety_trip(state, safety.reason or "safety") |
| 117 | - state.heat_pump = "off" |
|
| 119 | + logger.info("backflush aborted by safety; heat_mode -> sleep") |
|
| 120 | + state.heat_mode = "sleep" |
|
| 118 | 121 | return |
| 119 | 122 | |
| 120 | 123 | if state.heat_phase != "backflush": |
projects/hot-tub/src/control/safety.py
| ... | ... | @@ -47,6 +47,7 @@ def check_heat_allowed( |
| 47 | 47 | reading: dict[str, Any] | None, |
| 48 | 48 | *, |
| 49 | 49 | require_hot_in: bool = False, |
| 50 | + ignore_target: bool = False, |
|
| 50 | 51 | now: datetime | None = None, |
| 51 | 52 | ) -> SafetyResult: |
| 52 | 53 | """Evaluate whether heat pump may be turned on.""" |
| ... | ... | @@ -70,7 +71,7 @@ def check_heat_allowed( |
| 70 | 71 | hot = reading.get("hot_in_f") |
| 71 | 72 | state.hot_in_f = float(hot) if hot is not None else None |
| 72 | 73 | |
| 73 | - if state.tub_avg_f >= state.target_f: |
|
| 74 | + if not ignore_target and state.tub_avg_f >= state.target_f: |
|
| 74 | 75 | return SafetyResult(False, "at or above target") |
| 75 | 76 | |
| 76 | 77 | if require_hot_in and state.hot_in_f is None: |
projects/hot-tub/src/control/tests/test_heat_fsm.py
| ... | ... | @@ -63,6 +63,31 @@ class TestHeatFsm(unittest.TestCase): |
| 63 | 63 | self.assertEqual(state.heat_mode, "sleep") |
| 64 | 64 | self.assertEqual(state.heat_pump, "off") |
| 65 | 65 | |
| 66 | + def test_backflush_completes_when_at_or_above_target(self) -> None: |
|
| 67 | + state = ControlState(heat_mode="backflush", target_f=102.0) |
|
| 68 | + ctx = FsmContext() |
|
| 69 | + reading = _reading(105.0) |
|
| 70 | + tick_heat(state, ctx, self.cfg, reading) |
|
| 71 | + self.assertEqual(state.heat_phase, "backflush") |
|
| 72 | + self.assertEqual(state.heat_pump, "on") |
|
| 73 | + tick_heat(state, ctx, self.cfg, reading) |
|
| 74 | + self.assertEqual(state.heat_mode, "sleep") |
|
| 75 | + self.assertEqual(state.heat_pump, "off") |
|
| 76 | + |
|
| 77 | + def test_backflush_safety_trip_reverts_to_sleep(self) -> None: |
|
| 78 | + state = ControlState(heat_mode="backflush", target_f=102.0) |
|
| 79 | + ctx = FsmContext() |
|
| 80 | + tick_heat(state, ctx, self.cfg, _reading(90.0)) |
|
| 81 | + self.assertEqual(state.heat_phase, "backflush") |
|
| 82 | + self.assertEqual(state.heat_pump, "on") |
|
| 83 | + tick_heat(state, ctx, self.cfg, None) |
|
| 84 | + self.assertEqual(state.heat_mode, "sleep") |
|
| 85 | + self.assertEqual(state.heat_pump, "off") |
|
| 86 | + self.assertEqual(state.heat_phase, "error_safety") |
|
| 87 | + tick_heat(state, ctx, self.cfg, None) |
|
| 88 | + self.assertEqual(state.heat_pump, "off") |
|
| 89 | + self.assertNotEqual(state.heat_mode, "backflush") |
|
| 90 | + |
|
| 66 | 91 | |
| 67 | 92 | if __name__ == "__main__": |
| 68 | 93 | unittest.main() |
projects/hot-tub/src/health/overview.md
| ... | ... | @@ -0,0 +1,47 @@ |
| 1 | +# Health Daemon Overview |
|
| 2 | + |
|
| 3 | +## Overview |
|
| 4 | + |
|
| 5 | +The purpose of this daemon is to monitor the health of all of the other services and translate them into signals via 3 physical LED lights. There are 3 lights this will control: |
|
| 6 | + |
|
| 7 | +- Temp: |
|
| 8 | + - Off - No power, completely disconnected |
|
| 9 | + - Red - Daemon reporting an error |
|
| 10 | + - Orange, Solid - Daemon reporting a warning |
|
| 11 | + - Orange, slow blinking - No temp sensors connected |
|
| 12 | + - Orange, rapid blinking - Some temp sensors connected |
|
| 13 | + - Green - Temp daemon reporting temps across all sensors |
|
| 14 | +- Control: |
|
| 15 | + - Off - No power, completely disconnected |
|
| 16 | + - Red - Daemon reporting an error |
|
| 17 | + - Orange - Daemon reporting a warning |
|
| 18 | + - Green - Control daemon working properly |
|
| 19 | +- Web: |
|
| 20 | + - Off - No power, completely disconnected |
|
| 21 | + - Red - Daemon reporting an error |
|
| 22 | + - Warning - Daemon reporting a warning |
|
| 23 | + - Green - Web interface up and working properly |
|
| 24 | + |
|
| 25 | +Health for each service should be monitored in the following ways: |
|
| 26 | + |
|
| 27 | +- Temp - Read Redis health state and subscribe to temp events |
|
| 28 | + - if there are no temp events within the last 10 seconds, set LED to red |
|
| 29 | + - if there is an error reported, set LED to red |
|
| 30 | + - if there is a warning reported, set LED to orange |
|
| 31 | + - use the sensor-connected logic above in the case that any sensors are not listed |
|
| 32 | + - otherwise, set LED to green |
|
| 33 | +- Control - Read control health status from redis. Health should indicate: |
|
| 34 | + - Healthy - Green LED |
|
| 35 | + - Warning reported - Orange LED |
|
| 36 | + - Error reported - Red LED |
|
| 37 | + - Any health status timestamp older than 10 seconds - Orange LED |
|
| 38 | + - Any health status timestamp older than 1 minute - Red LED |
|
| 39 | +- Web |
|
| 40 | + - Http request to API health endpoint every 5 seconds |
|
| 41 | + - Healthy - Green LED |
|
| 42 | + - Warning reported - Orange LED |
|
| 43 | + - Error reported - Red LED |
|
| 44 | + - Http request to index every 5 seconds |
|
| 45 | + - On 2xx status - Green LED |
|
| 46 | + - On error status - Red LED |
|
| 47 | + - on response lasting longer than 1 second - Orange LED |
|
| ... | ... | \ No newline at end of file |
projects/hot-tub/src/temp/INSTALL.md
| ... | ... | @@ -43,6 +43,10 @@ The script installs `redis-server`, `python3-redis`, user `hot-tub`, config unde |
| 43 | 43 | |
| 44 | 44 | ## 3. Guided probe teach-in |
| 45 | 45 | |
| 46 | +**Preferred (with web UI):** Open **Diagnostic → Sensor Setup** on the hot tub web interface (`hot-tub-web` service). The wizard calls `temps.command` at runtime; you do not need to edit `setup_mode` or restart the daemon. |
|
| 47 | + |
|
| 48 | +**Manual / headless:** |
|
| 49 | + |
|
| 46 | 50 | ROM ids are learned once and stored in `/etc/hot-tub/temp-probes.json`. |
| 47 | 51 | |
| 48 | 52 | **Recommended order** |
projects/hot-tub/src/temp/README.md
| ... | ... | @@ -127,6 +127,7 @@ Suggested reading shape (aligned with the architecture doc): |
| 127 | 127 | { |
| 128 | 128 | "ts": "2026-10-19T20:00:00Z", |
| 129 | 129 | "probes": { "28-...": 101.2, "28-...": null }, |
| 130 | + "roles": { "hot_in": 112.0, "tub1": 101.2, "tub2": 101.0, "tub3": null, "tub4": 100.9 }, |
|
| 130 | 131 | "tub_avg_f": 101.0, |
| 131 | 132 | "hot_in_f": null, |
| 132 | 133 | "outdoor_f": null, |
projects/hot-tub/src/temp/commands.py
| ... | ... | @@ -0,0 +1,67 @@ |
| 1 | +"""Handle temps.command payloads (runtime sensor setup).""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import logging |
|
| 6 | +from typing import Any |
|
| 7 | + |
|
| 8 | +from .config import TempConfig |
|
| 9 | +from .runtime import TempRuntime |
|
| 10 | +from .identity import backup_probe_map, clear_probe_map, restore_probe_map |
|
| 11 | + |
|
| 12 | +logger = logging.getLogger("hot_tub.temp") |
|
| 13 | + |
|
| 14 | + |
|
| 15 | +class TempCommandError(ValueError): |
|
| 16 | + pass |
|
| 17 | + |
|
| 18 | + |
|
| 19 | +def apply_temp_command( |
|
| 20 | + cfg: TempConfig, |
|
| 21 | + runtime: TempRuntime, |
|
| 22 | + envelope: dict[str, Any], |
|
| 23 | +) -> None: |
|
| 24 | + op = envelope.get("op") |
|
| 25 | + if not op or not isinstance(op, str): |
|
| 26 | + raise TempCommandError("missing or invalid op") |
|
| 27 | + |
|
| 28 | + path = cfg.probe_map_path |
|
| 29 | + |
|
| 30 | + if op == "setup_start": |
|
| 31 | + backup_probe_map(path) |
|
| 32 | + clear_probe_map(path) |
|
| 33 | + runtime.setup_mode_override = True |
|
| 34 | + runtime.setup_active = True |
|
| 35 | + logger.info("sensor setup started (probe map cleared, backup saved)") |
|
| 36 | + return |
|
| 37 | + |
|
| 38 | + if op == "setup_finish": |
|
| 39 | + if runtime.last_setup_state != "ready": |
|
| 40 | + raise TempCommandError( |
|
| 41 | + f"setup_finish requires setup_state ready (got {runtime.last_setup_state!r})" |
|
| 42 | + ) |
|
| 43 | + runtime.setup_mode_override = False |
|
| 44 | + runtime.setup_active = False |
|
| 45 | + logger.info("sensor setup finished") |
|
| 46 | + return |
|
| 47 | + |
|
| 48 | + if op == "setup_cancel": |
|
| 49 | + restore_probe_map(path) |
|
| 50 | + runtime.setup_mode_override = False |
|
| 51 | + runtime.setup_active = False |
|
| 52 | + logger.info("sensor setup cancelled (probe map restored)") |
|
| 53 | + return |
|
| 54 | + |
|
| 55 | + raise TempCommandError(f"unknown op: {op!r}") |
|
| 56 | + |
|
| 57 | + |
|
| 58 | +def parse_command_message(raw: str) -> dict[str, Any]: |
|
| 59 | + import json |
|
| 60 | + |
|
| 61 | + try: |
|
| 62 | + data = json.loads(raw) |
|
| 63 | + except json.JSONDecodeError as exc: |
|
| 64 | + raise TempCommandError("invalid JSON") from exc |
|
| 65 | + if not isinstance(data, dict): |
|
| 66 | + raise TempCommandError("command must be a JSON object") |
|
| 67 | + return data |
projects/hot-tub/src/temp/daemon.py
| ... | ... | @@ -9,16 +9,19 @@ import time |
| 9 | 9 | from datetime import datetime, timezone |
| 10 | 10 | from typing import Any |
| 11 | 11 | |
| 12 | +from .commands import TempCommandError, apply_temp_command, parse_command_message |
|
| 12 | 13 | from .config import TempConfig |
| 13 | 14 | from .identity import ( |
| 14 | 15 | TUB_COUNT, |
| 15 | 16 | classify_readings, |
| 16 | 17 | evaluate_identity, |
| 17 | 18 | load_probe_map, |
| 19 | + role_readings, |
|
| 18 | 20 | save_probe_map, |
| 19 | 21 | ) |
| 20 | 22 | from .reader import read_all_f |
| 21 | -from .redis_out import RedisPublisher |
|
| 23 | +from .redis_out import CHAN_COMMAND, RedisPublisher |
|
| 24 | +from .runtime import TempRuntime |
|
| 22 | 25 | |
| 23 | 26 | logger = logging.getLogger("hot_tub.temp") |
| 24 | 27 | |
| ... | ... | @@ -49,11 +52,13 @@ def _health_status(setup_state: str, ok_count: int, visible_count: int) -> str: |
| 49 | 52 | |
| 50 | 53 | def build_sample( |
| 51 | 54 | cfg: TempConfig, |
| 55 | + runtime: TempRuntime | None = None, |
|
| 52 | 56 | ) -> tuple[dict[str, Any], Any, dict[str, Any], dict[str, Any]]: |
| 53 | 57 | from pathlib import Path |
| 54 | 58 | |
| 55 | 59 | path = cfg.probe_map_path |
| 56 | - setup_mode = cfg.setup_mode |
|
| 60 | + rt = runtime or TempRuntime() |
|
| 61 | + setup_mode = rt.effective_setup_mode(cfg) |
|
| 57 | 62 | probes_f = read_all_f(cfg.devices_path) |
| 58 | 63 | visible = list(probes_f.keys()) |
| 59 | 64 | pmap = load_probe_map(path) |
| ... | ... | @@ -66,10 +71,14 @@ def build_sample( |
| 66 | 71 | probes_f, identity.probe_map |
| 67 | 72 | ) |
| 68 | 73 | |
| 74 | + roles = role_readings(probes_f, identity.probe_map) |
|
| 75 | + rt.last_setup_state = identity.setup_state |
|
| 76 | + |
|
| 69 | 77 | reading: dict[str, Any] = { |
| 70 | 78 | "ts": _utc_now(), |
| 71 | 79 | "setup_state": identity.setup_state, |
| 72 | 80 | "probes": labeled, |
| 81 | + "roles": roles, |
|
| 73 | 82 | "tub_avg_f": tub_avg, |
| 74 | 83 | "hot_in_f": hot_in_f, |
| 75 | 84 | "outdoor_f": None, |
| ... | ... | @@ -83,6 +92,7 @@ def build_sample( |
| 83 | 92 | "state": identity.setup_state, |
| 84 | 93 | "detail": identity.detail, |
| 85 | 94 | "visible_ids": identity.visible_ids, |
| 95 | + "setup_active": rt.setup_active, |
|
| 86 | 96 | } |
| 87 | 97 | |
| 88 | 98 | health_payload = { |
| ... | ... | @@ -101,8 +111,10 @@ def run_once( |
| 101 | 111 | *, |
| 102 | 112 | dry_run: bool = False, |
| 103 | 113 | redis_pub: RedisPublisher | None = None, |
| 114 | + runtime: TempRuntime | None = None, |
|
| 104 | 115 | ) -> dict[str, Any]: |
| 105 | - reading, identity, setup_payload, health_payload = build_sample(cfg) |
|
| 116 | + rt = runtime or TempRuntime() |
|
| 117 | + reading, identity, setup_payload, health_payload = build_sample(cfg, runtime=rt) |
|
| 106 | 118 | logger.info( |
| 107 | 119 | "sample setup=%s tub_avg=%s hot_in=%s ok=%s/%s visible=%s", |
| 108 | 120 | identity.setup_state, |
| ... | ... | @@ -117,7 +129,7 @@ def run_once( |
| 117 | 129 | pub = redis_pub or RedisPublisher(cfg.redis_url) |
| 118 | 130 | try: |
| 119 | 131 | pub.publish_reading(reading) |
| 120 | - pub.publish_setup(setup_payload) |
|
| 132 | + pub.publish_setup(setup_payload, force=rt.setup_active) |
|
| 121 | 133 | pub.publish_health(health_payload) |
| 122 | 134 | finally: |
| 123 | 135 | if redis_pub is None: |
| ... | ... | @@ -142,11 +154,21 @@ def run_loop(cfg: TempConfig, *, dry_run: bool = False) -> None: |
| 142 | 154 | if not dry_run: |
| 143 | 155 | redis_pub = RedisPublisher(cfg.redis_url) |
| 144 | 156 | |
| 157 | + runtime = TempRuntime() |
|
| 145 | 158 | try: |
| 146 | 159 | while not _stop: |
| 147 | 160 | loop_start = time.monotonic() |
| 161 | + if redis_pub is not None: |
|
| 162 | + for raw in redis_pub.poll_commands(timeout_s=min(0.5, cfg.interval_s)): |
|
| 163 | + try: |
|
| 164 | + envelope = parse_command_message(raw) |
|
| 165 | + apply_temp_command(cfg, runtime, envelope) |
|
| 166 | + except TempCommandError as exc: |
|
| 167 | + logger.warning("temps.command rejected: %s", exc) |
|
| 168 | + except Exception: |
|
| 169 | + logger.exception("temps.command handling failed") |
|
| 148 | 170 | try: |
| 149 | - run_once(cfg, dry_run=dry_run, redis_pub=redis_pub) |
|
| 171 | + run_once(cfg, dry_run=dry_run, redis_pub=redis_pub, runtime=runtime) |
|
| 150 | 172 | except Exception: |
| 151 | 173 | logger.exception("Error in poll cycle") |
| 152 | 174 | elapsed = time.monotonic() - loop_start |
projects/hot-tub/src/temp/identity.py
| ... | ... | @@ -45,6 +45,34 @@ def load_probe_map(path: Path) -> ProbeMap: |
| 45 | 45 | return ProbeMap(hot_in=hot_in, tub=tub) |
| 46 | 46 | |
| 47 | 47 | |
| 48 | +def setup_backup_path(path: Path) -> Path: |
|
| 49 | + return path.with_name(path.name + ".setup-backup") |
|
| 50 | + |
|
| 51 | + |
|
| 52 | +def backup_probe_map(path: Path) -> None: |
|
| 53 | + """Save current probe map for setup_cancel restore.""" |
|
| 54 | + bak = setup_backup_path(path) |
|
| 55 | + bak.parent.mkdir(parents=True, exist_ok=True) |
|
| 56 | + if path.exists(): |
|
| 57 | + bak.write_text(path.read_text()) |
|
| 58 | + else: |
|
| 59 | + save_probe_map(bak, ProbeMap()) |
|
| 60 | + |
|
| 61 | + |
|
| 62 | +def restore_probe_map(path: Path) -> bool: |
|
| 63 | + bak = setup_backup_path(path) |
|
| 64 | + if not bak.exists(): |
|
| 65 | + return False |
|
| 66 | + restored = load_probe_map(bak) |
|
| 67 | + save_probe_map(path, restored) |
|
| 68 | + bak.unlink(missing_ok=True) |
|
| 69 | + return True |
|
| 70 | + |
|
| 71 | + |
|
| 72 | +def clear_probe_map(path: Path) -> None: |
|
| 73 | + save_probe_map(path, ProbeMap()) |
|
| 74 | + |
|
| 75 | + |
|
| 48 | 76 | def save_probe_map(path: Path, probe_map: ProbeMap) -> None: |
| 49 | 77 | path.parent.mkdir(parents=True, exist_ok=True) |
| 50 | 78 | payload = {"hot_in": probe_map.hot_in, "tub": sorted(probe_map.tub)} |
| ... | ... | @@ -179,3 +207,23 @@ def classify_readings( |
| 179 | 207 | spread = max(tub_temps) - min(tub_temps) |
| 180 | 208 | |
| 181 | 209 | return labeled, tub_avg, hot_in_f, ok_count, fail_count, spread |
| 210 | + |
|
| 211 | + |
|
| 212 | +def role_readings( |
|
| 213 | + probes_f: dict[str, float | None], |
|
| 214 | + probe_map: ProbeMap, |
|
| 215 | +) -> dict[str, float | None]: |
|
| 216 | + """Map hot_in and tub1..tub4 (sorted tub ROM order) to °F or null.""" |
|
| 217 | + roles: dict[str, float | None] = {} |
|
| 218 | + if probe_map.hot_in: |
|
| 219 | + roles["hot_in"] = probes_f.get(probe_map.hot_in) |
|
| 220 | + else: |
|
| 221 | + roles["hot_in"] = None |
|
| 222 | + sorted_tub = sorted(probe_map.tub) |
|
| 223 | + for i in range(TUB_COUNT): |
|
| 224 | + key = f"tub{i + 1}" |
|
| 225 | + if i < len(sorted_tub): |
|
| 226 | + roles[key] = probes_f.get(sorted_tub[i]) |
|
| 227 | + else: |
|
| 228 | + roles[key] = None |
|
| 229 | + return roles |
projects/hot-tub/src/temp/redis_out.py
| ... | ... | @@ -20,6 +20,7 @@ KEY_SETUP = "temps:setup" |
| 20 | 20 | CHAN_SETUP = "temps.setup" |
| 21 | 21 | KEY_HEALTH = "health:temps" |
| 22 | 22 | CHAN_HEALTH = "health.temps" |
| 23 | +CHAN_COMMAND = "temps.command" |
|
| 23 | 24 | |
| 24 | 25 | |
| 25 | 26 | class RedisPublisher: |
| ... | ... | @@ -28,6 +29,7 @@ class RedisPublisher: |
| 28 | 29 | raise RuntimeError("redis package not installed (apt: python3-redis)") |
| 29 | 30 | self._url = redis_url |
| 30 | 31 | self._client: redis.Redis | None = None |
| 32 | + self._pubsub: redis.client.PubSub | None = None |
|
| 31 | 33 | self._last_setup_json: str | None = None |
| 32 | 34 | self._backoff_s = 1.0 |
| 33 | 35 | |
| ... | ... | @@ -44,11 +46,46 @@ class RedisPublisher: |
| 44 | 46 | return client |
| 45 | 47 | except Exception as exc: |
| 46 | 48 | logger.warning("Redis unavailable: %s (retry in %.0fs)", exc, self._backoff_s) |
| 49 | + self._pubsub = None |
|
| 47 | 50 | self._client = None |
| 48 | 51 | time.sleep(min(self._backoff_s, 30.0)) |
| 49 | 52 | self._backoff_s = min(self._backoff_s * 2, 30.0) |
| 50 | 53 | return None |
| 51 | 54 | |
| 55 | + def subscribe_commands(self) -> redis.client.PubSub | None: |
|
| 56 | + client = self._ensure() |
|
| 57 | + if client is None: |
|
| 58 | + return None |
|
| 59 | + try: |
|
| 60 | + if self._pubsub is None: |
|
| 61 | + self._pubsub = client.pubsub(ignore_subscribe_messages=True) |
|
| 62 | + self._pubsub.subscribe(CHAN_COMMAND) |
|
| 63 | + return self._pubsub |
|
| 64 | + except Exception as exc: |
|
| 65 | + logger.warning("Redis subscribe failed: %s", exc) |
|
| 66 | + self._pubsub = None |
|
| 67 | + self._client = None |
|
| 68 | + return None |
|
| 69 | + |
|
| 70 | + def poll_commands(self, timeout_s: float = 0.5) -> list[str]: |
|
| 71 | + pubsub = self.subscribe_commands() |
|
| 72 | + if pubsub is None: |
|
| 73 | + return [] |
|
| 74 | + out: list[str] = [] |
|
| 75 | + deadline = time.monotonic() + timeout_s |
|
| 76 | + while time.monotonic() < deadline: |
|
| 77 | + msg = pubsub.get_message(timeout=0.1) |
|
| 78 | + if msg is None: |
|
| 79 | + continue |
|
| 80 | + if msg.get("type") != "message": |
|
| 81 | + continue |
|
| 82 | + data = msg.get("data") |
|
| 83 | + if isinstance(data, str): |
|
| 84 | + out.append(data) |
|
| 85 | + if len(out) >= 32: |
|
| 86 | + break |
|
| 87 | + return out |
|
| 88 | + |
|
| 52 | 89 | def publish_reading(self, payload: dict[str, Any]) -> bool: |
| 53 | 90 | client = self._ensure() |
| 54 | 91 | if client is None: |
| ... | ... | @@ -58,12 +95,12 @@ class RedisPublisher: |
| 58 | 95 | client.publish(CHAN_READING, body) |
| 59 | 96 | return True |
| 60 | 97 | |
| 61 | - def publish_setup(self, payload: dict[str, Any]) -> bool: |
|
| 98 | + def publish_setup(self, payload: dict[str, Any], *, force: bool = False) -> bool: |
|
| 62 | 99 | client = self._ensure() |
| 63 | 100 | if client is None: |
| 64 | 101 | return False |
| 65 | 102 | body = json.dumps(payload) |
| 66 | - if body == self._last_setup_json: |
|
| 103 | + if not force and body == self._last_setup_json: |
|
| 67 | 104 | return True |
| 68 | 105 | self._last_setup_json = body |
| 69 | 106 | client.set(KEY_SETUP, body) |
| ... | ... | @@ -80,6 +117,13 @@ class RedisPublisher: |
| 80 | 117 | return True |
| 81 | 118 | |
| 82 | 119 | def close(self) -> None: |
| 120 | + if self._pubsub is not None: |
|
| 121 | + try: |
|
| 122 | + self._pubsub.unsubscribe() |
|
| 123 | + self._pubsub.close() |
|
| 124 | + except Exception: |
|
| 125 | + pass |
|
| 126 | + self._pubsub = None |
|
| 83 | 127 | if self._client is not None: |
| 84 | 128 | try: |
| 85 | 129 | self._client.close() |
projects/hot-tub/src/temp/runtime.py
| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | +"""Runtime flags for the temperature daemon (not persisted in config file).""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +from dataclasses import dataclass |
|
| 6 | + |
|
| 7 | +from .config import TempConfig |
|
| 8 | + |
|
| 9 | + |
|
| 10 | +@dataclass |
|
| 11 | +class TempRuntime: |
|
| 12 | + setup_mode_override: bool | None = None |
|
| 13 | + setup_active: bool = False |
|
| 14 | + last_setup_state: str = "need_hot_in" |
|
| 15 | + |
|
| 16 | + def effective_setup_mode(self, cfg: TempConfig) -> bool: |
|
| 17 | + if self.setup_mode_override is not None: |
|
| 18 | + return self.setup_mode_override |
|
| 19 | + return cfg.setup_mode |
projects/hot-tub/src/temp/tests/test_commands.py
| ... | ... | @@ -0,0 +1,44 @@ |
| 1 | +"""Tests for temps.command handling.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import tempfile |
|
| 6 | +import unittest |
|
| 7 | +from pathlib import Path |
|
| 8 | + |
|
| 9 | +from temp.commands import TempCommandError, apply_temp_command |
|
| 10 | +from temp.config import TempConfig |
|
| 11 | +from temp.identity import ProbeMap, load_probe_map, save_probe_map |
|
| 12 | +from temp.runtime import TempRuntime |
|
| 13 | + |
|
| 14 | + |
|
| 15 | +class TestTempCommands(unittest.TestCase): |
|
| 16 | + def setUp(self) -> None: |
|
| 17 | + self.tmp = tempfile.TemporaryDirectory() |
|
| 18 | + self.path = Path(self.tmp.name) / "probes.json" |
|
| 19 | + save_probe_map(self.path, ProbeMap(hot_in="28-h", tub=["28-a", "28-b", "28-c", "28-d"])) |
|
| 20 | + self.cfg = TempConfig(probe_map_path=self.path) |
|
| 21 | + self.runtime = TempRuntime(last_setup_state="ready") |
|
| 22 | + |
|
| 23 | + def tearDown(self) -> None: |
|
| 24 | + self.tmp.cleanup() |
|
| 25 | + |
|
| 26 | + def test_setup_start_clears_map(self) -> None: |
|
| 27 | + apply_temp_command(self.cfg, self.runtime, {"op": "setup_start"}) |
|
| 28 | + self.assertTrue(self.runtime.setup_active) |
|
| 29 | + self.assertEqual(load_probe_map(self.path).hot_in, None) |
|
| 30 | + |
|
| 31 | + def test_setup_cancel_restores(self) -> None: |
|
| 32 | + apply_temp_command(self.cfg, self.runtime, {"op": "setup_start"}) |
|
| 33 | + apply_temp_command(self.cfg, self.runtime, {"op": "setup_cancel"}) |
|
| 34 | + self.assertFalse(self.runtime.setup_active) |
|
| 35 | + self.assertEqual(load_probe_map(self.path).hot_in, "28-h") |
|
| 36 | + |
|
| 37 | + def test_setup_finish_requires_ready(self) -> None: |
|
| 38 | + self.runtime.last_setup_state = "need_water" |
|
| 39 | + with self.assertRaises(TempCommandError): |
|
| 40 | + apply_temp_command(self.cfg, self.runtime, {"op": "setup_finish"}) |
|
| 41 | + |
|
| 42 | + |
|
| 43 | +if __name__ == "__main__": |
|
| 44 | + unittest.main() |
projects/hot-tub/src/temp/tests/test_identity.py
| ... | ... | @@ -8,8 +8,12 @@ from pathlib import Path |
| 8 | 8 | |
| 9 | 9 | from temp.identity import ( |
| 10 | 10 | ProbeMap, |
| 11 | + backup_probe_map, |
|
| 12 | + clear_probe_map, |
|
| 11 | 13 | evaluate_identity, |
| 12 | 14 | load_probe_map, |
| 15 | + restore_probe_map, |
|
| 16 | + role_readings, |
|
| 13 | 17 | save_probe_map, |
| 14 | 18 | ) |
| 15 | 19 | |
| ... | ... | @@ -78,5 +82,35 @@ class TestEvaluateIdentity(unittest.TestCase): |
| 78 | 82 | self.assertEqual(r.setup_state, "ready") |
| 79 | 83 | |
| 80 | 84 | |
| 85 | +class TestRoleReadings(unittest.TestCase): |
|
| 86 | + def test_roles_sorted_tub_order(self) -> None: |
|
| 87 | + pm = ProbeMap(hot_in="28-h", tub=["28-d", "28-b", "28-a", "28-c"]) |
|
| 88 | + probes = {"28-h": 112.0, "28-a": 101.0, "28-b": 101.2, "28-c": None, "28-d": 100.9} |
|
| 89 | + roles = role_readings(probes, pm) |
|
| 90 | + self.assertEqual(roles["hot_in"], 112.0) |
|
| 91 | + self.assertEqual(roles["tub1"], 101.0) |
|
| 92 | + self.assertEqual(roles["tub2"], 101.2) |
|
| 93 | + self.assertIsNone(roles["tub3"]) |
|
| 94 | + self.assertEqual(roles["tub4"], 100.9) |
|
| 95 | + |
|
| 96 | + def test_missing_probe_on_bus_is_null(self) -> None: |
|
| 97 | + pm = ProbeMap(hot_in="28-h", tub=["28-a"]) |
|
| 98 | + roles = role_readings({}, pm) |
|
| 99 | + self.assertIsNone(roles["hot_in"]) |
|
| 100 | + self.assertIsNone(roles["tub1"]) |
|
| 101 | + |
|
| 102 | + |
|
| 103 | +class TestProbeMapBackup(unittest.TestCase): |
|
| 104 | + def test_backup_clear_restore(self) -> None: |
|
| 105 | + with tempfile.TemporaryDirectory() as tmp: |
|
| 106 | + path = Path(tmp) / "probes.json" |
|
| 107 | + save_probe_map(path, ProbeMap(hot_in="28-h", tub=["28-a"])) |
|
| 108 | + backup_probe_map(path) |
|
| 109 | + clear_probe_map(path) |
|
| 110 | + self.assertEqual(load_probe_map(path).hot_in, None) |
|
| 111 | + self.assertTrue(restore_probe_map(path)) |
|
| 112 | + self.assertEqual(load_probe_map(path).hot_in, "28-h") |
|
| 113 | + |
|
| 114 | + |
|
| 81 | 115 | if __name__ == "__main__": |
| 82 | 116 | unittest.main() |
projects/hot-tub/src/temp_sim/simulator.py
| ... | ... | @@ -100,6 +100,13 @@ def build_payloads(state: SimState) -> tuple[dict[str, Any], dict[str, Any], dic |
| 100 | 100 | "ts": ts, |
| 101 | 101 | "setup_state": "ready", |
| 102 | 102 | "probes": probes, |
| 103 | + "roles": { |
|
| 104 | + "hot_in": state.hot_in_f, |
|
| 105 | + "tub1": state.tub_avg_f, |
|
| 106 | + "tub2": state.tub_avg_f, |
|
| 107 | + "tub3": state.tub_avg_f, |
|
| 108 | + "tub4": state.tub_avg_f, |
|
| 109 | + }, |
|
| 103 | 110 | "tub_avg_f": state.tub_avg_f, |
| 104 | 111 | "hot_in_f": state.hot_in_f, |
| 105 | 112 | "outdoor_f": None, |
| ... | ... | @@ -112,6 +119,7 @@ def build_payloads(state: SimState) -> tuple[dict[str, Any], dict[str, Any], dic |
| 112 | 119 | "state": "ready", |
| 113 | 120 | "detail": "temp_sim diagnostic simulator", |
| 114 | 121 | "visible_ids": list(TUB_PROBE_IDS) + [HOT_PROBE_ID], |
| 122 | + "setup_active": False, |
|
| 115 | 123 | } |
| 116 | 124 | health_payload = { |
| 117 | 125 | "service": "temps", |
projects/hot-tub/src/web/INSTALL.md
| ... | ... | @@ -0,0 +1,89 @@ |
| 1 | +# Web Service — Installation (Raspberry Pi) |
|
| 2 | + |
|
| 3 | +Serves the React UI and HTTP API on the LAN. Design details: [README.md](README.md). |
|
| 4 | + |
|
| 5 | +## Prerequisites |
|
| 6 | + |
|
| 7 | +- Raspberry Pi OS with Python 3 |
|
| 8 | +- **Redis** running locally |
|
| 9 | +- **Temperature daemon** (`hot-tub-temps`) and **control daemon** (`hot-tub-control`) recommended for live data |
|
| 10 | +- Built frontend at `src/web/frontend/dist/` (committed in the repo, or build on a dev machine with Node.js) |
|
| 11 | + |
|
| 12 | +## 1. Install service |
|
| 13 | + |
|
| 14 | +From the hot-tub repository on the Pi: |
|
| 15 | + |
|
| 16 | +```bash |
|
| 17 | +cd /path/to/hot-tub |
|
| 18 | +sudo deploy/web/install.sh |
|
| 19 | +``` |
|
| 20 | + |
|
| 21 | +Optional fixed install root: |
|
| 22 | + |
|
| 23 | +```bash |
|
| 24 | +sudo INSTALL_ROOT=/opt/hot-tub deploy/web/install.sh |
|
| 25 | +``` |
|
| 26 | + |
|
| 27 | +The script installs `python3-redis`, `python3-flask`, creates `/etc/hot-tub/web.json`, installs systemd unit `hot-tub-web`, and logrotate for `/var/log/hot-tub/web.log`. The service is **enabled on boot**. |
|
| 28 | + |
|
| 29 | +Install order: Redis → `hot-tub-temps` → `hot-tub-control` → `hot-tub-web`. |
|
| 30 | + |
|
| 31 | +## 2. Configure |
|
| 32 | + |
|
| 33 | +Edit `/etc/hot-tub/web.json` if needed: |
|
| 34 | + |
|
| 35 | +| Key | Default | Purpose | |
|
| 36 | +|---|---|---| |
|
| 37 | +| `bind_host` | `0.0.0.0` | Listen address (LAN) | |
|
| 38 | +| `bind_port` | `8080` | HTTP port | |
|
| 39 | +| `redis_url` | `redis://127.0.0.1:6379/0` | Local Redis only | |
|
| 40 | +| `static_dir` | path to `frontend/dist` | Built React app | |
|
| 41 | +| `stale_s` | `15` | Upstream snapshot age for degraded health | |
|
| 42 | + |
|
| 43 | +## 3. Verify |
|
| 44 | + |
|
| 45 | +```bash |
|
| 46 | +systemctl status hot-tub-web |
|
| 47 | +tail -f /var/log/hot-tub/web.log |
|
| 48 | +curl -s http://localhost:8080/api/status | python3 -m json.tool |
|
| 49 | +curl -N http://localhost:8080/api/events |
|
| 50 | +redis-cli GET health:web |
|
| 51 | +``` |
|
| 52 | + |
|
| 53 | +From a phone on Wi‑Fi: `http://<pi-ip>:8080/` |
|
| 54 | + |
|
| 55 | +## 4. Development (off the Pi) |
|
| 56 | + |
|
| 57 | +Build the UI when changing frontend code: |
|
| 58 | + |
|
| 59 | +```bash |
|
| 60 | +cd src/web/frontend |
|
| 61 | +npm install |
|
| 62 | +npm run dev # Vite dev server; proxies /api to :8080 |
|
| 63 | +npm run build # output: dist/ |
|
| 64 | +``` |
|
| 65 | + |
|
| 66 | +Run the Python service locally: |
|
| 67 | + |
|
| 68 | +```bash |
|
| 69 | +cd src |
|
| 70 | +PYTHONPATH=. python3 -m web --config web/config.example.json -v |
|
| 71 | +``` |
|
| 72 | + |
|
| 73 | +Stack test without hardware: local Redis, `temp_sim`, `python3 -m control --no-gpio`, and `python3 -m web`. |
|
| 74 | + |
|
| 75 | +## 5. Disable / remove |
|
| 76 | + |
|
| 77 | +```bash |
|
| 78 | +sudo systemctl disable --now hot-tub-web |
|
| 79 | +sudo rm /etc/systemd/system/hot-tub-web.service |
|
| 80 | +sudo systemctl daemon-reload |
|
| 81 | +``` |
|
| 82 | + |
|
| 83 | +## File locations |
|
| 84 | + |
|
| 85 | +| Path | Purpose | |
|
| 86 | +|---|---| |
|
| 87 | +| `/etc/hot-tub/web.json` | Service config | |
|
| 88 | +| `/var/log/hot-tub/web.log` | Application log | |
|
| 89 | +| `src/web/frontend/dist/` | Static UI served at `/` | |
projects/hot-tub/src/web/README.md
| ... | ... | @@ -0,0 +1,412 @@ |
| 1 | +# Web Interface + API — Developer Guide |
|
| 2 | + |
|
| 3 | +Handoff document for whoever builds the real web service and React UI (human or AI agent). It collects the architecture rules, the data contracts the UI depends on, what the HTML mockup already settles, what is still missing elsewhere in the system, and a suggested order of work. |
|
| 4 | + |
|
| 5 | +**Status (2026-10-19):** Flask web service, React UI (`frontend/`), systemd install, and temp-daemon setup commands are implemented. See [INSTALL.md](INSTALL.md). |
|
| 6 | + |
|
| 7 | +## Read these first |
|
| 8 | + |
|
| 9 | +| Document | Why it matters | |
|
| 10 | +|---|---| |
|
| 11 | +| [interface-requirements.md](interface-requirements.md) | The user's requirements for the UI. Source of truth for screens and behavior. | |
|
| 12 | +| [mockup/](mockup/) | Interactive HTML mockup of every screen, with a fake backend. Visual and behavioral reference. | |
|
| 13 | +| [../../software-architecture.md](../../software-architecture.md) | System design: four services + Redis, ownership rules, the original Web/API section. | |
|
| 14 | +| [../control/README.md](../control/README.md) | Control daemon behavior: modes, `heat_phase` values, commands, published state. **Supersedes the mode list in the architecture doc.** | |
|
| 15 | +| [../temp/README.md](../temp/README.md) | Temperature daemon: probe bundles, teach-in rules, `temps:latest` / `temps:setup`. | |
|
| 16 | +| [../temp_sim/README.md](../temp_sim/README.md) | Simulator that publishes fake temperatures to Redis; useful for testing the web service without probes. | |
|
| 17 | +| [../../pi-zero-control-panel.md](../../pi-zero-control-panel.md), [../../indoor-monitor.md](../../indoor-monitor.md) | Other clients that will talk to this API (see [API contract](#http-api)). | |
|
| 18 | + |
|
| 19 | +## Where the web service fits |
|
| 20 | + |
|
| 21 | +```text |
|
| 22 | + Browser / phone / tub console |
|
| 23 | + │ HTTP (LAN) |
|
| 24 | + ▼ |
|
| 25 | + ┌──────────────────────┐ PUBLISH control.command ┌─────────────────┐ |
|
| 26 | + │ Web service │ ──────────────────────────▶ │ Control daemon │──▶ GPIO relays |
|
| 27 | + │ (this package) │ │ (src/control) │ |
|
| 28 | + │ │ ◀── SUBSCRIBE control.state ─┤ │ |
|
| 29 | + │ - serves React app │ GET control:latest └─────────────────┘ |
|
| 30 | + │ - /api/events (SSE) │ |
|
| 31 | + │ - /api/command │ ◀── SUBSCRIBE temps.reading ┌─────────────────┐ |
|
| 32 | + │ - health heartbeat │ GET temps:latest/setup │ Temp daemon │──▶ 1-Wire probes |
|
| 33 | + └──────────┬───────────┘ ── (new) temps.command ────▶ │ (src/temp) │ |
|
| 34 | + │ └─────────────────┘ |
|
| 35 | + └── SET/PUBLISH health:web / health.web ──▶ Health daemon (LEDs, not built yet) |
|
| 36 | +``` |
|
| 37 | + |
|
| 38 | +### Rules that must not be broken |
|
| 39 | + |
|
| 40 | +1. **The web service never touches GPIO or the 1-Wire bus.** It only publishes *intents* to Redis. The control daemon is the only process that decides whether heat may run, and it re-validates everything (target clamping, safety interlocks). See "Design Principles" in the architecture doc. |
|
| 41 | +2. **"Intent in, Control decides, state out."** The UI displays what `control.state` says, not what it asked for. Don't flip the UI optimistically and leave it there; wait for the next state snapshot (it arrives within about one second). |
|
| 42 | +3. **Never fake a fresh-looking number.** If the live connection drops or data is stale, the UI must say so (the mockup greys the page and shows "Reconnecting…"). |
|
| 43 | +4. **Redis stays on localhost.** The web service is the only thing exposed on the LAN. |
|
| 44 | +5. **No auth in v1.** The Pi is on a private LAN. If remote access is ever wanted, use a VPN (e.g. Tailscale) rather than building a user system (architecture doc, "Open Decisions"). |
|
| 45 | + |
|
| 46 | +## What exists today |
|
| 47 | + |
|
| 48 | +| Piece | State | Location | |
|
| 49 | +|---|---|---| |
|
| 50 | +| Temperature daemon | Built, tested, has systemd/install | `src/temp/`, `deploy/temp/` | |
|
| 51 | +| Control daemon | Built, unit tests, has systemd/install | `src/control/`, `deploy/control/` | |
|
| 52 | +| Temperature simulator | Built (manual tool) | `src/temp_sim/` | |
|
| 53 | +| Health daemon (LEDs) | Not built | — | |
|
| 54 | +| Web service + React UI | Built | `src/web/`, `deploy/web/` | |
|
| 55 | +| UI mockup | Built | `src/web/mockup/` | |
|
| 56 | + |
|
| 57 | +## The mockup |
|
| 58 | + |
|
| 59 | +### Running it |
|
| 60 | + |
|
| 61 | +No build step and no dependencies; `mock-backend.js` runs inside the browser. |
|
| 62 | + |
|
| 63 | +```bash |
|
| 64 | +# Same machine: just open the file |
|
| 65 | +xdg-open src/web/mockup/index.html |
|
| 66 | + |
|
| 67 | +# From a phone on the LAN |
|
| 68 | +cd src/web/mockup && python3 -m http.server 8080 --bind 0.0.0.0 |
|
| 69 | +# then browse to http://<this-machine-ip>:8080/ |
|
| 70 | +``` |
|
| 71 | + |
|
| 72 | +Each browser tab runs its own independent simulated tub and resets on reload. |
|
| 73 | + |
|
| 74 | +### Files |
|
| 75 | + |
|
| 76 | +| File | Role | Fate in the real app | |
|
| 77 | +|---|---|---| |
|
| 78 | +| `index.html` | Shell that loads the two scripts | Becomes Vite's `index.html` | |
|
| 79 | +| `styles.css` | All styling, CSS variables, responsive breakpoints, animations | Reuse almost verbatim (global CSS or split per component) | |
|
| 80 | +| `app.js` | UI: state, routing, components, event handling | Port to React components (see map below) | |
|
| 81 | +| `mock-backend.js` | Fake API + fake tub physics + the dashed "Mock backend" panel | **Delete**; replace with a real API client. Optionally keep as a dev-only mock. | |
|
| 82 | + |
|
| 83 | +### Component map (mockup function → React component) |
|
| 84 | + |
|
| 85 | +Every PascalCase function in `app.js` takes a props object and returns markup, so each maps 1:1 to a React component: |
|
| 86 | + |
|
| 87 | +| Mockup function | Purpose | |
|
| 88 | +|---|---| |
|
| 89 | +| `App`, `Page` | Layout + route switch | |
|
| 90 | +| `Header`, `ConnectionBadge` | Brand, the two nav tabs, live/reconnecting badge | |
|
| 91 | +| `StatusLight` | Reusable green / yellow (pulsing) / red / grey indicator | |
|
| 92 | +| `ControlPage` | Main Control screen | |
|
| 93 | +| `ErrorBanner` | "Heater problem" banner with "Reset (set Sleep)" | |
|
| 94 | +| `TubGraphic`, `Duck` | Horse-trough SVG with animated water, steam, duck; temp shown in the water | |
|
| 95 | +| `Pump` | Fan SVG; blades spin + green light when running | |
|
| 96 | +| `TargetTemp` | Target °F with −/+ buttons (disabled at 50 / 110 °F) | |
|
| 97 | +| `ModeSelects` | Heat mode (Sleep / Maintain) and filter mode (Auto / On / Off) dropdowns | |
|
| 98 | +| `StatusSummary` | Heater phase label, hot-input temp, filter status | |
|
| 99 | +| `DiagnosticPage` | Backflush + Sensor Setup cards | |
|
| 100 | +| `BackflushAnimation` | Droplet with spinning arrows + bubbles | |
|
| 101 | +| `SensorReadings`, `ProbeRow` | Per-probe readings with status lights | |
|
| 102 | +| `SensorSetupPage`, `Stepper`, `SetupStep1..3` | Three-step sensor teach-in wizard | |
|
| 103 | + |
|
| 104 | +Non-component pieces and their React equivalents: |
|
| 105 | + |
|
| 106 | +| Mockup | React | |
|
| 107 | +|---|---| |
|
| 108 | +| Single `state` object + `setState` | One `useReducer` at the top (or a small context). State is small; no Redux needed. | |
|
| 109 | +| `data-action` attributes + delegated click/change handlers | `onClick` / `onChange` props | |
|
| 110 | +| Hash routes `#/control`, `#/diagnostic`, `#/diagnostic/sensor-setup/:step` | react-router. Hash routing (`HashRouter`) avoids needing server-side fallback routes; browser routing also works if the server returns `index.html` for unknown paths. | |
|
| 111 | +| `patch()` DOM differ | Delete. It exists only so CSS animations don't restart on each update; React does this already. **Keep SVG structure stable across renders** (toggle classes, don't conditionally mount the fan blades or water) or animations will restart. | |
|
| 112 | +| `window.HotTubApi` | An `api.js` module + a `useHotTubEvents()` hook wrapping `EventSource` | |
|
| 113 | + |
|
| 114 | +### UI decisions made in the mockup |
|
| 115 | + |
|
| 116 | +- **Two mode dropdowns, not one.** The requirements say "a dropdown to set the mode", but the control daemon has two independent modes (`heat_mode`, `filter_mode`), so the mockup shows both. |
|
| 117 | +- **Backflush is not in the heat-mode dropdown.** It is started only from the Diagnostic page. While it runs, the dropdown shows a disabled "Backflush (running)" option so the select still displays a valid value. |
|
| 118 | +- **Backflush button state:** disabled from the moment it's pressed (local `backflushRequested` flag) until `control.state` shows backflush has finished. The local flag covers the gap before the first state update arrives. It re-enables when `heat_mode` and `heat_phase` are no longer `backflush`. There's no countdown because the control daemon doesn't publish remaining time. |
|
| 119 | +- **Startup failure** (`heat_phase = error_startup`): red banner with the error detail and a "Reset (set Sleep)" button. Setting `heat_mode = sleep` is the control daemon's documented reset path. |
|
| 120 | +- **Steam** shows when the heat pump is on or water is ≥ 95 °F (purely decorative). |
|
| 121 | +- **Animations** respect `prefers-reduced-motion`. |
|
| 122 | +- **Responsive breakpoints:** ≥ 900 px two columns on Main Control; ≥ 760 px two columns on Diagnostic; < 600 px nav tabs move to a full-width second header row. |
|
| 123 | + |
|
| 124 | +### Where the mockup's fake data differs from the real system |
|
| 125 | + |
|
| 126 | +These are the traps when swapping in the real backend: |
|
| 127 | + |
|
| 128 | +| Topic | Mockup | Real system | What to do | |
|
| 129 | +|---|---|---|---| |
|
| 130 | +| Probe keys in `temps.reading` | `probes: { hot_in, tub1..tub4 }` keyed by **role** | `probes` keyed by **ROM id** (`"28-0123…"`) | Temp daemon should publish by role (see [Gap 1](#gap-1-role-labeled-probe-readings)). | |
|
| 131 | +| Unplugged probe | Present with value `null` | **Absent** from `probes`. `null` means "on the bus but the read failed" (CRC error / 85 °C sentinel) | UI must treat "missing key" and "null" as different states. | |
|
| 132 | +| Detection delay | Instant | The Linux 1-Wire driver can take several seconds to add or remove devices after a plug change, and readings arrive every 5 s | Wizard lights may lag; don't add timeouts that assume instant detection. | |
|
| 133 | +| Setup mode | `startSensorSetup()` etc. do nothing | No runtime way to enter teach-in today | See [Gap 2](#gap-2-runtime-sensor-setup-mode). | |
|
| 134 | +| Startup check timing | 3 s per attempt | 20 s per attempt + 5 s pause between attempts (`maintain.startup_delay_s`, `retry_off_s`) | A failed startup takes about a minute and a half to reach `error_startup`. | |
|
| 135 | +| Heating rate | 0.15 °F/s (demo speed) | Real propane heater: very slow | — | |
|
| 136 | +| Command acknowledgement | `sendCommand` resolves with `{id}` | Control daemon doesn't reply; it sets `last_command_id` in its next `control.state` | Web service generates the `id`, returns it from POST; UI can match it against `last_command_id` if it wants confirmation. | |
|
| 137 | +| `error_safety` phase | Never produced | Produced when temps go stale / `need_reinstall` while heating | UI already has a label for it ("Blocked by safety check"). Consider a banner too. | |
|
| 138 | + |
|
| 139 | +## Data contracts |
|
| 140 | + |
|
| 141 | +The UI should only depend on these shapes. They're produced by the other daemons today (except where marked). |
|
| 142 | + |
|
| 143 | +### `control.state` / `control:latest` (from `src/control/state.py`, `state_snapshot`) |
|
| 144 | + |
|
| 145 | +```json |
|
| 146 | +{ |
|
| 147 | + "ts": "2026-10-19T20:00:05Z", |
|
| 148 | + "heat_mode": "maintain", // sleep | maintain | backflush |
|
| 149 | + "filter_mode": "auto", // on | auto | off |
|
| 150 | + "target_f": 102.0, |
|
| 151 | + "tub_avg_f": 98.5, // null if no valid temps |
|
| 152 | + "hot_in_f": 112.0, // null if missing |
|
| 153 | + "heat_pump": "on", // on | off (actual relay state) |
|
| 154 | + "filter_pump": "off", |
|
| 155 | + "heat_phase": "heating", // idle | startup_1..3 | heating | backflush | error_startup | error_safety |
|
| 156 | + "startup_attempt": 1, |
|
| 157 | + "error": null, // or { "code": "startup_failed" | "safety", "detail": "..." } |
|
| 158 | + "last_command_id": "…", |
|
| 159 | + "filter_window_active": false, |
|
| 160 | + "filter_next_start": "2026-10-20T06:00:00Z" // UTC; null unless filter_mode=auto and outside window |
|
| 161 | +} |
|
| 162 | +``` |
|
| 163 | + |
|
| 164 | +Published on every change and at least every 5 s (`heartbeat_s`). Times are UTC with a `Z` suffix; display in local time. |
|
| 165 | + |
|
| 166 | +### `temps.reading` / `temps:latest` (from `src/temp/daemon.py`, `build_sample`) |
|
| 167 | + |
|
| 168 | +```json |
|
| 169 | +{ |
|
| 170 | + "ts": "2026-10-19T20:00:00Z", |
|
| 171 | + "setup_state": "ready", // ready | need_hot_in | need_water | need_reinstall |
|
| 172 | + "probes": { "28-aaa": 101.2, "28-bbb": null }, // by ROM id today, see Gap 1 |
|
| 173 | + "tub_avg_f": 101.0, |
|
| 174 | + "hot_in_f": 112.0, |
|
| 175 | + "outdoor_f": null, |
|
| 176 | + "ok_count": 4, |
|
| 177 | + "fail_count": 0, |
|
| 178 | + "spread_f": 0.6 |
|
| 179 | +} |
|
| 180 | +``` |
|
| 181 | + |
|
| 182 | +Published every 5 s (`interval_s`). The control daemon treats readings older than 15 s as stale. |
|
| 183 | + |
|
| 184 | +### `temps.setup` / `temps:setup` |
|
| 185 | + |
|
| 186 | +```json |
|
| 187 | +{ "ts": "…", "state": "need_water", "detail": "Learned hot_in=28-zzz", "visible_ids": ["28-zzz"] } |
|
| 188 | +``` |
|
| 189 | + |
|
| 190 | +Published only when it changes. Useful for the wizard (see [Gap 2](#gap-2-runtime-sensor-setup-mode)). |
|
| 191 | + |
|
| 192 | +### Commands → `control.command` (validated in `src/control/commands.py`) |
|
| 193 | + |
|
| 194 | +Envelope: |
|
| 195 | + |
|
| 196 | +```json |
|
| 197 | +{ "ts": "2026-10-19T20:00:00Z", "source": "web", "id": "uuid", "op": "set_heat_mode", "args": { "mode": "maintain" } } |
|
| 198 | +``` |
|
| 199 | + |
|
| 200 | +| `op` | `args` | Used by UI for | |
|
| 201 | +|---|---|---| |
|
| 202 | +| `set_heat_mode` | `mode`: `sleep` \| `maintain` \| `backflush` | Heat dropdown, Backflush button, Reset button | |
|
| 203 | +| `set_target_temp` | `target_f` | (absolute set; tub console / API) | |
|
| 204 | +| `target_temp_increment` | optional `delta_f` (default 1) | + button | |
|
| 205 | +| `target_temp_decrement` | optional `delta_f` (default 1) | − button | |
|
| 206 | +| `set_filter_mode` | `mode`: `on` \| `auto` \| `off` | Filter dropdown | |
|
| 207 | + |
|
| 208 | +Unknown ops or bad args are logged and ignored by the control daemon; there is no error reply. The web service should validate the same rules itself and return HTTP 400, so the UI gets immediate feedback. |
|
| 209 | + |
|
| 210 | +### Health → `health.web` / `health:web` (the web service must publish this) |
|
| 211 | + |
|
| 212 | +Same shape as the other services: |
|
| 213 | + |
|
| 214 | +```json |
|
| 215 | +{ "service": "web", "ts": "…", "status": "ok", "detail": "serving; redis ok", "pid": 1234 } |
|
| 216 | +``` |
|
| 217 | + |
|
| 218 | +| `status` | When | |
|
| 219 | +|---|---| |
|
| 220 | +| `ok` | Accepting HTTP, Redis reachable | |
|
| 221 | +| `degraded` | Redis flaky, or `control:latest` / `temps:latest` stale | |
|
| 222 | +| `error` | Redis down | |
|
| 223 | + |
|
| 224 | +Publish every ~5 s and on change. Use SET + PUBLISH with identical JSON, like `src/temp/redis_out.py`. |
|
| 225 | + |
|
| 226 | +## HTTP API |
|
| 227 | + |
|
| 228 | +Recommended contract. Settle it before writing the React client: |
|
| 229 | + |
|
| 230 | +| Method | Path | Body | Response / behavior | |
|
| 231 | +|---|---|---|---| |
|
| 232 | +| `GET` | `/api/events` | — | Server-Sent Events. On connect, immediately send current `control:latest` and `temps:latest` (and `temps:setup`), then forward each Redis message. Event names = channel names: `control.state`, `temps.reading`, `temps.setup`. Send a comment line every ~15 s as keepalive. | |
|
| 233 | +| `POST` | `/api/command` | `{ "op": "...", "args": {...} }` | Validate, add `ts` / `source: "web"` / `id`, publish to `control.command`. Return `202 { "id": "..." }` or `400 { "error": "..." }`. | |
|
| 234 | +| `GET` | `/api/status` | — | `{ "control": <control:latest>, "temps": <temps:latest>, "health": { temps, control, web } }` for polling clients | |
|
| 235 | +| `GET` | `/api/health` | — | Web service health + ages of upstream keys | |
|
| 236 | +| `POST` | `/api/sensor-setup/start` | — | Depends on Gap 2 | |
|
| 237 | +| `POST` | `/api/sensor-setup/finish` | — | Depends on Gap 2 | |
|
| 238 | +| `POST` | `/api/sensor-setup/cancel` | — | Depends on Gap 2 | |
|
| 239 | +| `GET` | `/` and static assets | — | Built React app | |
|
| 240 | + |
|
| 241 | +SSE was chosen over WebSockets because traffic is one-way (server to browser), browsers reconnect `EventSource` automatically, and commands are plain POSTs. The architecture doc lists this as an open decision, and the requirements accept "SSE or websockets". |
|
| 242 | + |
|
| 243 | +**Conflicting older contract:** [pi-zero-control-panel.md](../../pi-zero-control-panel.md) sketches `GET /status`, `POST /heating {enabled}`, `POST /target {target_f | delta_f}` with differently named fields (`average_f`, `heating_enabled`, …). It predates the control daemon. Either update that doc to use `/api/status` + `/api/command`, or add thin compatibility routes that translate (`enabled: true` → `set_heat_mode maintain`, `false` → `sleep`). Decide before the tub console is built. [indoor-monitor.md](../../indoor-monitor.md) only needs target ±1 and status, routed through the same command path. |
|
| 244 | + |
|
| 245 | +## Gaps in other services that the UI depends on |
|
| 246 | + |
|
| 247 | +### Gap 1: role-labeled probe readings |
|
| 248 | + |
|
| 249 | +The Diagnostic readings list and wizard step 3 show **Hot input, Tub 1–4**, but `temps:latest.probes` is keyed by ROM id. The temperature daemon already knows the roles (`ProbeMap` in `src/temp/identity.py`). |
|
| 250 | + |
|
| 251 | +**Recommended fix (temp daemon):** add a role-keyed field to the reading, e.g. |
|
| 252 | + |
|
| 253 | +```json |
|
| 254 | +"roles": { "hot_in": 112.0, "tub1": 101.2, "tub2": 101.0, "tub3": null, "tub4": 100.9 } |
|
| 255 | +``` |
|
| 256 | + |
|
| 257 | +Order `tub1..4` by the sorted ROM ids in the probe map (already sorted when saved). Distinguish "not on bus" from "read failed": either omit missing roles or add a parallel `"present": {...}` map. Keep `probes` as-is so nothing else breaks. Add unit tests next to `src/temp/tests/test_identity.py`. |
|
| 258 | + |
|
| 259 | +Translating in the web service using `/etc/hot-tub/temp-probes.json` is possible but duplicates identity logic in a second process. Avoid that. |
|
| 260 | + |
|
| 261 | +### Gap 2: runtime sensor setup mode |
|
| 262 | + |
|
| 263 | +Teach-in currently requires editing `setup_mode` in `/etc/hot-tub/temp.json` (or the `--setup` flag) and restarting `hot-tub-temps`. The wizard needs to start and stop it at runtime. |
|
| 264 | + |
|
| 265 | +How teach-in works (from `src/temp/identity.py`, `_try_learn`), which constrains the design: |
|
| 266 | + |
|
| 267 | +- Nothing is learned once the probe map is complete. **Starting setup must clear the probe map**, not just set `setup_mode`. |
|
| 268 | +- With an empty map: **1 probe visible → learned as hot-in**; **4 probes visible (setup mode only) → learned as the water set**. |
|
| 269 | +- With one side known: **5 visible → the other side is derived** by set difference. |
|
| 270 | +- **5 visible with an empty map → `need_reinstall`.** This is exactly why wizard step 1 makes the user unplug everything first. |
|
| 271 | + |
|
| 272 | +**Recommended design:** |
|
| 273 | + |
|
| 274 | +1. Temp daemon subscribes to a new channel `temps.command` with ops `setup_start` (back up then clear the probe map, enable setup mode), `setup_finish` (disable setup mode; only valid when `setup_state == ready`), `setup_cancel` (restore the backed-up map, disable setup mode). Publish the active flag in `temps:setup` (e.g. `"setup_active": true`). |
|
| 275 | +2. Web service maps `/api/sensor-setup/{start,finish,cancel}` to those ops. |
|
| 276 | +3. Wizard gating should use **`setup_state` / learned roles**, not just "a reading appeared": |
|
| 277 | + - Step 1 → 2: user confirms disconnection; call `start`. Show a warning if probes are still visible (the map would otherwise be learned wrong). |
|
| 278 | + - Step 2 Continue: enable when `setup_state` becomes `need_water` (hot-in learned) or `need_hot_in` with 4 tub roles present (water learned). |
|
| 279 | + - Step 3 Finish: enable when `setup_state == ready` and all five roles report values. Then call `finish`. |
|
| 280 | +4. While setup is active the control daemon sees no valid tub average and holds heat off. That's the desired behavior; the UI should say "Heating paused during sensor setup". |
|
| 281 | + |
|
| 282 | +The interface requirements say Continue/Finish enable "when temperature readings start coming in". Gating on learned roles meets that intent and avoids letting the user continue before the daemon has actually stored the IDs. |
|
| 283 | + |
|
| 284 | +## Known issues to fix first |
|
| 285 | + |
|
| 286 | +Found while writing this guide (reproduced with a direct `tick_heat` script). Both are in `src/control/heat_fsm.py`, in the `heat_mode == "backflush"` branch, and both directly affect the Backflush button: |
|
| 287 | + |
|
| 288 | +1. **Backflush never finishes when the tub is at or above target.** `check_heat_allowed` refuses heat when `tub_avg_f >= target_f`, and the backflush branch returns before checking the timer. Result: `heat_mode` stays `backflush` with the pump off forever, and the UI's Backflush button stays disabled. Observed: `tick 1..3: mode=backflush phase=backflush pump=off`. |
|
| 289 | +2. **With stale or missing temps, the heat pump turns on and off every second.** The safety trip sets `heat_phase = error_safety`; on the next tick the branch sees `heat_phase != "backflush"` and starts a new backflush with the pump on. Observed: `pump=on, off, on, off`. |
|
| 290 | + |
|
| 291 | +Likely fix direction: backflush should not use the maintain "below target" gate (the control README says only *other* safety interlocks apply). On a real safety trip it should end the backflush and revert `heat_mode` to `sleep` instead of re-entering it. Add regression tests to `src/control/tests/test_heat_fsm.py` for both cases. |
|
| 292 | + |
|
| 293 | +## Implementation plan |
|
| 294 | + |
|
| 295 | +Suggested order. Each step is independently testable. |
|
| 296 | + |
|
| 297 | +### Step 0: Fix control backflush bugs |
|
| 298 | + |
|
| 299 | +See above. Without this, the Backflush button can get stuck disabled. |
|
| 300 | + |
|
| 301 | +### Step 1: Temperature daemon changes (Gaps 1 and 2) |
|
| 302 | + |
|
| 303 | +- Role-keyed readings + tests. |
|
| 304 | +- `temps.command` subscription, setup start/finish/cancel with probe-map backup + tests. |
|
| 305 | +- Update `src/temp/README.md` and `INSTALL.md` (teach-in can now be done from the UI). |
|
| 306 | + |
|
| 307 | +### Step 2: Web service (Python) |
|
| 308 | + |
|
| 309 | +Suggested layout, mirroring `src/temp/` and `src/control/`: |
|
| 310 | + |
|
| 311 | +```text |
|
| 312 | +src/web/ |
|
| 313 | + README.md ← this file |
|
| 314 | + interface-requirements.md |
|
| 315 | + mockup/ ← keep as reference |
|
| 316 | + __init__.py |
|
| 317 | + __main__.py # python -m web --config /etc/hot-tub/web.json [--dev] |
|
| 318 | + config.py # bind host/port, redis_url, log_path, static_dir, stale thresholds |
|
| 319 | + logging_setup.py # /var/log/hot-tub/web.log (same pattern as other daemons) |
|
| 320 | + redis_io.py # GET snapshots, pubsub fan-out, publish command, health |
|
| 321 | + commands.py # validate op/args (mirror src/control/commands.py rules) |
|
| 322 | + server.py # HTTP routes + SSE |
|
| 323 | + health.py # web health computation + heartbeat thread |
|
| 324 | + config.example.json |
|
| 325 | + tests/ # unittest: command validation, SSE framing, health rules |
|
| 326 | + frontend/ # React app (Step 3) |
|
| 327 | +deploy/web/ |
|
| 328 | + hot-tub-web.service # User=hot-tub, After=redis-server.service, Restart=on-failure |
|
| 329 | + hot-tub-web.logrotate |
|
| 330 | + install.sh |
|
| 331 | +src/web/INSTALL.md |
|
| 332 | +``` |
|
| 333 | + |
|
| 334 | +Framework choice (open, recommendation first): |
|
| 335 | + |
|
| 336 | +- **Flask** (`python3-flask` via apt, matches the existing `temp-sensor/app.py`). SSE works with a streaming generator response; run threaded (`threaded=True` or `waitress`) because each SSE client holds a connection. Simplest to deploy. |
|
| 337 | +- **FastAPI + uvicorn** (`python3-fastapi`, `python3-uvicorn` in Debian bookworm). Cleaner async SSE, more moving parts. |
|
| 338 | + |
|
| 339 | +Either way, use **one Redis pubsub connection** that fans out to per-client queues, not one subscription per browser tab. Config defaults: bind `0.0.0.0:8080`, Redis `redis://127.0.0.1:6379/0`. |
|
| 340 | + |
|
| 341 | +Conventions to keep consistent with the other services: |
|
| 342 | + |
|
| 343 | +- Package importable as `web` with `PYTHONPATH=src`; runs as `python3 -m web`. |
|
| 344 | +- Runs as system user `hot-tub`; config in `/etc/hot-tub/web.json`; log to `/var/log/hot-tub/web.log` via stdlib `logging` + system logrotate (`copytruncate`). |
|
| 345 | +- Redis reconnect with exponential backoff (copy the pattern in `src/temp/redis_out.py` / `src/control/redis_io.py`). |
|
| 346 | +- Install script is idempotent and doesn't reinstall Redis if present (see `deploy/control/install.sh`). |
|
| 347 | +- Tests use stdlib `unittest`, runnable off the Pi: `PYTHONPATH=. python3 -m unittest discover -s web/tests -v`. |
|
| 348 | + |
|
| 349 | +### Step 3: React app |
|
| 350 | + |
|
| 351 | +- `src/web/frontend/`, Vite + React (TypeScript optional but recommended for the payload types above). |
|
| 352 | +- Port components from `mockup/app.js`; reuse `mockup/styles.css`. |
|
| 353 | +- `api.js`: `connect()` → `new EventSource("/api/events")` with `addEventListener("control.state", …)` etc.; `sendCommand(op, args)` → `fetch("/api/command", { method: "POST", … })`. |
|
| 354 | +- Connection badge: `open` → Live; `EventSource.onerror` → Reconnecting (it auto-retries). Also mark data stale if `ts` is older than ~15 s even while connected (upstream daemon died). |
|
| 355 | +- Dev: Vite dev server with `server.proxy` for `/api` → the Python service (on the Pi, or locally with Redis + `temp_sim` + control daemon in `--dry-run`). |
|
| 356 | +- **Build on a dev machine, not on the Pi 3.** Output to `src/web/frontend/dist/`; the Python service serves that directory. Recommended: commit `dist/` (or have `deploy/web/install.sh` require it) so the Pi never needs Node. |
|
| 357 | + |
|
| 358 | +### Step 4: End-to-end on the Pi |
|
| 359 | + |
|
| 360 | +- Install order: Redis → `hot-tub-temps` → `hot-tub-control` → `hot-tub-web`. |
|
| 361 | +- Verify from a phone: live temps, ± target, mode changes reflected in `redis-cli GET control:latest`, backflush completes and re-enables, wizard with real bundles, `health:web` present. |
|
| 362 | + |
|
| 363 | +## Testing without hardware |
|
| 364 | + |
|
| 365 | +Target setup for running the whole stack off the Pi (everything except real probes and relays): |
|
| 366 | + |
|
| 367 | +```bash |
|
| 368 | +redis-server & # local Redis |
|
| 369 | +cd src |
|
| 370 | +PYTHONPATH=. python3 -m temp_sim # fake temps; reacts to control state |
|
| 371 | +PYTHONPATH=. python3 -m control ... # needs a "Redis on, GPIO faked" mode (see below) |
|
| 372 | +PYTHONPATH=. python3 -m web # to be built |
|
| 373 | +``` |
|
| 374 | + |
|
| 375 | +**Missing piece:** `python3 -m control --dry-run` currently skips Redis *and* GPIO, so it can't take part in this loop, and without `--dry-run` it requires `gpiozero` and real pins. Add a small flag to the control daemon (e.g. `--no-gpio`) that builds `RelayOutputs(dry_run=True)` but keeps `ControlRedis`. Until then, inject state by hand with `redis-cli SET control:latest '…'` (examples in [temp_sim/README.md](../temp_sim/README.md)). |
|
| 376 | + |
|
| 377 | +Useful manual checks: |
|
| 378 | + |
|
| 379 | +```bash |
|
| 380 | +redis-cli SUBSCRIBE control.command # see what the web service publishes |
|
| 381 | +redis-cli PUBLISH control.command '{"op":"set_heat_mode","args":{"mode":"sleep"},"id":"t1","source":"cli"}' |
|
| 382 | +redis-cli GET control:latest |
|
| 383 | +curl -N http://localhost:8080/api/events # watch the SSE stream |
|
| 384 | +``` |
|
| 385 | + |
|
| 386 | +`temp_sim` always reports `setup_state: ready` with fake IDs, so it cannot exercise the wizard. Wizard testing needs real probes or an extension to `temp_sim`. |
|
| 387 | + |
|
| 388 | +## Open decisions |
|
| 389 | + |
|
| 390 | +| Decision | Recommendation | |
|
| 391 | +|---|---| |
|
| 392 | +| Flask vs FastAPI | Flask + threaded server (apt-installable, simplest) | |
|
| 393 | +| Hash vs browser routing | Hash routing (no server fallback needed) | |
|
| 394 | +| Commit built `dist/` or build during install | Commit `dist/` (Pi 3 shouldn't run Node) | |
|
| 395 | +| Old Pi Zero API (`/status`, `/heating`, `/target`) | Update that doc to the `/api/*` contract; add compatibility routes only if needed | |
|
| 396 | +| Expose filter auto schedule / `band_f` in UI | Not in v1; config-file only (control README "Open decisions") | |
|
| 397 | +| History charts (architecture "UI capabilities") | Out of scope until the control daemon writes history (SQLite not built) | |
|
| 398 | +| Health summary on the Diagnostic page | Nice to have once the Health daemon exists; `health:*` keys are readable now | |
|
| 399 | +| Auth | None in v1, LAN only | |
|
| 400 | + |
|
| 401 | +## Acceptance checklist (from the requirements) |
|
| 402 | + |
|
| 403 | +- [x] Responsive: large, medium, phone. |
|
| 404 | +- [x] Live updates via SSE; visible reconnecting/stale state. |
|
| 405 | +- [x] Main Control: trough graphic with current °F in the water. |
|
| 406 | +- [x] Heat and filter fans below the tub; spin + green light when running. |
|
| 407 | +- [x] Target °F with + / − buttons. |
|
| 408 | +- [x] Mode dropdown(s). |
|
| 409 | +- [x] Diagnostic: big Backflush button: starts backflush, disables, shows animation, re-enables when finished. |
|
| 410 | +- [x] Diagnostic: big Sensor Setup button leading to the 3-step wizard with indicator lights and gated Continue / Finish, returning to Diagnostic. |
|
| 411 | +- [x] Web publishes `health:web`. |
|
| 412 | +- [x] systemd unit, logrotate, install script, `INSTALL.md`. |
projects/hot-tub/src/web/__init__.py
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +"""Hot tub web service and API.""" |
projects/hot-tub/src/web/__main__.py
| ... | ... | @@ -0,0 +1,71 @@ |
| 1 | +"""CLI entry: python -m web""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import argparse |
|
| 6 | +import signal |
|
| 7 | +import sys |
|
| 8 | +import threading |
|
| 9 | +from pathlib import Path |
|
| 10 | + |
|
| 11 | +from .config import load_config |
|
| 12 | +from .health import start_health_thread |
|
| 13 | +from .logging_setup import setup_logging |
|
| 14 | +from .redis_io import WebRedis |
|
| 15 | +from .server import run_server |
|
| 16 | + |
|
| 17 | + |
|
| 18 | +def _path_writable(dir_path: Path) -> bool: |
|
| 19 | + try: |
|
| 20 | + dir_path.mkdir(parents=True, exist_ok=True) |
|
| 21 | + test = dir_path / ".write_test" |
|
| 22 | + test.write_text("") |
|
| 23 | + test.unlink() |
|
| 24 | + return True |
|
| 25 | + except OSError: |
|
| 26 | + return False |
|
| 27 | + |
|
| 28 | + |
|
| 29 | +def main() -> None: |
|
| 30 | + parser = argparse.ArgumentParser(description="Hot tub web service") |
|
| 31 | + parser.add_argument( |
|
| 32 | + "--config", |
|
| 33 | + type=Path, |
|
| 34 | + default=Path("/etc/hot-tub/web.json"), |
|
| 35 | + help="Path to JSON config (default: /etc/hot-tub/web.json)", |
|
| 36 | + ) |
|
| 37 | + parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging") |
|
| 38 | + args = parser.parse_args() |
|
| 39 | + |
|
| 40 | + cfg_path = args.config if args.config.exists() else None |
|
| 41 | + if cfg_path is None: |
|
| 42 | + dev_example = Path(__file__).resolve().parent / "config.example.json" |
|
| 43 | + cfg_path = dev_example if dev_example.exists() else None |
|
| 44 | + |
|
| 45 | + cfg = load_config(cfg_path) |
|
| 46 | + |
|
| 47 | + if not _path_writable(cfg.log_path.parent): |
|
| 48 | + cfg.log_path = Path("/tmp/hot-tub-web.log") |
|
| 49 | + |
|
| 50 | + setup_logging(cfg.log_path, verbose=args.verbose) |
|
| 51 | + |
|
| 52 | + redis_io = WebRedis(cfg.redis_url) |
|
| 53 | + stop = threading.Event() |
|
| 54 | + |
|
| 55 | + def _shutdown(_signum: int, _frame: object) -> None: |
|
| 56 | + stop.set() |
|
| 57 | + sys.exit(0) |
|
| 58 | + |
|
| 59 | + signal.signal(signal.SIGTERM, _shutdown) |
|
| 60 | + signal.signal(signal.SIGINT, _shutdown) |
|
| 61 | + |
|
| 62 | + start_health_thread(redis_io, cfg, stop) |
|
| 63 | + try: |
|
| 64 | + run_server(cfg, redis_io) |
|
| 65 | + finally: |
|
| 66 | + stop.set() |
|
| 67 | + redis_io.close() |
|
| 68 | + |
|
| 69 | + |
|
| 70 | +if __name__ == "__main__": |
|
| 71 | + main() |
projects/hot-tub/src/web/commands.py
| ... | ... | @@ -0,0 +1,76 @@ |
| 1 | +"""Validate control.command payloads before publishing.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +from typing import Any |
|
| 6 | + |
|
| 7 | +HEAT_MODES = frozenset({"sleep", "maintain", "backflush"}) |
|
| 8 | +FILTER_MODES = frozenset({"on", "auto", "off"}) |
|
| 9 | +CONTROL_OPS = frozenset( |
|
| 10 | + { |
|
| 11 | + "set_heat_mode", |
|
| 12 | + "set_target_temp", |
|
| 13 | + "target_temp_increment", |
|
| 14 | + "target_temp_decrement", |
|
| 15 | + "set_filter_mode", |
|
| 16 | + } |
|
| 17 | +) |
|
| 18 | +TEMP_SETUP_OPS = frozenset({"setup_start", "setup_finish", "setup_cancel"}) |
|
| 19 | + |
|
| 20 | +TARGET_F_MIN = 50.0 |
|
| 21 | +TARGET_F_MAX = 110.0 |
|
| 22 | + |
|
| 23 | + |
|
| 24 | +class CommandValidationError(ValueError): |
|
| 25 | + pass |
|
| 26 | + |
|
| 27 | + |
|
| 28 | +def _clamp_target(target: float) -> float: |
|
| 29 | + return max(TARGET_F_MIN, min(TARGET_F_MAX, target)) |
|
| 30 | + |
|
| 31 | + |
|
| 32 | +def validate_control_command(body: dict[str, Any]) -> tuple[str, dict[str, Any]]: |
|
| 33 | + op = body.get("op") |
|
| 34 | + if not op or not isinstance(op, str): |
|
| 35 | + raise CommandValidationError("missing or invalid op") |
|
| 36 | + if op not in CONTROL_OPS: |
|
| 37 | + raise CommandValidationError(f"unknown op: {op!r}") |
|
| 38 | + |
|
| 39 | + args = body.get("args") |
|
| 40 | + if args is not None and not isinstance(args, dict): |
|
| 41 | + raise CommandValidationError("args must be an object") |
|
| 42 | + args = dict(args or {}) |
|
| 43 | + |
|
| 44 | + if op == "set_heat_mode": |
|
| 45 | + mode = args.get("mode") |
|
| 46 | + if mode not in HEAT_MODES: |
|
| 47 | + raise CommandValidationError(f"invalid heat mode: {mode!r}") |
|
| 48 | + return op, {"mode": mode} |
|
| 49 | + |
|
| 50 | + if op == "set_filter_mode": |
|
| 51 | + mode = args.get("mode") |
|
| 52 | + if mode not in FILTER_MODES: |
|
| 53 | + raise CommandValidationError(f"invalid filter mode: {mode!r}") |
|
| 54 | + return op, {"mode": mode} |
|
| 55 | + |
|
| 56 | + if op == "set_target_temp": |
|
| 57 | + raw = args.get("target_f") |
|
| 58 | + if raw is None: |
|
| 59 | + raise CommandValidationError("target_f required") |
|
| 60 | + return op, {"target_f": _clamp_target(float(raw))} |
|
| 61 | + |
|
| 62 | + if op == "target_temp_increment": |
|
| 63 | + delta = float(args.get("delta_f", 1)) |
|
| 64 | + return op, {"delta_f": delta} |
|
| 65 | + |
|
| 66 | + if op == "target_temp_decrement": |
|
| 67 | + delta = float(args.get("delta_f", 1)) |
|
| 68 | + return op, {"delta_f": delta} |
|
| 69 | + |
|
| 70 | + raise CommandValidationError(f"unknown op: {op!r}") |
|
| 71 | + |
|
| 72 | + |
|
| 73 | +def validate_temp_setup_op(op: str) -> str: |
|
| 74 | + if op not in TEMP_SETUP_OPS: |
|
| 75 | + raise CommandValidationError(f"unknown setup op: {op!r}") |
|
| 76 | + return op |
projects/hot-tub/src/web/config.example.json
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +{ |
|
| 2 | + "bind_host": "0.0.0.0", |
|
| 3 | + "bind_port": 8080, |
|
| 4 | + "redis_url": "redis://127.0.0.1:6379/0", |
|
| 5 | + "log_path": "/var/log/hot-tub/web.log", |
|
| 6 | + "static_dir": "frontend/dist", |
|
| 7 | + "stale_s": 15, |
|
| 8 | + "heartbeat_s": 5, |
|
| 9 | + "sse_keepalive_s": 15 |
|
| 10 | +} |
projects/hot-tub/src/web/config.py
| ... | ... | @@ -0,0 +1,45 @@ |
| 1 | +"""Load web service configuration.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import json |
|
| 6 | +from dataclasses import dataclass |
|
| 7 | +from pathlib import Path |
|
| 8 | + |
|
| 9 | + |
|
| 10 | +@dataclass |
|
| 11 | +class WebConfig: |
|
| 12 | + bind_host: str = "0.0.0.0" |
|
| 13 | + bind_port: int = 8080 |
|
| 14 | + redis_url: str = "redis://127.0.0.1:6379/0" |
|
| 15 | + log_path: Path = Path("/var/log/hot-tub/web.log") |
|
| 16 | + static_dir: Path = Path(__file__).resolve().parent / "frontend" / "dist" |
|
| 17 | + stale_s: float = 15.0 |
|
| 18 | + heartbeat_s: float = 5.0 |
|
| 19 | + sse_keepalive_s: float = 15.0 |
|
| 20 | + |
|
| 21 | + |
|
| 22 | +def load_config(path: Path | None) -> WebConfig: |
|
| 23 | + cfg = WebConfig() |
|
| 24 | + if path is None or not path.exists(): |
|
| 25 | + return cfg |
|
| 26 | + data = json.loads(path.read_text()) |
|
| 27 | + if "bind_host" in data: |
|
| 28 | + cfg.bind_host = str(data["bind_host"]) |
|
| 29 | + if "bind_port" in data: |
|
| 30 | + cfg.bind_port = int(data["bind_port"]) |
|
| 31 | + if "redis_url" in data: |
|
| 32 | + cfg.redis_url = str(data["redis_url"]) |
|
| 33 | + if "log_path" in data: |
|
| 34 | + cfg.log_path = Path(data["log_path"]) |
|
| 35 | + if "static_dir" in data: |
|
| 36 | + cfg.static_dir = Path(data["static_dir"]) |
|
| 37 | + if not cfg.static_dir.is_absolute(): |
|
| 38 | + cfg.static_dir = Path(__file__).resolve().parent / cfg.static_dir |
|
| 39 | + if "stale_s" in data: |
|
| 40 | + cfg.stale_s = float(data["stale_s"]) |
|
| 41 | + if "heartbeat_s" in data: |
|
| 42 | + cfg.heartbeat_s = float(data["heartbeat_s"]) |
|
| 43 | + if "sse_keepalive_s" in data: |
|
| 44 | + cfg.sse_keepalive_s = float(data["sse_keepalive_s"]) |
|
| 45 | + return cfg |
projects/hot-tub/src/web/frontend/.gitignore
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +# Logs |
|
| 2 | +logs |
|
| 3 | +*.log |
|
| 4 | +npm-debug.log* |
|
| 5 | +yarn-debug.log* |
|
| 6 | +yarn-error.log* |
|
| 7 | +pnpm-debug.log* |
|
| 8 | +lerna-debug.log* |
|
| 9 | + |
|
| 10 | +node_modules |
|
| 11 | +dist-ssr |
|
| 12 | +*.local |
|
| 13 | + |
|
| 14 | +# Editor directories and files |
|
| 15 | +.vscode/* |
|
| 16 | +!.vscode/extensions.json |
|
| 17 | +.idea |
|
| 18 | +.DS_Store |
|
| 19 | +*.suo |
|
| 20 | +*.ntvs* |
|
| 21 | +*.njsproj |
|
| 22 | +*.sln |
|
| 23 | +*.sw? |
projects/hot-tub/src/web/frontend/.oxlintrc.json
| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | +{ |
|
| 2 | + "$schema": "./node_modules/oxlint/configuration_schema.json", |
|
| 3 | + "plugins": ["react", "typescript", "oxc"], |
|
| 4 | + "rules": { |
|
| 5 | + "react/rules-of-hooks": "error", |
|
| 6 | + "react/only-export-components": ["warn", { "allowConstantExport": true }] |
|
| 7 | + } |
|
| 8 | +} |
projects/hot-tub/src/web/frontend/README.md
| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | +# React + TypeScript + Vite |
|
| 2 | + |
|
| 3 | +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. |
|
| 4 | + |
|
| 5 | +Currently, two official plugins are available: |
|
| 6 | + |
|
| 7 | +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) |
|
| 8 | +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) |
|
| 9 | + |
|
| 10 | +## React Compiler |
|
| 11 | + |
|
| 12 | +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). |
|
| 13 | + |
|
| 14 | +## Expanding the Oxlint configuration |
|
| 15 | + |
|
| 16 | +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: |
|
| 17 | + |
|
| 18 | +```json |
|
| 19 | +{ |
|
| 20 | + "$schema": "./node_modules/oxlint/configuration_schema.json", |
|
| 21 | + "plugins": ["react", "typescript", "oxc"], |
|
| 22 | + "options": { |
|
| 23 | + "typeAware": true |
|
| 24 | + }, |
|
| 25 | + "rules": { |
|
| 26 | + "react/rules-of-hooks": "error", |
|
| 27 | + "react/only-export-components": ["warn", { "allowConstantExport": true }] |
|
| 28 | + } |
|
| 29 | +} |
|
| 30 | +``` |
|
| 31 | + |
|
| 32 | +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. |
projects/hot-tub/src/web/frontend/dist/assets/index-BUdY2Uw4.js
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t,t.exports),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.for(`react.view_transition`),m=Symbol.iterator;function h(e){return typeof e!=`object`||!e?null:(e=m&&e[m]||e[`@@iterator`],typeof e==`function`?e:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,v={};function y(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}y.prototype.isReactComponent={},y.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function b(){}b.prototype=y.prototype;function ee(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}var te=ee.prototype=new b;te.constructor=ee,_(te,y.prototype),te.isPureReactComponent=!0;var x=Array.isArray;function ne(){}var S={H:null,A:null,T:null,S:null},re=Object.prototype.hasOwnProperty;function C(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ie(e,t){return C(e.type,t,e.props)}function ae(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function w(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var T=/\/+/g;function oe(e,t){return typeof e==`object`&&e&&e.key!=null?w(``+e.key):t.toString(36)}function se(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(ne,ne):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ce(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ce(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+oe(e,0):a,x(o)?(i=``,c!=null&&(i=c.replace(T,`$&/`)+`/`),ce(o,r,i,``,function(e){return e})):o!=null&&(ae(o)&&(o=ie(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(T,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(x(e))for(var u=0;u<e.length;u++)a=e[u],s=l+oe(a,u),c+=ce(a,r,i,s,o);else if(u=h(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+oe(a,u++),c+=ce(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ce(se(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function E(e,t,n){if(e==null)return e;var r=[],i=0;return ce(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function le(e){if(e._status===-1){var t=e._result,n=t();n.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t,n.status===void 0&&(n.status=`fulfilled`,n.value=t))},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t,n.status===void 0&&(n.status=`rejected`,n.reason=t))}),e._status===-1&&(e._status=0,e._result=n)}if(e._status===1)return e._result.default;throw e._result}var ue=typeof reportError==`function`?reportError:function(e){if(typeof window==`object`&&typeof window.ErrorEvent==`function`){var t=new window.ErrorEvent(`error`,{bubbles:!0,cancelable:!0,message:typeof e==`object`&&e&&typeof e.message==`string`?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process==`object`&&typeof process.emit==`function`){process.emit(`uncaughtException`,e);return}console.error(e)};function de(e){var t=S.T,n={};n.types=t===null?null:t.types,S.T=n;try{var r=e(),i=S.S;i!==null&&i(n,r),typeof r==`object`&&r&&typeof r.then==`function`&&r.then(ne,ue)}catch(e){ue(e)}finally{t!==null&&n.types!==null&&(t.types=n.types),S.T=t}}function fe(e){var t=S.T;if(t!==null){var n=t.types;n===null?t.types=[e]:n.indexOf(e)===-1&&n.push(e)}else de(fe.bind(null,e))}var pe={map:E,forEach:function(e,t,n){E(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return E(e,function(){t++}),t},toArray:function(e){return E(e,function(e){return e})||[]},only:function(e){if(!ae(e))throw Error(`React.Children.only expected to receive a single React element child.`);return e}};e.Activity=f,e.Children=pe,e.Component=y,e.Fragment=r,e.Profiler=a,e.PureComponent=ee,e.StrictMode=i,e.Suspense=l,e.ViewTransition=p,e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=S,e.__COMPILER_RUNTIME={__proto__:null,c:function(e){return S.H.useMemoCache(e)}},e.addTransitionType=fe,e.cache=function(e){return function(){return e.apply(null,arguments)}},e.cacheSignal=function(){return null},e.cloneElement=function(e,t,n){if(e==null)throw Error(`The argument must be a React element, but you passed `+e+`.`);var r=_({},e.props),i=e.key;if(t!=null)for(a in t.key!==void 0&&(i=``+t.key),t)!re.call(t,a)||a===`key`||a===`__self`||a===`__source`||a===`ref`&&t.ref===void 0||(r[a]=t[a]);var a=arguments.length-2;if(a===1)r.children=n;else if(1<a){for(var o=Array(a),s=0;s<a;s++)o[s]=arguments[s+2];r.children=o}return C(e.type,i,r)},e.createContext=function(e){return e={$$typeof:s,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null},e.Provider=e,e.Consumer={$$typeof:o,_context:e},e},e.createElement=function(e,t,n){var r,i={},a=null;if(t!=null)for(r in t.key!==void 0&&(a=``+t.key),t)re.call(t,r)&&r!==`key`&&r!==`__self`&&r!==`__source`&&(i[r]=t[r]);var o=arguments.length-2;if(o===1)i.children=n;else if(1<o){for(var s=Array(o),c=0;c<o;c++)s[c]=arguments[c+2];i.children=s}if(e&&e.defaultProps)for(r in o=e.defaultProps,o)i[r]===void 0&&(i[r]=o[r]);return C(e,a,i)},e.createRef=function(){return{current:null}},e.forwardRef=function(e){return{$$typeof:c,render:e}},e.isValidElement=ae,e.lazy=function(e){return{$$typeof:d,_payload:{_status:-1,_result:e},_init:le}},e.memo=function(e,t){return{$$typeof:u,type:e,compare:t===void 0?null:t}},e.startTransition=de,e.unstable_useCacheRefresh=function(){return S.H.useCacheRefresh()},e.use=function(e){return S.H.use(e)},e.useActionState=function(e,t,n){return S.H.useActionState(e,t,n)},e.useCallback=function(e,t){return S.H.useCallback(e,t)},e.useContext=function(e){return S.H.useContext(e)},e.useDebugValue=function(){},e.useDeferredValue=function(e,t){return S.H.useDeferredValue(e,t)},e.useEffect=function(e,t){return S.H.useEffect(e,t)},e.useEffectEvent=function(e){return S.H.useEffectEvent(e)},e.useId=function(){return S.H.useId()},e.useImperativeHandle=function(e,t,n){return S.H.useImperativeHandle(e,t,n)},e.useInsertionEffect=function(e,t){return S.H.useInsertionEffect(e,t)},e.useLayoutEffect=function(e,t){return S.H.useLayoutEffect(e,t)},e.useMemo=function(e,t){return S.H.useMemo(e,t)},e.useOptimistic=function(e,t){return S.H.useOptimistic(e,t)},e.useReducer=function(e,t,n){return S.H.useReducer(e,t,n)},e.useRef=function(e){return S.H.useRef(e)},e.useState=function(e){return S.H.useState(e)},e.useSyncExternalStore=function(e,t,n){return S.H.useSyncExternalStore(e,t,n)},e.useTransition=function(){return S.H.useTransition()},e.version=`19.3.1`})),u=o(((e,t)=>{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0<n;){var r=n-1>>>1,a=e[r];if(0<i(a,t))e[r]=t,e[n]=a,n=r;else break a}}function n(e){return e.length===0?null:e[0]}function r(e){if(e.length===0)return null;var t=e[0],n=e.pop();if(n!==t){e[0]=n;a:for(var r=0,a=e.length,o=a>>>1;r<o;){var s=2*(r+1)-1,c=e[s],l=s+1,u=e[l];if(0>i(c,n))l<a&&0>i(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(l<a&&0>i(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function ee(e){if(h=!1,b(e),!m){if(n(c)!==null)m=!0,te||(te=!0,ie());else{var t=n(l);t!==null&&T(ee,t.startTime-e)}}}var te=!1,x=-1,ne=5,S=-1;function re(){return g?!0:!(e.unstable_now()-S<ne)}function C(){if(g=!1,te){var t=e.unstable_now();S=t;var i=!0;try{a:{m=!1,h&&(h=!1,v(x),x=-1),p=!0;var a=f;try{b:{for(b(t),d=n(c);d!==null&&!(d.expirationTime>t&&re());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&T(ee,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ie():te=!1}}}var ie;if(typeof y==`function`)ie=function(){y(C)};else if(typeof MessageChannel<`u`){var ae=new MessageChannel,w=ae.port2;ae.port1.onmessage=C,ie=function(){w.postMessage(null)}}else ie=function(){_(C,0)};function T(t,n){x=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125<e?console.error(`forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported`):ne=0<e?Math.floor(1e3/e):5},e.unstable_getCurrentPriorityLevel=function(){return f},e.unstable_next=function(e){switch(f){case 1:case 2:case 3:var t=3;break;default:t=f}var n=f;f=t;try{return e()}finally{f=n}},e.unstable_requestPaint=function(){g=!0},e.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=f;f=e;try{return t()}finally{f=n}},e.unstable_scheduleCallback=function(r,i,a){var o=e.unstable_now();switch(typeof a==`object`&&a?(a=a.delay,a=typeof a==`number`&&0<a?o+a:o):a=o,r){case 1:var s=-1;break;case 2:s=250;break;case 5:s=1073741823;break;case 4:s=1e4;break;default:s=5e3}return s=a+s,r={id:u++,callback:i,priorityLevel:r,startTime:a,expirationTime:s,sortIndex:-1},a>o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(x),x=-1):h=!0,T(ee,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,te||(te=!0,ie()))),r},e.unstable_shouldYield=re,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1<arguments.length){t+=`?args[]=`+encodeURIComponent(arguments[1]);for(var n=2;n<arguments.length;n++)t+=`&args[]=`+encodeURIComponent(arguments[n])}return`Minified React error #`+e+`; visit `+t+` for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`}function r(){}var i={d:{f:r,r:function(){throw Error(n(522))},D:r,C:r,L:r,m:r,X:r,S:r,M:r},p:0,findDOMNode:null},a=Symbol.for(`react.portal`),o=Symbol.for(`react.recoverable`),s=Symbol.for(`react.optimistic_key`);function c(e,t,n){var r=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:a,key:r==null?null:r===s?s:``+r,children:e,containerInfo:t,implementation:n}}var l=t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(e,t){if(e===`font`)return``;if(typeof t==`string`)return t===`use-credentials`?t:``}e.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=i,e.browser=function(e){return{$$typeof:o,_reason:e}},e.createPortal=function(e,t){var r=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!t||t.nodeType!==1&&t.nodeType!==9&&t.nodeType!==11)throw Error(n(299));return c(e,t,null,r)},e.flushSync=function(e){var t=l.T,n=i.p;try{if(l.T=null,i.p=2,e)return e()}finally{l.T=t,i.p=n,i.d.f()}},e.preconnect=function(e,t){typeof e==`string`&&(t?(t=t.crossOrigin,t=typeof t==`string`?t===`use-credentials`?t:``:void 0):t=null,i.d.C(e,t))},e.prefetchDNS=function(e){typeof e==`string`&&i.d.D(e)},e.preinit=function(e,t){if(typeof e==`string`&&t&&typeof t.as==`string`){var n=t.as,r=d(n,t.crossOrigin),a=typeof t.integrity==`string`?t.integrity:void 0,o=typeof t.fetchPriority==`string`?t.fetchPriority:void 0;n===`style`?i.d.S(e,typeof t.precedence==`string`?t.precedence:void 0,{crossOrigin:r,integrity:a,fetchPriority:o}):n===`script`&&i.d.X(e,{crossOrigin:r,integrity:a,fetchPriority:o,nonce:typeof t.nonce==`string`?t.nonce:void 0})}},e.preinitModule=function(e,t){if(typeof e==`string`){if(typeof t==`object`&&t){if(t.as==null||t.as===`script`){var n=d(t.as,t.crossOrigin);i.d.M(e,{crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0,fetchPriority:typeof t.fetchPriority==`string`?t.fetchPriority:void 0})}}else t??i.d.M(e)}},e.preload=function(e,t){if(typeof e==`string`&&typeof t==`object`&&t&&typeof t.as==`string`){var n=t.as,r=d(n,t.crossOrigin);i.d.L(e,n,{crossOrigin:r,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0,type:typeof t.type==`string`?t.type:void 0,fetchPriority:typeof t.fetchPriority==`string`?t.fetchPriority:void 0,referrerPolicy:typeof t.referrerPolicy==`string`?t.referrerPolicy:void 0,imageSrcSet:typeof t.imageSrcSet==`string`?t.imageSrcSet:void 0,imageSizes:typeof t.imageSizes==`string`?t.imageSizes:void 0,media:typeof t.media==`string`?t.media:void 0})}},e.preloadModule=function(e,t){if(typeof e==`string`){if(t){var n=d(t.as,t.crossOrigin);i.d.m(e,{as:typeof t.as==`string`&&t.as!==`script`?t.as:void 0,crossOrigin:n,integrity:typeof t.integrity==`string`?t.integrity:void 0,nonce:typeof t.nonce==`string`?t.nonce:void 0,fetchPriority:typeof t.fetchPriority==`string`?t.fetchPriority:void 0})}else i.d.m(e)}},e.requestFormReset=function(e){i.d.r(e)},e.unstable_batchedUpdates=function(e,t){return e(t)},e.useFormState=function(e,t,n){return l.H.useFormState(e,t,n)},e.useFormStatus=function(){return l.H.useHostTransitionStatus()},e.version=`19.3.1`})),m=o(((e,t)=>{function n(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE==`function`)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1<arguments.length){t+=`?args[]=`+encodeURIComponent(arguments[1]);for(var n=2;n<arguments.length;n++)t+=`&args[]=`+encodeURIComponent(arguments[n])}return`Minified React error #`+e+`; visit `+t+` for the full message or use the non-minified dev environment for full errors and additional helpful warnings.`}function a(e){return!(!e||e.nodeType!==1&&e.nodeType!==9&&e.nodeType!==11)}function o(e){for(var t=e,n=t;n&&!n.alternate;)t=n,t.flags&4098&&(e=t.return),n=t.return;for(;t.return;)t=t.return;return t.tag===3?e:null}function s(e){if(e.tag===13){var t=e.memoizedState;if(t===null&&(e=e.alternate,e!==null&&(t=e.memoizedState)),t!==null)return t.dehydrated}return null}function c(e){if(e.tag===31){var t=e.memoizedState;if(t===null&&(e=e.alternate,e!==null&&(t=e.memoizedState)),t!==null)return t.dehydrated}return null}function l(e){if(o(e)!==e)throw Error(i(188))}function d(e){var t=e.alternate;if(!t){if(t=o(e),t===null)throw Error(i(188));return t===e?e:null}for(var n=e,r=t;;){var a=n.return;if(a===null)break;var s=a.alternate;if(s===null){if(r=a.return,r!==null){n=r;continue}break}if(a.child===s.child){for(s=a.child;s;){if(s===n)return l(a),e;if(s===r)return l(a),t;s=s.sibling}throw Error(i(188))}if(n.return!==r.return)n=a,r=s;else{for(var c=!1,u=a.child;u;){if(u===n){c=!0,n=a,r=s;break}if(u===r){c=!0,r=a,n=s;break}u=u.sibling}if(!c){for(u=s.child;u;){if(u===n){c=!0,n=s,r=a;break}if(u===r){c=!0,r=s,n=a;break}u=u.sibling}if(!c)throw Error(i(189))}}if(n.alternate!==r)throw Error(i(190))}if(n.tag!==3)throw Error(i(188));return n.stateNode.current===n?e:t}function p(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e;for(e=e.child;e!==null;){if(t=p(e),t!==null)return t;e=e.sibling}return null}function h(e,t,n,r,i,a){for(;e!==null;){if((e.tag===5||e.tag===27||e.tag===6)&&n(e,r,i,a)||(e.tag!==22||e.memoizedState===null)&&(t||e.tag!==5&&e.tag!==27)&&h(e.child,t,n,r,i,a))return!0;e=e.sibling}return!1}function g(e){for(e=e.return;e!==null;){if(e.tag===3||e.tag===5||e.tag===27)return e;e=e.return}return null}function _(e){var t=!1;for(e=e.return;e!==null&&(e.tag===4&&(t=!0),e.tag!==3&&e.tag!==5&&e.tag!==27);)e=e.return;return t}function v(e){var t=[null,null],n=g(e);return n===null||y(t,e,n.child,{foundSelf:!1}),t}function y(e,t,n,r){for(;n!==null;){if(n===t)r.foundSelf=!0;else if(n.tag===5||n.tag===27||n.tag===6){if(r.foundSelf)return e[1]=n,!0;e[0]=n}else if((n.tag!==22||n.memoizedState===null)&&y(e,t,n.child,r))return!0;n=n.sibling}return!1}function b(e){switch(e.tag){case 5:case 27:case 6:return e.stateNode;case 3:return e.stateNode.containerInfo;default:throw Error(i(559))}}var ee=null,te=null;function x(e,t,n){return e===n||e===t&&(ee=e,!0)}function ne(e,t,n){return e===n?(te=e,!1):e===t&&(te!==null&&(ee=e),!0)}function S(e){if(e===null)return null;do e=e===null?null:e.return;while(e&&e.tag!==5&&e.tag!==27&&e.tag!==3);return e||null}function re(e,t,n){for(var r=0,i=e;i;i=n(i))r++;i=0;for(var a=t;a;a=n(a))i++;for(;0<r-i;)e=n(e),r--;for(;0<i-r;)t=n(t),i--;for(;r--;){if(e===t||t!==null&&e===t.alternate)return e;e=n(e),t=n(t)}return null}var C=Object.assign,ie=Symbol.for(`react.element`),ae=Symbol.for(`react.transitional.element`),w=Symbol.for(`react.portal`),T=Symbol.for(`react.fragment`),oe=Symbol.for(`react.strict_mode`),se=Symbol.for(`react.profiler`),ce=Symbol.for(`react.consumer`),E=Symbol.for(`react.context`),le=Symbol.for(`react.forward_ref`),ue=Symbol.for(`react.suspense`),de=Symbol.for(`react.suspense_list`),fe=Symbol.for(`react.memo`),pe=Symbol.for(`react.lazy`),me=Symbol.for(`react.activity`),he=Symbol.for(`react.legacy_hidden`),ge=Symbol.for(`react.memo_cache_sentinel`),_e=Symbol.for(`react.view_transition`),ve=Symbol.for(`react.recoverable`),ye=Symbol.iterator;function be(e){return typeof e!=`object`||!e?null:(e=ye&&e[ye]||e[`@@iterator`],typeof e==`function`?e:null)}var xe=Symbol.for(`react.client.reference`);function Se(e){if(e==null)return null;if(typeof e==`function`)return e.$$typeof===xe?null:e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case T:return`Fragment`;case se:return`Profiler`;case oe:return`StrictMode`;case ue:return`Suspense`;case de:return`SuspenseList`;case me:return`Activity`;case _e:return`ViewTransition`}if(typeof e==`object`)switch(e.$$typeof){case w:return`Portal`;case E:return e.displayName||`Context`;case ce:return(e._context.displayName||`Context`)+`.Consumer`;case le:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case fe:return t=e.displayName||null,t===null?Se(e.type)||`Memo`:t;case pe:t=e._payload,e=e._init;try{return Se(e(t))}catch{}}return null}var Ce=Array.isArray,D=n.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,O=r.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,we={pending:!1,data:null,method:null,action:null},Te=[],Ee=-1;function De(e){return{current:e}}function Oe(e){0>Ee||(e.current=Te[Ee],Te[Ee]=null,Ee--)}function k(e,t){Ee++,Te[Ee]=e.current,e.current=t}var ke=De(null),Ae=De(null),je=De(null),Me=De(null);function Ne(e,t){switch(k(je,t),k(Ae,e),k(ke,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?up(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=up(t),e=dp(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}Oe(ke),k(ke,e)}function Pe(){Oe(ke),Oe(Ae),Oe(je)}function Fe(e){var t=e.memoizedState;t!==null&&(sh._currentValue=t.memoizedState,k(Me,e)),t=ke.current;var n=dp(t,e.type);t!==n&&(k(Ae,e),k(ke,n))}function Ie(e){Ae.current===e&&(Oe(ke),Oe(Ae)),Me.current===e&&(Oe(Me),sh._currentValue=we)}var Le,Re;function ze(e){if(Le===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Le=t&&t[1]||``,Re=-1<e.stack.indexOf(` |
|
| 2 | + at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return` |
|
| 3 | +`+Le+e+Re}var Be=!1;function Ve(e,t){if(!e||Be)return``;Be=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,"props",{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}n=!1;try{var i=Object.getOwnPropertyDescriptor(e.prototype,`props`);Object.defineProperty(e.prototype,"props",{configurable:!0,set:function(){throw Error()}}),n=!0,new e}finally{n&&(i===void 0?delete e.prototype.props:Object.defineProperty(e.prototype,"props",i))}}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(` |
|
| 4 | +`),l=s.split(` |
|
| 5 | +`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=` |
|
| 6 | +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Be=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ze(n):``}function He(e,t){switch(e.tag){case 26:case 27:case 5:return ze(e.type);case 16:return ze(`Lazy`);case 13:return e.child!==t&&t!==null?ze(`Suspense Fallback`):ze(`Suspense`);case 19:return ze(`SuspenseList`);case 0:case 15:return Ve(e.type,!1);case 11:return Ve(e.type.render,!1);case 1:return Ve(e.type,!0);case 31:return ze(`Activity`);case 30:return ze(`ViewTransition`);default:return``}}function Ue(e){try{var t=``,n=null;do t+=He(e,n),n=e,e=e.return;while(e);return t}catch(e){return` |
|
| 7 | +Error generating stack: `+e.message+` |
|
| 8 | +`+e.stack}}var We=Object.prototype.hasOwnProperty,Ge=t.unstable_scheduleCallback,Ke=t.unstable_cancelCallback,qe=t.unstable_shouldYield,Je=t.unstable_requestPaint,Ye=t.unstable_now,Xe=t.unstable_getCurrentPriorityLevel,Ze=t.unstable_ImmediatePriority,Qe=t.unstable_UserBlockingPriority,$e=t.unstable_NormalPriority,et=t.unstable_LowPriority,tt=t.unstable_IdlePriority,nt=t.log,rt=t.unstable_setDisableYieldValue,it=null,at=null;function ot(e){if(typeof nt==`function`&&rt(e),at&&typeof at.setStrictMode==`function`)try{at.setStrictMode(it,e)}catch{}}var st=Math.clz32?Math.clz32:ut,ct=Math.log,lt=Math.LN2;function ut(e){return e>>>=0,e===0?32:31-(ct(e)/lt|0)|0}var dt=256,ft=262144,pt=4194304;function mt(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&-e;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function ht(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=mt(n))):i=mt(o):i=mt(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=mt(n))):i=mt(o)):i=mt(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function gt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function _t(e,t){t&8&&(t|=t&32);var n=e.entangledLanes;if(n!==0)for(e=e.entanglements,n&=t;0<n;){var r=31-st(n),i=1<<r;t|=e[r],n&=~i}return t}function vt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function yt(){var e=pt;return pt<<=1,!(pt&62914560)&&(pt=4194304),e}function bt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function xt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function St(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0<n;){var u=31-st(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&Ct(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function Ct(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-st(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function wt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-st(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function Tt(e,t){var n=t&-t;return n=n&42?1:Et(n),(n&(e.suspendedLanes|t))===0?n:0}function Et(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Dt(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function Ot(){var e=O.p;return e===0?(e=window.event,e===void 0?32:Ch(e.type)):e}function kt(e,t){var n=O.p;try{return O.p=e,t()}finally{O.p=n}}var At=Math.random().toString(36).slice(2),jt=`__reactFiber$`+At,Mt=`__reactProps$`+At,Nt=`__reactContainer$`+At,Pt=`__reactEvents$`+At,Ft=`__reactListeners$`+At,It=`__reactHandles$`+At,Lt=`__reactResources$`+At,Rt=`__reactMarker$`+At,zt=`__reactLoad$`+At;function Bt(e){delete e[jt],delete e[Mt],delete e[Ft],delete e[It]}function Vt(e){var t;if(t=e[jt])return t;for(var n=e.parentNode;n;){if(t=n[Nt]||n[jt]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=fm(e);e!==null;){if(n=e[jt])return n;e=fm(e)}return t}e=n,n=e.parentNode}return null}function Ht(e){if(e=e[jt]||e[Nt]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function Ut(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function Wt(e){var t=e[Lt];return t||=e[Lt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function A(e){e[Rt]=!0}function Gt(e){e[zt]=void 0}var Kt=new Set,qt={};function Jt(e,t){Yt(e,t),Yt(e+`Capture`,t)}function Yt(e,t){for(qt[e]=t,e=0;e<t.length;e++)Kt.add(t[e])}var Xt=RegExp(`^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$`),Zt={},Qt={};function $t(e){return We.call(Qt,e)?!0:We.call(Zt,e)?!1:Xt.test(e)?Qt[e]=!0:(Zt[e]=!0,!1)}var j=!1;function en(){var e=j;return j=!1,e}function tn(e,t,n){if($t(t)){if(n===null)e.removeAttribute(t);else{switch(typeof n){case`undefined`:case`function`:case`symbol`:e.removeAttribute(t);return;case`boolean`:var r=t.toLowerCase().slice(0,5);if(r!==`data-`&&r!==`aria-`){e.removeAttribute(t);return}}e.setAttribute(t,n)}}}function nn(e,t,n){if(n===null)e.removeAttribute(t);else{switch(typeof n){case`undefined`:case`function`:case`symbol`:case`boolean`:e.removeAttribute(t);return}e.setAttribute(t,n)}}function rn(e,t,n,r){if(r===null)e.removeAttribute(n);else{switch(typeof r){case`undefined`:case`function`:case`symbol`:case`boolean`:e.removeAttribute(n);return}e.setAttributeNS(t,n,r)}}function an(e){switch(typeof e){case`bigint`:case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function on(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function sn(e,t,n){var r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t);if(!e.hasOwnProperty(t)&&r!==void 0&&typeof r.get==`function`&&typeof r.set==`function`){var i=r.get,a=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){n=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(e){n=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function cn(e){if(!e._valueTracker){var t=on(e)?`checked`:`value`;e._valueTracker=sn(e,t,``+e[t])}}function ln(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=on(e)?e.checked?`true`:`false`:e.value),e=r,e!==n&&(t.setValue(e),!0)}var un=/[\n"\\]/g;function dn(e){return e.replace(un,function(e){return`\\`+e.charCodeAt(0).toString(16)+` `})}function fn(e,t,n,r,i,a,o,s){e.name=``,o!=null&&typeof o!=`function`&&typeof o!=`symbol`&&typeof o!=`boolean`?e.type=o:e.removeAttribute(`type`),t==null?o!==`submit`&&o!==`reset`||e.removeAttribute(`value`):o===`number`?(t===0&&e.value===``||e.value!=t)&&(e.value=``+an(t)):e.value!==``+an(t)&&(e.value=``+an(t)),t==null?n==null?r!=null&&e.removeAttribute(`value`):mn(e,an(n)):o===`number`&&e.value==t?mn(e,an(e.value)):mn(e,an(t)),i==null&&a!=null&&(e.defaultChecked=!!a),i!=null&&(e.checked=i&&typeof i!=`function`&&typeof i!=`symbol`),s!=null&&typeof s!=`function`&&typeof s!=`symbol`&&typeof s!=`boolean`?e.name=``+an(s):e.removeAttribute(`name`)}function pn(e,t,n,r,i,a,o,s){if(a!=null&&typeof a!=`function`&&typeof a!=`symbol`&&typeof a!=`boolean`&&(e.type=a),t!=null||n!=null){if(!(a!==`submit`&&a!==`reset`||t!=null)){cn(e);return}n=n==null?``:``+an(n),t=t==null?n:``+an(t),s||t===e.value||(e.value=t),e.defaultValue=t}r??=i,r=typeof r!=`function`&&typeof r!=`symbol`&&!!r,e.checked=s?e.checked:!!r,e.defaultChecked=!!r,o!=null&&typeof o!=`function`&&typeof o!=`symbol`&&typeof o!=`boolean`&&(e.name=o),cn(e)}function mn(e,t){e.defaultValue!==``+t&&(e.defaultValue=``+t)}function hn(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i<n.length;i++)t[`$`+n[i]]=!0;for(n=0;n<e.length;n++)i=t.hasOwnProperty(`$`+e[n].value),e[n].selected!==i&&(e[n].selected=i),i&&r&&(e[n].defaultSelected=!0)}else{for(n=``+an(n),t=null,i=0;i<e.length;i++){if(e[i].value===n){e[i].selected=!0,r&&(e[i].defaultSelected=!0);return}t!==null||e[i].disabled||(t=e[i])}t!==null&&(t.selected=!0)}}function gn(e,t,n){t!=null&&(t=``+an(t),t!==e.value&&(e.value=t),n==null)?e.defaultValue!==t&&(e.defaultValue=t):e.defaultValue=n==null?``:``+an(n)}function _n(e,t,n,r){if(t==null){if(r!=null){if(n!=null)throw Error(i(92));if(Ce(r)){if(1<r.length)throw Error(i(93));r=r[0]}n=r}n??=``,t=n}n=an(t),e.defaultValue=n,r=e.textContent,r===n&&r!==``&&r!==null&&(e.value=r),cn(e)}function vn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var yn=new Set(`animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp`.split(` `));function bn(e,t,n){var r=t.indexOf(`--`)===0;n==null||typeof n==`boolean`||n===``?r?e.setProperty(t,``):t===`float`?e.cssFloat=``:e[t]=``:r?e.setProperty(t,n):typeof n!=`number`||n===0||yn.has(t)?t===`float`?e.cssFloat=n:e[t]=(``+n).trim():e[t]=n+`px`}function xn(e,t,n){if(t!=null&&typeof t!=`object`)throw Error(i(62));if(e=e.style,n!=null){for(var r in n)!n.hasOwnProperty(r)||t!=null&&t.hasOwnProperty(r)||(r.indexOf(`--`)===0?e.setProperty(r,``):r===`float`?e.cssFloat=``:e[r]=``,j=!0);for(var a in t)r=t[a],t.hasOwnProperty(a)&&n[a]!==r&&(bn(e,a,r),j=!0)}else for(var o in t)t.hasOwnProperty(o)&&bn(e,o,t[o])}function Sn(e){if(e.indexOf(`-`)===-1)return!1;switch(e){case`annotation-xml`:case`color-profile`:case`font-face`:case`font-face-src`:case`font-face-uri`:case`font-face-format`:case`font-face-name`:case`missing-glyph`:return!1;default:return!0}}var Cn=new Map([[`acceptCharset`,`accept-charset`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`],[`crossOrigin`,`crossorigin`],[`accentHeight`,`accent-height`],[`alignmentBaseline`,`alignment-baseline`],[`arabicForm`,`arabic-form`],[`baselineShift`,`baseline-shift`],[`capHeight`,`cap-height`],[`clipPath`,`clip-path`],[`clipRule`,`clip-rule`],[`colorInterpolation`,`color-interpolation`],[`colorInterpolationFilters`,`color-interpolation-filters`],[`colorProfile`,`color-profile`],[`colorRendering`,`color-rendering`],[`dominantBaseline`,`dominant-baseline`],[`enableBackground`,`enable-background`],[`fillOpacity`,`fill-opacity`],[`fillRule`,`fill-rule`],[`floodColor`,`flood-color`],[`floodOpacity`,`flood-opacity`],[`fontFamily`,`font-family`],[`fontSize`,`font-size`],[`fontSizeAdjust`,`font-size-adjust`],[`fontStretch`,`font-stretch`],[`fontStyle`,`font-style`],[`fontVariant`,`font-variant`],[`fontWeight`,`font-weight`],[`glyphName`,`glyph-name`],[`glyphOrientationHorizontal`,`glyph-orientation-horizontal`],[`glyphOrientationVertical`,`glyph-orientation-vertical`],[`horizAdvX`,`horiz-adv-x`],[`horizOriginX`,`horiz-origin-x`],[`imageRendering`,`image-rendering`],[`letterSpacing`,`letter-spacing`],[`lightingColor`,`lighting-color`],[`markerEnd`,`marker-end`],[`markerMid`,`marker-mid`],[`markerStart`,`marker-start`],[`maskType`,`mask-type`],[`overlinePosition`,`overline-position`],[`overlineThickness`,`overline-thickness`],[`paintOrder`,`paint-order`],[`panose-1`,`panose-1`],[`pointerEvents`,`pointer-events`],[`renderingIntent`,`rendering-intent`],[`shapeRendering`,`shape-rendering`],[`stopColor`,`stop-color`],[`stopOpacity`,`stop-opacity`],[`strikethroughPosition`,`strikethrough-position`],[`strikethroughThickness`,`strikethrough-thickness`],[`strokeDasharray`,`stroke-dasharray`],[`strokeDashoffset`,`stroke-dashoffset`],[`strokeLinecap`,`stroke-linecap`],[`strokeLinejoin`,`stroke-linejoin`],[`strokeMiterlimit`,`stroke-miterlimit`],[`strokeOpacity`,`stroke-opacity`],[`strokeWidth`,`stroke-width`],[`textAnchor`,`text-anchor`],[`textDecoration`,`text-decoration`],[`textRendering`,`text-rendering`],[`transformOrigin`,`transform-origin`],[`underlinePosition`,`underline-position`],[`underlineThickness`,`underline-thickness`],[`unicodeBidi`,`unicode-bidi`],[`unicodeRange`,`unicode-range`],[`unitsPerEm`,`units-per-em`],[`vAlphabetic`,`v-alphabetic`],[`vHanging`,`v-hanging`],[`vIdeographic`,`v-ideographic`],[`vMathematical`,`v-mathematical`],[`vectorEffect`,`vector-effect`],[`vertAdvY`,`vert-adv-y`],[`vertOriginX`,`vert-origin-x`],[`vertOriginY`,`vert-origin-y`],[`wordSpacing`,`word-spacing`],[`writingMode`,`writing-mode`],[`xmlnsXlink`,`xmlns:xlink`],[`xHeight`,`x-height`]]),wn=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;function Tn(e){return wn.test(``+e)?`javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')`:e}function En(){}var Dn=null;function On(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var kn=null,An=null;function jn(e){var t=Ht(e);if(t&&(e=t.stateNode)){var n=e[Mt]||null;a:switch(e=t.stateNode,t.type){case`input`:if(fn(e,n.value,n.defaultValue,n.defaultValue,n.checked,n.defaultChecked,n.type,n.name),t=n.name,n.type===`radio`&&t!=null){for(n=e;n.parentNode;)n=n.parentNode;for(n=n.querySelectorAll(`input[name="`+dn(``+t)+`"][type="radio"]`),t=0;t<n.length;t++){var r=n[t];if(r!==e&&r.form===e.form){var a=r[Mt]||null;if(!a)throw Error(i(90));fn(r,a.value,a.defaultValue,a.defaultValue,a.checked,a.defaultChecked,a.type,a.name)}}for(t=0;t<n.length;t++)r=n[t],r.form===e.form&&ln(r)}break a;case`textarea`:gn(e,n.value,n.defaultValue);break a;case`select`:t=n.value,t!=null&&hn(e,!!n.multiple,t,!1)}}}var Mn=!1;function Nn(e,t,n){if(Mn)return e(t,n);Mn=!0;try{return e(t)}finally{if(Mn=!1,(kn!==null||An!==null)&&(zd(),kn&&(t=kn,e=An,An=kn=null,jn(t),e)))for(t=0;t<e.length;t++)jn(e[t])}}function Pn(e,t){var n=e.stateNode;if(n===null)return null;var r=n[Mt]||null;if(r===null)return null;n=r[t];a:switch(t){case`onClick`:case`onClickCapture`:case`onDoubleClick`:case`onDoubleClickCapture`:case`onMouseDown`:case`onMouseDownCapture`:case`onMouseMove`:case`onMouseMoveCapture`:case`onMouseUp`:case`onMouseUpCapture`:case`onMouseEnter`:(r=!r.disabled)||(e=e.type,r=e!==`button`&&e!==`input`&&e!==`select`&&e!==`textarea`),e=!r;break a;default:e=!1}if(e)return null;if(n&&typeof n!=`function`)throw Error(i(231,t,typeof n));return n}var Fn=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0,In=!1;if(Fn)try{var Ln={};Object.defineProperty(Ln,"passive",{get:function(){In=!0}}),window.addEventListener(`test`,Ln,Ln),window.removeEventListener(`test`,Ln,Ln)}catch{In=!1}var Rn=null,zn=null,Bn=null;function Vn(){if(Bn)return Bn;var e,t=zn,n=t.length,r,i=`value`in Rn?Rn.value:Rn.textContent,a=i.length;for(e=0;e<n&&t[e]===i[e];e++);var o=n-e;for(r=1;r<=o&&t[n-r]===i[a-r];r++);return Bn=i.slice(e,1<r?1-r:void 0)}function Hn(e){var t=e.keyCode;return`charCode`in e?(e=e.charCode,e===0&&t===13&&(e=13)):e=t,e===10&&(e=13),32<=e||e===13?e:0}function Un(){return!0}function Wn(){return!1}function Gn(e){function t(t,n,r,i,a){for(var o in this._reactName=t,this._targetInst=r,this.type=n,this.nativeEvent=i,this.target=a,this.currentTarget=null,e)e.hasOwnProperty(o)&&(t=e[o],this[o]=t?t(i):i[o]);return this.isDefaultPrevented=(i.defaultPrevented==null?!1===i.returnValue:i.defaultPrevented)?Un:Wn,this.isPropagationStopped=Wn,this}return C(t.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():typeof e.returnValue!=`unknown`&&(e.returnValue=!1),this.isDefaultPrevented=Un)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():typeof e.cancelBubble!=`unknown`&&(e.cancelBubble=!0),this.isPropagationStopped=Un)},persist:function(){},isPersistent:Un}),t}var Kn={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},qn=Gn(Kn),Jn=C({},Kn,{view:0,detail:0}),Yn=Gn(Jn),Xn,Zn,Qn,M=C({},Jn,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:lr,button:0,buttons:0,relatedTarget:function(e){return e.relatedTarget===void 0?e.fromElement===e.srcElement?e.toElement:e.fromElement:e.relatedTarget},movementX:function(e){return`movementX`in e?e.movementX:(e!==Qn&&(Qn&&e.type===`mousemove`?(Xn=e.screenX-Qn.screenX,Zn=e.screenY-Qn.screenY):Zn=Xn=0,Qn=e),Xn)},movementY:function(e){return`movementY`in e?e.movementY:Zn}}),$n=Gn(M),er=Gn(C({},M,{dataTransfer:0})),tr=Gn(C({},Jn,{relatedTarget:0})),nr=Gn(C({},Kn,{animationName:0,elapsedTime:0,pseudoElement:0})),rr=Gn(C({},Kn,{clipboardData:function(e){return`clipboardData`in e?e.clipboardData:window.clipboardData}})),ir=Gn(C({},Kn,{data:0})),ar={Esc:`Escape`,Spacebar:` `,Left:`ArrowLeft`,Up:`ArrowUp`,Right:`ArrowRight`,Down:`ArrowDown`,Del:`Delete`,Win:`OS`,Menu:`ContextMenu`,Apps:`ContextMenu`,Scroll:`ScrollLock`,MozPrintableKey:`Unidentified`},or={8:`Backspace`,9:`Tab`,12:`Clear`,13:`Enter`,16:`Shift`,17:`Control`,18:`Alt`,19:`Pause`,20:`CapsLock`,27:`Escape`,32:` `,33:`PageUp`,34:`PageDown`,35:`End`,36:`Home`,37:`ArrowLeft`,38:`ArrowUp`,39:`ArrowRight`,40:`ArrowDown`,45:`Insert`,46:`Delete`,112:`F1`,113:`F2`,114:`F3`,115:`F4`,116:`F5`,117:`F6`,118:`F7`,119:`F8`,120:`F9`,121:`F10`,122:`F11`,123:`F12`,144:`NumLock`,145:`ScrollLock`,224:`Meta`},sr={Alt:`altKey`,Control:`ctrlKey`,Meta:`metaKey`,Shift:`shiftKey`};function cr(e){var t=this.nativeEvent;return t.getModifierState?t.getModifierState(e):(e=sr[e])?!!t[e]:!1}function lr(){return cr}var ur=Gn(C({},Jn,{key:function(e){if(e.key){var t=ar[e.key]||e.key;if(t!==`Unidentified`)return t}return e.type===`keypress`?(e=Hn(e),e===13?`Enter`:String.fromCharCode(e)):e.type===`keydown`||e.type===`keyup`?or[e.keyCode]||`Unidentified`:``},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:lr,charCode:function(e){return e.type===`keypress`?Hn(e):0},keyCode:function(e){return e.type===`keydown`||e.type===`keyup`?e.keyCode:0},which:function(e){return e.type===`keypress`?Hn(e):e.type===`keydown`||e.type===`keyup`?e.keyCode:0}})),dr=Gn(C({},M,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0})),fr=Gn(C({},Kn,{submitter:0})),pr=Gn(C({},Jn,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:lr})),mr=Gn(C({},Kn,{propertyName:0,elapsedTime:0,pseudoElement:0})),hr=Gn(C({},M,{deltaX:function(e){return`deltaX`in e?e.deltaX:`wheelDeltaX`in e?-e.wheelDeltaX:0},deltaY:function(e){return`deltaY`in e?e.deltaY:`wheelDeltaY`in e?-e.wheelDeltaY:`wheelDelta`in e?-e.wheelDelta:0},deltaZ:0,deltaMode:0})),gr=Gn(C({},Kn,{newState:0,oldState:0,source:0})),_r=[9,13,27,32],vr=Fn&&`CompositionEvent`in window,yr=null;Fn&&`documentMode`in document&&(yr=document.documentMode);var br=Fn&&`TextEvent`in window&&!yr,xr=Fn&&(!vr||yr&&8<yr&&11>=yr),Sr=` `,Cr=!1;function wr(e,t){switch(e){case`keyup`:return _r.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Tr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Er=!1;function Dr(e,t){switch(e){case`compositionend`:return Tr(t);case`keypress`:return t.which===32?(Cr=!0,Sr):null;case`textInput`:return e=t.data,e===Sr&&Cr?null:e;default:return null}}function Or(e,t){if(Er)return e===`compositionend`||!vr&&wr(e,t)?(e=Vn(),Bn=zn=Rn=null,Er=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1<t.char.length)return t.char;if(t.which)return String.fromCharCode(t.which)}return null;case`compositionend`:return xr&&t.locale!==`ko`?null:t.data;default:return null}}var kr={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Ar(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t===`input`?!!kr[e.type]:t===`textarea`}function jr(e,t,n,r){kn?An?An.push(r):An=[r]:kn=r,t=Jf(t,`onChange`),0<t.length&&(n=new qn(`onChange`,`change`,null,n,r),e.push({event:n,listeners:t}))}var Mr=null,Nr=null;function Pr(e){Vf(e,0)}function Fr(e){if(ln(Ut(e)))return e}function Ir(e,t){if(e===`change`)return t}var Lr=!1;if(Fn){var Rr;if(Fn){var zr=`oninput`in document;if(!zr){var Br=document.createElement(`div`);Br.setAttribute(`oninput`,`return;`),zr=typeof Br.oninput==`function`}Rr=zr}else Rr=!1;Lr=Rr&&(!document.documentMode||9<document.documentMode)}function Vr(){Mr&&(Mr.detachEvent(`onpropertychange`,Hr),Nr=Mr=null)}function Hr(e){if(e.propertyName===`value`&&Fr(Nr)){var t=[];jr(t,Nr,e,On(e)),Nn(Pr,t)}}function Ur(e,t,n){e===`focusin`?(Vr(),Mr=t,Nr=n,Mr.attachEvent(`onpropertychange`,Hr)):e===`focusout`&&Vr()}function Wr(e){if(e===`selectionchange`||e===`keyup`||e===`keydown`)return Fr(Nr)}function Gr(e,t){if(e===`click`)return Fr(t)}function Kr(e,t){if(e===`input`||e===`change`)return Fr(t)}function qr(e,t){return e===t&&(e!==0||1/e==1/t)||e!==e&&t!==t}var Jr=typeof Object.is==`function`?Object.is:qr;function Yr(e,t){if(Jr(e,t))return!0;if(typeof e!=`object`||!e||typeof t!=`object`||!t)return!1;var n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(r=0;r<n.length;r++){var i=n[r];if(!We.call(t,i)||!Jr(e[i],t[i]))return!1}return!0}function Xr(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function Zr(e){for(;e&&e.firstChild;)e=e.firstChild;return e}function Qr(e,t){var n=Zr(e);e=0;for(var r;n;){if(n.nodeType===3){if(r=e+n.textContent.length,e<=t&&r>=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Zr(n)}}function $r(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?$r(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ei(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Xr(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Xr(e.document)}return t}function ti(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var ni=Fn&&`documentMode`in document&&11>=document.documentMode,ri=null,ii=null,ai=null,oi=!1;function si(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;oi||ri==null||ri!==Xr(r)||(r=ri,`selectionStart`in r&&ti(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),ai&&Yr(ai,r)||(ai=r,r=Jf(ii,`onSelect`),0<r.length&&(t=new qn(`onSelect`,`select`,null,t,n),e.push({event:t,listeners:r}),t.target=ri)))}function ci(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n[`Webkit`+e]=`webkit`+t,n[`Moz`+e]=`moz`+t,n}var li={animationend:ci(`Animation`,`AnimationEnd`),animationiteration:ci(`Animation`,`AnimationIteration`),animationstart:ci(`Animation`,`AnimationStart`),transitionrun:ci(`Transition`,`TransitionRun`),transitionstart:ci(`Transition`,`TransitionStart`),transitioncancel:ci(`Transition`,`TransitionCancel`),transitionend:ci(`Transition`,`TransitionEnd`)},ui={},di={};Fn&&(di=document.createElement(`div`).style,`AnimationEvent`in window||(delete li.animationend.animation,delete li.animationiteration.animation,delete li.animationstart.animation),`TransitionEvent`in window||delete li.transitionend.transition);function fi(e){if(ui[e])return ui[e];if(!li[e])return e;var t=li[e],n;for(n in t)if(t.hasOwnProperty(n)&&n in di)return ui[e]=t[n];return e}var pi=fi(`animationend`),mi=fi(`animationiteration`),hi=fi(`animationstart`),gi=fi(`transitionrun`),_i=fi(`transitionstart`),vi=fi(`transitioncancel`),yi=fi(`transitionend`),bi=new Map,xi=`abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error fullscreenChange fullscreenError gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel`.split(` `);xi.push(`scrollEnd`);function Si(e,t){bi.set(e,t),Jt(t,[e])}var Ci=0;function wi(e,t){if(e.name!=null&&e.name!==`auto`)return e.name;if(t.autoName!==null)return t.autoName;e=bd.identifierPrefix;var n=Ci++;return e=`_`+e+`t_`+n.toString(32)+`_`,t.autoName=e}function Ti(e){if(e==null||typeof e==`string`)return e;var t=null,n=Od;if(n!==null)for(var r=0;r<n.length;r++){var i=e[n[r]];if(i!=null){if(i===`none`)return`none`;t=t==null?i:t+(` `+i)}}return t??e.default}function Ei(e,t){return e=Ti(e),t=Ti(t),t==null?e===`auto`?null:e:t===`auto`?null:t}var Di=typeof reportError==`function`?reportError:function(e){if(typeof window==`object`&&typeof window.ErrorEvent==`function`){var t=new window.ErrorEvent(`error`,{bubbles:!0,cancelable:!0,message:typeof e==`object`&&e&&typeof e.message==`string`?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process==`object`&&typeof process.emit==`function`){process.emit(`uncaughtException`,e);return}console.error(e)},Oi=[],ki=0,Ai=0;function ji(){for(var e=ki,t=Ai=ki=0;t<e;){var n=Oi[t];Oi[t++]=null;var r=Oi[t];Oi[t++]=null;var i=Oi[t];Oi[t++]=null;var a=Oi[t];if(Oi[t++]=null,r!==null&&i!==null){var o=r.pending;o===null?i.next=i:(i.next=o.next,o.next=i),r.pending=i}a!==0&&Fi(n,i,a)}}function Mi(e,t,n,r){Oi[ki++]=e,Oi[ki++]=t,Oi[ki++]=n,Oi[ki++]=r,Ai|=r,e.lanes|=r,e=e.alternate,e!==null&&(e.lanes|=r)}function Ni(e,t,n,r){return Mi(e,t,n,r),Ii(e)}function Pi(e,t){return Mi(e,null,null,t),Ii(e)}function Fi(e,t,n){e.lanes|=n;var r=e.alternate;r!==null&&(r.lanes|=n);for(var i=!1,a=e.return;a!==null;)a.childLanes|=n,r=a.alternate,r!==null&&(r.childLanes|=n),a.tag===22&&(e=a.stateNode,e===null||e._visibility&1||(i=!0)),e=a,a=a.return;return e.tag===3?(a=e.stateNode,i&&t!==null&&(i=31-st(n),e=a.hiddenUpdates,r=e[i],r===null?e[i]=[t]:r.push(t),t.lane=n|536870912),a):null}function Ii(e){if(50<kd)throw kd=0,Ad=null,Error(i(185));for(var t=e.return;t!==null;)e=t,t=e.return;return e.tag===3?e.stateNode:null}var Li={};function Ri(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.refCleanup=this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function zi(e,t,n,r){return new Ri(e,t,n,r)}function Bi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vi(e,t){var n=e.alternate;return n===null?(n=zi(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&1206910976,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n.refCleanup=e.refCleanup,n}function Hi(e,t){e.flags&=1206910978;var n=e.alternate;return n===null?(e.childLanes=0,e.lanes=t,e.child=null,e.subtreeFlags=0,e.memoizedProps=null,e.memoizedState=null,e.updateQueue=null,e.dependencies=null,e.stateNode=null):(e.childLanes=n.childLanes,e.lanes=n.lanes,e.child=n.child,e.subtreeFlags=0,e.deletions=null,e.memoizedProps=n.memoizedProps,e.memoizedState=n.memoizedState,e.updateQueue=n.updateQueue,e.type=n.type,t=n.dependencies,e.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext}),e}function Ui(e,t,n,r,a,o){var s=0;if(r=e,typeof r==`function`)Bi(r)&&(s=1);else if(typeof r==`string`)s=qm(e,n,ke.current)?26:e===`html`||e===`head`||e===`body`?27:5;else a:switch(r){case me:return e=zi(31,n,t,a),e.elementType=me,e.lanes=o,e;case T:return Wi(n.children,a,o,t);case oe:s=8,a|=24;break;case se:return e=zi(12,n,t,a|2),e.elementType=se,e.lanes=o,e;case ue:return e=zi(13,n,t,a),e.elementType=ue,e.lanes=o,e;case de:return e=zi(19,n,t,a),e.elementType=de,e.lanes=o,e;case he:case _e:return e=a|32,e=zi(30,n,t,e),e.elementType=_e,e.lanes=o,e.stateNode={autoName:null,paired:null,clones:null,ref:null},e;default:if(typeof r==`object`&&r)switch(r.$$typeof){case E:s=10;break a;case ce:s=9;break a;case le:s=11;break a;case fe:s=14;break a;case pe:s=16,r=null;break a}s=29,n=Error(i(130,e===null?`null`:typeof e,``)),r=null}return t=zi(s,n,t,a),t.elementType=e,t.type=r,t.lanes=o,t}function Wi(e,t,n,r){return e=zi(7,e,r,t),e.lanes=n,e}function Gi(e,t,n){return e=zi(6,e,null,t),e.lanes=n,e}function Ki(e){var t=zi(18,null,null,0);return t.stateNode=e,t}function qi(e,t,n){return t=zi(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}var Ji=new WeakMap;function Yi(e,t){if(typeof e==`object`&&e){var n=Ji.get(e);return n===void 0?(t={value:e,source:t,stack:Ue(t)},Ji.set(e,t),t):n}return{value:e,source:t,stack:Ue(t)}}var Xi=[],Zi=0,Qi=null,$i=0,ea=[],ta=0,na=null,ra=1,ia=``;function aa(e,t){Xi[Zi++]=$i,Xi[Zi++]=Qi,Qi=e,$i=t}function oa(e,t,n){ea[ta++]=ra,ea[ta++]=ia,ea[ta++]=na,na=e;var r=ra;e=ia;var i=32-st(r)-1;r&=~(1<<i),n+=1;var a=32-st(t)+i;if(30<a){var o=i-i%5;a=(r&(1<<o)-1).toString(32),r>>=o,i-=o,ra=1<<32-st(t)+i|n<<i|r,ia=a+e}else ra=1<<a|n<<i|r,ia=e}function sa(e){e.return!==null&&(aa(e,1),oa(e,1,0))}function ca(e){for(;e===Qi;)Qi=Xi[--Zi],Xi[Zi]=null,$i=Xi[--Zi],Xi[Zi]=null;for(;e===na;)na=ea[--ta],ea[ta]=null,ia=ea[--ta],ea[ta]=null,ra=ea[--ta],ea[ta]=null}function la(e,t){ea[ta++]=ra,ea[ta++]=ia,ea[ta++]=na,ra=t.id,ia=t.overflow,na=e}var ua=null,N=null,P=!1,da=null,fa=!1,pa=Error(i(519));function ma(e){throw ba(Yi(Error(i(418,1<arguments.length&&arguments[1]!==void 0&&arguments[1]?`text`:`HTML`,``)),e)),pa}function ha(e){var t=e.stateNode,n=e.type,r=e.memoizedProps;switch(t[jt]=e,t[Mt]=r,n){case`dialog`:Q(`cancel`,t),Q(`close`,t);break;case`iframe`:case`object`:case`embed`:Q(`load`,t);break;case`video`:case`audio`:for(n=0;n<zf.length;n++)Q(zf[n],t);break;case`source`:Q(`error`,t);break;case`img`:case`image`:case`link`:Q(`error`,t),Q(`load`,t);break;case`details`:Q(`toggle`,t);break;case`input`:Q(`invalid`,t),pn(t,r.value,r.defaultValue,r.checked,r.defaultChecked,r.type,r.name,!0);break;case`select`:Q(`invalid`,t);break;case`textarea`:Q(`invalid`,t),_n(t,r.value,r.defaultValue,r.children)}n=r.children,typeof n!=`string`&&typeof n!=`number`&&typeof n!=`bigint`||t.textContent===``+n||!0===r.suppressHydrationWarning||ep(t.textContent,n)?(r.popover!=null&&(Q(`beforetoggle`,t),Q(`toggle`,t)),r.onScroll!=null&&Q(`scroll`,t),r.onScrollEnd!=null&&Q(`scrollend`,t),r.onClick!=null&&(t.onclick=En),t=!0):t=!1,t||ma(e,!0)}function ga(e){for(ua=e.return;ua;)switch(ua.tag){case 5:case 31:case 13:fa=!1;return;case 27:case 3:fa=!0;return;default:ua=ua.return}}function _a(e){if(e!==ua)return!1;if(!P)return ga(e),P=!0,!1;var t=e.tag,n;if((n=t!==3&&t!==27)&&((n=t===5)&&(n=e.type,n=n===`form`||n===`button`||pp(e.type,e.memoizedProps)),n=!n),n&&N&&ma(e),ga(e),t===13){if(e=e.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(317));N=dm(e)}else if(t===31){if(e=e.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(317));N=dm(e)}else t===27?(t=N,Sp(e.type)?(e=um,um=null,N=e):N=t):N=ua?lm(e.stateNode.nextSibling):null;return!0}function va(){N=ua=null,P=!1}function ya(){var e=da;return e!==null&&(pd===null?pd=e:pd.push.apply(pd,e),da=null),e}function ba(e){da===null?da=[e]:da.push(e)}var xa=De(null),Sa=null,Ca=null;function wa(e,t,n){k(xa,t._currentValue),t._currentValue=n}function Ta(e){e._currentValue=xa.current,Oe(xa)}function Ea(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Da(e,t,n,r){var a=e.child;for(a!==null&&(a.return=e);a!==null;){var o=a.dependencies;if(o!==null){var s=a.child;o=o.firstContext;a:for(;o!==null;){var c=o;o=a;for(var l=0;l<t.length;l++)if(c.context===t[l]){o.lanes|=n,c=o.alternate,c!==null&&(c.lanes|=n),Ea(o.return,n,e),r||(s=null);break a}o=c.next}}else if(a.tag===18){if(s=a.return,s===null)throw Error(i(341));s.lanes|=n,o=s.alternate,o!==null&&(o.lanes|=n),Ea(s,n,e),s=null}else a.tag===13&&a.memoizedState!==null&&a.memoizedState.dehydrated===null?(a.lanes|=n,s=a.alternate,s!==null&&(s.lanes|=n),Ea(a.return,n,e),s=a.child,s=s===null?null:s.sibling):s=a.child;if(s!==null)s.return=a;else for(s=a;s!==null;){if(s===e){s=null;break}if(a=s.sibling,a!==null){a.return=s.return,s=a;break}s=s.return}a=s}}function Oa(e,t,n,r){e=null;for(var a=t,o=!1;a!==null;){if(!o){if(a.flags&524288)o=!0;else if(a.flags&262144)break}if(a.tag===10){var s=a.alternate;if(s===null)throw Error(i(387));if(s=s.memoizedProps,s!==null){var c=a.type;Jr(a.pendingProps.value,s.value)||(e===null?e=[c]:e.push(c))}}else if(a===Me.current){if(s=a.alternate,s===null)throw Error(i(387));s.memoizedState.memoizedState!==a.memoizedState.memoizedState&&(e===null?e=[sh]:e.push(sh))}a=a.return}return e!==null&&Da(t,e,n,r),t.flags|=262144,e!==null}function ka(e){for(e=e.firstContext;e!==null;){if(!Jr(e.context._currentValue,e.memoizedValue))return!0;e=e.next}return!1}function Aa(e){Sa=e,Ca=null,e=e.dependencies,e!==null&&(e.firstContext=null)}function ja(e){return Na(Sa,e)}function Ma(e,t){return Sa===null&&Aa(e),Na(e,t)}function Na(e,t){var n=t._currentValue;if(t={context:t,memoizedValue:n,next:null},Ca===null){if(e===null)throw Error(i(308));Ca=t,e.dependencies={lanes:0,firstContext:t},e.flags|=524288}else Ca=Ca.next=t;return n}var Pa=typeof AbortController<`u`?AbortController:function(){var e=[],t=this.signal={aborted:!1,addEventListener:function(t,n){e.push(n)}};this.abort=function(){t.aborted=!0,e.forEach(function(e){return e()})}},Fa=t.unstable_scheduleCallback,Ia=t.unstable_NormalPriority,F={$$typeof:E,Consumer:null,Provider:null,_currentValue:null,_currentValue2:null,_threadCount:0};function La(){return{controller:new Pa,data:new Map,refCount:0}}function Ra(e){e.refCount--,e.refCount===0&&Fa(Ia,function(){e.controller.abort()})}function za(e,t){if(e.pendingLanes&4194048){var n=e.transitionTypes;for(n===null&&(n=e.transitionTypes=[]),e=0;e<t.length;e++){var r=t[e];n.indexOf(r)===-1&&n.push(r)}}}var Ba=null;function Va(e){var t=e.transitionTypes;return e.transitionTypes=null,t}var Ha=null,Ua=0,Wa=0,Ga=null;function Ka(e,t){if(Ha===null){var n=Ha=[];Ua=0,Wa=Pf(),Ga={status:`pending`,value:void 0,then:function(e){n.push(e)}}}return Ua++,t.then(qa,qa),t}function qa(){if(--Ua===0&&(Ba=null,Ha!==null)){Ga!==null&&(Ga.status=`fulfilled`);var e=Ha;Ha=null,Wa=0,Ga=null;for(var t=0;t<e.length;t++)(0,e[t])()}}function Ja(e,t){var n=[],r={status:`pending`,value:null,reason:null,then:function(e){n.push(e)}};return e.then(function(){r.status=`fulfilled`,r.value=t;for(var e=0;e<n.length;e++)(0,n[e])(t)},function(e){for(r.status=`rejected`,r.reason=e,e=0;e<n.length;e++)(0,n[e])(void 0)}),r}var Ya=D.S;D.S=function(e,t){if(gd=Ye(),typeof t==`object`&&t&&typeof t.then==`function`&&Ka(e,t),Ba!==null)for(var n=bf;n!==null;)za(n,Ba),n=n.next;if(n=e.types,n!==null){for(var r=bf;r!==null;)za(r,n),r=r.next;if(Wa!==0){r=Ba,r===null&&(r=Ba=[]);for(var i=0;i<n.length;i++){var a=n[i];r.indexOf(a)===-1&&r.push(a)}}}Ya!==null&&Ya(e,t)};var Xa=De(null);function Za(){var e=Xa.current;return e===null?G.pooledCache:e}function Qa(e,t){t===null?k(Xa,Xa.current):k(Xa,t.pool)}function $a(){var e=Za();return e===null?null:{parent:F._currentValue,pool:e}}var eo=Error(i(460)),to=Error(i(474)),no=Error(i(542)),ro={then:function(){}};function io(e){return e=e.status,e===`fulfilled`||e===`rejected`}function ao(e,t,n){switch(n=e[n],n===void 0?e.push(t):n!==t&&(t.then(En,En),t=n),t.status){case`fulfilled`:return t.value;case`rejected`:throw e=t.reason,lo(e),e===void 0&&!(`reason`in t)?Error(i(600)):e;default:if(typeof t.status==`string`)t.then(En,En);else{if(e=G,e!==null&&100<e.shellSuspendCounter)throw Error(i(482));e=t,e.status=`pending`,e.then(function(e){if(t.status===`pending`){var n=t;n.status=`fulfilled`,n.value=e}},function(e){if(t.status===`pending`){var n=t;n.status=`rejected`,n.reason=e}})}switch(t.status){case`fulfilled`:return t.value;case`rejected`:throw e=t.reason,lo(e),e}throw so=t,eo}}function oo(e){try{var t=e._init;return t(e._payload)}catch(e){throw typeof e==`object`&&e&&typeof e.then==`function`?(so=e,eo):e}}var so=null;function co(){if(so===null)throw Error(i(459));var e=so;return so=null,e}function lo(e){if(e===eo||e===no)throw Error(i(483))}var uo=null,fo=0;function po(e){var t=fo;return fo+=1,uo===null&&(uo=[]),ao(uo,e,t)}function mo(e,t){t=t.props.ref,e.ref=t===void 0?null:t}function ho(e,t){throw t.$$typeof===ie?Error(i(525)):(e=Object.prototype.toString.call(t),Error(i(31,e===`[object Object]`?`object with keys {`+Object.keys(t).join(`, `)+`}`:e)))}function go(e){function t(t,n){if(e){var r=t.deletions;r===null?(t.deletions=[n],t.flags|=16):r.push(n)}}function n(n,r){if(!e)return null;for(;r!==null;)t(n,r),r=r.sibling;return null}function r(e){for(var t=new Map;e!==null;)e.key===null?t.set(e.index,e):t.set(e.key,e),e=e.sibling;return t}function a(e,t){return e=Vi(e,t),e.index=0,e.sibling=null,e}function o(t,n,r){return t.index=r,e?(r=t.alternate,r===null?(t.flags|=134217730,n):(r=r.index,r<n?(t.flags|=2,n):r)):(t.flags|=1048576,n)}function s(t){return e&&t.alternate===null&&(t.flags|=134217730),t}function c(e,t,n,r){return t===null||t.tag!==6?(t=Gi(n,e.mode,r),t.return=e,t):(t=a(t,n),t.return=e,t)}function l(e,t,n,r){var i=n.type;return i===T?(e=d(e,t,n.props.children,r,n.key),mo(e,n),e):t!==null&&(t.elementType===i||typeof i==`object`&&i&&i.$$typeof===pe&&oo(i)===t.type)?(t=a(t,n.props),mo(t,n),t.return=e,t):(t=Ui(n.type,n.key,n.props,null,e.mode,r),mo(t,n),t.return=e,t)}function u(e,t,n,r){return t===null||t.tag!==4||t.stateNode.containerInfo!==n.containerInfo||t.stateNode.implementation!==n.implementation?(t=qi(n,e.mode,r),t.return=e,t):(t=a(t,n.children||[]),t.return=e,t)}function d(e,t,n,r,i){return t===null||t.tag!==7?(t=Wi(n,e.mode,r,i),t.return=e,t):(t=a(t,n),t.return=e,t)}function f(e,t,n){if(typeof t==`string`&&t!==``||typeof t==`number`||typeof t==`bigint`)return t=Gi(``+t,e.mode,n),t.return=e,t;if(typeof t==`object`&&t){switch(t.$$typeof){case ae:return n=Ui(t.type,t.key,t.props,null,e.mode,n),mo(n,t),n.return=e,n;case w:return t=qi(t,e.mode,n),t.return=e,t;case pe:return t=oo(t),f(e,t,n)}if(Ce(t)||be(t))return t=Wi(t,e.mode,n,null),t.return=e,t;if(typeof t.then==`function`)return f(e,po(t),n);if(t.$$typeof===E)return f(e,Ma(e,t),n);ho(e,t)}return null}function p(e,t,n,r){var i=t===null?null:t.key;if(typeof n==`string`&&n!==``||typeof n==`number`||typeof n==`bigint`)return i===null?c(e,t,``+n,r):null;if(typeof n==`object`&&n){switch(n.$$typeof){case ae:return n.key===i?l(e,t,n,r):null;case w:return n.key===i?u(e,t,n,r):null;case pe:return n=oo(n),p(e,t,n,r)}if(Ce(n)||be(n))return i===null?d(e,t,n,r,null):null;if(typeof n.then==`function`)return p(e,t,po(n),r);if(n.$$typeof===E)return p(e,t,Ma(e,n),r);ho(e,n)}return null}function m(e,t,n,r,i){if(typeof r==`string`&&r!==``||typeof r==`number`||typeof r==`bigint`)return e=e.get(n)||null,c(t,e,``+r,i);if(typeof r==`object`&&r){switch(r.$$typeof){case ae:return e=e.get(r.key===null?n:r.key)||null,l(t,e,r,i);case w:return e=e.get(r.key===null?n:r.key)||null,u(t,e,r,i);case pe:return r=oo(r),m(e,t,n,r,i)}if(Ce(r)||be(r))return e=e.get(n)||null,d(t,e,r,i,null);if(typeof r.then==`function`)return m(e,t,n,po(r),i);if(r.$$typeof===E)return m(e,t,n,Ma(t,r),i);ho(t,r)}return null}function h(i,a,s,c){for(var l=null,u=null,d=a,h=a=0,g=null;d!==null&&h<s.length;h++){d.index>h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),P&&aa(i,h),l;if(d===null){for(;h<s.length;h++)d=f(i,s[h],c),d!==null&&(a=o(d,a,h),u===null?l=d:u.sibling=d,u=d);return P&&aa(i,h),l}for(d=r(d);h<s.length;h++)g=m(d,i,h,s[h],c),g!==null&&(e&&(_=g.alternate,_!==null&&d.delete(_.key===null?h:_.key)),a=o(g,a,h),u===null?l=g:u.sibling=g,u=g);return e&&d.forEach(function(e){return t(i,e)}),P&&aa(i,h),l}function g(a,s,c,l){if(c==null)throw Error(i(151));for(var u=null,d=null,h=s,g=s=0,_=null,v=c.next();h!==null&&!v.done;g++,v=c.next()){h.index>g?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),P&&aa(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return P&&aa(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&(_=v.alternate,_!==null&&h.delete(_.key===null?g:_.key)),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),P&&aa(a,g),u}function _(e,r,o,c){if(typeof o==`object`&&o&&o.type===T&&o.key===null&&o.props.ref===void 0&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case ae:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===T){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),mo(c,o),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===pe&&oo(l)===r.type){n(e,r.sibling),c=a(r,o.props),mo(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===T?(c=Wi(o.props.children,e.mode,c,o.key),mo(c,o),c.return=e,e=c):(c=Ui(o.type,o.key,o.props,null,e.mode,c),mo(c,o),c.return=e,e=c)}return s(e);case w:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=qi(o,e.mode,c),c.return=e,e=c}return s(e);case pe:return o=oo(o),_(e,r,o,c)}if(Ce(o))return h(e,r,o,c);if(be(o)){if(l=be(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return _(e,r,po(o),c);if(o.$$typeof===E)return _(e,r,Ma(e,o),c);ho(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=Gi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{fo=0;var i=_(e,t,n,r);return uo=null,i}catch(t){if(t===eo||t===no)throw t;var a=zi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var _o=go(!0),vo=go(!1),yo=!1;function bo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function xo(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function So(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Co(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,W&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Ii(e),Fi(e,null,n),t}return Mi(e,r,t,n),Ii(e)}function wo(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,wt(e,n)}}function To(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n}else e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Eo=!1;function Do(){if(Eo){var e=Ga;if(e!==null)throw e}}function Oo(e,t,n,r){Eo=!1;var i=e.updateQueue;yo=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(q&f)===f:(r&f)===f){f!==0&&f===Wa&&(Eo=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,h=s;f=t;var g=n;switch(h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(g,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(g,d,f):m,f==null)break a;d=C({},d,f);break a;case 2:yo=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),sd|=o,e.lanes=o,e.memoizedState=d}}function ko(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ao(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;e<n.length;e++)ko(n[e],t)}var jo=De(null),Mo=De(0);function No(e,t){e=od,k(Mo,e),k(jo,t),od=e|t.baseLanes}function Po(){k(Mo,od),k(jo,jo.current)}function Fo(){od=Mo.current,Oe(jo),Oe(Mo)}var Io=De(null),Lo=null;function Ro(e){var t=e.alternate;k(Uo,Uo.current&1),k(Io,e),Lo===null&&(t===null||jo.current!==null||t.memoizedState!==null)&&(Lo=e)}function zo(e){k(Uo,Uo.current),k(Io,e),Lo===null&&(Lo=e)}function Bo(e){e.tag===22?(k(Uo,Uo.current),k(Io,e),Lo===null&&(Lo=e)):Vo()}function Vo(){k(Uo,Uo.current),k(Io,Io.current)}function Ho(e){Oe(Io),Lo===e&&(Lo=null),Oe(Uo)}var Uo=De(0);function Wo(e,t){k(Io,Io.current),k(Uo,t)}function Go(e){Oe(Uo),Oe(Io),Lo===e&&(Lo=null)}function Ko(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||om(n)||sm(n)))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==`independent`){if(t.flags&128)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var qo=0,I=null,L=null,Jo=null,Yo=!1,Xo=!1,Zo=!1,Qo=0,$o=0,es=null,ts=0;function R(){throw Error(i(321))}function ns(e,t){if(t===null)return!1;for(var n=0;n<t.length&&n<e.length;n++)if(!Jr(e[n],t[n]))return!1;return!0}function rs(e,t,n,r,i,a){return qo=a,I=t,t.memoizedState=null,t.updateQueue=null,t.lanes=0,D.H=e===null||e.memoizedState===null?yc:bc,Zo=!1,a=n(r,i),Zo=!1,Xo&&(a=as(t,n,r,i)),is(e),a}function is(e){D.H=vc;var t=L!==null&&L.next!==null;if(qo=0,Jo=L=I=null,Yo=!1,$o=0,es=null,t)throw Error(i(300));e===null||B||(e=e.dependencies,e!==null&&ka(e)&&(B=!0))}function as(e,t,n,r){I=e;var a=0;do{if(Xo&&(es=null),$o=0,Xo=!1,25<=a)throw Error(i(301));if(a+=1,Jo=L=null,e.updateQueue!=null){var o=e.updateQueue;o.lastEffect=null,o.events=null,o.stores=null,o.memoCache!=null&&(o.memoCache.index=0)}D.H=xc,o=t(n,r)}while(Xo);return o}function os(){var e=D.H,t=e.useState()[0];return t=typeof t.then==`function`?fs(t):t,e=e.useState()[0],(L===null?null:L.memoizedState)!==e&&(I.flags|=1024),t}function ss(){var e=Qo!==0;return Qo=0,e}function cs(e,t,n){t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~n}function ls(e){if(Yo){for(e=e.memoizedState;e!==null;){var t=e.queue;t!==null&&(t.pending=null),e=e.next}Yo=!1}qo=0,Jo=L=I=null,Xo=!1,$o=Qo=0,es=null}function us(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Jo===null?I.memoizedState=Jo=e:Jo=Jo.next=e,Jo}function z(){if(L===null){var e=I.alternate;e=e===null?null:e.memoizedState}else e=L.next;var t=Jo===null?I.memoizedState:Jo.next;if(t!==null)Jo=t,L=e;else{if(e===null)throw I.alternate===null?Error(i(467)):Error(i(310));L=e,e={memoizedState:L.memoizedState,baseState:L.baseState,baseQueue:L.baseQueue,queue:L.queue,next:null},Jo===null?I.memoizedState=Jo=e:Jo=Jo.next=e}return Jo}function ds(){return{lastEffect:null,events:null,stores:null,memoCache:null}}function fs(e){var t=$o;return $o+=1,es===null&&(es=[]),e=ao(es,e,t),t=I,(Jo===null?t.memoizedState:Jo.next)===null&&(t=t.alternate,D.H=t===null||t.memoizedState===null?yc:bc),e}function ps(e){if(typeof e==`object`&&e){if(typeof e.then==`function`)return fs(e);if(e.$$typeof===ve)return;if(e.$$typeof===E)return ja(e)}throw Error(i(438,String(e)))}function ms(e){var t=null,n=I.updateQueue;if(n!==null&&(t=n.memoCache),t==null){var r=I.alternate;r!==null&&(r=r.updateQueue,r!==null&&(r=r.memoCache,r!=null&&(t={data:r.data.map(function(e){return e.slice()}),index:0})))}if(t??={data:[],index:0},n===null&&(n=ds(),I.updateQueue=n),n.memoCache=t,n=t.data[t.index],n===void 0)for(n=t.data[t.index]=Array(e),r=0;r<e;r++)n[r]=ge;return t.index++,n}function hs(e,t){return typeof t==`function`?t(e):t}function gs(e){return _s(z(),L,e)}function _s(e,t,n){var r=e.queue;if(r===null)throw Error(i(311));r.lastRenderedReducer=n;var a=e.baseQueue,o=r.pending;if(o!==null){if(a!==null){var s=a.next;a.next=o.next,o.next=s}t.baseQueue=a=o,r.pending=null}if(o=e.baseState,a===null)e.memoizedState=o;else{t=a.next;var c=s=null,l=null,u=t,d=!1;do{var f=u.lane&-536870913;if(f===u.lane?(qo&f)===f:(q&f)===f){var p=u.revertLane;if(p===0)l!==null&&(l=l.next={lane:0,revertLane:0,gesture:null,action:u.action,hasEagerState:u.hasEagerState,eagerState:u.eagerState,next:null}),f===Wa&&(d=!0);else if((qo&p)===p){u=u.next,p===Wa&&(d=!0);continue}else f={lane:0,revertLane:u.revertLane,gesture:null,action:u.action,hasEagerState:u.hasEagerState,eagerState:u.eagerState,next:null},l===null?(c=l=f,s=o):l=l.next=f,I.lanes|=p,sd|=p;f=u.action,Zo&&n(o,f),o=u.hasEagerState?u.eagerState:n(o,f)}else p={lane:f,revertLane:u.revertLane,gesture:u.gesture,action:u.action,hasEagerState:u.hasEagerState,eagerState:u.eagerState,next:null},l===null?(c=l=p,s=o):l=l.next=p,I.lanes|=f,sd|=f;u=u.next}while(u!==null&&u!==t);if(l===null?s=o:l.next=c,!Jr(o,e.memoizedState)&&(B=!0,d&&(n=Ga,n!==null)))throw n;e.memoizedState=o,e.baseState=s,e.baseQueue=l,r.lastRenderedState=o}return a===null&&(r.lanes=0),[e.memoizedState,r.dispatch]}function vs(e){var t=z(),n=t.queue;if(n===null)throw Error(i(311));n.lastRenderedReducer=e;var r=n.dispatch,a=n.pending,o=t.memoizedState;if(a!==null){n.pending=null;var s=a=a.next;do o=e(o,s.action),s=s.next;while(s!==a);Jr(o,t.memoizedState)||(B=!0),t.memoizedState=o,t.baseQueue===null&&(t.baseState=o),n.lastRenderedState=o}return[o,r]}function ys(e,t,n){var r=I,a=z(),o=P;if(o){if(n===void 0)throw Error(i(407));n=n()}else n=t();var s=!Jr((L||a).memoizedState,n);if(s&&(a.memoizedState=n,B=!0),a=a.queue,Ws(Ss.bind(null,r,a,e),[e]),e=a.getSnapshot!==t||s||Jo!==null&&!!(Jo.memoizedState.tag&1),zs(e?9:8,{destroy:void 0},xs.bind(null,r,a,n,t),null),e){if(r.flags|=2048,G===null)throw Error(i(349));o||qo&127||bs(r,t,n)}return n}function bs(e,t,n){e.flags|=16384,e={getSnapshot:t,value:n},t=I.updateQueue,t===null?(t=ds(),I.updateQueue=t,t.stores=[e]):(n=t.stores,n===null?t.stores=[e]:n.push(e))}function xs(e,t,n,r){t.value=n,t.getSnapshot=r,Cs(t)&&ws(e)}function Ss(e,t,n){return n(function(){Cs(t)&&ws(e)})}function Cs(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!Jr(e,n)}catch{return!0}}function ws(e){var t=Pi(e,2);t!==null&&Pd(t,e,2)}function Ts(e){var t=us();if(typeof e==`function`){var n=e;if(e=n(),Zo){ot(!0);try{n()}finally{ot(!1)}}}return t.memoizedState=t.baseState=e,t.queue={pending:null,lanes:0,dispatch:null,lastRenderedReducer:hs,lastRenderedState:e},t}function Es(e,t,n,r){return e.baseState=n,_s(e,L,typeof r==`function`?r:hs)}function Ds(e,t,n,r,a){if(hc(e))throw Error(i(485));if(e=t.action,e!==null){var o={payload:a,action:e,next:null,isTransition:!0,status:`pending`,value:null,reason:null,listeners:[],then:function(e){o.listeners.push(e)}};D.T===null?o.isTransition=!1:n(!0),r(o),n=t.pending,n===null?(o.next=t.pending=o,Os(t,o)):(o.next=n.next,t.pending=n.next=o)}}function Os(e,t){var n=t.action,r=t.payload,i=e.state;if(t.isTransition){var a=D.T,o={};o.types=a===null?null:a.types,D.T=o;try{var s=n(i,r),c=D.S;c!==null&&c(o,s),ks(e,t,s)}catch(n){js(e,t,n)}finally{a!==null&&o.types!==null&&(a.types=o.types),D.T=a}}else try{a=n(i,r),ks(e,t,a)}catch(n){js(e,t,n)}}function ks(e,t,n){typeof n==`object`&&n&&typeof n.then==`function`?n.then(function(n){As(e,t,n)},function(n){return js(e,t,n)}):As(e,t,n)}function As(e,t,n){t.status=`fulfilled`,t.value=n,Ms(t),e.state=n,t=e.pending,t!==null&&(n=t.next,n===t?e.pending=null:(n=n.next,t.next=n,Os(e,n)))}function js(e,t,n){var r=e.pending;if(e.pending=null,r!==null){r=r.next;do t.status=`rejected`,t.reason=n,Ms(t),t=t.next;while(t!==r)}e.action=null}function Ms(e){e=e.listeners;for(var t=0;t<e.length;t++)(0,e[t])()}function Ns(e,t){return t}function Ps(e,t){if(P){var n=G.formState;if(n!==null){a:{var r=I;if(P){if(N){b:{for(var i=N,a=fa;i.nodeType!==8;)if(!a||(i=lm(i.nextSibling),i===null)){i=null;break b}a=i.data,i=a===`F!`||a===`F`?i:null}if(i){N=lm(i.nextSibling),r=i.data===`F!`;break a}}ma(r)}r=!1}r&&(t=n[0])}}return n=us(),n.memoizedState=n.baseState=t,r={pending:null,lanes:0,dispatch:null,lastRenderedReducer:Ns,lastRenderedState:t},n.queue=r,n=fc.bind(null,I,r),r.dispatch=n,r=Ts(!1),a=mc.bind(null,I,!1,r.queue),r=us(),i={state:t,dispatch:null,action:e,pending:null},r.queue=i,n=Ds.bind(null,I,i,a,n),i.dispatch=n,r.memoizedState=e,[t,n,!1]}function Fs(e){return Is(z(),L,e)}function Is(e,t,n){if(t=_s(e,t,Ns)[0],e=gs(hs)[0],typeof t==`object`&&t&&typeof t.then==`function`)try{var r=fs(t)}catch(e){throw e===eo?no:e}else r=t;t=z();var i=t.queue,a=i.dispatch;return n!==t.memoizedState&&(I.flags|=2048,zs(9,{destroy:void 0},Ls.bind(null,i,n),null)),[r,a,e]}function Ls(e,t){e.action=t}function Rs(e){var t=z(),n=L;if(n!==null)return Is(t,n,e);z(),t=t.memoizedState,n=z();var r=n.queue.dispatch;return n.memoizedState=e,[t,r,!1]}function zs(e,t,n,r){return e={tag:e,create:n,deps:r,inst:t,next:null},t=I.updateQueue,t===null&&(t=ds(),I.updateQueue=t),n=t.lastEffect,n===null?t.lastEffect=e.next=e:(r=n.next,n.next=e,e.next=r,t.lastEffect=e),e}function Bs(){return z().memoizedState}function Vs(e,t,n,r){var i=us();I.flags|=e,i.memoizedState=zs(1|t,{destroy:void 0},n,r===void 0?null:r)}function Hs(e,t,n,r){var i=z();r=r===void 0?null:r;var a=i.memoizedState.inst;L!==null&&r!==null&&ns(r,L.memoizedState.deps)?i.memoizedState=zs(t,a,n,r):(I.flags|=e,i.memoizedState=zs(1|t,a,n,r))}function Us(e,t){Vs(8390656,8,e,t)}function Ws(e,t){Hs(2048,8,e,t)}function Gs(e){I.flags|=4;var t=I.updateQueue;if(t===null)t=ds(),I.updateQueue=t,t.events=[e];else{var n=t.events;n===null?t.events=[e]:n.push(e)}}function Ks(e){var t=z().memoizedState;return Gs({ref:t,nextImpl:e}),function(){if(W&2)throw Error(i(440));return t.impl.apply(void 0,arguments)}}function qs(e,t){return Hs(4,2,e,t)}function Js(e,t){return Hs(4,4,e,t)}function Ys(e,t){if(typeof t==`function`){e=e();var n=t(e);return function(){typeof n==`function`?n():t(null)}}if(t!=null)return e=e(),t.current=e,function(){t.current=null}}function Xs(e,t,n){n=n==null?null:n.concat([e]),Hs(4,4,Ys.bind(null,t,e),n)}function Zs(){}function Qs(e,t){var n=z();t=t===void 0?null:t;var r=n.memoizedState;return t!==null&&ns(t,r[1])?r[0]:(n.memoizedState=[e,t],e)}function $s(e,t){var n=z();t=t===void 0?null:t;var r=n.memoizedState;if(t!==null&&ns(t,r[1]))return r[0];if(r=e(),Zo){ot(!0);try{e()}finally{ot(!1)}}return n.memoizedState=[r,t],r}function ec(e,t,n){return n===void 0||qo&1073741824&&!(q&261930)?e.memoizedState=t:(e.memoizedState=n,e=Md(),I.lanes|=e,sd|=e,n)}function tc(e,t,n,r){return Jr(n,t)?n:jo.current===null?!(qo&106)||qo&1073741824&&!(q&261930)?(B=!0,e.memoizedState=n):(e=Md(),I.lanes|=e,sd|=e,t):(e=ec(e,n,r),Jr(e,t)||(B=!0),e)}function nc(e,t,n,r,i){var a=O.p;O.p=a!==0&&8>a?a:8;var o=D.T,s={};s.types=o===null?null:o.types,D.T=s,mc(e,!1,t,n);try{var c=i(),l=D.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?pc(e,t,Ja(c,r),jd(e)):pc(e,t,r,jd(e))}catch(n){pc(e,t,{then:function(){},status:`rejected`,reason:n},jd())}finally{O.p=a,o!==null&&s.types!==null&&(o.types=s.types),D.T=o}}function rc(){}function ic(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ac(e).queue;nc(e,a,t,we,n===null?rc:function(){return oc(e),n(r)})}function ac(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:we,baseState:we,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:hs,lastRenderedState:we},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:hs,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function oc(e){var t=ac(e);t.next===null&&(t=e.alternate.memoizedState),pc(e,t.next.queue,{},jd())}function sc(){return ja(sh)}function cc(){return z().memoizedState}function lc(){return z().memoizedState}function uc(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=jd();e=So(n);var r=Co(t,e,n);r!==null&&(Pd(r,t,n),wo(r,t,n)),t={cache:La()},e.payload=t;return}t=t.return}}function dc(e,t,n){var r=jd();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},hc(e)?gc(t,n):(n=Ni(e,t,n,r),n!==null&&(Pd(n,e,r),_c(n,t,r)))}function fc(e,t,n){pc(e,t,n,jd())}function pc(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(hc(e))gc(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Jr(s,o))return Mi(e,t,i,0),G===null&&ji(),!1}catch{}if(n=Ni(e,t,i,r),n!==null)return Pd(n,e,r),_c(n,t,r),!0}return!1}function mc(e,t,n,r){if(r={lane:2,revertLane:Pf(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},hc(e)){if(t)throw Error(i(479))}else t=Ni(e,n,r,2),t!==null&&Pd(t,e,2)}function hc(e){var t=e.alternate;return e===I||t!==null&&t===I}function gc(e,t){Xo=Yo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function _c(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,wt(e,n)}}var vc={readContext:ja,use:ps,useCallback:R,useContext:R,useEffect:R,useImperativeHandle:R,useLayoutEffect:R,useInsertionEffect:R,useMemo:R,useReducer:R,useRef:R,useState:R,useDebugValue:R,useDeferredValue:R,useTransition:R,useSyncExternalStore:R,useId:R,useHostTransitionStatus:R,useFormState:R,useActionState:R,useOptimistic:R,useMemoCache:R,useCacheRefresh:R,useEffectEvent:R},yc={readContext:ja,use:ps,useCallback:function(e,t){return us().memoizedState=[e,t===void 0?null:t],e},useContext:ja,useEffect:Us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Vs(4194308,4,Ys.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Vs(4194308,4,e,t)},useInsertionEffect:function(e,t){Vs(4,2,e,t)},useMemo:function(e,t){var n=us();t=t===void 0?null:t;var r=e();if(Zo){ot(!0);try{e()}finally{ot(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=us();if(n!==void 0){var i=n(t);if(Zo){ot(!0);try{n(t)}finally{ot(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=dc.bind(null,I,e),[r.memoizedState,e]},useRef:function(e){var t=us();return e={current:e},t.memoizedState=e},useState:function(e){e=Ts(e);var t=e.queue,n=fc.bind(null,I,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Zs,useDeferredValue:function(e,t){return ec(us(),e,t)},useTransition:function(){var e=Ts(!1);return e=nc.bind(null,I,e.queue,!0,!1),us().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=I,a=us();if(P){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),G===null)throw Error(i(349));q&127||bs(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Us(Ss.bind(null,r,o,e),[e]),r.flags|=2048,zs(9,{destroy:void 0},xs.bind(null,r,o,n,t),null),n},useId:function(){var e=us(),t=G.identifierPrefix;if(P){var n=ia,r=ra;n=(r&~(1<<32-st(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=Qo++,0<n&&(t+=`H`+n.toString(32)),t+=`_`}else n=ts++,t=`_`+t+`r_`+n.toString(32)+`_`;return e.memoizedState=t},useHostTransitionStatus:sc,useFormState:Ps,useActionState:Ps,useOptimistic:function(e){var t=us();t.memoizedState=t.baseState=e;var n={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return t.queue=n,t=mc.bind(null,I,!0,n),n.dispatch=t,[e,t]},useMemoCache:ms,useCacheRefresh:function(){return us().memoizedState=uc.bind(null,I)},useEffectEvent:function(e){var t=us(),n={impl:e};return t.memoizedState=n,function(){if(W&2)throw Error(i(440));return n.impl.apply(void 0,arguments)}}},bc={readContext:ja,use:ps,useCallback:Qs,useContext:ja,useEffect:Ws,useImperativeHandle:Xs,useInsertionEffect:qs,useLayoutEffect:Js,useMemo:$s,useReducer:gs,useRef:Bs,useState:function(){return gs(hs)},useDebugValue:Zs,useDeferredValue:function(e,t){return tc(z(),L.memoizedState,e,t)},useTransition:function(){var e=gs(hs)[0],t=z().memoizedState;return[typeof e==`boolean`?e:fs(e),t]},useSyncExternalStore:ys,useId:cc,useHostTransitionStatus:sc,useFormState:Fs,useActionState:Fs,useOptimistic:function(e,t){return Es(z(),L,e,t)},useMemoCache:ms,useCacheRefresh:lc,useEffectEvent:Ks},xc={readContext:ja,use:ps,useCallback:Qs,useContext:ja,useEffect:Ws,useImperativeHandle:Xs,useInsertionEffect:qs,useLayoutEffect:Js,useMemo:$s,useReducer:vs,useRef:Bs,useState:function(){return vs(hs)},useDebugValue:Zs,useDeferredValue:function(e,t){var n=z();return L===null?ec(n,e,t):tc(n,L.memoizedState,e,t)},useTransition:function(){var e=vs(hs)[0],t=z().memoizedState;return[typeof e==`boolean`?e:fs(e),t]},useSyncExternalStore:ys,useId:cc,useHostTransitionStatus:sc,useFormState:Rs,useActionState:Rs,useOptimistic:function(e,t){var n=z();return L===null?(n.baseState=e,[e,n.queue.dispatch]):Es(n,L,e,t)},useMemoCache:ms,useCacheRefresh:lc,useEffectEvent:Ks};function Sc(e,t,n,r){t=e.memoizedState,n=n(r,t),n=n==null?t:C({},t,n),e.memoizedState=n,e.lanes===0&&(e.updateQueue.baseState=n)}var Cc={enqueueSetState:function(e,t,n){e=e._reactInternals;var r=jd(),i=So(r);i.payload=t,n!=null&&(i.callback=n),t=Co(e,i,r),t!==null&&(Pd(t,e,r),wo(t,e,r))},enqueueReplaceState:function(e,t,n){e=e._reactInternals;var r=jd(),i=So(r);i.tag=1,i.payload=t,n!=null&&(i.callback=n),t=Co(e,i,r),t!==null&&(Pd(t,e,r),wo(t,e,r))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var n=jd(),r=So(n);r.tag=2,t!=null&&(r.callback=t),t=Co(e,r,n),t!==null&&(Pd(t,e,n),wo(t,e,n))}};function wc(e,t,n,r,i,a,o){return e=e.stateNode,typeof e.shouldComponentUpdate==`function`?e.shouldComponentUpdate(r,a,o):t.prototype&&t.prototype.isPureReactComponent?!Yr(n,r)||!Yr(i,a):!0}function Tc(e,t,n,r){e=t.state,typeof t.componentWillReceiveProps==`function`&&t.componentWillReceiveProps(n,r),typeof t.UNSAFE_componentWillReceiveProps==`function`&&t.UNSAFE_componentWillReceiveProps(n,r),t.state!==e&&Cc.enqueueReplaceState(t,t.state,null)}function Ec(e,t){var n=t;if(`ref`in t)for(var r in n={},t)r!==`ref`&&(n[r]=t[r]);if(e=e.defaultProps)for(var i in n===t&&(n=C({},n)),e)n[i]===void 0&&(n[i]=e[i]);return n}function Dc(e){Di(e)}function Oc(e){console.error(e)}function kc(e){Di(e)}function Ac(e,t){try{var n=e.onUncaughtError;n(t.value,{componentStack:t.stack})}catch(e){setTimeout(function(){throw e})}}function jc(e,t,n){try{var r=e.onCaughtError;r(n.value,{componentStack:n.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(e){setTimeout(function(){throw e})}}function Mc(e,t,n){return n=So(n),n.tag=3,n.payload={element:null},n.callback=function(){Ac(e,t)},n}function Nc(e){return e=So(e),e.tag=3,e}function Pc(e,t,n,r){var i=n.type.getDerivedStateFromError;if(typeof i==`function`){var a=r.value;e.payload=function(){return i(a)},e.callback=function(){jc(t,n,r)}}var o=n.stateNode;o!==null&&typeof o.componentDidCatch==`function`&&(e.callback=function(){jc(t,n,r),typeof i!=`function`&&(yd===null?yd=new Set([this]):yd.add(this));var e=r.stack;this.componentDidCatch(r.value,{componentStack:e===null?``:e})})}function Fc(e,t,n,r,a){if(n.flags|=32768,typeof r==`object`&&r&&typeof r.then==`function`){if(t=n.alternate,t!==null&&Oa(t,n,a,!0),n=Io.current,n!==null){switch(n.tag){case 31:case 13:case 19:return Lo===null?Kd():n.alternate===null&&Y===0&&(Y=3),n.flags&=-257,n.flags|=65536,n.lanes=a,r===ro?n.flags|=16384:(t=n.updateQueue,t===null?n.updateQueue=new Set([r]):t.add(r),mf(e,r,a)),!1;case 22:return n.flags|=65536,r===ro?n.flags|=16384:(t=n.updateQueue,t===null?(t={transitions:null,markerInstances:null,retryQueue:new Set([r])},n.updateQueue=t):(n=t.retryQueue,n===null?t.retryQueue=new Set([r]):n.add(r)),mf(e,r,a)),!1}throw Error(i(435,n.tag))}return mf(e,r,a),Kd(),!1}if(P)return t=Io.current,t===null?(r!==pa&&(t=Error(i(423),{cause:r}),ba(Yi(t,n))),e=e.current.alternate,e.flags|=65536,a&=-a,e.lanes|=a,r=Yi(r,n),a=Mc(e.stateNode,r,a),To(e,a),Y!==4&&(Y=2)):(!(t.flags&65536)&&(t.flags|=256),t.flags|=65536,t.lanes=a,r!==pa&&(e=Error(i(422),{cause:r}),ba(Yi(e,n)))),!1;var o=Error(i(520),{cause:r});if(o=Yi(o,n),fd===null?fd=[o]:fd.push(o),Y!==4&&(Y=2),t===null)return!0;r=Yi(r,n),n=t;do{switch(n.tag){case 3:return n.flags|=65536,e=a&-a,n.lanes|=e,e=Mc(n.stateNode,r,e),To(n,e),!1;case 1:if(t=n.type,o=n.stateNode,!(n.flags&128)&&(typeof t.getDerivedStateFromError==`function`||o!==null&&typeof o.componentDidCatch==`function`&&(yd===null||!yd.has(o))))return n.flags|=65536,a&=-a,n.lanes|=a,a=Nc(a),Pc(a,e,n,r),To(n,a),!1;break;case 22:if(n.memoizedState!==null)return n.flags|=65536,!1}n=n.return}while(n!==null);return!1}var Ic=Error(i(461)),B=!1;function Lc(e,t,n,r){t.child=e===null?vo(t,null,n,r):_o(t,e.child,n,r)}function Rc(e,t,n,r,i){n=n.render;var a=t.ref;if(`ref`in r){var o={};for(var s in r)s!==`ref`&&(o[s]=r[s])}else o=r;return Aa(t),r=rs(e,t,n,o,a,i),s=ss(),e!==null&&!B?(cs(e,t,i),fl(e,t,i)):(P&&s&&sa(t),t.flags|=1,Lc(e,t,r,i),t.child)}function zc(e,t,n,r,i){if(e===null){var a=n.type;return typeof a==`function`&&!Bi(a)&&a.defaultProps===void 0&&n.compare===null?(t.tag=15,t.type=a,Bc(e,t,a,r,i)):(e=Ui(n.type,null,r,t,t.mode,i),e.ref=t.ref,e.return=t,t.child=e)}if(a=e.child,!pl(e,i)){var o=a.memoizedProps;if(n=n.compare,n=n===null?Yr:n,n(o,r)&&e.ref===t.ref)return fl(e,t,i)}return t.flags|=1,e=Vi(a,r),e.ref=t.ref,e.return=t,t.child=e}function Bc(e,t,n,r,i){if(e!==null){var a=e.memoizedProps;if(Yr(a,r)&&e.ref===t.ref){if(B=!1,t.pendingProps=r=a,pl(e,i))e.flags&131072&&(B=!0);else return t.lanes=e.lanes,fl(e,t,i)}}return Jc(e,t,n,r,i)}function Vc(e,t,n,r){var i=r.children,a=e===null?null:e.memoizedState;if(e===null&&t.stateNode===null&&(t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),r.mode===`hidden`){if(t.flags&128){if(a=a===null?n:a.baseLanes|n,e!==null){for(r=t.child=e.child,i=0;r!==null;)i=i|r.lanes|r.childLanes,r=r.sibling;r=i&~a}else r=0,t.child=null;return Uc(e,t,a,n,r)}if(n&536870912)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&Qa(t,a===null?null:a.cachePool),a===null?Po():No(t,a),Bo(t);else return r=t.lanes=536870912,Uc(e,t,a===null?n:a.baseLanes|n,n,r)}else a===null?(e!==null&&Qa(t,null),Po(),Vo()):(Qa(t,a.cachePool),No(t,a),Vo(),t.memoizedState=null);return Lc(e,t,i,n),t.child}function Hc(e,t){return e!==null&&e.tag===22||t.stateNode!==null||(t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),t.sibling}function Uc(e,t,n,r,i){var a=Za();return a=a===null?null:{parent:F._currentValue,pool:a},t.memoizedState={baseLanes:n,cachePool:a},e!==null&&Qa(t,null),Po(),Bo(t),e!==null&&Oa(e,t,r,!0),t.childLanes=i,null}function Wc(e,t){return t=rl({mode:t.mode,children:t.children},e.mode),t.ref=e.ref,e.child=t,t.return=e,t}function Gc(e,t,n){return _o(t,e.child,null,n),e=Wc(t,t.pendingProps),e.flags|=2,Ho(t),t.memoizedState=null,e}function Kc(e,t,n){var r=t.pendingProps,a=!!(t.flags&128);if(t.flags&=-129,e===null){if(P){if(r.mode===`hidden`)return e=Wc(t,r),t.lanes=536870912,e.memoizedState={baseLanes:0,cachePool:null},Hc(null,e);if(zo(t),(e=N)?(e=am(e,fa),e=e!==null&&e.data===`&`?e:null,e!==null&&(t.memoizedState={dehydrated:e,treeContext:na===null?null:{id:ra,overflow:ia},retryLane:536870912,hydrationErrors:null},n=Ki(e),n.return=t,t.child=n,ua=t,N=null)):e=null,e===null)throw ma(t);return t.lanes=536870912,null}return Wc(t,r)}var o=e.memoizedState;if(o!==null){var s=o.dehydrated;if(zo(t),a){if(t.flags&256)t.flags&=-257,t=Gc(e,t,n);else if(t.memoizedState!==null)t.child=e.child,t.flags|=128,t=null;else throw Error(i(558))}else if(B||Oa(e,t,n,!1),a=(n&e.childLanes)!==0,B||a){if(jo.current===null){if(r=G,r!==null&&(s=Tt(r,n),s!==0&&s!==o.retryLane))throw o.retryLane=s,Pi(e,s),Pd(r,e,s),Ic;Kd()}t=Gc(e,t,n)}else e=o.treeContext,N=lm(s.nextSibling),ua=t,P=!0,da=null,fa=!1,e!==null&&la(t,e),t=Wc(t,r),t.flags|=134221824;return t}return e=Vi(e.child,{mode:r.mode,children:r.children}),e.ref=t.ref,t.child=e,e.return=t,e}function qc(e,t){var n=t.ref;if(n===null)e!==null&&e.ref!==null&&(t.flags|=4194816);else{if(typeof n!=`function`&&typeof n!=`object`)throw Error(i(284));(e===null||e.ref!==n)&&(t.flags|=4194816)}}function Jc(e,t,n,r,i){return Aa(t),n=rs(e,t,n,r,void 0,i),r=ss(),e!==null&&!B?(cs(e,t,i),fl(e,t,i)):(P&&r&&sa(t),t.flags|=1,Lc(e,t,n,i),t.child)}function Yc(e,t,n,r,i,a){return Aa(t),t.updateQueue=null,n=as(t,r,n,i),is(e),r=ss(),e!==null&&!B?(cs(e,t,a),fl(e,t,a)):(P&&r&&sa(t),t.flags|=1,Lc(e,t,n,a),t.child)}function Xc(e,t,n,r,i){if(Aa(t),t.stateNode===null){var a=Li,o=n.contextType;typeof o==`object`&&o&&(a=ja(o)),a=new n(r,a),t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,a.updater=Cc,t.stateNode=a,a._reactInternals=t,a=t.stateNode,a.props=r,a.state=t.memoizedState,a.refs={},bo(t),o=n.contextType,a.context=typeof o==`object`&&o?ja(o):Li,a.state=t.memoizedState,o=n.getDerivedStateFromProps,typeof o==`function`&&(Sc(t,n,o,r),a.state=t.memoizedState),typeof n.getDerivedStateFromProps==`function`||typeof a.getSnapshotBeforeUpdate==`function`||typeof a.UNSAFE_componentWillMount!=`function`&&typeof a.componentWillMount!=`function`||(o=a.state,typeof a.componentWillMount==`function`&&a.componentWillMount(),typeof a.UNSAFE_componentWillMount==`function`&&a.UNSAFE_componentWillMount(),o!==a.state&&Cc.enqueueReplaceState(a,a.state,null),Oo(t,r,a,i),Do(),a.state=t.memoizedState),typeof a.componentDidMount==`function`&&(t.flags|=4194308),r=!0}else if(e===null){a=t.stateNode;var s=t.memoizedProps,c=Ec(n,s);a.props=c;var l=a.context,u=n.contextType;o=Li,typeof u==`object`&&u&&(o=ja(u));var d=n.getDerivedStateFromProps;u=typeof d==`function`||typeof a.getSnapshotBeforeUpdate==`function`,s=t.pendingProps!==s,u||typeof a.UNSAFE_componentWillReceiveProps!=`function`&&typeof a.componentWillReceiveProps!=`function`||(s||l!==o)&&Tc(t,a,r,o),yo=!1;var f=t.memoizedState;a.state=f,Oo(t,r,a,i),Do(),l=t.memoizedState,s||f!==l||yo?(typeof d==`function`&&(Sc(t,n,d,r),l=t.memoizedState),(c=yo||wc(t,n,c,r,f,l,o))?(u||typeof a.UNSAFE_componentWillMount!=`function`&&typeof a.componentWillMount!=`function`||(typeof a.componentWillMount==`function`&&a.componentWillMount(),typeof a.UNSAFE_componentWillMount==`function`&&a.UNSAFE_componentWillMount()),typeof a.componentDidMount==`function`&&(t.flags|=4194308)):(typeof a.componentDidMount==`function`&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=l),a.props=r,a.state=l,a.context=o,r=c):(typeof a.componentDidMount==`function`&&(t.flags|=4194308),r=!1)}else{a=t.stateNode,xo(e,t),o=t.memoizedProps,u=Ec(n,o),a.props=u,d=t.pendingProps,f=a.context,l=n.contextType,c=Li,typeof l==`object`&&l&&(c=ja(l)),s=n.getDerivedStateFromProps,(l=typeof s==`function`||typeof a.getSnapshotBeforeUpdate==`function`)||typeof a.UNSAFE_componentWillReceiveProps!=`function`&&typeof a.componentWillReceiveProps!=`function`||(o!==d||f!==c)&&Tc(t,a,r,c),yo=!1,f=t.memoizedState,a.state=f,Oo(t,r,a,i),Do();var p=t.memoizedState;o!==d||f!==p||yo||e!==null&&e.dependencies!==null&&ka(e.dependencies)?(typeof s==`function`&&(Sc(t,n,s,r),p=t.memoizedState),(u=yo||wc(t,n,u,r,f,p,c)||e!==null&&e.dependencies!==null&&ka(e.dependencies))?(l||typeof a.UNSAFE_componentWillUpdate!=`function`&&typeof a.componentWillUpdate!=`function`||(typeof a.componentWillUpdate==`function`&&a.componentWillUpdate(r,p,c),typeof a.UNSAFE_componentWillUpdate==`function`&&a.UNSAFE_componentWillUpdate(r,p,c)),typeof a.componentDidUpdate==`function`&&(t.flags|=4),typeof a.getSnapshotBeforeUpdate==`function`&&(t.flags|=1024)):(typeof a.componentDidUpdate!=`function`||o===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),typeof a.getSnapshotBeforeUpdate!=`function`||o===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=p),a.props=r,a.state=p,a.context=c,r=u):(typeof a.componentDidUpdate!=`function`||o===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),typeof a.getSnapshotBeforeUpdate!=`function`||o===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),r=!1)}return a=r,qc(e,t),r=!!(t.flags&128),a||r?(a=t.stateNode,n=r&&typeof n.getDerivedStateFromError!=`function`?null:a.render(),t.flags|=1,e!==null&&r?(t.child=_o(t,e.child,null,i),t.child=_o(t,null,n,i)):Lc(e,t,n,i),t.memoizedState=a.state,e=t.child):e=fl(e,t,i),e}function Zc(e,t,n,r){return va(),t.flags|=256,Lc(e,t,n,r),t.child}var Qc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function $c(e){return{baseLanes:e,cachePool:$a()}}function el(e,t,n){return e=e===null?0:e.childLanes&~n,t&&(e|=ud),e}function tl(e,t,n){var r=t.pendingProps,i=!1,a=!!(t.flags&128),o;if((o=a)||(o=e!==null&&e.memoizedState===null?!1:!!(Uo.current&2)),o&&(i=!0,t.flags&=-129),o=!!(t.flags&32),t.flags&=-33,e===null){if(P){if(i?Ro(t):Vo(),(e=N)?(e=am(e,fa),e=e!==null&&e.data!==`&`?e:null,e!==null&&(t.memoizedState={dehydrated:e,treeContext:na===null?null:{id:ra,overflow:ia},retryLane:536870912,hydrationErrors:null},n=Ki(e),n.return=t,t.child=n,ua=t,N=null)):e=null,e===null)throw ma(t);return t.lanes=sm(e)?32:536870912,null}return a=r.children,r=r.fallback,i?(Vo(),i=t.mode,a=rl({mode:`hidden`,children:a},i),r=Wi(r,i,n,null),a.return=t,r.return=t,a.sibling=r,t.child=a,r=t.child,r.memoizedState=$c(n),r.childLanes=el(e,o,n),t.memoizedState=Qc,Hc(null,r)):(Ro(t),nl(t,a))}var s=e.memoizedState;if(s!==null){var c=s.dehydrated;if(c!==null)return al(e,t,a,o,r,c,s,n)}return i?(Vo(),i=r.fallback,a=t.mode,s=e.child,c=s.sibling,r=Vi(s,{mode:`hidden`,children:r.children}),r.subtreeFlags=s.subtreeFlags&1206910976,c===null?(i=Wi(i,a,n,null),i.flags|=2):i=Vi(c,i),i.return=t,r.return=t,r.sibling=i,t.child=r,Hc(null,r),r=t.child,i=e.child.memoizedState,i===null?i=$c(n):(a=i.cachePool,a===null?a=$a():(s=F._currentValue,a=a.parent===s?a:{parent:s,pool:s}),i={baseLanes:i.baseLanes|n,cachePool:a}),r.memoizedState=i,r.childLanes=el(e,o,n),t.memoizedState=Qc,Hc(e.child,r)):(Ro(t),n=e.child,e=n.sibling,n=Vi(n,{mode:`visible`,children:r.children}),n.return=t,n.sibling=null,e!==null&&(o=t.deletions,o===null?(t.deletions=[e],t.flags|=16):o.push(e)),t.child=n,t.memoizedState=null,n)}function nl(e,t){return t=rl({mode:`visible`,children:t},e.mode),t.return=e,e.child=t}function rl(e,t){return e=zi(22,e,null,t),e.lanes=0,e}function il(e,t,n){return _o(t,e.child,null,n),e=nl(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function al(e,t,n,r,a,o,s,c){if(n)return t.flags&256?(Ro(t),t.flags&=-257,il(e,t,c)):t.memoizedState===null?(Vo(),o=a.fallback,s=t.mode,a=rl({mode:`visible`,children:a.children},s),o=Wi(o,s,c,null),o.flags|=2,a.return=t,o.return=t,a.sibling=o,t.child=a,_o(t,e.child,null,c),a=t.child,a.memoizedState=$c(c),a.childLanes=el(e,r,c),t.memoizedState=Qc,Hc(null,a)):(Vo(),t.child=e.child,t.flags|=128,null);if(Ro(t),sm(o)){if(r=o.nextSibling&&o.nextSibling.dataset,r)var l=r.dgst;return r=l,r!==``&&(a=Error(i(419)),a.stack=``,a.digest=r,ba({value:a,source:null,stack:null})),il(e,t,c)}if(B||Oa(e,t,c,!1),r=(c&e.childLanes)!==0,B||r){if(jo.current!==null)return il(e,t,c);if(r=G,r!==null&&(a=Tt(r,c),a!==0&&a!==s.retryLane))throw s.retryLane=a,Pi(e,a),Pd(r,e,a),Ic;return om(o)||Kd(),il(e,t,c)}return om(o)?(t.flags|=192,t.child=e.child,null):(e=s.treeContext,N=lm(o.nextSibling),ua=t,P=!0,da=null,fa=!1,e!==null&&la(t,e),t=nl(t,a.children),t.flags|=134221824,t)}function ol(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Ea(e.return,t,n)}function sl(e){for(var t=null;e!==null;){var n=e.alternate;n!==null&&Ko(n)===null&&(t=e),e=e.sibling}return t}function cl(e,t,n,r,i,a){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:i,treeForkCount:a}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=i,o.treeForkCount=a)}function ll(e){var t=e.child;for(e.child=null;t!==null;){var n=t.sibling;t.sibling=e.child,e.child=t,t=n}}function ul(e,t,n){var r=t.pendingProps,i=r.revealOrder,a=r.tail;r=r.children;var o=Uo.current;if(t.flags&128)return Wo(t,o),null;var s=!!(o&2);if(s?(o=o&1|2,t.flags|=128):o&=1,Wo(t,o),i===`backwards`&&e!==null?(ll(e),Lc(e,t,r,n),ll(e)):Lc(e,t,r,n),r=P?$i:0,!s&&e!==null&&e.flags&128)a:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&ol(e,n,t);else if(e.tag===19)ol(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break a;for(;e.sibling===null;){if(e.return===null||e.return===t)break a;e=e.return}e.sibling.return=e.return,e=e.sibling}switch(i){case`backwards`:n=sl(t.child),n===null?(i=t.child,t.child=null):(i=n.sibling,n.sibling=null,ll(t)),cl(t,!0,i,null,a,r);break;case`unstable_legacy-backwards`:for(n=null,i=t.child,t.child=null;i!==null;){if(e=i.alternate,e!==null&&Ko(e)===null){t.child=i;break}e=i.sibling,i.sibling=n,n=i,i=e}cl(t,!0,n,null,a,r);break;case`together`:cl(t,!1,null,null,void 0,r);break;case`independent`:t.memoizedState=null;break;default:n=sl(t.child),n===null?(i=t.child,t.child=null):(i=n.sibling,n.sibling=null),cl(t,!1,i,n,a,r)}return t.child}function dl(e,t,n){var r=t.pendingProps;return wa(t,t.type,r.value),Lc(e,t,r.children,n),t.child}function fl(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),sd|=t.lanes,(n&t.childLanes)===0){if(e!==null){if(Oa(e,t,n,!1),(n&t.childLanes)===0)return null}else return null}if(e!==null&&t.child!==e.child)throw Error(i(153));if(t.child!==null){for(e=t.child,n=Vi(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Vi(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function pl(e,t){return(e.lanes&t)!==0||(e=e.dependencies,!!(e!==null&&ka(e)))}function ml(e,t,n){switch(t.tag){case 3:Ne(t,t.stateNode.containerInfo),wa(t,F,e.memoizedState.cache),va();break;case 27:case 5:Fe(t);break;case 4:Ne(t,t.stateNode.containerInfo);break;case 10:wa(t,t.type,t.memoizedProps.value);break;case 31:if(t.memoizedState!==null)return t.flags|=128,zo(t),null;break;case 13:var r=t.memoizedState;if(r!==null){if(r.dehydrated!==null)return Ro(t),t.flags|=128,null;r=Oa(e,t,n,!1);var i=t.child.childLanes;return r||(n&i)!==0?tl(e,t,n):(Ro(t),e=fl(e,t,n),e===null?null:e.sibling)}Ro(t);break;case 19:if(t.flags&128)return ul(e,t,n);if(i=!!(e.flags&128),r=(n&t.childLanes)!==0,r||=(Oa(e,t,n,!1),(n&t.childLanes)!==0),i){if(r)return ul(e,t,n);t.flags|=128}if(i=t.memoizedState,i!==null&&(i.rendering=null,i.tail=null,i.lastEffect=null),Wo(t,Uo.current),r)break;return null;case 22:return t.lanes=0,Vc(e,t,n,t.pendingProps);case 24:wa(t,F,e.memoizedState.cache)}return fl(e,t,n)}function hl(e,t,n){if(e!==null){if(e.memoizedProps!==t.pendingProps)B=!0;else{if(!pl(e,n)&&!(t.flags&128))return B=!1,ml(e,t,n);B=!!(e.flags&131072)}}else B=!1,P&&t.flags&1048576&&oa(t,$i,t.index);switch(t.lanes=0,t.tag){case 16:a:{var r=t.pendingProps;if(e=oo(t.elementType),t.type=e,typeof e==`function`)Bi(e)?(r=Ec(e,r),t.tag=1,t=Xc(null,t,e,r,n)):(t.tag=0,t=Jc(null,t,e,r,n));else{if(e!=null){var a=e.$$typeof;if(a===le){t.tag=11,t=Rc(null,t,e,r,n);break a}if(a===fe){t.tag=14,t=zc(null,t,e,r,n);break a}if(a===E){t.tag=10,t.type=e,t=dl(null,t,n);break a}}throw t=Se(e)||e,Error(i(306,t,``))}}return t;case 0:return Jc(e,t,t.type,t.pendingProps,n);case 1:return r=t.type,a=Ec(r,t.pendingProps),Xc(e,t,r,a,n);case 3:a:{if(Ne(t,t.stateNode.containerInfo),e===null)throw Error(i(387));r=t.pendingProps;var o=t.memoizedState;a=o.element,xo(e,t),Oo(t,r,null,n);var s=t.memoizedState;if(r=s.cache,wa(t,F,r),r!==o.cache&&Da(t,[F],n,!0),Do(),r=s.element,o.isDehydrated){if(o={element:r,isDehydrated:!1,cache:s.cache},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){t=Zc(e,t,r,n);break a}if(r!==a){a=Yi(Error(i(424)),t),ba(a),t=Zc(e,t,r,n);break a}switch(e=t.stateNode.containerInfo,e.nodeType){case 9:e=e.body;break;default:e=e.nodeName===`HTML`?e.ownerDocument.body:e}for(N=lm(e.firstChild),ua=t,P=!0,da=null,fa=!0,n=vo(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|134221824,n=n.sibling}else{if(va(),r===a){t=fl(e,t,n);break a}Lc(e,t,r,n)}t=t.child}return t;case 26:return qc(e,t),e===null?(n=Nm(t.type,null,t.pendingProps,null))?t.memoizedState=n:P||(t.stateNode=fp(t.type,t.pendingProps,je.current,t)):t.memoizedState=Nm(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return Fe(t),e===null&&P&&(r=t.stateNode=hm(t.type,t.pendingProps,je.current),ua=t,fa=!0,a=N,Sp(t.type)?(um=a,N=lm(r.firstChild)):N=a),Lc(e,t,t.pendingProps.children,n),qc(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&P&&((a=r=N)&&(r=rm(r,t.type,t.pendingProps,fa),r===null?a=!1:(t.stateNode=r,ua=t,N=lm(r.firstChild),fa=!1,a=!0)),a||ma(t)),Fe(t),a=t.type,o=t.pendingProps,s=e===null?null:e.memoizedProps,r=o.children,pp(a,o)?r=null:s!==null&&pp(a,s)&&(t.flags|=32),t.memoizedState!==null&&(a=rs(e,t,os,null,null,n),sh._currentValue=a),qc(e,t),Lc(e,t,r,n),t.child;case 6:return e===null&&P&&((e=n=N)&&(n=im(n,t.pendingProps,fa),n===null?e=!1:(t.stateNode=n,ua=t,N=null,e=!0)),e||ma(t)),null;case 13:return tl(e,t,n);case 4:return Ne(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=_o(t,null,r,n):Lc(e,t,r,n),t.child;case 11:return Rc(e,t,t.type,t.pendingProps,n);case 7:return r=t.pendingProps,qc(e,t),Lc(e,t,r,n),t.child;case 8:return Lc(e,t,t.pendingProps.children,n),t.child;case 12:return Lc(e,t,t.pendingProps.children,n),t.child;case 10:return dl(e,t,n);case 9:return a=t.type._context,r=t.pendingProps.children,Aa(t),a=ja(a),r=r(a),t.flags|=1,Lc(e,t,r,n),t.child;case 14:return zc(e,t,t.type,t.pendingProps,n);case 15:return Bc(e,t,t.type,t.pendingProps,n);case 19:return ul(e,t,n);case 31:return Kc(e,t,n);case 22:return Vc(e,t,n,t.pendingProps);case 24:return Aa(t),r=ja(F),e===null?(a=Za(),a===null&&(a=G,o=La(),a.pooledCache=o,o.refCount++,o!==null&&(a.pooledCacheLanes|=n),a=o),t.memoizedState={parent:r,cache:a},bo(t),wa(t,F,a)):((e.lanes&n)!==0&&(xo(e,t),Oo(t,null,null,n),Do()),a=e.memoizedState,o=t.memoizedState,a.parent===r?(r=o.cache,wa(t,F,r),r!==a.cache&&Da(t,[F],n,!0)):(a={parent:r,cache:r},t.memoizedState=a,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=a),wa(t,F,r))),Lc(e,t,t.pendingProps.children,n),t.child;case 30:return t.stateNode===null&&(t.stateNode={autoName:null,paired:null,clones:null,ref:null}),r=t.pendingProps,r.name!=null&&r.name!==`auto`?t.flags|=e===null?18882560:18874368:P&&sa(t),e!==null&&e.memoizedProps.name!==r.name?t.flags|=4194816:qc(e,t),Lc(e,t,r.children,n),t.child;case 29:throw t.pendingProps}throw Error(i(156,t.tag))}function gl(e){e.flags|=4}function _l(e,t,n,r,i){var a;if((a=!!(e.mode&32))&&(a=n===null?Jm(t,r):Jm(t,r)&&(r.src!==n.src||r.srcSet!==n.srcSet)),a){if(e.flags|=16777216,(i&335544128)===i){if(e.stateNode.complete)e.flags|=8192;else if(Ud())e.flags|=8192;else throw so=ro,to}}else e.flags&=-16777217}function vl(e,t){if(t.type!==`stylesheet`||t.state.loading&4)e.flags&=-16777217;else if(e.flags|=16777216,!Ym(t)){if(Ud())e.flags|=8192;else throw so=ro,to}}function yl(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag===22?536870912:yt(),e.lanes|=t,dd|=t)}function bl(e,t){if(!P)switch(e.tailMode){case`visible`:break;case`collapsed`:for(var n=e.tail,r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null;break;default:for(t=e.tail,n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null}}function V(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var i=e.child;i!==null;)n|=i.lanes|i.childLanes,r|=i.subtreeFlags&1206910976,r|=i.flags&1206910976,i.return=e,i=i.sibling;else for(i=e.child;i!==null;)n|=i.lanes|i.childLanes,r|=i.subtreeFlags,r|=i.flags,i.return=e,i=i.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function xl(e,t,n){var r=t.pendingProps;switch(ca(t),t.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return V(t),null;case 1:return V(t),null;case 3:return n=t.stateNode,r=null,e!==null&&(r=e.memoizedState.cache),t.memoizedState.cache!==r&&(t.flags|=2048),Ta(F),Pe(),n.pendingContext&&(n.context=n.pendingContext,n.pendingContext=null),(e===null||e.child===null)&&(_a(t)?gl(t):e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,ya())),V(t),null;case 26:var a=t.type,o=t.memoizedState;return e===null?(gl(t),o===null?(V(t),_l(t,a,null,r,n)):(V(t),vl(t,o))):o?o===e.memoizedState?(V(t),t.flags&=-16777217):(gl(t),V(t),vl(t,o)):(e=e.memoizedProps,e!==r&&gl(t),V(t),_l(t,a,e,r,n)),null;case 27:if(Ie(t),n=je.current,a=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==r&&gl(t);else{if(!r){if(t.stateNode===null)throw Error(i(166));return V(t),t.subtreeFlags&=-33554433,null}e=ke.current,_a(t)?ha(t,e):(e=hm(a,r,n),t.stateNode=e,gl(t))}return V(t),t.subtreeFlags&=-33554433,null;case 5:if(Ie(t),a=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==r&&gl(t);else{if(!r){if(t.stateNode===null)throw Error(i(166));return V(t),t.subtreeFlags&=-33554433,null}if(o=ke.current,_a(t))ha(t,o);else{var s=lp(je.current);switch(o){case 1:o=s.createElementNS(`http://www.w3.org/2000/svg`,a);break;case 2:o=s.createElementNS(`http://www.w3.org/1998/Math/MathML`,a);break;default:switch(a){case`svg`:o=s.createElementNS(`http://www.w3.org/2000/svg`,a);break;case`math`:o=s.createElementNS(`http://www.w3.org/1998/Math/MathML`,a);break;case`script`:o=s.createElement(`div`),o.innerHTML=`<script><\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[jt]=t,o[Mt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(np(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&gl(t)}}return V(t),t.subtreeFlags&=-33554433,_l(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&gl(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=je.current,_a(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=ua,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[jt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||ep(e.nodeValue,n)),e||ma(t,!0)}else e=lp(e).createTextNode(r),e[jt]=t,t.stateNode=e}return V(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=_a(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[jt]=t}else va(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;V(t),e=!1}else n=ya(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Ho(t),t):(Ho(t),null);if(t.flags&128)throw Error(i(558))}return V(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=_a(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[jt]=t}else va(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;V(t),a=!1}else a=ya(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Ho(t),t):(Ho(t),null)}return Ho(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),yl(t,t.updateQueue),V(t),null);case 4:return Pe(),e===null&&Wf(t.stateNode.containerInfo),t.flags|=67108864,V(t),null;case 10:return Ta(t.type),V(t),null;case 19:if(Go(t),r=t.memoizedState,r===null)return V(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)bl(r,!1);else{if(Y!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=Ko(e),o!==null){for(t.flags|=128,bl(r,!1),e=o.updateQueue,t.updateQueue=e,yl(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Hi(n,e),n=n.sibling;return Wo(t,Uo.current&1|2),P&&aa(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ye()>_d&&(t.flags|=128,a=!0,bl(r,!1),t.lanes=4194304)}}else{if(!a){if(e=Ko(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,yl(t,e),bl(r,!0),r.tail===null&&r.tailMode!==`collapsed`&&r.tailMode!==`visible`&&!o.alternate&&!P)return V(t),null}else 2*Ye()-r.renderingStartTime>_d&&n!==536870912&&(t.flags|=128,a=!0,bl(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}if(r.tail!==null){e=r.tail;a:{for(n=e;n!==null;){if(n.alternate!==null){n=!1;break a}n=n.sibling}n=!0}return r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ye(),e.sibling=null,o=Uo.current,o=a?o&1|2:o&1,r.tailMode===`visible`||r.tailMode===`collapsed`||!n||P?Wo(t,o):(n=o,k(Io,t),k(Uo,n),Lo===null&&(Lo=t)),P&&aa(t,r.treeForkCount),e}return V(t),null;case 22:case 23:return Ho(t),Fo(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(V(t),t.subtreeFlags&6&&(t.flags|=8192)):V(t),n=t.updateQueue,n!==null&&yl(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&Oe(Xa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Ta(F),V(t),null;case 25:return null;case 30:return t.flags|=33554432,V(t),null}throw Error(i(156,t.tag))}function Sl(e,t){switch(ca(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ta(F),Pe(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Ie(t),null;case 31:if(t.memoizedState!==null){if(Ho(t),t.alternate===null)throw Error(i(340));va()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Ho(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));va()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Go(t),e=t.flags,e&65536?(t.flags=e&-65537|128,e=t.memoizedState,e!==null&&(e.rendering=null,e.tail=null),t.flags|=4,t):null;case 4:return Pe(),null;case 10:return Ta(t.type),null;case 22:case 23:return Ho(t),Fo(),e!==null&&Oe(Xa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ta(F),null;case 25:return null;default:return null}}function Cl(e,t){switch(ca(t),t.tag){case 3:Ta(F),Pe();break;case 26:case 27:case 5:Ie(t);break;case 4:Pe();break;case 31:t.memoizedState!==null&&Ho(t);break;case 13:Ho(t);break;case 19:Go(t);break;case 10:Ta(t.type);break;case 22:case 23:Ho(t),Fo(),e!==null&&Oe(Xa);break;case 24:Ta(F)}}function wl(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Tl(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function El(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ao(t,n)}catch(t){Z(e,e.return,t)}}}function Dl(e,t,n){n.props=Ec(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Ol(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:var i=e.stateNode,a=wi(e.memoizedProps,i);(i.ref===null||i.ref.name!==a)&&(i.ref=Pp(a)),r=i.ref;break;case 7:if(e.stateNode===null){var o=new Fp(e);h(e.child,!1,Qp,o,void 0,void 0),e.stateNode=o}r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function kl(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}}function Al(e,t){if((e.tag===5||e.tag===27||e.tag===6)&&e.alternate===null&&t!==null)for(var n=0;n<t.length;n++)em(e.stateNode,t[n])}function jl(e){for(var t=e.return;t!==null&&(Pl(t)&&em(e.stateNode,t.stateNode),!Nl(t));)t=t.return}function Ml(e){for(var t=e.return;t!==null&&(Pl(t)&&tm(e.stateNode,t.stateNode),!Nl(t));)t=t.return}function Nl(e){return e.tag===5||e.tag===3||e.tag===27}function Pl(e){return e&&e.tag===7&&e.stateNode!==null}function Fl(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Il(e,t,n){try{var r=e.stateNode;ip(r,e.type,n,t),r[Mt]=t}catch(t){Z(e,e.return,t)}}function Ll(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Sp(e.type)||e.tag===4}function Rl(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Ll(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Sp(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function zl(e,t,n,r){var i=e.tag;if(i===5||i===6)i=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(i,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(i),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=En)),Al(e,r),j=!0;else if(i!==4&&(i===27&&(Al(e,r),r=null,Sp(e.type)&&(n=e.stateNode,t=null)),e=e.child,e!==null))for(zl(e,t,n,r),e=e.sibling;e!==null;)zl(e,t,n,r),e=e.sibling}function Bl(e,t,n,r){var i=e.tag;if(i===5||i===6)i=e.stateNode,t?n.insertBefore(i,t):n.appendChild(i),Al(e,r),j=!0;else if(i!==4&&(i===27&&(Al(e,r),r=null,Sp(e.type)&&(n=e.stateNode)),e=e.child,e!==null))for(Bl(e,t,n,r),e=e.sibling;e!==null;)Bl(e,t,n,r),e=e.sibling}function Vl(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);np(t,r,n),t[jt]=e,t[Mt]=n}catch(t){Z(e,e.return,t)}}var Hl=!1,Ul=null;function Wl(e){(e.tag===30||e.subtreeFlags&33554432)&&(Hl=!0)}var Gl=null;function Kl(){var e=Gl;return Gl=null,e}var ql=0;function Jl(e,t,n,r,i){return ql=0,Yl(e.child,t,n,r,i)}function Yl(e,t,n,r,i){for(var a=!1;e!==null;){if(e.tag===5){var o=e.stateNode;if(r!==null){var s=Op(o);r.push(s),s.view&&(a=!0)}else a||Op(o).view&&(a=!0);Hl=!0,Tp(o,ql===0?t:t+`_`+ql,n),ql++}else(e.tag!==22||e.memoizedState===null)&&(e.tag===30&&i||Yl(e.child,t,n,r,i)&&(a=!0));e=e.sibling}return a}function Xl(e,t){for(;e!==null;)e.tag===5?Ep(e.stateNode,e.memoizedProps):(e.tag!==22||e.memoizedState===null)&&(e.tag===30&&t||Xl(e.child,t)),e=e.sibling}function Zl(e){if(e.subtreeFlags&18874368)for(e=e.child;e!==null;){if((e.tag!==22||e.memoizedState===null)&&(Zl(e),e.tag===30&&e.flags&18874368&&e.stateNode.paired)){var t=e.memoizedProps;if(t.name==null||t.name===`auto`)throw Error(i(544));var n=t.name;t=Ei(t.default,t.share),t!==`none`&&(Jl(e,n,t,null,!1)||Xl(e.child,!1))}e=e.sibling}}function Ql(e,t){if(e.tag===30){var n=e.stateNode,r=e.memoizedProps,i=wi(r,n),a=Ei(r.default,n.paired?r.share:r.enter);a===`none`?Zl(e):Jl(e,i,a,null,!1)?(Zl(e),n.paired||t||Nd(e,r.onEnter)):Xl(e.child,!1)}else if(e.subtreeFlags&33554432)for(e=e.child;e!==null;)Ql(e,t),e=e.sibling;else Zl(e)}function $l(e){if(Ul!==null&&Ul.size!==0){var t=Ul;if(e.subtreeFlags&18874368)for(e=e.child;e!==null;){if(e.tag!==22||e.memoizedState===null){if(e.tag===30&&e.flags&18874368){var n=e.memoizedProps,r=n.name;if(r!=null&&r!==`auto`){var i=t.get(r);if(i!==void 0){var a=Ei(n.default,n.share);if(a!==`none`&&(Jl(e,r,a,null,!1)?(a=e.stateNode,i.paired=a,a.paired=i,Nd(e,n.onShare)):Xl(e.child,!1)),t.delete(r),t.size===0)break}}}$l(e)}e=e.sibling}}}function eu(e){if(e.tag===30){var t=e.memoizedProps,n=wi(t,e.stateNode),r=Ul===null?void 0:Ul.get(n),i=Ei(t.default,r===void 0?t.exit:t.share);i!==`none`&&(Jl(e,n,i,null,!1)?r===void 0?Nd(e,t.onExit):(i=e.stateNode,r.paired=i,i.paired=r,Ul.delete(n),Nd(e,t.onShare)):Xl(e.child,!1)),Ul!==null&&$l(e)}else if(e.subtreeFlags&33554432)for(e=e.child;e!==null;)eu(e),e=e.sibling;else Ul!==null&&$l(e)}function tu(e){for(e=e.child;e!==null;){if(e.tag===30){var t=e.memoizedProps,n=wi(t,e.stateNode);t=Ei(t.default,t.update),e.flags&=-5,t!==`none`&&Jl(e,n,t,e.memoizedState=[],!1)}else e.subtreeFlags&33554432&&tu(e);e=e.sibling}}function nu(e){if(e.subtreeFlags&18874368)for(e=e.child;e!==null;){if(e.tag!==22||e.memoizedState===null){if(e.tag===30&&e.flags&18874368){var t=e.stateNode;t.paired!==null&&(t.paired=null,Xl(e.child,!1))}nu(e)}e=e.sibling}}function ru(e){if(e.tag===30)e.stateNode.paired=null,Xl(e.child,!1),nu(e);else if(e.subtreeFlags&33554432)for(e=e.child;e!==null;)ru(e),e=e.sibling;else nu(e)}function iu(e){for(e=e.child;e!==null;)e.tag===30?Xl(e.child,!1):e.subtreeFlags&33554432&&iu(e),e=e.sibling}function au(e,t,n,r,i,a,o){for(var s=!1;t!==null;){if(t.tag===5){var c=t.stateNode;if(a!==null&&ql<a.length){var l=a[ql],u=Op(c);(l.view||u.view)&&(s=!0);var d;if(d=!(e.flags&4)){if(u.clip)d=!0;else{d=l.rect;var f=u.rect;d=d.y!==f.y||d.x!==f.x||d.height!==f.height||d.width!==f.width}}d&&(e.flags|=4),u.abs?u=!l.abs:(l=l.rect,u=u.rect,u=l.height!==u.height||l.width!==u.width),u&&(e.flags|=32)}else e.flags|=32;e.flags&4&&Tp(c,ql===0?n:n+`_`+ql,i),s&&e.flags&4||(Gl===null&&(Gl=[]),Gl.push(c,ql===0?r:r+`_`+ql,t.memoizedProps)),ql++}else(t.tag!==22||t.memoizedState===null)&&(t.tag===30&&o?e.flags|=t.flags&32:au(e,t.child,n,r,i,a,o)&&(s=!0));t=t.sibling}return s}function ou(e,t){for(e=e.child;e!==null;){if(e.tag===30){var n=e.memoizedProps,r=e.stateNode,i=wi(n,r),a=Ei(n.default,n.update);if(t){r=r.clones;var o=r===null?null:r.map(kp)}else o=e.memoizedState,e.memoizedState=null;r=e;var s=e.child;ql=0,i=au(r,s,i,i,a,o,!1),e.flags&4&&i&&(t||Nd(e,n.onUpdate))}else e.subtreeFlags&33554432&&ou(e,t);e=e.sibling}}var su=!1,H=!1,cu=!1,lu=!1,uu=typeof WeakSet==`function`?WeakSet:Set,du=null,fu=!1,pu=!1,mu=!1,hu=!1;function gu(e,t,n){if(e=e.containerInfo,sp=gh,e=ei(e),ti(e)){if(`selectionStart`in e)var r={start:e.selectionStart,end:e.selectionEnd};else a:{r=(r=e.ownerDocument)&&r.defaultView||window;var i=r.getSelection&&r.getSelection();if(i&&i.rangeCount!==0){r=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{r.nodeType,o.nodeType}catch{r=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==r||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===r&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}r=c===-1||l===-1?null:{start:c,end:l}}else r=null}r||={start:0,end:0}}else r=null;for(cp={focusedElem:e,selectionRange:r},gh=!1,n=(n&335544064)===n,du=t,t=n?9270:1024;du!==null;){if(e=du,n&&(r=e.deletions,r!==null))for(a=0;a<r.length;a++)n&&eu(r[a]);if(e.alternate===null&&e.flags&2)n&&Wl(e),_u(n);else{if(e.tag===22){if(r=e.alternate,e.memoizedState!==null){r!==null&&r.memoizedState===null&&n&&eu(r),_u(n);continue}if(r!==null&&r.memoizedState!==null){n&&Wl(e),_u(n);continue}}r=e.child,(e.subtreeFlags&t)!==0&&r!==null?(r.return=e,du=r):(n&&tu(e),_u(n))}}Ul=null}function _u(e){for(;du!==null;){var t=du,n=e,r=t.alternate,a=t.flags;switch(t.tag){case 0:case 11:case 15:break;case 1:if(a&1024&&r!==null){n=void 0,a=r.memoizedProps,r=r.memoizedState;var o=t.stateNode;try{var s=Ec(t.type,a);n=o.getSnapshotBeforeUpdate(s,r),o.__reactInternalSnapshotBeforeUpdate=n}catch(e){Z(t,t.return,e)}}break;case 3:if(a&1024){if(r=t.stateNode.containerInfo,n=r.nodeType,n===9)nm(r);else if(n===1)switch(r.nodeName){case`HEAD`:case`HTML`:case`BODY`:nm(r);break;default:r.textContent=``}}break;case 5:case 26:case 27:case 6:case 4:case 17:break;case 30:n&&r!==null&&(n=wi(r.memoizedProps,r.stateNode),a=t.memoizedProps,a=Ei(a.default,a.update),a!==`none`&&Jl(r,n,a,r.memoizedState=[],!0));break;default:if(a&1024)throw Error(i(163))}if(r=t.sibling,r!==null){r.return=t.return,du=r;break}du=t.return}}function vu(e,t,n){var r=n.flags;switch(n.tag){case 0:case 11:case 15:Lu(e,n),r&4&&wl(5,n);break;case 1:if(Lu(e,n),r&4){if(e=n.stateNode,t===null)try{e.componentDidMount()}catch(e){Z(n,n.return,e)}else{var i=Ec(n.type,t.memoizedProps);t=t.memoizedState;try{e.componentDidUpdate(i,t,e.__reactInternalSnapshotBeforeUpdate)}catch(e){Z(n,n.return,e)}}}r&64&&El(n),r&512&&Ol(n,n.return);break;case 3:if(Lu(e,n),r&64&&(e=n.updateQueue,e!==null)){if(t=null,n.child!==null)switch(n.child.tag){case 27:case 5:t=n.child.stateNode;break;case 1:t=n.child.stateNode}try{Ao(e,t)}catch(e){Z(n,n.return,e)}}break;case 27:t===null&&r&4&&Vl(n);case 26:case 5:Lu(e,n),t===null&&r&4&&Fl(n),r&512&&Ol(n,n.return);break;case 12:Lu(e,n);break;case 31:Lu(e,n),r&4&&Eu(e,n);break;case 13:Lu(e,n),r&4&&Du(e,n),r&64&&(e=n.memoizedState,e!==null&&(e=e.dehydrated,e!==null&&(n=_f.bind(null,n),cm(e,n))));break;case 22:if(r=n.memoizedState!==null||su,!r){var a=t!==null&&t.memoizedState!==null||H;t=su,i=H,su=r,(H=a)&&!i?(r=2,n.subtreeFlags&8772&&(r|=1),zu(e,n,r)):Lu(e,n),su=t,H=i}break;case 30:Lu(e,n),r&512&&Ol(n,n.return);break;case 7:r&512&&Ol(n,n.return);default:Lu(e,n)}}function yu(e,t){for(e=e.child;e!==null;)bu(e,t),e=e.sibling}function bu(e,t){switch(e.tag){case 5:case 26:try{var n=e.stateNode;if(t){var r=n.style;typeof r.setProperty==`function`?r.setProperty(`display`,`none`,`important`):r.display=`none`}else{var i=e.stateNode,a=e.memoizedProps.style,o=a!=null&&a.hasOwnProperty(`display`)?a.display:null;i.style.display=o==null||typeof o==`boolean`?``:(``+o).trim()}}catch(t){Z(e,e.return,t)}xu(e,t);break;case 6:try{e.stateNode.nodeValue=t?``:e.memoizedProps,j=!0}catch(t){Z(e,e.return,t)}break;case 18:try{var s=e.stateNode;t?wp(s,!0):wp(e.stateNode,!1)}catch(t){Z(e,e.return,t)}break;case 22:case 23:e.memoizedState===null&&yu(e,t);break;default:yu(e,t)}}function xu(e,t){if(e.subtreeFlags&67108864)for(e=e.child;e!==null;){a:{var n=e,r=t;switch(n.tag){case 4:bu(n,r);break a;case 22:n.memoizedState===null&&xu(n,r);break a;default:xu(n,r)}}e=e.sibling}}function Su(e){var t=e.alternate;t!==null&&(e.alternate=null,Su(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&Bt(t)),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}var U=null,Cu=!1;function wu(e,t,n){for(n=n.child;n!==null;)Tu(e,t,n),n=n.sibling}function Tu(e,t,n){if(at&&typeof at.onCommitFiberUnmount==`function`)try{at.onCommitFiberUnmount(it,n)}catch{}switch(n.tag){case 26:H||kl(n,t),wu(e,t,n),n.memoizedState?n.memoizedState.count--:n.stateNode&&!H&&(n=n.stateNode,n.parentNode.removeChild(n));break;case 27:H||kl(n,t),Ml(n);var r=U,i=Cu;Sp(n.type)&&(U=n.stateNode,Cu=!1),wu(e,t,n),gm(n.stateNode,n.type,n.memoizedProps),U=r,Cu=i;break;case 5:H||kl(n,t),Ml(n);case 6:if(n.tag===6&&Ml(n),r=U,i=Cu,U=null,wu(e,t,n),U=r,Cu=i,U!==null){if(Cu)try{(U.nodeType===9?U.body:U.nodeName===`HTML`?U.ownerDocument.body:U).removeChild(n.stateNode),j=!0}catch(e){Z(n,t,e)}else try{U.removeChild(n.stateNode),j=!0}catch(e){Z(n,t,e)}}break;case 18:U!==null&&(Cu?(e=U,Cp(e.nodeType===9?e.body:e.nodeName===`HTML`?e.ownerDocument.body:e,n.stateNode),Hh(e)):Cp(U,n.stateNode));break;case 4:r=U,i=Cu,U=n.stateNode.containerInfo,Cu=!0,wu(e,t,n),U=r,Cu=i;break;case 0:case 11:case 14:case 15:Tl(2,n,t),H||Tl(4,n,t),wu(e,t,n);break;case 1:H||(kl(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`&&Dl(n,t,r)),wu(e,t,n);break;case 21:wu(e,t,n);break;case 22:H=(r=H)||n.memoizedState!==null,wu(e,t,n),H=r;break;case 30:kl(n,t),wu(e,t,n);break;case 7:H||kl(n,t),wu(e,t,n);break;default:wu(e,t,n)}}function Eu(e,t){if(t.memoizedState===null&&(e=t.alternate,e!==null&&(e=e.memoizedState,e!==null))){e=e.dehydrated;try{Hh(e)}catch(e){Z(t,t.return,e)}}}function Du(e,t){if(t.memoizedState===null&&(e=t.alternate,e!==null&&(e=e.memoizedState,e!==null&&(e=e.dehydrated,e!==null))))try{Hh(e)}catch(e){Z(t,t.return,e)}}function Ou(e){switch(e.tag){case 31:case 13:case 19:var t=e.stateNode;return t===null&&(t=e.stateNode=new uu),t;case 22:return e=e.stateNode,t=e._retryCache,t===null&&(t=e._retryCache=new uu),t;default:throw Error(i(435,e.tag))}}function ku(e,t){var n=Ou(e);t.forEach(function(t){if(!n.has(t)){n.add(t);var r=vf.bind(null,e,t);t.then(r,r)}})}function Au(e,t,n){var r=t.deletions;if(r!==null)for(var a=0;a<r.length;a++){var o=r[a],s=e,c=t,l=c;a:for(;l!==null;){switch(l.tag){case 27:if(Sp(l.type)){U=l.stateNode,Cu=!1;break a}break;case 5:U=l.stateNode,Cu=!1;break a;case 3:case 4:U=l.stateNode.containerInfo,Cu=!0;break a}l=l.return}if(U===null)throw Error(i(160));Tu(s,c,o),U=null,Cu=!1,s=o.alternate,s!==null&&(s.return=null),o.return=null}if(t.subtreeFlags&13886)for(t=t.child;t!==null;)Mu(t,e,n),t=t.sibling}var ju=null;function Mu(e,t,n){var r=e.alternate,a=e.flags;switch(e.tag){case 0:case 11:case 14:case 15:if(a&4&&(r=e.updateQueue,r=r===null?null:r.events,r!==null))for(var o=0;o<r.length;o++){var s=r[o];s.ref.impl=s.nextImpl}Au(t,e,n),Nu(e),a&4&&(Tl(3,e,e.return),wl(3,e),Tl(5,e,e.return));break;case 1:Au(t,e,n),Nu(e),a&512&&(H||r===null||kl(r,r.return)),a&64&&su&&(e=e.updateQueue,e!==null&&(t=e.callbacks,t!==null&&(n=e.shared.hiddenCallbacks,e.shared.hiddenCallbacks=n===null?t:n.concat(t))));break;case 26:if(o=ju,Au(t,e,n),Nu(e),a&512&&(H||r===null||kl(r,r.return)),a&4){if(a=r===null?null:r.memoizedState,n=e.memoizedState,r===null){if(n===null){if(e.stateNode===null){if(su)e.stateNode=fp(e.type,e.memoizedProps,t.containerInfo,e);else{a:{t=e.type,n=e.memoizedProps,a=o.ownerDocument||o;b:switch(t){case`title`:r=a.getElementsByTagName(`title`)[0],(!r||r[Rt]||r[jt]||r.namespaceURI===`http://www.w3.org/2000/svg`||r.hasAttribute(`itemprop`))&&(r=a.createElement(t),a.head.insertBefore(r,a.querySelector(`head > title`))),np(r,t,n),r[jt]=e,A(r),t=r;break a;case`link`:if(o=Gm(`link`,`href`,a).get(t+(n.href||``))){for(s=0;s<o.length;s++)if(r=o[s],r.getAttribute(`href`)===(n.href==null||n.href===``?null:n.href)&&r.getAttribute(`rel`)===(n.rel==null?null:n.rel)&&r.getAttribute(`title`)===(n.title==null?null:n.title)&&r.getAttribute(`crossorigin`)===(n.crossOrigin==null?null:n.crossOrigin)){o.splice(s,1);break b}}r=a.createElement(t),np(r,t,n),a.head.appendChild(r);break;case`meta`:if(o=Gm(`meta`,`content`,a).get(t+(n.content||``))){for(s=0;s<o.length;s++)if(r=o[s],r.getAttribute(`content`)===(n.content==null?null:``+n.content)&&r.getAttribute(`name`)===(n.name==null?null:n.name)&&r.getAttribute(`property`)===(n.property==null?null:n.property)&&r.getAttribute(`http-equiv`)===(n.httpEquiv==null?null:n.httpEquiv)&&r.getAttribute(`charset`)===(n.charSet==null?null:n.charSet)){o.splice(s,1);break b}}r=a.createElement(t),np(r,t,n),a.head.appendChild(r);break;default:throw Error(i(468,t))}r[jt]=e,A(r),t=r}e.stateNode=t}}else su||Km(o,e.type,e.stateNode)}else e.stateNode=Bm(o,n,e.memoizedProps)}else a===n?n===null&&e.stateNode!==null&&Il(e,e.memoizedProps,r.memoizedProps):(a===null?(t=r.stateNode,t===null||H||t.parentNode.removeChild(t)):a.count--,n===null?su||Km(o,e.type,e.stateNode):Bm(o,n,e.memoizedProps))}break;case 27:Au(t,e,n),Nu(e),a&512&&(H||r===null||kl(r,r.return)),r!==null&&a&4&&Il(e,e.memoizedProps,r.memoizedProps);break;case 5:if(o=cu,cu=!1,Au(t,e,n),cu=o,Nu(e),a&512&&(H||r===null||kl(r,r.return)),e.flags&32){t=e.stateNode;try{vn(t,``),j=!0}catch(t){Z(e,e.return,t)}}a&4&&e.stateNode!=null&&(t=e.memoizedProps,Il(e,t,r===null?t:r.memoizedProps)),a&1024&&(lu=!0);break;case 6:if(Au(t,e,n),Nu(e),a&4){if(e.stateNode===null)throw Error(i(162));t=e.memoizedProps,n=e.stateNode;try{n.nodeValue=t,j=!0}catch(t){Z(e,e.return,t)}}break;case 3:if(j=!1,Wm=null,o=ju,ju=bm(t.containerInfo),Au(t,e,n),ju=o,Nu(e),a&4&&r!==null&&r.memoizedState.isDehydrated)try{Hh(t.containerInfo)}catch(t){Z(e,e.return,t)}lu&&(lu=!1,Pu(e)),j=!1;break;case 4:a=cu,cu=su,r=en(),o=ju,ju=bm(e.stateNode.containerInfo),Au(t,e,n),Nu(e),ju=o,j&&pu&&(mu=!0),j=r,cu=a;break;case 12:Au(t,e,n),Nu(e);break;case 31:Au(t,e,n),Nu(e),a&4&&(t=e.updateQueue,t!==null&&(e.updateQueue=null,ku(e,t)));break;case 13:Au(t,e,n),Nu(e),e.child.flags&8192&&e.memoizedState!==null!=(r!==null&&r.memoizedState!==null)&&(hd=Ye()),a&4&&(t=e.updateQueue,t!==null&&(e.updateQueue=null,ku(e,t)));break;case 22:o=e.memoizedState!==null,s=r!==null&&r.memoizedState!==null;var c=su,l=H,u=cu;su=c||o,cu=u||o,H=l||s,Au(t,e,n),H=l,cu=u,su=c,Nu(e),a&8192&&(t=e.stateNode,t._visibility=o?t._visibility&-2:t._visibility|1,!o||r===null||s||su||H||(t=s||H,n=su,r=H,su=o||su,H=t,Ru(e,2),su=n,H=r),!o&&cu||yu(e,o)),a&4&&(t=e.updateQueue,t!==null&&(n=t.retryQueue,n!==null&&(t.retryQueue=null,ku(e,n))));break;case 19:Au(t,e,n),Nu(e),a&4&&(t=e.updateQueue,t!==null&&(e.updateQueue=null,ku(e,t)));break;case 30:a&512&&(H||r===null||kl(r,r.return)),a=en(),o=pu,s=(n&335544064)===n,c=e.memoizedProps,pu=s&&Ei(c.default,c.update)!==`none`,Au(t,e,n),Nu(e),s&&r!==null&&j&&(e.flags|=4),pu=o,j=a;break;case 21:break;case 7:a&512&&(H||r===null||kl(r,r.return)),r&&r.stateNode!==null&&(r.stateNode._fragmentFiber=e);default:Au(t,e,n),Nu(e)}}function Nu(e){var t=e.flags;if(t&2){try{for(var n,r=e.return;r!==null;){if(Ll(r)){n=r;break}r=r.return}r=null;for(var a=e.return;a!==null;){if(Pl(a)){var o=a.stateNode;r===null?r=[o]:r.push(o)}if(Nl(a))break;a=a.return}var s=r;if(n==null)throw Error(i(160));switch(n.tag){case 27:var c=n.stateNode;Bl(e,Rl(e),c,s);break;case 5:var l=n.stateNode;n.flags&32&&(vn(l,``),n.flags&=-33),Bl(e,Rl(e),l,s);break;case 3:case 4:var u=n.stateNode.containerInfo;zl(e,Rl(e),u,s);break;default:throw Error(i(161))}}catch(t){Z(e,e.return,t)}e.flags&=-3}t&4096&&(e.flags&=-4097)}function Pu(e){if(e.subtreeFlags&1024)for(e=e.child;e!==null;){var t=e;Pu(t),t.tag===5&&t.flags&1024&&(t=t.stateNode,gh=!0,t.reset(),gh=!1),e=e.sibling}}function Fu(e,t){if(t.subtreeFlags&9270)for(t=t.child;t!==null;)Iu(t,e),t=t.sibling;else ou(t,!1)}function Iu(e,t){var n=e.alternate;if(n===null)Ql(e,!1);else switch(e.tag){case 3:if(hu=fu=!1,Kl(),Fu(t,e),!fu&&!mu){if(e=Gl,e!==null)for(var r=0;r<e.length;r+=3){n=e[r];var i=e[r+1];Ep(n,e[r+2]),n=n.ownerDocument.documentElement,n!==null&&n.animate({opacity:[0,0],pointerEvents:[`none`,`none`]},{duration:0,fill:`forwards`,pseudoElement:`::view-transition-group(`+i+`)`})}e=t.containerInfo,e=e.nodeType===9?e.documentElement:e.ownerDocument.documentElement,e!==null&&e.style.viewTransitionName===``&&(e.style.viewTransitionName=`none`,e.animate({opacity:[0,0],pointerEvents:[`none`,`none`]},{duration:0,fill:`forwards`,pseudoElement:`::view-transition-group(root)`}),e.animate({width:[0,0],height:[0,0]},{duration:0,fill:`forwards`,pseudoElement:`::view-transition`})),hu=!0}Gl=null;break;case 5:Fu(t,e);break;case 4:r=fu,fu=!1,Fu(t,e),fu&&(mu=!0),fu=r;break;case 22:e.memoizedState===null&&(n.memoizedState===null?Fu(t,e):Ql(e,!1));break;case 30:r=fu,i=Kl(),fu=!1,Fu(t,e),fu&&(e.flags|=4);var a=e.memoizedProps,o=e.stateNode;t=wi(a,o),o=wi(n.memoizedProps,o);var s=Ei(a.default,a.update);s===`none`?t=!1:(a=n.memoizedState,n.memoizedState=null,n=e.child,ql=0,t=au(e,n,t,o,s,a,!0),ql!==(a===null?0:a.length)&&(e.flags|=32)),e.flags&4&&t?(Nd(e,e.memoizedProps.onUpdate),Gl=i):i!==null&&(i.push.apply(i,Gl),Gl=i),fu=e.flags&32?!0:r;break;default:Fu(t,e)}}function Lu(e,t){if(t.subtreeFlags&8772)for(t=t.child;t!==null;)vu(e,t.alternate,t),t=t.sibling}function Ru(e,t){for(e=e.child;e!==null;){var n=e,r=t;switch(n.tag){case 0:case 11:case 14:case 15:Tl(4,n,n.return),Ru(n,r);break;case 1:kl(n,n.return);var i=n.stateNode;typeof i.componentWillUnmount==`function`&&Dl(n,n.return,i),Ru(n,r);break;case 27:r&2&&gm(n.stateNode,n.type,n.memoizedProps);case 5:kl(n,n.return),n.tag!==5&&n.tag!==27||Ml(n),Ru(n,r);break;case 6:Ml(n);break;case 26:kl(n,n.return),i=n.stateNode,n.memoizedState!==null||i===null||H||i.parentNode.removeChild(i),Ru(n,r);break;case 22:n.memoizedState===null&&Ru(n,r);break;case 30:kl(n,n.return),Ru(n,r);break;case 7:kl(n,n.return);default:Ru(n,r)}e=e.sibling}}function zu(e,t,n){for(n=t.subtreeFlags&8772?n:n&-2,t=t.child;t!==null;){var r=t.alternate,i=e,a=t,o=a.flags,s=!!(n&1);switch(a.tag){case 0:case 11:case 15:zu(i,a,n),wl(4,a);break;case 1:if(zu(i,a,n),r=a,i=r.stateNode,typeof i.componentDidMount==`function`)try{i.componentDidMount()}catch(e){Z(r,r.return,e)}if(r=a,i=r.updateQueue,i!==null){var c=r.stateNode;try{var l=i.shared.hiddenCallbacks;if(l!==null)for(i.shared.hiddenCallbacks=null,i=0;i<l.length;i++)ko(l[i],c)}catch(e){Z(r,r.return,e)}}s&&o&64&&El(a),Ol(a,a.return);break;case 27:n&2&&Vl(a);case 5:a.tag!==5&&a.tag!==27||jl(a),zu(i,a,n),s&&r===null&&o&4&&Fl(a),Ol(a,a.return);break;case 6:jl(a);break;case 26:c=a.stateNode,a.memoizedState!==null||c===null||su||Km(bm(c.ownerDocument),a.type,c),zu(i,a,n),s&&r===null&&o&4&&Fl(a),Ol(a,a.return);break;case 12:zu(i,a,n);break;case 31:zu(i,a,n),s&&o&4&&Eu(i,a);break;case 13:zu(i,a,n),s&&o&4&&Du(i,a);break;case 22:a.memoizedState===null&&zu(i,a,n),Ol(a,a.return);break;case 30:zu(i,a,n),Ol(a,a.return);break;case 7:Ol(a,a.return);default:zu(i,a,n)}t=t.sibling}}function Bu(e,t){var n=null;e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),e=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(e=t.memoizedState.cachePool.pool),e!==n&&(e!=null&&e.refCount++,n!=null&&Ra(n))}function Vu(e,t){e=null,t.alternate!==null&&(e=t.alternate.memoizedState.cache),t=t.memoizedState.cache,t!==e&&(t.refCount++,e!=null&&Ra(e))}function Hu(e,t,n,r){var i=(n&335544064)===n;if(t.subtreeFlags&(i?10262:10256))for(t=t.child;t!==null;)Uu(e,t,n,r),t=t.sibling;else i&&iu(t)}function Uu(e,t,n,r){var i=(n&335544064)===n;i&&t.alternate===null&&t.return!==null&&t.return.alternate!==null&&ru(t);var a=t.flags;switch(t.tag){case 0:case 11:case 15:Hu(e,t,n,r),a&2048&&wl(9,t);break;case 1:Hu(e,t,n,r);break;case 3:Hu(e,t,n,r),i&&hu&&(e=e.containerInfo,e=e.nodeType===9?e.body:e.nodeName===`HTML`?e.ownerDocument.body:e,e.style.viewTransitionName===`root`&&(e.style.viewTransitionName=``),e=e.ownerDocument.documentElement,e!==null&&e.style.viewTransitionName===`none`&&(e.style.viewTransitionName=``)),a&2048&&(a=null,t.alternate!==null&&(a=t.alternate.memoizedState.cache),t=t.memoizedState.cache,t!==a&&(t.refCount++,a!=null&&Ra(a)));break;case 12:if(a&2048){Hu(e,t,n,r),a=t.stateNode;try{var o=t.memoizedProps,s=o.id,c=o.onPostCommit;typeof c==`function`&&c(s,t.alternate===null?`mount`:`update`,a.passiveEffectDuration,-0)}catch(e){Z(t,t.return,e)}}else Hu(e,t,n,r);break;case 31:Hu(e,t,n,r);break;case 13:Hu(e,t,n,r);break;case 23:break;case 22:o=t.stateNode,s=t.alternate,t.memoizedState===null?(i&&s!==null&&s.memoizedState!==null&&ru(t),o._visibility&2?Hu(e,t,n,r):(o._visibility|=2,Wu(e,t,n,r,!!(t.subtreeFlags&10256)||!1))):(i&&s!==null&&s.memoizedState===null&&ru(s),o._visibility&2?Hu(e,t,n,r):Gu(e,t)),a&2048&&Bu(s,t);break;case 24:Hu(e,t,n,r),a&2048&&Vu(t.alternate,t);break;case 30:i&&(a=t.alternate,a!==null&&(Xl(a.child,!0),Xl(t.child,!0))),Hu(e,t,n,r);break;default:Hu(e,t,n,r)}}function Wu(e,t,n,r,i){for(i&&=!!(t.subtreeFlags&10256)||!1,t=t.child;t!==null;){var a=e,o=t,s=n,c=r,l=o.flags;switch(o.tag){case 0:case 11:case 15:Wu(a,o,s,c,i),wl(8,o);break;case 23:break;case 22:var u=o.stateNode;o.memoizedState===null?(u._visibility|=2,Wu(a,o,s,c,i)):u._visibility&2?Wu(a,o,s,c,i):Gu(a,o),i&&l&2048&&Bu(o.alternate,o);break;case 24:Wu(a,o,s,c,i),i&&l&2048&&Vu(o.alternate,o);break;default:Wu(a,o,s,c,i)}t=t.sibling}}function Gu(e,t){if(t.subtreeFlags&10256)for(t=t.child;t!==null;){var n=e,r=t,i=r.flags;switch(r.tag){case 22:Gu(n,r),i&2048&&Bu(r.alternate,r);break;case 24:Gu(n,r),i&2048&&Vu(r.alternate,r);break;default:Gu(n,r)}t=t.sibling}}var Ku=8192;function qu(e,t,n){if(e.subtreeFlags&Ku)for(e=e.child;e!==null;)Ju(e,t,n),e=e.sibling}function Ju(e,t,n){switch(e.tag){case 26:qu(e,t,n),e.flags&Ku&&(e.memoizedState===null?(e=e.stateNode,(t&335544128)===t&&Zm(n,e)):Qm(n,ju,e.memoizedState,e.memoizedProps));break;case 5:qu(e,t,n),e.flags&Ku&&(e=e.stateNode,(t&335544128)===t&&Zm(n,e));break;case 3:case 4:var r=ju;ju=bm(e.stateNode.containerInfo),qu(e,t,n),ju=r;break;case 22:e.memoizedState===null&&(r=e.alternate,r!==null&&r.memoizedState!==null?(r=Ku,Ku=16777216,qu(e,t,n),Ku=r):qu(e,t,n));break;case 30:if((e.flags&Ku)!==0&&(r=e.memoizedProps.name,r!=null&&r!==`auto`)){var i=e.stateNode;i.paired=null,Ul===null&&(Ul=new Map),Ul.set(r,i)}qu(e,t,n);break;default:qu(e,t,n)}}function Yu(e){var t=e.alternate;if(t!==null&&(e=t.child,e!==null)){t.child=null;do t=e.sibling,e.sibling=null,e=t;while(e!==null)}}function Xu(e){var t=e.deletions;if(e.flags&16){if(t!==null)for(var n=0;n<t.length;n++){var r=t[n];du=r,$u(r,e)}Yu(e)}if(e.subtreeFlags&10256)for(e=e.child;e!==null;)Zu(e),e=e.sibling}function Zu(e){switch(e.tag){case 0:case 11:case 15:Xu(e),e.flags&2048&&Tl(9,e,e.return);break;case 3:Xu(e);break;case 12:Xu(e);break;case 22:var t=e.stateNode;e.memoizedState!==null&&t._visibility&2&&(e.return===null||e.return.tag!==13)?(t._visibility&=-3,Qu(e)):Xu(e);break;default:Xu(e)}}function Qu(e){var t=e.deletions;if(e.flags&16){if(t!==null)for(var n=0;n<t.length;n++){var r=t[n];du=r,$u(r,e)}Yu(e)}for(e=e.child;e!==null;){switch(t=e,t.tag){case 0:case 11:case 15:Tl(8,t,t.return),Qu(t);break;case 22:n=t.stateNode,n._visibility&2&&(n._visibility&=-3,Qu(t));break;default:Qu(t)}e=e.sibling}}function $u(e,t){for(;du!==null;){var n=du;switch(n.tag){case 0:case 11:case 15:Tl(8,n,t);break;case 23:case 22:if(n.memoizedState!==null&&n.memoizedState.cachePool!==null){var r=n.memoizedState.cachePool.pool;r!=null&&r.refCount++}break;case 24:Ra(n.memoizedState.cache)}if(r=n.child,r!==null)r.return=n,du=r;else a:for(n=e;du!==null;){r=du;var i=r.sibling,a=r.return;if(Su(r),r===n){du=null;break a}if(i!==null){i.return=a,du=i;break a}du=a}}}var ed={getCacheForType:function(e){var t=ja(F),n=t.data.get(e);return n===void 0&&(n=e(),t.data.set(e,n)),n},cacheSignal:function(){return ja(F).controller.signal}},td=typeof WeakMap==`function`?WeakMap:Map,W=0,G=null,K=null,q=0,J=0,nd=null,rd=!1,id=!1,ad=!1,od=0,Y=0,sd=0,cd=0,ld=0,ud=0,dd=0,fd=null,pd=null,md=!1,hd=0,gd=0,_d=1/0,vd=null,yd=null,X=0,bd=null,xd=null,Sd=0,Cd=0,wd=null,Td=null,Ed=null,Dd=null,Od=null,kd=0,Ad=null;function jd(){return W&2&&q!==0?q&-q:D.T===null?Ot():Pf()}function Md(){if(ud===0){if(!(q&536870912)||P){var e=ft;ft<<=1,!(ft&3932160)&&(ft=262144),ud=e}else ud=536870912}return e=Io.current,e!==null&&(e.flags|=32),ud}function Nd(e,t){if(t!=null){var n=e.stateNode,r=n.ref;r===null&&(r=n.ref=Pp(wi(e.memoizedProps,n))),Dd===null&&(Dd=[]),Dd.push(t.bind(null,r))}}function Pd(e,t,n){(e===G&&(J===2||J===9)||e.cancelPendingCommit!==null)&&(Vd(e,0),Rd(e,q,ud,!1)),xt(e,n),(!(W&2)||e!==G)&&(e===G&&(!(W&2)&&(cd|=n),Y===4&&Rd(e,q,ud,!1)),Ef(e))}function Fd(e,t,n){if(W&6)throw Error(i(327));var r=!n&&!(t&127)&&(t&e.expiredLanes)===0||gt(e,t),a=r?Yd(e,t):qd(e,t,!0),o=r;do{if(a===0){id&&!r&&Rd(e,t,0,!1);break}if(n=e.current.alternate,o&&!Ld(n))a=qd(e,t,!1),o=!1;else{if(a===2){if(o=t,e.errorRecoveryDisabledLanes&o)var s=0;else s=e.pendingLanes&-536870913,s=s===0?s&536870912?536870912:0:s;if(s!==0){t=s;a:{var c=e;a=fd;var l=c.current.memoizedState.isDehydrated;if(l&&(Vd(c,s).flags|=256),s=qd(c,s,!1),s!==2&&s!==6){if(ad&&!l){c.errorRecoveryDisabledLanes|=o,cd|=o,a=4;break a}o=pd,pd=a,o!==null&&(pd===null?pd=o:pd.push.apply(pd,o))}a=s}if(o=!1,a!==2)continue}}if(a===1){Vd(e,0),Rd(e,t,0,!0);break}a:{switch(r=e,o=a,o){case 0:case 1:throw Error(i(345));case 4:if((t&4194048)!==t&&(t&62914560)!==t)break;case 6:Rd(r,t,ud,!rd);break a;case 2:pd=null;break;case 3:case 5:break;default:throw Error(i(329))}if((t&62914560)===t&&(a=hd+300-Ye(),10<a)){if(Rd(r,t,ud,!rd),ht(r,0,!0)!==0)break a;Sd=t,r.timeoutHandle=gp(Id.bind(null,r,n,pd,vd,md,t,ud,cd,dd,rd,o,`Throttled`,-0,0),a);break a}Id(r,n,pd,vd,md,t,ud,cd,dd,rd,o,null,-0,0)}break}}while(1);Ef(e)}function Id(e,t,n,r,i,a,o,s,c,l,u,d,f,p){e.timeoutHandle=-1;var m=t.subtreeFlags,h=(a&335544064)===a;d=null,(h||m&8192||(m&16785408)==16785408)&&(d={stylesheets:null,count:0,imgCount:0,imgBytes:0,suspenseyImages:[],waitingForImages:!0,waitingForViewTransition:!1,unsuspend:En},Ul=null,Ju(t,a,d),h&&(m=d,h=e.containerInfo,h=(h.nodeType===9?h:h.ownerDocument).__reactViewTransition,h!=null&&(m.count++,m.waitingForViewTransition=!0,m=nh.bind(m),h.finished.then(m,m))),m=(a&62914560)===a?hd-Ye():(a&4194048)===a?gd-Ye():0,m=eh(d,m),m!==null)?(Sd=a,e.cancelPendingCommit=m(nf.bind(null,e,t,a,n,r,i,o,s,c,l,u,d,null,f,p)),Rd(e,a,o,!l)):nf(e,t,a,n,r,i,o,s,c,l,u,d)}function Ld(e){for(var t=e;;){var n=t.tag;if((n===0||n===11||n===15)&&t.flags&16384&&(n=t.updateQueue,n!==null&&(n=n.stores,n!==null)))for(var r=0;r<n.length;r++){var i=n[r],a=i.getSnapshot;i=i.value;try{if(!Jr(a(),i))return!1}catch{return!1}}if(n=t.child,t.subtreeFlags&16384&&n!==null)n.return=t,t=n;else{if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return!0;t=t.return}t.sibling.return=t.return,t=t.sibling}}return!0}function Rd(e,t,n,r){t=_t(e,t),t&=~ld,t&=~cd,e.suspendedLanes|=t,e.pingedLanes&=~t,r&&(e.warmLanes|=t),r=e.expirationTimes;for(var i=t;0<i;){var a=31-st(i),o=1<<a;r[a]=-1,i&=~o}n!==0&&Ct(e,n,t)}function zd(){return W&6?!0:(Df(0,!1),!1)}function Bd(){if(K!==null){if(J===0)var e=K.return;else e=K,Ca=Sa=null,ls(e),uo=null,fo=0,e=K;for(;e!==null;)Cl(e.alternate,e),e=e.return;K=null}}function Vd(e,t){var n=e.timeoutHandle;return n!==-1&&(e.timeoutHandle=-1,_p(n)),n=e.cancelPendingCommit,n!==null&&(e.cancelPendingCommit=null,n()),Sd=0,Bd(),G=e,K=n=Vi(e.current,null),q=t,J=0,nd=null,rd=!1,id=gt(e,t),ad=!1,dd=ud=ld=cd=sd=Y=0,pd=fd=null,md=!1,od=_t(e,t),ji(),n}function Hd(e,t){I=null,D.H=vc,t===eo||t===no?(t=co(),J=3):t===to?(t=co(),J=4):J=t===Ic?8:typeof t==`object`&&t&&typeof t.then==`function`?6:1,nd=t,K===null&&(Y=1,Ac(e,Yi(t,e.current)))}function Ud(){var e=Io.current;return e===null?!0:(q&4194048)===q?Lo===null:(q&62914560)===q||q&536870912?e===Lo:!1}function Wd(){var e=D.H;return D.H=vc,e===null?vc:e}function Gd(){var e=D.A;return D.A=ed,e}function Kd(){Y=4,rd||(q&4194048)!==q&&Io.current!==null||(id=!0),!(sd&134217727)&&!(cd&134217727)||G===null||Rd(G,q,ud,!1)}function qd(e,t,n){var r=W;W|=2;var i=Wd(),a=Gd();(G!==e||q!==t)&&(vd=null,Vd(e,t)),t=!1;var o=Y;a:do try{if(J!==0&&K!==null){var s=K,c=nd;switch(J){case 8:Bd(),o=6;break a;case 3:case 2:case 9:case 6:Io.current===null&&(t=!0);var l=J;if(J=0,nd=null,$d(e,s,c,l),n&&id){o=0;break a}break;default:l=J,J=0,nd=null,$d(e,s,c,l)}}Jd(),o=Y;break}catch(t){Hd(e,t)}while(1);return t&&e.shellSuspendCounter++,Ca=Sa=null,W=r,D.H=i,D.A=a,K===null&&(G=null,q=0,ji()),o}function Jd(){for(;K!==null;)Zd(K)}function Yd(e,t){var n=W;W|=2;var r=Wd(),a=Gd();G!==e||q!==t?(vd=null,_d=Ye()+500,Vd(e,t)):id=gt(e,t);a:do try{if(J!==0&&K!==null){t=K;var o=nd;b:switch(J){case 1:J=0,nd=null,$d(e,t,o,1);break;case 2:case 9:if(io(o)){J=0,nd=null,Qd(t);break}t=function(){J!==2&&J!==9||G!==e||(J=7),Ef(e)},o.then(t,t);break a;case 3:J=7;break a;case 4:J=5;break a;case 7:io(o)?(J=0,nd=null,Qd(t)):(J=0,nd=null,$d(e,t,o,7));break;case 5:var s=null;switch(K.tag){case 26:s=K.memoizedState;case 5:case 27:var c=K;if(s?Ym(s):c.stateNode.complete){J=0,nd=null;var l=c.sibling;if(l!==null)K=l;else{var u=c.return;u===null?K=null:(K=u,ef(u))}break b}}J=0,nd=null,$d(e,t,o,5);break;case 6:J=0,nd=null,$d(e,t,o,6);break;case 8:Bd(),Y=6;break a;default:throw Error(i(462))}}Xd();break}catch(t){Hd(e,t)}while(1);return Ca=Sa=null,D.H=r,D.A=a,W=n,K===null?(G=null,q=0,ji(),Y):0}function Xd(){for(;K!==null&&!qe();)Zd(K)}function Zd(e){var t=hl(e.alternate,e,od);e.memoizedProps=e.pendingProps,t===null?ef(e):K=t}function Qd(e){var t=e,n=t.alternate;switch(t.tag){case 15:case 0:t=Yc(n,t,t.pendingProps,t.type,void 0,q);break;case 11:t=Yc(n,t,t.pendingProps,t.type.render,t.ref,q);break;case 5:ls(t);var r=t;r===ua&&(P?(ga(r),r.tag===5&&r.stateNode!=null&&(N=r.stateNode)):(ga(r),P=!0));default:Cl(n,t),t=K=Hi(t,od),t=hl(n,t,od)}e.memoizedProps=e.pendingProps,t===null?ef(e):K=t}function $d(e,t,n,r){Ca=Sa=null,ls(t),uo=null,fo=0;var i=t.return;try{if(Fc(e,i,t,n,q)){Y=1,Ac(e,Yi(n,e.current)),K=null;return}}catch(t){if(i!==null)throw K=i,t;Y=1,Ac(e,Yi(n,e.current)),K=null;return}t.flags&32768?(P||r===1?e=!0:id||q&536870912?e=!1:(rd=e=!0,(r===2||r===9||r===3||r===6)&&(r=Io.current,r!==null&&r.tag===13&&(r.flags|=16384))),tf(t,e)):ef(t)}function ef(e){var t=e;do{if(t.flags&32768){tf(t,rd);return}e=t.return;var n=xl(t.alternate,t,od);if(n!==null){K=n;return}if(t=t.sibling,t!==null){K=t;return}K=t=e}while(t!==null);Y===0&&(Y=5)}function tf(e,t){do{var n=Sl(e.alternate,e);if(n!==null){n.flags&=32767,K=n;return}if(n=e.return,n!==null&&(n.flags|=32768,n.subtreeFlags=0,n.deletions=null),!t&&(e=e.sibling,e!==null)){K=e;return}K=e=n}while(e!==null);Y=6,K=null}function nf(e,t,n,r,a,o,s,c,l,u,d,f){e.cancelPendingCommit=null;do df();while(X!==0);if(W&6)throw Error(i(327));if(t!==null){if(t===e.current)throw Error(i(177));e===G&&(K=G=null,q=0),xd=t,bd=e,Sd=n,wd=a,Td=r,rf(e,t,n,s,c,l,f)}}function rf(e,t,n,r,i,a,o){var s=t.lanes|t.childLanes;if(Cd=s,s|=Ai,St(e,n,s,r,i,a),Dd=null,(n&335544064)===n?(Od=Va(e),r=10262):(Od=null,r=10256),(t.subtreeFlags&r)!==0||(t.flags&r)!==0?(e.callbackNode=null,e.callbackPriority=0,yf($e,function(){return ff(),null})):(e.callbackNode=null,e.callbackPriority=0),Hl=!1,r=!!(t.flags&13878),t.subtreeFlags&13878||r){r=D.T,D.T=null,i=O.p,O.p=2,a=W,W|=4;try{gu(e,t,n)}finally{W=a,O.p=i,D.T=r}}X=1,Hl?Ed=Mp(o,e.containerInfo,Od,sf,cf,of,lf,ff,af,null,null):(sf(),cf(),lf())}function af(e){if(X!==0){var t=bd.onRecoverableError;t(e,{componentStack:null})}}function of(){X===3&&(X=0,Iu(xd,bd),X=4)}function sf(){if(X===1){X=0;var e=bd,t=xd,n=Sd,r=!!(t.flags&13878);if(t.subtreeFlags&13878||r){r=D.T,D.T=null;var i=O.p;O.p=2;var a=W;W|=4;try{pu=mu=!1,Mu(t,e,n),n=cp;var o=ei(e.containerInfo),s=n.focusedElem,c=n.selectionRange;if(o!==s&&s&&s.ownerDocument&&$r(s.ownerDocument.documentElement,s)){if(c!==null&&ti(s)){var l=c.start,u=c.end;if(u===void 0&&(u=l),`selectionStart`in s)s.selectionStart=l,s.selectionEnd=Math.min(u,s.value.length);else{var d=s.ownerDocument||document,f=d&&d.defaultView||window;if(f.getSelection){var p=f.getSelection(),m=s.textContent.length,h=Math.min(c.start,m),g=c.end===void 0?h:Math.min(c.end,m);!p.extend&&h>g&&(o=g,g=h,h=o);var _=Qr(s,h),v=Qr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;s<d.length;s++){var b=d[s];b.element.scrollLeft=b.left,b.element.scrollTop=b.top}}gh=!!sp,cp=sp=null}finally{W=a,O.p=i,D.T=r}}e.current=t,X=2}}function cf(){if(X===2){X=0;var e=bd,t=xd,n=!!(t.flags&8772);if(t.subtreeFlags&8772||n){n=D.T,D.T=null;var r=O.p;O.p=2;var i=W;W|=4;try{vu(e,t.alternate,t)}finally{W=i,O.p=r,D.T=n}}X=3}}function lf(){if(X===4||X===3){X=0;var e=Ed;Ed=null,Je();var t=bd,n=xd,r=Sd,i=Td,a=(r&335544064)===r?10262:10256;if((n.subtreeFlags&a)!==0||(n.flags&a)!==0?X=5:(X=0,xd=bd=null,uf(t,t.pendingLanes)),a=t.pendingLanes,a===0&&(yd=null),Dt(r),n=n.stateNode,at&&typeof at.onCommitFiberRoot==`function`)try{at.onCommitFiberRoot(it,n,void 0,(n.current.flags&128)==128)}catch{}if(i!==null){n=D.T,a=O.p,O.p=2,D.T=null;try{for(var o=t.onRecoverableError,s=0;s<i.length;s++){var c=i[s];o(c.value,{componentStack:c.stack})}}finally{D.T=n,O.p=a}}if(i=Dd,o=Od,Od=null,i!==null&&(Dd=null,o===null&&(o=[]),e!==null))for(c=0;c<i.length;c++)n=(0,i[c])(o),n!==void 0&&e.finished.finally(n);Sd&3&&df(),Ef(t),a=t.pendingLanes,r&261930&&a&42?t===Ad?kd++:(kd=0,Ad=t):(kd=0,Ad=null),Df(0,!1)}}function uf(e,t){(e.pooledCacheLanes&=t)===0&&(t=e.pooledCache,t!=null&&(e.pooledCache=null,Ra(t)))}function df(){return Ed!==null&&(Ed.skipTransition(),Ed=null),sf(),cf(),lf(),ff()}function ff(){if(X!==5)return!1;var e=bd,t=Cd;Cd=0;var n=Dt(Sd),r=D.T,a=O.p;try{O.p=32>n?32:n,D.T=null,n=wd,wd=null;var o=bd,s=Sd;if(X=0,xd=bd=null,Sd=0,W&6)throw Error(i(331));var c=W;if(W|=4,Zu(o.current),Uu(o,o.current,s,n),W=c,Df(0,!1),at&&typeof at.onPostCommitFiberRoot==`function`)try{at.onPostCommitFiberRoot(it,o)}catch{}return!0}finally{O.p=a,D.T=r,uf(e,t)}}function pf(e,t,n){t=Yi(n,t),t=Mc(e.stateNode,t,2),e=Co(e,t,2),e!==null&&(xt(e,2),Ef(e))}function Z(e,t,n){if(e.tag===3)pf(e,e,n);else for(;t!==null;){if(t.tag===3){pf(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(yd===null||!yd.has(r))){e=Yi(n,e),n=Nc(2),r=Co(t,n,2),r!==null&&(Pc(n,r,t,e),xt(r,2),Ef(r));break}}t=t.return}}function mf(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new td;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(ad=!0,i.add(n),e=hf.bind(null,e,t,n),t.then(e,e))}function hf(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,G===e&&(q&n)===n&&(Y===4||Y===3&&(q&62914560)===q&&300>Ye()-hd?W&2?ld|=n:Vd(e,0):ld|=n,dd===q&&(dd=0)),Ef(e)}function gf(e,t){t===0&&(t=yt()),e=Pi(e,t),e!==null&&(xt(e,t),Ef(e))}function _f(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),gf(e,n)}function vf(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),gf(e,n)}function yf(e,t){return Ge(e,t)}var bf=null,xf=null,Sf=!1,Cf=!1,wf=!1,Tf=0;function Ef(e){e!==xf&&e.next===null&&(xf===null?bf=xf=e:xf=xf.next=e),Cf=!0,Sf||(Sf=!0,Nf())}function Df(e,t){if(!wf&&Cf){wf=!0;do for(var n=!1,r=bf;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-st(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,Mf(r,a))}else a=q,a=ht(r,r===G?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||gt(r,a)||(n=!0,Mf(r,a))}r=r.next}while(n);wf=!1}}function Of(){kf()}function kf(){Cf=Sf=!1;var e=0;Tf!==0&&hp()&&(e=Tf);for(var t=Ye(),n=null,r=bf;r!==null;){var i=r.next,a=Af(r,t);a===0?(r.next=null,n===null?bf=i:n.next=i,i===null&&(xf=n)):(n=r,(e!==0||a&3)&&(Cf=!0)),r=i}X!==0&&X!==5||Df(e,!1),Tf!==0&&(Tf=0)}function Af(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0<a;){var o=31-st(a),s=1<<o,c=i[o];c===-1?((s&n)===0||(s&r)!==0)&&(i[o]=vt(s,t)):c<=t&&(e.expiredLanes|=s),a&=~s}if(t=G,n=q,n=ht(e,e===t?n:0,e.cancelPendingCommit!==null||e.timeoutHandle!==-1),r=e.callbackNode,n===0||e===t&&(J===2||J===9)||e.cancelPendingCommit!==null)return r!==null&&r!==null&&Ke(r),e.callbackNode=null,e.callbackPriority=0;if(!(n&3)||gt(e,n)){if(t=n&-n,t===e.callbackPriority)return t;switch(r!==null&&Ke(r),Dt(n)){case 2:case 8:n=Qe;break;case 32:n=$e;break;case 268435456:n=tt;break;default:n=$e}return r=jf.bind(null,e),n=Ge(n,r),e.callbackPriority=t,e.callbackNode=n,t}return r!==null&&r!==null&&Ke(r),e.callbackPriority=2,e.callbackNode=null,2}function jf(e,t){if(X!==0&&X!==5)return e.callbackNode=null,e.callbackPriority=0,null;var n=e.callbackNode;if(df()&&e.callbackNode!==n)return null;var r=q;return r=ht(e,e===G?r:0,e.cancelPendingCommit!==null||e.timeoutHandle!==-1),r===0?null:(Fd(e,r,t),Af(e,Ye()),e.callbackNode!=null&&e.callbackNode===n?jf.bind(null,e):null)}function Mf(e,t){if(df())return null;Fd(e,t,!0)}function Nf(){bp(function(){W&6?Ge(Ze,Of):kf()})}function Pf(){if(Tf===0){var e=Wa;e===0&&(e=dt,dt<<=1,!(dt&261888)&&(dt=256)),Tf=e}return Tf}function Ff(e){return e==null||typeof e==`symbol`||typeof e==`boolean`?null:typeof e==`function`?e:Tn(e)}function If(e,t,n,r,i){if(t===`submit`&&n&&n.stateNode===i){var a=Ff((i[Mt]||null).action),o=r.submitter;o&&(t=(t=o[Mt]||null)?Ff(t.formAction):o.getAttribute(`formAction`),t!==null&&(a=t,o=null));var s=new qn(`action`,`action`,null,r,i);e.push({event:s,listeners:[{instance:null,listener:function(){if(r.defaultPrevented){if(Tf!==0){var e=new FormData(i,o);ic(n,{pending:!0,data:e,method:i.method,action:a},null,e)}}else typeof a==`function`&&(s.preventDefault(),e=new FormData(i,o),ic(n,{pending:!0,data:e,method:i.method,action:a},a,e))},currentTarget:i}]})}}for(var Lf=0;Lf<xi.length;Lf++){var Rf=xi[Lf];Si(Rf.toLowerCase(),`on`+(Rf[0].toUpperCase()+Rf.slice(1)))}Si(pi,`onAnimationEnd`),Si(mi,`onAnimationIteration`),Si(hi,`onAnimationStart`),Si(`dblclick`,`onDoubleClick`),Si(`focusin`,`onFocus`),Si(`focusout`,`onBlur`),Si(gi,`onTransitionRun`),Si(_i,`onTransitionStart`),Si(vi,`onTransitionCancel`),Si(yi,`onTransitionEnd`),Yt(`onMouseEnter`,[`mouseout`,`mouseover`]),Yt(`onMouseLeave`,[`mouseout`,`mouseover`]),Yt(`onPointerEnter`,[`pointerout`,`pointerover`]),Yt(`onPointerLeave`,[`pointerout`,`pointerover`]),Jt(`onChange`,`change click focusin focusout input keydown keyup selectionchange`.split(` `)),Jt(`onSelect`,`focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange`.split(` `)),Jt(`onBeforeInput`,[`compositionend`,`keypress`,`textInput`,`paste`]),Jt(`onCompositionEnd`,`compositionend focusout keydown keypress keyup mousedown`.split(` `)),Jt(`onCompositionStart`,`compositionstart focusout keydown keypress keyup mousedown`.split(` `)),Jt(`onCompositionUpdate`,`compositionupdate focusout keydown keypress keyup mousedown`.split(` `));var zf=`abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting`.split(` `),Bf=new Set(`beforetoggle cancel close invalid load scroll scrollend toggle`.split(` `).concat(zf));function Vf(e,t){t=!!(t&4);for(var n=0;n<e.length;n++){var r=e[n],i=r.event;r=r.listeners;a:{var a=void 0;if(t)for(var o=r.length-1;0<=o;o--){var s=r[o],c=s.instance,l=s.currentTarget;if(s=s.listener,c!==a&&i.isPropagationStopped())break a;a=s,i.currentTarget=l;try{a(i)}catch(e){Di(e)}i.currentTarget=null,a=c}else for(o=0;o<r.length;o++){if(s=r[o],c=s.instance,l=s.currentTarget,s=s.listener,c!==a&&i.isPropagationStopped())break a;a=s,i.currentTarget=l;try{a(i)}catch(e){Di(e)}i.currentTarget=null,a=c}}}}function Q(e,t){var n=t[Pt];n===void 0&&(n=t[Pt]=new Set);var r=e+`__bubble`;n.has(r)||(Gf(t,e,2,!1),n.add(r))}function Hf(e,t,n){var r=0;t&&(r|=4),Gf(n,e,r,t)}var Uf=`_reactListening`+Math.random().toString(36).slice(2);function Wf(e){if(!e[Uf]){e[Uf]=!0,Kt.forEach(function(t){t!==`selectionchange`&&(Bf.has(t)||Hf(t,!1,e),Hf(t,!0,e))});var t=e.nodeType===9?e:e.ownerDocument;t===null||t[Uf]||(t[Uf]=!0,Hf(`selectionchange`,!1,t))}}function Gf(e,t,n,r){switch(Ch(t)){case 2:var i=_h;break;case 8:i=vh;break;default:i=yh}n=i.bind(null,t,n,e),i=void 0,!In||t!==`touchstart`&&t!==`touchmove`&&t!==`wheel`||(i=!0),r?i===void 0?e.addEventListener(t,n,!0):e.addEventListener(t,n,{capture:!0,passive:i}):i===void 0?e.addEventListener(t,n,!1):e.addEventListener(t,n,{passive:i})}function Kf(e,t,n,r,i){var a=r;if(!(t&1)&&!(t&2)&&r!==null)a:for(;;){if(r===null)return;var s=r.tag;if(s===3||s===4){var c=r.stateNode.containerInfo;if(c===i)break;if(s===4)for(s=r.return;s!==null;){var l=s.tag;if((l===3||l===4)&&s.stateNode.containerInfo===i)return;s=s.return}for(;c!==null;){if(s=Vt(c),s===null)return;if(l=s.tag,l===5||l===6||l===26||l===27){r=a=s;continue a}c=c.parentNode}}r=r.return}Nn(function(){var r=a,i=On(n),s=[];a:{var c=bi.get(e);if(c!==void 0){var l=qn,u=e;switch(e){case`keypress`:if(Hn(n)===0)break a;case`keydown`:case`keyup`:l=ur;break;case`focusin`:u=`focus`,l=tr;break;case`focusout`:u=`blur`,l=tr;break;case`beforeblur`:case`afterblur`:l=tr;break;case`click`:if(n.button===2)break a;case`auxclick`:case`dblclick`:case`mousedown`:case`mousemove`:case`mouseup`:case`mouseout`:case`mouseover`:case`contextmenu`:l=$n;break;case`drag`:case`dragend`:case`dragenter`:case`dragexit`:case`dragleave`:case`dragover`:case`dragstart`:case`drop`:l=er;break;case`touchcancel`:case`touchend`:case`touchmove`:case`touchstart`:l=pr;break;case pi:case mi:case hi:l=nr;break;case yi:l=mr;break;case`scroll`:case`scrollend`:l=Yn;break;case`wheel`:l=hr;break;case`copy`:case`cut`:case`paste`:l=rr;break;case`gotpointercapture`:case`lostpointercapture`:case`pointercancel`:case`pointerdown`:case`pointermove`:case`pointerout`:case`pointerover`:case`pointerup`:l=dr;break;case`submit`:l=fr;break;case`toggle`:case`beforetoggle`:l=gr}var d=!!(t&4),f=!d&&(e===`scroll`||e===`scrollend`),p=d?c===null?null:c+`Capture`:c;d=[];for(var m=r,h;m!==null;){var g=m;if(h=g.stateNode,g=g.tag,g!==5&&g!==26&&g!==27||h===null||p===null||(g=Pn(m,p),g!=null&&d.push(qf(m,g,h))),f)break;m=m.return}0<d.length&&(c=new l(c,u,null,n,i),s.push({event:c,listeners:d}))}}if(!(t&7)){a:{if(l=e===`mouseover`||e===`pointerover`,c=e===`mouseout`||e===`pointerout`,l&&n!==Dn&&(u=n.relatedTarget||n.fromElement)&&(Vt(u)||u[Nt]))break a;(c||l)&&(u=i.window===i?i:(l=i.ownerDocument)?l.defaultView||l.parentWindow:window,c?(l=n.relatedTarget||n.toElement,c=r,l=l?Vt(l):null,l!==null&&(f=o(l),d=l.tag,l!==f||d!==5&&d!==27&&d!==6)&&(l=null)):(c=null,l=r),c!==l&&(d=$n,g=`onMouseLeave`,p=`onMouseEnter`,m=`mouse`,(e===`pointerout`||e===`pointerover`)&&(d=dr,g=`onPointerLeave`,p=`onPointerEnter`,m=`pointer`),f=c==null?u:Ut(c),h=l==null?u:Ut(l),u=new d(g,m+`leave`,c,n,i),u.target=f,u.relatedTarget=h,g=null,Vt(i)===r&&(d=new d(p,m+`enter`,l,n,i),d.target=h,d.relatedTarget=f,g=d),f=g,d=c&&l?re(c,l,Yf):null,c!==null&&Xf(s,u,c,d,!1),l!==null&&f!==null&&Xf(s,f,l,d,!0)))}a:{if(c=r?Ut(r):window,l=c.nodeName&&c.nodeName.toLowerCase(),l===`select`||l===`input`&&c.type===`file`)var _=Ir;else if(Ar(c)){if(Lr)_=Kr;else{_=Wr;var v=Ur}}else l=c.nodeName,!l||l.toLowerCase()!==`input`||c.type!==`checkbox`&&c.type!==`radio`?r&&Sn(r.elementType)&&(_=Ir):_=Gr;if(_&&=_(e,r)){jr(s,_,n,i);break a}v&&v(e,c,r)}switch(v=r?Ut(r):window,e){case`focusin`:(Ar(v)||v.contentEditable===`true`)&&(ri=v,ii=r,ai=null);break;case`focusout`:ai=ii=ri=null;break;case`mousedown`:oi=!0;break;case`contextmenu`:case`mouseup`:case`dragend`:oi=!1,si(s,n,i);break;case`selectionchange`:if(ni)break;case`keydown`:case`keyup`:si(s,n,i)}var y;if(vr)b:{switch(e){case`compositionstart`:var b=`onCompositionStart`;break b;case`compositionend`:b=`onCompositionEnd`;break b;case`compositionupdate`:b=`onCompositionUpdate`;break b}b=void 0}else Er?wr(e,n)&&(b=`onCompositionEnd`):e===`keydown`&&n.keyCode===229&&(b=`onCompositionStart`);b&&(xr&&n.locale!==`ko`&&(Er||b!==`onCompositionStart`?b===`onCompositionEnd`&&Er&&(y=Vn()):(Rn=i,zn=`value`in Rn?Rn.value:Rn.textContent,Er=!0)),v=Jf(r,b),0<v.length&&(b=new ir(b,e,null,n,i),s.push({event:b,listeners:v}),y?b.data=y:(y=Tr(n),y!==null&&(b.data=y)))),(y=br?Dr(e,n):Or(e,n))&&(b=Jf(r,`onBeforeInput`),0<b.length&&(v=new ir(`onBeforeInput`,`beforeinput`,null,n,i),s.push({event:v,listeners:b}),v.data=y)),If(s,e,r,n,i)}Vf(s,t)})}function qf(e,t,n){return{instance:e,listener:t,currentTarget:n}}function Jf(e,t){for(var n=t+`Capture`,r=[];e!==null;){var i=e,a=i.stateNode;if(i=i.tag,i!==5&&i!==26&&i!==27||a===null||(i=Pn(e,n),i!=null&&r.unshift(qf(e,i,a)),i=Pn(e,t),i!=null&&r.push(qf(e,i,a))),e.tag===3)return r;e=e.return}return[]}function Yf(e){if(e===null)return null;do e=e.return;while(e&&e.tag!==5&&e.tag!==27);return e||null}function Xf(e,t,n,r,i){for(var a=t._reactName,o=[];n!==null&&n!==r;){var s=n,c=s.alternate,l=s.stateNode;if(s=s.tag,c!==null&&c===r)break;s!==5&&s!==26&&s!==27||l===null||(c=l,i?(l=Pn(n,a),l!=null&&o.unshift(qf(n,l,c))):i||(l=Pn(n,a),l!=null&&o.push(qf(n,l,c)))),n=n.return}o.length!==0&&e.push({event:t,listeners:o})}var Zf=/\r\n?/g,Qf=/\u0000|\uFFFD/g;function $f(e){return(typeof e==`string`?e:``+e).replace(Zf,` |
|
| 9 | +`).replace(Qf,``)}function ep(e,t){return t=$f(t),$f(e)===t}function $(e,t,n,r,a,o){switch(n){case`children`:if(typeof r==`string`)t===`body`||t===`textarea`&&r===``||vn(e,r);else if(typeof r==`number`||typeof r==`bigint`)t!==`body`&&vn(e,``+r);else return;break;case`className`:nn(e,`class`,r);break;case`tabIndex`:nn(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:nn(e,n,r);break;case`style`:xn(e,r,o);return;case`data`:if(t!==`object`){nn(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)||r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=Tn(r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}if(typeof o==`function`&&(n===`formAction`?(t!==`input`&&$(e,t,`name`,a.name,a,null),$(e,t,`formEncType`,a.formEncType,a,null),$(e,t,`formMethod`,a.formMethod,a,null),$(e,t,`formTarget`,a.formTarget,a,null)):($(e,t,`encType`,a.encType,a,null),$(e,t,`method`,a.method,a,null),$(e,t,`target`,a.target,a,null))),r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=Tn(r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=En);return;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=Tn(r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`credentialless`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Q(`beforetoggle`,e),Q(`toggle`,e),tn(e,`popover`,r);break;case`xlinkActuate`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:rn(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:rn(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:rn(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:rn(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:tn(e,`is`,r);break;case`innerText`:case`textContent`:return;default:if(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)n=Cn.get(n)||n,tn(e,n,r);else return}j=!0}function tp(e,t,n,r,a,o){switch(n){case`style`:xn(e,r,o);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`children`:if(typeof r==`string`)vn(e,r);else if(typeof r==`number`||typeof r==`bigint`)vn(e,``+r);else return;break;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`onClick`:r!=null&&(e.onclick=En);return;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`innerHTML`:case`ref`:return;case`innerText`:case`textContent`:return;default:if(!qt.hasOwnProperty(n))a:{if(n[0]===`o`&&n[1]===`n`&&(a=n.endsWith(`Capture`),o=n.slice(2,a?n.length-7:void 0),t=e[Mt]||null,t=t==null?null:t[n],typeof t==`function`&&e.removeEventListener(o,t,a),typeof r==`function`)){typeof t!=`function`&&t!==null&&(n in e?e[n]=null:e.hasAttribute(n)&&e.removeAttribute(n)),e.addEventListener(o,r,a);break a}j=!0,n in e?e[n]=r:!0===r?e.setAttribute(n,``):tn(e,n,r)}return}j=!0}function np(e,t,n){switch(t){case`div`:case`span`:case`svg`:case`path`:case`a`:case`g`:case`p`:case`li`:break;case`img`:Q(`error`,e),Q(`load`,e);var r=!1,a=!1,o;for(o in n)if(n.hasOwnProperty(o)){var s=n[o];if(s!=null)switch(o){case`src`:r=!0;break;case`srcSet`:a=!0;break;case`children`:case`dangerouslySetInnerHTML`:throw Error(i(137,t));default:$(e,t,o,s,n,null)}}a&&$(e,t,`srcSet`,n.srcSet,n,null),r&&$(e,t,`src`,n.src,n,null);return;case`input`:Q(`invalid`,e);var c=o=s=a=null,l=null,u=null;for(r in n)if(n.hasOwnProperty(r)){var d=n[r];if(d!=null)switch(r){case`name`:a=d;break;case`type`:s=d;break;case`checked`:l=d;break;case`defaultChecked`:u=d;break;case`value`:o=d;break;case`defaultValue`:c=d;break;case`children`:case`dangerouslySetInnerHTML`:if(d!=null)throw Error(i(137,t));break;default:$(e,t,r,d,n,null)}}pn(e,o,c,l,u,s,a,!1);return;case`select`:for(a in Q(`invalid`,e),r=s=o=null,n)if(n.hasOwnProperty(a)&&(c=n[a],c!=null))switch(a){case`value`:o=c;break;case`defaultValue`:s=c;break;case`multiple`:r=c;default:$(e,t,a,c,n,null)}t=o,n=s,e.multiple=!!r,t==null?n!=null&&hn(e,!!r,n,!0):hn(e,!!r,t,!1);return;case`textarea`:for(s in Q(`invalid`,e),o=a=r=null,n)if(n.hasOwnProperty(s)&&(c=n[s],c!=null))switch(s){case`value`:r=c;break;case`defaultValue`:a=c;break;case`children`:o=c;break;case`dangerouslySetInnerHTML`:if(c!=null)throw Error(i(91));break;default:$(e,t,s,c,n,null)}_n(e,r,a,o);return;case`option`:for(l in n)if(n.hasOwnProperty(l)&&(r=n[l],r!=null))switch(l){case`selected`:e.selected=r&&typeof r!=`function`&&typeof r!=`symbol`;break;default:$(e,t,l,r,n,null)}return;case`dialog`:Q(`beforetoggle`,e),Q(`toggle`,e),Q(`cancel`,e),Q(`close`,e);break;case`iframe`:case`object`:Q(`load`,e);break;case`video`:case`audio`:for(r=0;r<zf.length;r++)Q(zf[r],e);break;case`image`:Q(`error`,e),Q(`load`,e);break;case`details`:Q(`toggle`,e);break;case`embed`:case`source`:case`link`:Q(`error`,e),Q(`load`,e);case`area`:case`base`:case`br`:case`col`:case`hr`:case`keygen`:case`meta`:case`param`:case`track`:case`wbr`:case`menuitem`:for(u in n)if(n.hasOwnProperty(u)&&(r=n[u],r!=null))switch(u){case`children`:case`dangerouslySetInnerHTML`:throw Error(i(137,t));default:$(e,t,u,r,n,null)}return;default:if(Sn(t)){for(d in n)n.hasOwnProperty(d)&&(r=n[d],r!==void 0&&tp(e,t,d,r,n,void 0));return}}for(c in n)n.hasOwnProperty(c)&&(r=n[c],r!=null&&$(e,t,c,r,n,null))}var rp={};function ip(e,t,n,r){switch(t){case`div`:case`span`:case`svg`:case`path`:case`a`:case`g`:case`p`:case`li`:break;case`input`:var a=null,o=null,s=null,c=null,l=null,u=null,d=null;for(m in n){var f=n[m];if(n.hasOwnProperty(m)&&f!=null)switch(m){case`checked`:break;case`value`:break;case`defaultValue`:l=f;default:r.hasOwnProperty(m)||$(e,t,m,null,r,f)}}for(var p in r){var m=r[p];if(f=n[p],r.hasOwnProperty(p)&&(m!=null||f!=null))switch(p){case`type`:m!==f&&(j=!0),o=m;break;case`name`:m!==f&&(j=!0),a=m;break;case`checked`:m!==f&&(j=!0),u=m;break;case`defaultChecked`:m!==f&&(j=!0),d=m;break;case`value`:m!==f&&(j=!0),s=m;break;case`defaultValue`:m!==f&&(j=!0),c=m;break;case`children`:case`dangerouslySetInnerHTML`:if(m!=null)throw Error(i(137,t));break;default:m!==f&&$(e,t,p,m,r,f)}}fn(e,s,c,l,u,d,o,a);return;case`select`:for(o in m=s=c=p=null,n)if(l=n[o],n.hasOwnProperty(o)&&l!=null)switch(o){case`value`:break;case`multiple`:m=l;default:r.hasOwnProperty(o)||$(e,t,o,null,r,l)}for(a in r)if(o=r[a],l=n[a],r.hasOwnProperty(a)&&(o!=null||l!=null))switch(a){case`value`:o!==l&&(j=!0),p=o;break;case`defaultValue`:o!==l&&(j=!0),c=o;break;case`multiple`:o!==l&&(j=!0),s=o;default:o!==l&&$(e,t,a,o,r,l)}t=c,n=s,r=m,p==null?!!r!=!!n&&(t==null?hn(e,!!n,n?[]:``,!1):hn(e,!!n,t,!0)):hn(e,!!n,p,!1);return;case`textarea`:for(c in m=p=null,n)if(a=n[c],n.hasOwnProperty(c)&&a!=null&&!r.hasOwnProperty(c))switch(c){case`value`:break;case`children`:break;default:$(e,t,c,null,r,a)}for(s in r)if(a=r[s],o=n[s],r.hasOwnProperty(s)&&(a!=null||o!=null))switch(s){case`value`:a!==o&&(j=!0),p=a;break;case`defaultValue`:a!==o&&(j=!0),m=a;break;case`children`:break;case`dangerouslySetInnerHTML`:if(a!=null)throw Error(i(91));break;default:a!==o&&$(e,t,s,a,r,o)}gn(e,p,m);return;case`option`:for(var h in n)if(p=n[h],n.hasOwnProperty(h)&&p!=null&&!r.hasOwnProperty(h))switch(h){case`selected`:e.selected=!1;break;default:$(e,t,h,null,r,p)}for(l in r)if(p=r[l],m=n[l],r.hasOwnProperty(l)&&p!==m&&(p!=null||m!=null))switch(l){case`selected`:p!==m&&(j=!0),e.selected=p&&typeof p!=`function`&&typeof p!=`symbol`;break;default:$(e,t,l,p,r,m)}return;case`img`:case`link`:case`area`:case`base`:case`br`:case`col`:case`embed`:case`hr`:case`keygen`:case`meta`:case`param`:case`source`:case`track`:case`wbr`:case`menuitem`:for(var g in n)p=n[g],n.hasOwnProperty(g)&&p!=null&&!r.hasOwnProperty(g)&&$(e,t,g,null,r,p);for(u in r)if(p=r[u],m=n[u],r.hasOwnProperty(u)&&p!==m&&(p!=null||m!=null))switch(u){case`children`:case`dangerouslySetInnerHTML`:if(p!=null)throw Error(i(137,t));break;default:$(e,t,u,p,r,m)}return;default:if(Sn(t)){for(var _ in n)p=n[_],n.hasOwnProperty(_)&&p!==void 0&&!r.hasOwnProperty(_)&&tp(e,t,_,void 0,r,p);for(d in r)p=r[d],m=n[d],!r.hasOwnProperty(d)||p===m||p===void 0&&m===void 0||tp(e,t,d,p,r,m);return}}for(var v in n)p=n[v],n.hasOwnProperty(v)&&p!=null&&!r.hasOwnProperty(v)&&$(e,t,v,null,r,p);for(f in r)p=r[f],m=n[f],!r.hasOwnProperty(f)||p===m||p==null&&m==null||$(e,t,f,p,r,m)}function ap(e){switch(e){case`css`:case`script`:case`font`:case`img`:case`image`:case`input`:case`link`:return!0;default:return!1}}function op(){if(typeof performance.getEntriesByType==`function`){for(var e=0,t=0,n=performance.getEntriesByType(`resource`),r=0;r<n.length;r++){var i=n[r],a=i.transferSize,o=i.initiatorType,s=i.duration;if(a&&s&&ap(o)){for(o=0,s=i.responseEnd,r+=1;r<n.length;r++){var c=n[r],l=c.startTime;if(l>s)break;var u=c.transferSize,d=c.initiatorType;u&&ap(d)&&(c=c.responseEnd,o+=u*(c<s?1:(s-l)/(c-l)))}if(--r,t+=8*(a+o)/(i.duration/1e3),e++,10<e)break}}if(0<e)return t/e/1e6}return navigator.connection&&(e=navigator.connection.downlink,typeof e==`number`)?e:5}var sp=null,cp=null;function lp(e){return e.nodeType===9?e:e.ownerDocument}function up(e){switch(e){case`http://www.w3.org/2000/svg`:return 1;case`http://www.w3.org/1998/Math/MathML`:return 2;default:return 0}}function dp(e,t){if(e===0)switch(t){case`svg`:return 1;case`math`:return 2;default:return 0}return e===1&&t===`foreignObject`?0:e}function fp(e,t,n,r){return n=lp(n).createElement(e),n[jt]=r,n[Mt]=t,np(n,e,t),A(n),n}function pp(e,t){return e===`textarea`||e===`noscript`||typeof t.children==`string`||typeof t.children==`number`||typeof t.children==`bigint`||typeof t.dangerouslySetInnerHTML==`object`&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var mp=null;function hp(){var e=window.event;return e&&e.type===`popstate`?e!==mp&&(mp=e,!0):(mp=null,!1)}var gp=typeof setTimeout==`function`?setTimeout:void 0,_p=typeof clearTimeout==`function`?clearTimeout:void 0,vp=typeof Promise==`function`?Promise:void 0,yp=typeof requestAnimationFrame==`function`?requestAnimationFrame:gp,bp=typeof queueMicrotask==`function`?queueMicrotask:vp===void 0?gp:function(e){return vp.resolve(null).then(e).catch(xp)};function xp(e){setTimeout(function(){throw e})}function Sp(e){return e===`head`}function Cp(e,t){var n=t,r=0;do{var i=n.nextSibling;if(e.removeChild(n),i&&i.nodeType===8){if(n=i.data,n===`/$`||n===`/&`){if(r===0){e.removeChild(i),Hh(t);return}r--}else if(n===`$`||n===`$?`||n===`$~`||n===`$!`||n===`&`)r++;else if(n===`html`)_m(e.ownerDocument.documentElement);else if(n===`head`){n=e.ownerDocument.head,_m(n);for(var a=n.firstChild;a;){var o=a.nextSibling,s=a.nodeName;a[Rt]||s===`SCRIPT`||s===`STYLE`||s===`LINK`&&a.rel.toLowerCase()===`stylesheet`||n.removeChild(a),a=o}}else n===`body`&&_m(e.ownerDocument.body)}n=i}while(n);Hh(t)}function wp(e,t){var n=e;e=0;do{var r=n.nextSibling;if(n.nodeType===1?t?(n._stashedDisplay=n.style.display,n.style.display=`none`):(n.style.display=n._stashedDisplay||``,n.getAttribute(`style`)===``&&n.removeAttribute(`style`)):n.nodeType===3&&(t?(n._stashedText=n.nodeValue,n.nodeValue=``):n.nodeValue=n._stashedText||``),r&&r.nodeType===8){if(n=r.data,n===`/$`){if(e===0)break;e--}else n!==`$`&&n!==`$?`&&n!==`$~`&&n!==`$!`||e++}n=r}while(n)}function Tp(e,t,n){if(t=CSS.escape(t)===t?t:`r-`+btoa(t).replace(/=/g,``),e.style.viewTransitionName=t,n!=null&&(e.style.viewTransitionClass=n),n=getComputedStyle(e),n.display===`inline`){if(t=e.getClientRects(),t.length===1)var r=1;else for(var i=r=0;i<t.length;i++){var a=t[i];0<a.width&&0<a.height&&r++}r===1&&(e=e.style,e.display=t.length===1?`inline-block`:`block`,e.marginTop=`-`+n.paddingTop,e.marginBottom=`-`+n.paddingBottom)}}function Ep(e,t){e=e.style,t=t.style;var n=t==null?null:t.hasOwnProperty(`viewTransitionName`)?t.viewTransitionName:t.hasOwnProperty(`view-transition-name`)?t[`view-transition-name`]:null;e.viewTransitionName=n==null||typeof n==`boolean`?``:(``+n).trim(),n=t==null?null:t.hasOwnProperty(`viewTransitionClass`)?t.viewTransitionClass:t.hasOwnProperty(`view-transition-class`)?t[`view-transition-class`]:null,e.viewTransitionClass=n==null||typeof n==`boolean`?``:(``+n).trim(),e.display===`inline-block`&&(t==null?e.display=e.margin=``:(n=t.display,e.display=n==null||typeof n==`boolean`?``:n,n=t.margin,n==null?(n=t.hasOwnProperty(`marginTop`)?t.marginTop:t[`margin-top`],e.marginTop=n==null||typeof n==`boolean`?``:n,t=t.hasOwnProperty(`marginBottom`)?t.marginBottom:t[`margin-bottom`],e.marginBottom=t==null||typeof t==`boolean`?``:t):e.margin=n))}function Dp(e,t,n){return n=n.ownerDocument.defaultView,{rect:e,abs:t.position===`absolute`||t.position===`fixed`,clip:t.clipPath!==`none`||t.overflow!==`visible`||t.filter!==`none`||t.mask!==`none`||t.mask!==`none`||t.borderRadius!==`0px`,view:0<=e.bottom&&0<=e.right&&e.top<=n.innerHeight&&e.left<=n.innerWidth}}function Op(e){return Dp(e.getBoundingClientRect(),getComputedStyle(e),e)}function kp(e){var t=e.getBoundingClientRect();t=new DOMRect(t.x+2e4,t.y+2e4,t.width,t.height);var n=getComputedStyle(e);return Dp(t,n,e)}function Ap(e){return e.documentElement.clientHeight}function jp(e){this.addEventListener(`load`,e),this.addEventListener(`error`,e)}function Mp(e,t,n,r,i,a,o,s,c){var l=t.nodeType===9?t:t.ownerDocument;try{var u=l.startViewTransition({update:function(){var t=l.defaultView,n=t.navigation&&t.navigation.transition,o=l.fonts.status;r();var s=[];if(o===`loaded`&&(Ap(l),l.fonts.status===`loading`&&s.push(l.fonts.ready)),o=s.length,e!==null)for(var c=e.suspenseyImages,u=0,d=0;d<c.length;d++){var f=c[d];if(!f.complete){var p=f.getBoundingClientRect();if(0<p.bottom&&0<p.right&&p.top<t.innerHeight&&p.left<t.innerWidth){if(u+=Xm(f),u>$m){s.length=o;break}f=new Promise(jp.bind(f)),s.push(f)}}}if(0<s.length)return t=Promise.race([Promise.all(s),new Promise(function(e){return setTimeout(e,500)})]).then(i,i),(n?Promise.allSettled([n.finished,t]):t).then(a,a);if(i(),n)return n.finished.then(a,a);a()},types:n});l.__reactViewTransition=u;var d=[];return u.ready.then(function(){for(var e=l.documentElement.getAnimations({subtree:!0}),t=0;t<e.length;t++){var n=e[t],r=n.effect,i=r.pseudoElement;if(i!=null&&i.startsWith(`::view-transition`)){d.push(n),n=r.getKeyframes();for(var a=i=void 0,s=!0,c=0;c<n.length;c++){var u=n[c],f=u.width;if(i===void 0)i=f;else if(i!==f){s=!1;break}if(f=u.height,a===void 0)a=f;else if(a!==f){s=!1;break}delete u.width,delete u.height,u.transform===`none`&&delete u.transform}s&&i!==void 0&&a!==void 0&&(r.setKeyframes(n),s=getComputedStyle(r.target,r.pseudoElement),s.width!==i||s.height!==a)&&(s=n[0],s.width=i,s.height=a,s=n[n.length-1],s.width=i,s.height=a,r.setKeyframes(n))}}o()},function(e){l.__reactViewTransition===u&&(l.__reactViewTransition=null);try{if(typeof e==`object`&&e)switch(e.name){case`InvalidStateError`:(e.message===`View transition was skipped because document visibility state is hidden.`||e.message===`Skipping view transition because document visibility state has become hidden.`||e.message===`Skipping view transition because viewport size changed.`||e.message===`Transition was aborted because of invalid state`)&&(e=null)}e!==null&&c(e)}finally{r(),i(),o()}}),u.finished.finally(function(){for(var e=0;e<d.length;e++)d[e].cancel();l.__reactViewTransition===u&&(l.__reactViewTransition=null),s()}),u}catch{return r(),i(),o(),null}}function Np(e,t){this._scope=document.documentElement,this._selector=`::view-transition-`+e+`(`+t+`)`}Np.prototype.animate=function(e,t){return t=typeof t==`number`?{duration:t}:C({},t),t.pseudoElement=this._selector,this._scope.animate(e,t)},Np.prototype.getAnimations=function(){for(var e=this._scope,t=this._selector,n=e.getAnimations({subtree:!0}),r=[],i=0;i<n.length;i++){var a=n[i].effect;a!==null&&a.target===e&&a.pseudoElement===t&&r.push(n[i])}return r},Np.prototype.getComputedStyle=function(){return getComputedStyle(this._scope,this._selector)};function Pp(e){return{name:e,group:new Np(`group`,e),imagePair:new Np(`image-pair`,e),old:new Np(`old`,e),new:new Np(`new`,e)}}function Fp(e){this._fragmentFiber=e,this._observers=this._eventListeners=null}Fp.prototype.addEventListener=function(e,t,n){var r=null,i=null;if(!(n!=null&&typeof n!=`boolean`&&(r=n.signal||null,r!==null&&r.aborted))){this._eventListeners===null&&(this._eventListeners=[]);var a=this._eventListeners;if(Bp(a,e,t,n)===-1){var o=this,s=t;n!=null&&typeof n!=`boolean`&&!0===n.once&&(s=function(r){o.removeEventListener(e,t,n),typeof t==`function`?t.call(this,r):t.handleEvent(r)}),r!==null&&(i=o.removeEventListener.bind(o,e,t,n),r.addEventListener(`abort`,i,{once:!0}),i=r.removeEventListener.bind(r,`abort`,i)),r=Rp(n),a.push({type:e,listener:t,optionsOrUseCapture:n,attachedListener:s,cleanup:i}),h(this._fragmentFiber.child,!1,Ip,e,s,r)}this._eventListeners=a}};function Ip(e,t,n,r){return b(e).addEventListener(t,n,r),!1}Fp.prototype.removeEventListener=function(e,t,n){var r=this._eventListeners;if(r!==null&&(t=Bp(r,e,t,n),t!==-1)){var i=r[t];n=i.attachedListener;var a=i.cleanup;i=Rp(i.optionsOrUseCapture),h(this._fragmentFiber.child,!1,Lp,e,n,i),r.splice(t,1),a!==null&&a()}};function Lp(e,t,n,r){return b(e).removeEventListener(t,n,r),!1}function Rp(e){return e!=null&&typeof e!=`boolean`&&(!0===e.once||e.signal instanceof AbortSignal)?{capture:e.capture,passive:e.passive}:e}function zp(e){return e==null?`c=0`:typeof e==`boolean`?`c=`+(e?`1`:`0`):`c=`+(e.capture?`1`:`0`)}function Bp(e,t,n,r){if(e.length===0)return-1;r=zp(r);for(var i=0;i<e.length;i++){var a=e[i];if(a.type===t&&a.listener===n&&zp(a.optionsOrUseCapture)===r)return i}return-1}Fp.prototype.dispatchEvent=function(e){var t=g(this._fragmentFiber);if(t===null)return!0;t=b(t);var n=this._eventListeners;if(n!==null&&0<n.length||!e.bubbles){var r=t.nodeType===9?t.createComment(``):document.createTextNode(``);if(n)for(var i=0;i<n.length;i++){var a=n[i];r.addEventListener(a.type,a.attachedListener,Rp(a.optionsOrUseCapture))}if(t.appendChild(r),e=r.dispatchEvent(e),n)for(i=0;i<n.length;i++)a=n[i],r.removeEventListener(a.type,a.attachedListener,Rp(a.optionsOrUseCapture));return t.removeChild(r),e}return t.dispatchEvent(e)},Fp.prototype.focus=function(e){h(this._fragmentFiber.child,!0,Vp,e,void 0,void 0)};function Vp(e,t){return e.tag!==6&&(e=b(e),pm(e,t))}Fp.prototype.focusLast=function(e){var t=[];h(this._fragmentFiber.child,!0,Hp,t,void 0,void 0);for(var n=t.length-1;0<=n&&!Vp(t[n],e);n--);};function Hp(e,t){return t.push(e),!1}Fp.prototype.blur=function(){var e=g(this._fragmentFiber);e!==null&&(e=b(e),e=lp(e).activeElement,e!==null&&h(this._fragmentFiber.child,!1,Up,e,void 0,void 0))};function Up(e,t){return e.tag!==6&&(e=b(e),e===t||e.contains(t)?(t.blur(),!0):!1)}Fp.prototype.observeUsing=function(e){this._observers===null&&(this._observers=new Set),this._observers.add(e),h(this._fragmentFiber.child,!1,Wp,e,void 0,void 0)};function Wp(e,t){return e.tag===6||(e=b(e),t.observe(e)),!1}Fp.prototype.unobserveUsing=function(e){var t=this._observers;if(t!==null&&t.has(e)){t.delete(e),h(this._fragmentFiber.child,!1,Gp,e,void 0,void 0);for(var n=t=0;n<Kp.length;n++){var r=Kp[n];r.fragmentInstance===this&&r.observer===e?e.unobserve(r.instance):Kp[t++]=r}Kp.length=t}};function Gp(e,t){return e.tag===6||(e=b(e),t.unobserve(e)),!1}var Kp=[],qp=!1;function Jp(e,t,n){Kp.push({fragmentInstance:e,observer:t,instance:n}),qp||(qp=!0,mm(function(){qp=!1;var e=Kp;Kp=[];for(var t=0;t<e.length;t++){var n=e[t];n.observer.unobserve(n.instance)}}))}Fp.prototype.getClientRects=function(){var e=[];return h(this._fragmentFiber.child,!1,Yp,e,void 0,void 0),e};function Yp(e,t){if(e.tag===6){e=e.stateNode;var n=e.ownerDocument.createRange();n.selectNodeContents(e),t.push.apply(t,n.getClientRects())}else e=b(e),t.push.apply(t,e.getClientRects());return!1}Fp.prototype.getRootNode=function(e){var t=g(this._fragmentFiber);return t===null?this:b(t).getRootNode(e)},Fp.prototype.compareDocumentPosition=function(e){var t=g(this._fragmentFiber);if(t===null)return Node.DOCUMENT_POSITION_DISCONNECTED;var n=[];h(this._fragmentFiber.child,!1,Hp,n,void 0,void 0);var r=b(t);if(n.length===0){if(n=r,_(this._fragmentFiber)){a:{for(t=this._fragmentFiber.return;t!==null;){if(t.tag===4){t=t.stateNode.containerInfo;break a}if(t.tag===3||t.tag===5||t.tag===27)break;t=t.return}t=null}t!=null&&(n=t)}t=this._fragmentFiber;var i=r=n.compareDocumentPosition(e);return n===e?i=Node.DOCUMENT_POSITION_CONTAINS:r&Node.DOCUMENT_POSITION_CONTAINED_BY&&(n=v(t)[1],n===null?i=Node.DOCUMENT_POSITION_PRECEDING:(e=b(n).compareDocumentPosition(e),i=e===0||e&Node.DOCUMENT_POSITION_FOLLOWING?Node.DOCUMENT_POSITION_FOLLOWING:Node.DOCUMENT_POSITION_PRECEDING)),i|=Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC}t=b(n[0]),i=b(n[n.length-1]);var a=_(this._fragmentFiber)?t.parentElement:r;if(a==null)return Node.DOCUMENT_POSITION_DISCONNECTED;r=a.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY,a=a.compareDocumentPosition(i)&Node.DOCUMENT_POSITION_CONTAINED_BY;var o=t.compareDocumentPosition(e),s=i.compareDocumentPosition(e),c=o&Node.DOCUMENT_POSITION_CONTAINED_BY||s&Node.DOCUMENT_POSITION_CONTAINED_BY;return s=r&&a&&o&Node.DOCUMENT_POSITION_FOLLOWING&&s&Node.DOCUMENT_POSITION_PRECEDING,t=r&&t===e||a&&i===e||c||s?Node.DOCUMENT_POSITION_CONTAINED_BY:!r&&t===e||!a&&i===e?Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC:o,t&Node.DOCUMENT_POSITION_DISCONNECTED||t&Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC||Xp(t,this._fragmentFiber,n[0],n[n.length-1],e)?t:Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC};function Xp(e,t,n,r,i){var a=Vt(i);if(e&Node.DOCUMENT_POSITION_CONTAINED_BY){if(n=!!a)a:{for(;a!==null;){if(a.tag===7&&(a===t||a.alternate===t)){n=!0;break a}a=a.return}n=!1}return n}if(e&Node.DOCUMENT_POSITION_CONTAINS){if(a===null)return a=i.ownerDocument,i===a||i===a.documentElement||i===a.body;a:{for(a=t,t=g(t);a!==null;){if(!(a.tag!==5&&a.tag!==3&&a.tag!==27||a!==t&&a.alternate!==t)){a=!0;break a}a=a.return}a=!1}return a}return e&Node.DOCUMENT_POSITION_PRECEDING?((t=!!a)&&!(t=a===n)&&(t=re(n,a,S),t===null?t=!1:(h(t,!0,x,a,n),a=ee,ee=null,t=a!==null)),t):e&Node.DOCUMENT_POSITION_FOLLOWING?((t=!!a)&&!(t=a===r)&&(t=re(r,a,S),t===null?t=!1:(h(t,!0,ne,a,r),a=ee,te=ee=null,t=a!==null)),t):!1}function Zp(e,t){var n=e.ownerDocument.createRange();n.selectNodeContents(e),e=n.getBoundingClientRect(),window.scrollTo(window.scrollX+e.left,t?window.scrollY+e.top:window.scrollY+e.bottom-window.innerHeight)}Fp.prototype.scrollIntoView=function(e){if(typeof e==`object`)throw Error(i(566));var t=[];h(this._fragmentFiber.child,!1,Hp,t,void 0,void 0);var n=!1!==e;if(t.length===0){var r=v(this._fragmentFiber);if(r=n?r[1]||r[0]||g(this._fragmentFiber):r[0]||r[1],r===null)return;if(r.tag===6){e=b(r),Zp(e,n);return}if(r=b(r),r.nodeType!==9){if(r.nodeType===11){n=`host`in r?r.host:null,n!==null&&n.scrollIntoView(e);return}r.scrollIntoView(e)}}for(r=n?t.length-1:0;r!==(n?-1:t.length);){var a=t[r];a.tag===6?(a=b(a),Zp(a,n)):b(a).scrollIntoView(e),r+=n?-1:1}};function Qp(e,t){return e=b(e),$p(e,t),!1}function $p(e,t){e.reactFragments??=new Set,e.reactFragments.add(t)}function em(e,t){var n=t._eventListeners;if(n!==null)for(var r=0;r<n.length;r++){var i=n[r];e.addEventListener(i.type,i.attachedListener,Rp(i.optionsOrUseCapture))}e.nodeType!==3&&(n=t._observers,n!==null&&n.forEach(function(n){for(var r=0,i=0;i<Kp.length;i++){var a=Kp[i];(a.fragmentInstance!==t||a.observer!==n||a.instance!==e)&&(Kp[r++]=a)}Kp.length=r,n.observe(e)}),$p(e,t))}function tm(e,t){var n=t._eventListeners;if(n!==null)for(var r=0;r<n.length;r++){var i=n[r];e.removeEventListener(i.type,i.attachedListener,Rp(i.optionsOrUseCapture))}e.nodeType!==3&&(n=t._observers,n!==null&&n.forEach(function(n){typeof n.rootMargin==`string`?Jp(t,n,e):n.unobserve(e)}),e.reactFragments!=null&&e.reactFragments.delete(t))}function nm(e){var t=e.firstChild;for(t&&t.nodeType===10&&(t=t.nextSibling);t;){var n=t;switch(t=t.nextSibling,n.nodeName){case`HTML`:case`HEAD`:case`BODY`:nm(n),Bt(n);continue;case`SCRIPT`:case`STYLE`:continue;case`LINK`:if(n.rel.toLowerCase()===`stylesheet`)continue}e.removeChild(n)}}function rm(e,t,n,r){for(;e.nodeType===1;){var i=n;if(e.nodeName.toLowerCase()!==t.toLowerCase()){if(!r&&(e.nodeName!==`INPUT`||e.type!==`hidden`))break}else if(!r){if(t===`input`&&e.type===`hidden`){var a=i.name==null?null:``+i.name;if(i.type===`hidden`&&e.getAttribute(`name`)===a)return e}else return e}else if(!e[Rt])switch(t){case`meta`:if(!e.hasAttribute(`itemprop`))break;return e;case`link`:if(a=e.getAttribute(`rel`),a===`stylesheet`&&e.hasAttribute(`data-precedence`)||a!==i.rel||e.getAttribute(`href`)!==(i.href==null||i.href===``?null:i.href)||e.getAttribute(`crossorigin`)!==(i.crossOrigin==null?null:i.crossOrigin)||e.getAttribute(`title`)!==(i.title==null?null:i.title))break;return e;case`style`:if(e.hasAttribute(`data-precedence`))break;return e;case`script`:if(a=e.getAttribute(`src`),(a!==(i.src==null?null:i.src)||e.getAttribute(`type`)!==(i.type==null?null:i.type)||e.getAttribute(`crossorigin`)!==(i.crossOrigin==null?null:i.crossOrigin))&&a&&e.hasAttribute(`async`)&&!e.hasAttribute(`itemprop`))break;return e;default:return e}if(e=lm(e.nextSibling),e===null)break}return null}function im(e,t,n){if(t===``)return null;for(;e.nodeType!==3;)if((e.nodeType!==1||e.nodeName!==`INPUT`||e.type!==`hidden`)&&!n||(e=lm(e.nextSibling),e===null))return null;return e}function am(e,t){for(;e.nodeType!==8;)if((e.nodeType!==1||e.nodeName!==`INPUT`||e.type!==`hidden`)&&!t||(e=lm(e.nextSibling),e===null))return null;return e}function om(e){return e.data===`$?`||e.data===`$~`}function sm(e){return e.data===`$!`||e.data===`$?`&&e.ownerDocument.readyState!==`loading`}function cm(e,t){var n=e.ownerDocument;if(e.data===`$~`)e._reactRetry=t;else if(e.data!==`$?`||n.readyState!==`loading`)t();else{var r=function(){t(),n.removeEventListener(`DOMContentLoaded`,r)};n.addEventListener(`DOMContentLoaded`,r),e._reactRetry=r}}function lm(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t===`$`||t===`$!`||t===`$?`||t===`$~`||t===`&`||t===`F!`||t===`F`)break;if(t===`/$`||t===`/&`)return null}}return e}var um=null;function dm(e){e=e.nextSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n===`/$`||n===`/&`){if(t===0)return lm(e.nextSibling);t--}else n!==`$`&&n!==`$!`&&n!==`$?`&&n!==`$~`&&n!==`&`||t++}e=e.nextSibling}return null}function fm(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n===`$`||n===`$!`||n===`$?`||n===`$~`||n===`&`){if(t===0)return e;t--}else n!==`/$`&&n!==`/&`||t++}e=e.previousSibling}return null}function pm(e,t){function n(){r=!0}if(e.ownerDocument.activeElement===e)return!0;var r=!1;try{e.ownerDocument.addEventListener(`focus`,n,!0),(e.focus||HTMLElement.prototype.focus).call(e,t)}finally{e.ownerDocument.removeEventListener(`focus`,n,!0)}return r}function mm(e){yp(function(){yp(function(t){return e(t)})})}function hm(e,t,n){switch(t=lp(n),e){case`html`:if(e=t.documentElement,!e)throw Error(i(452));return e;case`head`:if(e=t.head,!e)throw Error(i(453));return e;case`body`:if(e=t.body,!e)throw Error(i(454));return e;default:throw Error(i(451))}}function gm(e,t,n){for(var r in n){var i=n[r];n.hasOwnProperty(r)&&i!=null&&$(e,t,r,null,rp,i)}n.dangerouslySetInnerHTML!=null&&(e.textContent=``),e.onclick===En&&(e.onclick=null),Bt(e)}function _m(e){for(var t=e.attributes;t.length;)e.removeAttributeNode(t[0]);Bt(e)}var vm=new Map,ym=new Set;function bm(e){if(typeof e.getRootNode==`function`){var t=e.getRootNode();if(t.nodeType===9||t.nodeType===11)return t}return e.nodeType===9?e:e.ownerDocument}var xm=O.d;O.d={f:Sm,r:Cm,D:Em,C:Dm,L:Om,m:km,X:jm,S:Am,M:Mm};function Sm(){var e=xm.f(),t=zd();return e||t}function Cm(e){var t=Ht(e);t!==null&&t.tag===5&&t.type===`form`?oc(t):xm.r(e)}var wm=typeof document>`u`?null:document;function Tm(e,t,n){var r=wm;if(r&&typeof t==`string`&&t){var i=dn(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),ym.has(i)||(ym.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),np(t,`link`,e),A(t),r.head.appendChild(t)))}}function Em(e){xm.D(e),Tm(`dns-prefetch`,e,null)}function Dm(e,t){xm.C(e,t),Tm(`preconnect`,e,t)}function Om(e,t,n){xm.L(e,t,n);var r=wm;if(r&&e&&t){var i=`link[rel="preload"][as="`+dn(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+dn(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+dn(n.imageSizes)+`"]`)):i+=`[href="`+dn(e)+`"]`;var a=i;switch(t){case`style`:a=Pm(e);break;case`script`:a=Rm(e)}if(!(vm.has(a)||(e=C({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vm.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Fm(a))||t===`script`&&r.querySelector(zm(a))))){var o=r.createElement(`link`);np(o,`link`,e),t===`style`&&(o[zt]=!0,o.onload=o.onerror=function(){Gt(o)}),A(o),r.head.appendChild(o)}}}function km(e,t){xm.m(e,t);var n=wm;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+dn(r)+`"][href="`+dn(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rm(e)}if(!vm.has(a)&&(e=C({rel:`modulepreload`,href:e},t),vm.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zm(a)))return}r=n.createElement(`link`),np(r,`link`,e),A(r),n.head.appendChild(r)}}}function Am(e,t,n){xm.S(e,t,n);var r=wm;if(r&&e){var i=Wt(r).hoistableStyles,a=Pm(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Fm(a)))s.loading=5;else{e=C({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vm.get(a))&&Hm(e,n);var c=o=r.createElement(`link`);A(c),np(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vm(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jm(e,t){xm.X(e,t);var n=wm;if(n&&e){var r=Wt(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=C({src:e,async:!0},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),A(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mm(e,t){xm.M(e,t);var n=wm;if(n&&e){var r=Wt(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=C({src:e,async:!0,type:`module`},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),A(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nm(e,t,n,r){var a=(a=je.current)?bm(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(n=Pm(n.href),t=Wt(a).hoistableStyles,r=t.get(n),r||(r={type:`style`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pm(n.href);var o=Wt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Fm(e)))?o._p||(s.instance=o,s.state.loading=5):(o=vm.get(e),o||(o={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vm.set(e,o)),Lm(a,e,o,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(n=Rm(n),t=Wt(a).hoistableScripts,r=t.get(n),r||(r={type:`script`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pm(e){return`href="`+dn(e)+`"`}function Fm(e){return`link[rel="stylesheet"][`+e+`]`}function Im(e){return C({},e,{"data-precedence":e.precedence,precedence:null})}function Lm(e,t,n,r){if(t=e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)){if(!0!==t[zt]){r.loading=1;return}}else t=e.createElement(`link`),t[zt]=!0,t.onload=t.onerror=Gt.bind(null,t),np(t,`link`,n),A(t),e.head.appendChild(t);r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2})}function Rm(e){return`[src="`+dn(e)+`"]`}function zm(e){return`script[async]`+e}function Bm(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+dn(n.href)+`"]`);if(r)return t.instance=r,A(r),r;var a=C({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),A(r),np(r,`style`,a),Vm(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pm(n.href);var o=e.querySelector(Fm(a));if(o)return t.state.loading|=4,t.instance=o,A(o),o;r=Im(n),(a=vm.get(a))&&Hm(r,a),o=(e.ownerDocument||e).createElement(`link`),A(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),np(o,`link`,r),t.state.loading|=4,Vm(o,n.precedence,e),t.instance=o;case`script`:return o=Rm(n.src),(a=e.querySelector(zm(o)))?(t.instance=a,A(a),a):(r=n,(a=vm.get(o))&&(r=C({},n),Um(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),A(a),np(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vm(r,n.precedence,e));return t.instance}function Vm(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o<r.length;o++){var s=r[o];if(s.dataset.precedence===t)a=s;else if(a!==i)break}a?a.parentNode.insertBefore(e,a.nextSibling):(t=n.nodeType===9?n.head:n,t.insertBefore(e,t.firstChild))}function Hm(e,t){e.crossOrigin??=t.crossOrigin,e.referrerPolicy??=t.referrerPolicy,e.title??=t.title}function Um(e,t){e.crossOrigin??=t.crossOrigin,e.referrerPolicy??=t.referrerPolicy,e.integrity??=t.integrity}var Wm=null;function Gm(e,t,n){if(Wm===null){var r=new Map,i=Wm=new Map;i.set(n,r)}else i=Wm,r=i.get(n),r||(r=new Map,i.set(n,r));if(r.has(e))return r;for(r.set(e,null),n=n.getElementsByTagName(e),i=0;i<n.length;i++){var a=n[i];if(!(a[Rt]||a[jt]||e===`link`&&a.getAttribute(`rel`)===`stylesheet`)&&a.namespaceURI!==`http://www.w3.org/2000/svg`){var o=a.getAttribute(t)||``;o=e+o;var s=r.get(o);s?s.push(a):r.set(o,[a])}}return r}function Km(e,t,n){e=e.ownerDocument||e,e.head.insertBefore(n,t===`title`?e.querySelector(`head > title`):null)}function qm(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jm(e,t){return e===`img`&&t.src!=null&&t.src!==``&&t.onLoad==null&&t.loading!==`lazy`}function Ym(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Xm(e){return(e.width||100)*(e.height||100)*(typeof devicePixelRatio==`number`?devicePixelRatio:1)*.25}function Zm(e,t){typeof t.decode==`function`&&(e.imgCount++,t.complete||(e.imgBytes+=Xm(t),e.suspenseyImages.push(t)),e=rh.bind(e),t.decode().then(e,e))}function Qm(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pm(r.href),a=t.querySelector(Fm(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=nh.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,A(a);return}a=t.ownerDocument||t,r=Im(r),(i=vm.get(i))&&Hm(r,i),a=a.createElement(`link`),A(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),np(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=nh.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var $m=0;function eh(e,t){return e.stylesheets&&e.count===0&&ah(e,e.stylesheets),0<e.count||0<e.imgCount?function(n){var r=setTimeout(function(){if(e.stylesheets&&ah(e,e.stylesheets),e.unsuspend){var t=e.unsuspend;e.unsuspend=null,t()}},6e4+t);0<e.imgBytes&&$m===0&&($m=62500*op());var i=setTimeout(function(){if(e.waitingForImages=!1,e.count===0&&(e.stylesheets&&ah(e,e.stylesheets),e.unsuspend)){var t=e.unsuspend;e.unsuspend=null,t()}},(e.imgBytes>$m?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function th(e){if(e.count===0&&(e.imgCount===0||!e.waitingForImages)){if(e.stylesheets)ah(e,e.stylesheets);else if(e.unsuspend){var t=e.unsuspend;e.unsuspend=null,t()}}}function nh(){this.count--,th(this)}function rh(){this.imgCount--,th(this)}var ih=null;function ah(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ih=new Map,t.forEach(oh,e),ih=null,nh.call(e))}function oh(e,t){if(!(t.state.loading&4)){var n=ih.get(e);if(n)var r=n.get(null);else{n=new Map,ih.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a<i.length;a++){var o=i[a];(o.nodeName===`LINK`||o.getAttribute(`media`)!==`not all`)&&(n.set(o.dataset.precedence,o),r=o)}r&&n.set(null,r)}i=t.instance,o=i.getAttribute(`data-precedence`),a=n.get(o)||r,a===r&&n.set(null,i),n.set(o,i),this.count++,r=nh.bind(this),i.addEventListener(`load`,r),i.addEventListener(`error`,r),a?a.parentNode.insertBefore(i,a.nextSibling):(e=e.nodeType===9?e.head:e,e.insertBefore(i,e.firstChild)),t.state.loading|=4}}var sh={$$typeof:E,Provider:null,Consumer:null,_currentValue:we,_currentValue2:we,_threadCount:0};function ch(e,t,n,r,i,a,o,s,c){this.tag=1,this.containerInfo=e,this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.next=this.pendingContext=this.context=this.cancelPendingCommit=null,this.callbackPriority=0,this.expirationTimes=bt(-1),this.entangledLanes=this.shellSuspendCounter=this.errorRecoveryDisabledLanes=this.expiredLanes=this.warmLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=bt(0),this.hiddenUpdates=bt(null),this.identifierPrefix=r,this.onUncaughtError=i,this.onCaughtError=a,this.onRecoverableError=o,this.pooledCache=null,this.pooledCacheLanes=0,this.formState=c,this.transitionTypes=null,this.incompleteTransitions=new Map}function lh(e,t,n,r,i,a,o,s,c,l,u,d){return e=new ch(e,t,n,o,c,l,u,d,s),t=1,!0===a&&(t|=24),a=zi(3,null,null,t),e.current=a,a.stateNode=e,t=La(),t.refCount++,e.pooledCache=t,t.refCount++,a.memoizedState={element:r,isDehydrated:n,cache:t},bo(a),e}function uh(e){return e?(e=Li,e):Li}function dh(e,t,n,r,i,a){i=uh(i),r.context===null?r.context=i:r.pendingContext=i,r=So(t),r.payload={element:n},a=a===void 0?null:a,a!==null&&(r.callback=a),n=Co(e,r,t),n!==null&&(Pd(n,e,t),wo(n,e,t))}function fh(e,t){if(e=e.memoizedState,e!==null&&e.dehydrated!==null){var n=e.retryLane;e.retryLane=n!==0&&n<t?n:t}}function ph(e,t){fh(e,t),(e=e.alternate)&&fh(e,t)}function mh(e){if(e.tag===13||e.tag===31){var t=Pi(e,67108864);t!==null&&Pd(t,e,67108864),ph(e,67108864)}}function hh(e){if(e.tag===13||e.tag===31){var t=jd();t=Et(t);var n=Pi(e,t);n!==null&&Pd(n,e,t),ph(e,t)}}var gh=!0;function _h(e,t,n,r){var i=D.T;D.T=null;var a=O.p;try{O.p=2,yh(e,t,n,r)}finally{O.p=a,D.T=i}}function vh(e,t,n,r){var i=D.T;D.T=null;var a=O.p;try{O.p=8,yh(e,t,n,r)}finally{O.p=a,D.T=i}}function yh(e,t,n,r){if(gh){var i=bh(r);if(i===null)Kf(e,t,r,xh,n),Mh(e,r);else if(Ph(i,e,t,n,r))r.stopPropagation();else if(Mh(e,r),t&4&&-1<jh.indexOf(e)){for(;i!==null;){var a=Ht(i);if(a!==null)switch(a.tag){case 3:if(a=a.stateNode,a.current.memoizedState.isDehydrated){var o=mt(a.pendingLanes);if(o!==0){var s=a;for(s.pendingLanes|=2,s.entangledLanes|=2;o;){var c=1<<31-st(o);s.entanglements[1]|=c,o&=~c}Ef(a),!(W&6)&&(_d=Ye()+500,Df(0,!1))}}break;case 31:case 13:s=Pi(a,2),s!==null&&Pd(s,a,2),zd(),ph(a,2)}if(a=bh(r),a===null&&Kf(e,t,r,xh,n),a===i)break;i=a}i!==null&&r.stopPropagation()}else Kf(e,t,r,null,n)}}function bh(e){return e=On(e),Sh(e)}var xh=null;function Sh(e){if(xh=null,e=Vt(e),e!==null){var t=o(e);if(t===null)e=null;else{var n=t.tag;if(n===13){if(e=s(t),e!==null)return e;e=null}else if(n===31){if(e=c(t),e!==null)return e;e=null}else if(n===3){if(t.stateNode.current.memoizedState.isDehydrated)return t.tag===3?t.stateNode.containerInfo:null;e=null}else t!==e&&(e=null)}}return xh=e,null}function Ch(e){switch(e){case`beforetoggle`:case`cancel`:case`click`:case`close`:case`contextmenu`:case`copy`:case`cut`:case`auxclick`:case`dblclick`:case`dragend`:case`dragstart`:case`drop`:case`focusin`:case`focusout`:case`input`:case`invalid`:case`keydown`:case`keypress`:case`keyup`:case`mousedown`:case`mouseup`:case`paste`:case`pause`:case`play`:case`pointercancel`:case`pointerdown`:case`pointerup`:case`ratechange`:case`reset`:case`seeked`:case`submit`:case`toggle`:case`touchcancel`:case`touchend`:case`touchstart`:case`volumechange`:case`change`:case`selectionchange`:case`textInput`:case`compositionstart`:case`compositionend`:case`compositionupdate`:case`beforeblur`:case`afterblur`:case`beforeinput`:case`blur`:case`fullscreenchange`:case`fullscreenerror`:case`focus`:case`hashchange`:case`popstate`:case`select`:case`selectstart`:return 2;case`drag`:case`dragenter`:case`dragexit`:case`dragleave`:case`dragover`:case`mousemove`:case`mouseout`:case`mouseover`:case`pointermove`:case`pointerout`:case`pointerover`:case`resize`:case`scroll`:case`touchmove`:case`wheel`:case`mouseenter`:case`mouseleave`:case`pointerenter`:case`pointerleave`:return 8;case`message`:switch(Xe()){case Ze:return 2;case Qe:return 8;case $e:case et:return 32;case tt:return 268435456;default:return 32}default:return 32}}var wh=!1,Th=null,Eh=null,Dh=null,Oh=new Map,kh=new Map,Ah=[],jh=`mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset`.split(` `);function Mh(e,t){switch(e){case`focusin`:case`focusout`:Th=null;break;case`dragenter`:case`dragleave`:Eh=null;break;case`mouseover`:case`mouseout`:Dh=null;break;case`pointerover`:case`pointerout`:Oh.delete(t.pointerId);break;case`gotpointercapture`:case`lostpointercapture`:kh.delete(t.pointerId)}}function Nh(e,t,n,r,i,a){return e===null||e.nativeEvent!==a?(e={blockedOn:t,domEventName:n,eventSystemFlags:r,nativeEvent:a,targetContainers:[i]},t!==null&&(t=Ht(t),t!==null&&mh(t)),e):(e.eventSystemFlags|=r,t=e.targetContainers,i!==null&&t.indexOf(i)===-1&&t.push(i),e)}function Ph(e,t,n,r,i){switch(t){case`focusin`:return Th=Nh(Th,e,t,n,r,i),!0;case`dragenter`:return Eh=Nh(Eh,e,t,n,r,i),!0;case`mouseover`:return Dh=Nh(Dh,e,t,n,r,i),!0;case`pointerover`:var a=i.pointerId;return Oh.set(a,Nh(Oh.get(a)||null,e,t,n,r,i)),!0;case`gotpointercapture`:return a=i.pointerId,kh.set(a,Nh(kh.get(a)||null,e,t,n,r,i)),!0}return!1}function Fh(e){var t=Vt(e.target);if(t!==null){var n=o(t);if(n!==null){if(t=n.tag,t===13){if(t=s(n),t!==null){e.blockedOn=t,kt(e.priority,function(){hh(n)});return}}else if(t===31){if(t=c(n),t!==null){e.blockedOn=t,kt(e.priority,function(){hh(n)});return}}else if(t===3&&n.stateNode.current.memoizedState.isDehydrated){e.blockedOn=n.tag===3?n.stateNode.containerInfo:null;return}}}e.blockedOn=null}function Ih(e){if(e.blockedOn!==null)return!1;for(var t=e.targetContainers;0<t.length;){var n=bh(e.nativeEvent);if(n===null){n=e.nativeEvent;var r=new n.constructor(n.type,n);Dn=r,n.target.dispatchEvent(r),Dn=null}else return t=Ht(n),t!==null&&mh(t),e.blockedOn=n,!1;t.shift()}return!0}function Lh(e,t,n){Ih(e)&&n.delete(t)}function Rh(){wh=!1,Th!==null&&Ih(Th)&&(Th=null),Eh!==null&&Ih(Eh)&&(Eh=null),Dh!==null&&Ih(Dh)&&(Dh=null),Oh.forEach(Lh),kh.forEach(Lh)}function zh(e,n){e.blockedOn===n&&(e.blockedOn=null,wh||(wh=!0,t.unstable_scheduleCallback(t.unstable_NormalPriority,Rh)))}var Bh=null;function Vh(e){Bh!==e&&(Bh=e,t.unstable_scheduleCallback(t.unstable_NormalPriority,function(){Bh===e&&(Bh=null);for(var t=0;t<e.length;t+=3){var n=e[t],r=e[t+1],i=e[t+2];if(typeof r!=`function`){if(Sh(r||n)===null)continue;break}var a=Ht(n);a!==null&&(e.splice(t,3),t-=3,ic(a,{pending:!0,data:i,method:n.method,action:r},r,i))}}))}function Hh(e){function t(t){return zh(t,e)}Th!==null&&zh(Th,e),Eh!==null&&zh(Eh,e),Dh!==null&&zh(Dh,e),Oh.forEach(t),kh.forEach(t);for(var n=0;n<Ah.length;n++){var r=Ah[n];r.blockedOn===e&&(r.blockedOn=null)}for(;0<Ah.length&&(n=Ah[0],n.blockedOn===null);)Fh(n),n.blockedOn===null&&Ah.shift();if(n=(e.ownerDocument||e).$$reactFormReplay,n!=null)for(r=0;r<n.length;r+=3){var i=n[r],a=n[r+1],o=i[Mt]||null;if(typeof a==`function`)o||Vh(n);else if(o){var s=null;if(a&&a.hasAttribute(`formAction`)){if(i=a,o=a[Mt]||null)s=o.formAction;else if(Sh(i)!==null)continue}else s=o.action;typeof s==`function`?n[r+1]=s:(n.splice(r,3),r-=3),Vh(n)}}}function Uh(){function e(e){e.canIntercept&&e.info===`react-transition`&&e.intercept({handler:function(){return new Promise(function(e){return i=e})},focusReset:`manual`,scroll:`manual`})}function t(){i!==null&&(i(),i=null),r||setTimeout(n,20)}function n(){if(!r&&!navigation.transition){var e=navigation.currentEntry;e&&e.url!=null&&navigation.navigate(e.url,{state:e.getState(),info:`react-transition`,history:`replace`})}}if(typeof navigation==`object`){var r=!1,i=null;return navigation.addEventListener(`navigate`,e),navigation.addEventListener(`navigatesuccess`,t),navigation.addEventListener(`navigateerror`,t),setTimeout(n,100),function(){r=!0,navigation.removeEventListener(`navigate`,e),navigation.removeEventListener(`navigatesuccess`,t),navigation.removeEventListener(`navigateerror`,t),i!==null&&(i(),i=null)}}}function Wh(e){this._internalRoot=e}Gh.prototype.render=Wh.prototype.render=function(e){var t=this._internalRoot;if(t===null)throw Error(i(409));var n=t.current;dh(n,jd(),e,t,null,null)},Gh.prototype.unmount=Wh.prototype.unmount=function(){var e=this._internalRoot;if(e!==null){this._internalRoot=null;var t=e.containerInfo;dh(e.current,2,null,e,null,null),zd(),t[Nt]=null}};function Gh(e){this._internalRoot=e}Gh.prototype.unstable_scheduleHydration=function(e){if(e){var t=Ot();e={blockedOn:null,target:e,priority:t};for(var n=0;n<Ah.length&&t!==0&&t<Ah[n].priority;n++);Ah.splice(n,0,e),n===0&&Fh(e)}};var Kh=n.version;if(Kh!==`19.3.1`)throw Error(i(527,Kh,`19.3.1`));O.findDOMNode=function(e){var t=e._reactInternals;if(t===void 0)throw typeof e.render==`function`?Error(i(188)):(e=Object.keys(e).join(`,`),Error(i(268,e)));return e=d(t),e=e===null?null:p(e),e=e===null?null:e.stateNode,e};var qh={bundleType:0,version:`19.3.1`,rendererPackageName:`react-dom`,currentDispatcherRef:D,reconcilerVersion:`19.3.1`};if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`){var Jh=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!Jh.isDisabled&&Jh.supportsFiber)try{it=Jh.inject(qh),at=Jh}catch{}}e.createRoot=function(e,t){if(!a(e))throw Error(i(299));var n=!1,r=``,o=Dc,s=Oc,c=kc;return t!=null&&(!0===t.unstable_strictMode&&(n=!0),t.identifierPrefix!==void 0&&(r=t.identifierPrefix),t.onUncaughtError!==void 0&&(o=t.onUncaughtError),t.onCaughtError!==void 0&&(s=t.onCaughtError),t.onRecoverableError!==void 0&&(c=t.onRecoverableError)),t=lh(e,1,!1,null,null,n,r,null,o,s,c,Uh),e[Nt]=t.current,Wf(e),new Wh(t)}})),g=o(((e,t)=>{function n(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE==`function`)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=`modulepreload`,v=function(e){return`/`+e},y={},b=function(e){return e.pathname.endsWith(`.css`)},ee=function(e,t,n){if(t in e)return e[t];let r=n();if(!r){e[t]=void 0;return}let i=r.then(()=>{e[t]=void 0},n=>{throw e[t]=void 0,n});return e[t]=i,i},te=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e,i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?new URL(import.meta.resolve(e)):new URL(e,import.meta.url)}r=o(t.map(t=>{t=v(t,n);let r=s(t),i=b(r);return ee(y,r.href,()=>{if(e===void 0){e={all:new Set,styles:new Set};let t=document.getElementsByTagName(`link`);for(let n=t.length-1;n>=0;n--){let r=t[n];e.all.add(r.href),r.rel===`stylesheet`&&e.styles.add(r.href)}}if((i?e.styles:e.all).has(r.href))return;let t=document.createElement(`link`);if(t.rel=i?`stylesheet`:_,i||(t.as=`script`),t.crossOrigin=``,t.href=r.href,a&&t.setAttribute(`nonce`,a),document.head.appendChild(t),i)return new Promise((e,n)=>{t.addEventListener(`load`,e),t.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${r}`)))})})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},x=c(u(),1),ne=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,S=/^[\\/]{2}/;function re(e,t){return t+e.replace(/\\/g,`/`)}var C=`popstate`;function ie(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function ae(e={}){function t(e,t){let{pathname:n=`/`,search:r=``,hash:i=``}=le(e.location.hash.substring(1));return!n.startsWith(`/`)&&!n.startsWith(`.`)&&(n=`/`+n),ce(``,{pathname:n,search:r,hash:i},t.state&&t.state.usr||null,t.state&&t.state.key||`default`)}function n(e,t){let n=e.document.querySelector(`base`),r=``;if(n&&n.getAttribute(`href`)){let t=e.location.href,n=t.indexOf(`#`);r=n===-1?t:t.slice(0,n)}return r+`#`+(typeof t==`string`?t:E(t))}function r(e,t){T(e.pathname.charAt(0)===`/`,`relative pathnames are not supported in hash history.push(${JSON.stringify(t)})`)}return ue(t,n,r,e)}function w(e,t){if(e===!1||e==null)throw Error(t)}function T(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function oe(){return Math.random().toString(36).substring(2,10)}function se(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function ce(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?le(t):t,state:n,key:t&&t.key||r||oe(),mask:i}}function E({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function le(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function ue(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=ie(e)?e:ce(h.location,e,t);n&&n(r,e),l=u()+1;let d=se(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=ie(e)?e:ce(h.location,e,t);n&&n(r,e),l=u();let i=se(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return de(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(C,d),c=e,()=>{i.removeEventListener(C,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function de(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),w(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:E(t);return i=i.replace(/ $/,`%20`),!n&&S.test(i)&&(i=r+i),new URL(i,r)}function fe(e,t,n=`/`){return pe(e,t,n,!1)}function pe(e,t,n,r,i){let a=ke((typeof t==`string`?le(t):t).pathname||`/`,n);if(a==null)return null;let o=i??me(e),s=null,c=k(a);for(let e=0;s==null&&e<o.length;++e)s=Te(o[e],c,r);return s}function me(e){let t=he(e);return _e(t),t}function he(e,t=[],n=[],r=``,i=!1){let a=(e,a,o=i,s)=>{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;w(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Le([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(w(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),he(e.children,t,u,l,o)),(e.path!=null||e.index)&&t.push({path:l,score:O(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=Oe(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of ge(e.path))a(e,t,!0,n)}),t}function ge(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=ge(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function _e(e){e.sort((e,t)=>e.score===t.score?we(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ve=/^:[\w-]+$/,ye=3,be=2,xe=1,Se=10,Ce=-2,D=e=>e===`*`;function O(e,t){let n=e.split(`/`),r=n.length;return n.some(D)&&(r+=Ce),t&&(r+=be),n.filter(e=>!D(e)).reduce((e,t)=>e+(ve.test(t)?ye:t===``?xe:Se),r)}function we(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function Te(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e<r.length;++e){let s=r[e],c=e===r.length-1,l=a===`/`?t:t.slice(a.length)||`/`,u={path:s.relativePath,caseSensitive:s.caseSensitive,end:c},d=s.matcher&&s.compiledParams?De(u,l,s.matcher,s.compiledParams):Ee(u,l),f=s.route;if(!d&&c&&n&&!r[r.length-1].route.index&&(d=Ee({path:s.relativePath,caseSensitive:s.caseSensitive,end:!1},l)),!d)return null;Object.assign(i,d.params),o.push({params:i,pathname:Le([a,d.pathname]),pathnameBase:ze(Le([a,d.pathnameBase])),route:f}),d.pathnameBase!==`/`&&(a=Le([a,d.pathnameBase]))}return o}function Ee(e,t){typeof e==`string`&&(e={path:e,caseSensitive:!1,end:!0});let[n,r]=Oe(e.path,e.caseSensitive,e.end);return De(e,t,n,r)}function De(e,t,n,r){let i=t.match(n);if(!i)return null;let a=i[0],o=Re(a,1),s=i.slice(1);return{params:r.reduce((e,{paramName:t,isOptional:n},r)=>{if(t===`*`){let e=s[r]||``;o=Re(a.slice(0,a.length-e.length),1)}let i=s[r];return e[t]=n&&!i?void 0:(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function Oe(e,t=!1,n=!0){T(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function k(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return T(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ke(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function Ae(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?le(e):e,a;return n?(n=Ie(n),a=n.startsWith(`/`)||n.startsWith(`\\`)?je(n.substring(1),`/`):je(n,t)):a=t,{pathname:a,search:Be(r),hash:Ve(i)}}function je(e,t){let n=Re(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Me(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in <Link to="..."> and the router will parse it for you.`}function Ne(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Pe(e){let t=Ne(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Fe(e,t,n,r=!1){let i;typeof e==`string`?i=le(e):(i={...e},w(!i.pathname||!i.pathname.includes(`?`),Me(`?`,`pathname`,`search`,i)),w(!i.pathname||!i.pathname.includes(`#`),Me(`#`,`pathname`,`hash`,i)),w(!i.search||!i.search.includes(`#`),Me(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Ae(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Ie=e=>e.replace(/[\\/]{2,}/g,`/`),Le=e=>Ie(e.join(`/`));function Re(e,t=0){let n=e.length;for(;n>t&&e.charCodeAt(n-1)===47;)n--;return n===e.length?e:e.slice(0,n)}var ze=e=>Re(e).replace(/^\/*/,`/`),Be=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Ve=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,He=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Ue(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function We(e){return Le(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ge=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ke(e,t){let n=e;if(typeof n!=`string`||!ne.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ge)try{let e=new URL(window.location.href),r=S.test(n)?new URL(re(n,e.protocol)):new URL(n),a=ke(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{T(!1,`<Link to="${n}"> contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var qe=new URL(`http://localhost`);function Je(e){if(e.createURL)return e.createURL(`/`);try{return new URL(e.createHref(`/`),qe)}catch{return qe}}function Ye(e,t){return e.origin===t.origin&&(e.origin!==`null`||e.protocol===t.protocol&&e.host===t.host)}function Xe(e,t){if(e.startsWith(`//`))return!0;let n=t.protocol.toLowerCase();return e.toLowerCase().startsWith(n)?t.host===``||e.slice(n.length).startsWith(`//`):!1}function Ze(e,t,n,r){let i=null;try{i=e==null?null:new URL(e,n)}catch{}let a=new URL(t,n),o=i!=null&&!Ye(i,n),s=!Ye(a,n);if(r===`reject`){if(o||s)throw Error(`External navigation is not allowed`)}else if(s&&(i==null||!Xe(e,i)||!Ye(i,a)))throw Error(`External navigation is not allowed`)}var Qe=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Qe);var $e=[`GET`,...Qe];new Set($e);var et=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function tt(e){try{return et.includes(new URL(e).protocol)}catch{return!1}}var nt=x.createContext(null);nt.displayName=`DataRouter`;var rt=x.createContext(null);rt.displayName=`DataRouterState`;var it=x.createContext(!1);function at(){return x.useContext(it)}var ot=x.createContext({isTransitioning:!1});ot.displayName=`ViewTransition`;var st=x.createContext(new Map);st.displayName=`Fetchers`;var ct=x.createContext(null);ct.displayName=`Await`;var lt=x.createContext(null);lt.displayName=`Navigation`;var ut=x.createContext(null);ut.displayName=`Location`;var dt=x.createContext({outlet:null,matches:[],isDataRoute:!1});dt.displayName=`Route`;var ft=x.createContext(null);ft.displayName=`RouteError`;var pt=`REACT_ROUTER_ERROR`,mt=`REDIRECT`,ht=`ROUTE_ERROR_RESPONSE`;function gt(e){if(e.startsWith(`${pt}:${mt}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function _t(e){if(e.startsWith(`${pt}:${ht}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new He(t.status,t.statusText,t.data)}catch{}}function vt(e,{relative:t}={}){w(yt(),`useHref() may be used only in the context of a <Router> component.`);let{basename:n,navigator:r}=x.useContext(lt),{hash:i,pathname:a,search:o}=Et(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Le([n,a])),r.createHref({pathname:s,search:o,hash:i})}function yt(){return x.useContext(ut)!=null}function bt(){return w(yt(),`useLocation() may be used only in the context of a <Router> component.`),x.useContext(ut).location}var xt=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function St(e){x.useContext(lt).static||x.useLayoutEffect(e)}function Ct(){let{isDataRoute:e}=x.useContext(dt);return e?Ut():wt()}function wt(){w(yt(),`useNavigate() may be used only in the context of a <Router> component.`);let e=x.useContext(nt),{basename:t,navigator:n}=x.useContext(lt),{matches:r}=x.useContext(dt),{pathname:i}=bt(),a=JSON.stringify(Pe(r)),o=x.useRef(!1);return St(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(T(o.current,xt),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Fe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Le([t,c.pathname])),Ze(typeof r==`string`?r:E(r),n.createHref(c),Je(n),`reject`),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}x.createContext(null);function Tt(){let{matches:e}=x.useContext(dt);return e[e.length-1]?.params??{}}function Et(e,{relative:t}={}){let{matches:n}=x.useContext(dt),{pathname:r}=bt(),i=JSON.stringify(Pe(n));return x.useMemo(()=>Fe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function Dt(e,t){return Ot(e,t)}function Ot(e,t,n){w(yt(),`useRoutes() may be used only in the context of a <Router> component.`);let{navigator:r}=x.useContext(lt),{matches:i}=x.useContext(dt),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;A(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant <Routes> (or called \`useRoutes()\`) at "${s}" (under <Route path="${e}">) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. |
|
| 10 | + |
|
| 11 | +Please change the parent <Route path="${e}"> to <Route path="${e===`/`?`*`:`${e}/*`}">.`)}let u=bt(),d;if(t){let e=typeof t==`string`?le(t):t;w(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):fe(e,{pathname:p});T(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),T(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.`);let h=Ft(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Le([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Le([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(ut.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function kt(){let e=Ht(),t=Ue(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var At=x.createElement(kt,null),jt=class extends x.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=_t(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(dt.Provider,{value:this.props.routeContext},x.createElement(ft.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(Nt,{error:e},t):t}};jt.contextType=it;var Mt=new WeakMap;function Nt({children:e,error:t}){let{basename:n,navigator:r}=x.useContext(lt);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=gt(t.digest);if(e){let i=Mt.get(t);if(i)throw i;let a=Ke(e.location,n),o=a.absoluteURL||a.to;if(Ze(e.location,o,Je(r),`allow-explicit`),tt(o))throw Error(`Invalid redirect location`);if(Ge&&!Mt.get(t)){if(a.isExternal||e.reloadDocument)window.location.href=o;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(a.to,{replace:e.replace}));throw Mt.set(t,n),n}}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${o}`})}}return e}function Pt({routeContext:e,match:t,children:n}){let r=x.useContext(nt);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(dt.Provider,{value:e},n)}function Ft(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);w(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e<i.length;e++){let t=i[e];if((t.route.HydrateFallback||t.route.hydrateFallbackElement)&&(s=e),t.route.id){let{loaderData:e,errors:a}=r,c=t.route.loader&&!e.hasOwnProperty(t.route.id)&&(!a||a[t.route.id]===void 0);if(t.route.lazy||c){n.isStatic&&(o=!0),i=s>=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:We(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||At,o&&(s<0&&c===0?(A(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(Pt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(jt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function It(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Lt(e){let t=x.useContext(nt);return w(t,It(e)),t}function Rt(e){let t=x.useContext(rt);return w(t,It(e)),t}function zt(e){let t=x.useContext(dt);return w(t,It(e)),t}function Bt(e){let t=zt(e),n=t.matches[t.matches.length-1];return w(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Vt(){return Bt(`useRouteId`)}function Ht(){let e=x.useContext(ft),t=Rt(`useRouteError`),n=Bt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Ut(){let{router:e}=Lt(`useNavigate`),t=Bt(`useNavigate`),n=x.useRef(!1);return St(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{T(n.current,xt),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Wt={};function A(e,t,n){!t&&!Wt[e]&&(Wt[e]=!0,T(!1,n))}x.memo(Gt);function Gt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return Ot(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Kt({to:e,replace:t,state:n,relative:r}){w(yt(),`<Navigate> may be used only in the context of a <Router> component.`);let{static:i,navigator:a}=x.useContext(lt);T(!i,`<Navigate> must not be used on the initial render in a <StaticRouter>. This is a no-op, but you should modify your code so the <Navigate> is only ever rendered in response to some user interaction or state change.`);let{matches:o}=x.useContext(dt),{pathname:s}=bt(),c=Ct(),l=Fe(e,Pe(o),s,r===`path`);Ze(typeof e==`string`?e:E(e),a.createHref(l),Je(a),`reject`);let u=JSON.stringify(l);return x.useEffect(()=>{c(JSON.parse(u),{replace:t,state:n,relative:r})},[c,u,r,t,n]),null}function qt(e){w(!1,`A <Route> is only ever to be used as the child of <Routes> element, never rendered directly. Please wrap your <Route> in a <Routes>.`)}function Jt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){w(!yt(),`You cannot render a <Router> inside another <Router>. You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=le(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=ke(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return T(h!=null,`<Router basename="${s}"> is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the <Router> won't render anything.`),h==null?null:x.createElement(lt.Provider,{value:c},x.createElement(ut.Provider,{children:t,value:h}))}function Yt({children:e,location:t}){return Dt(Xt(e),t)}x.Component;function Xt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,Xt(e.props.children,i));return}w(e.type===qt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>`),w(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Xt(e.props.children,i)),n.push(a)}),n}var Zt=`get`,Qt=`application/x-www-form-urlencoded`;function $t(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function j(e){return $t(e)&&e.tagName.toLowerCase()===`button`}function en(e){return $t(e)&&e.tagName.toLowerCase()===`form`}function tn(e){return $t(e)&&e.tagName.toLowerCase()===`input`}function nn(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function rn(e,t){return e.button===0&&(!t||t===`_self`)&&!nn(e)}var an=null;function on(){if(an===null)try{new FormData(document.createElement(`form`),0),an=!1}catch{an=!0}return an}var sn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function cn(e){return e!=null&&!sn.has(e)?(T(!1,`"${e}" is not a valid \`encType\` for \`<Form>\`/\`<fetcher.Form>\` and will default to "${Qt}"`),null):e}function ln(e,t){let n,r,i,a,o;if(en(e)){let o=e.getAttribute(`action`);r=o?ke(o,t):null,n=e.getAttribute(`method`)||Zt,i=cn(e.getAttribute(`enctype`))||Qt,a=new FormData(e)}else if(j(e)||tn(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a <button> or <input type="submit"> without a <form>`);let s=e.getAttribute(`formaction`)||o.getAttribute(`action`);if(r=s?ke(s,t):null,n=e.getAttribute(`formmethod`)||o.getAttribute(`method`)||Zt,i=cn(e.getAttribute(`formenctype`))||cn(o.getAttribute(`enctype`))||Qt,a=new FormData(o,e),!on()){let{name:t,type:n,value:r}=e;if(n===`image`){let e=t?`${t}.`:``;a.append(`${e}x`,`0`),a.append(`${e}y`,`0`)}else t&&a.append(t,r)}}else if($t(e))throw Error(`Cannot submit element that is not <form>, <button>, or <input type="submit|image">`);else n=Zt,r=null,i=Qt,o=e;return a&&i===`text/plain`&&(o=a,a=void 0),{action:r,method:n.toLowerCase(),encType:i,formData:a,body:o}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);function un(e,t){if(e===!1||e==null)throw Error(t)}function dn(e,t,n,r){let i=typeof e==`string`?new URL(e,typeof window>`u`?`server://singlefetch/`:window.location.origin):e;return i.pathname=n?i.pathname.endsWith(`/`)?`${i.pathname}_.${r}`:`${i.pathname}.${r}`:i.pathname===`/`?`_root.${r}`:t&&ke(i.pathname,t)===`/`?`${Re(t)}/_root.${r}`:`${Re(i.pathname)}.${r}`,i}async function fn(e,t){if(e.id in t)return t[e.id];try{let n=await te(()=>import(e.module),[]);return t[e.id]=n,n}catch(t){return console.error(`Error loading route module \`${e.module}\`, reloading page...`),console.error(t),window.__reactRouterContext&&window.__reactRouterContext.isSpaMode,window.location.reload(),new Promise(()=>{})}}function pn(e){return e!=null&&typeof e.page==`string`}function mn(e){return e==null?!1:e.href==null?e.rel===`preload`&&typeof e.imageSrcSet==`string`&&typeof e.imageSizes==`string`:typeof e.rel==`string`&&typeof e.href==`string`}async function hn(e,t,n){return bn((await Promise.all(e.map(async e=>{let r=t.routes[e.route.id];if(r){let e=await fn(r,n);return e.links?e.links():[]}return[]}))).flat(1).filter(mn).filter(e=>e.rel===`stylesheet`||e.rel===`preload`).map(e=>e.rel===`stylesheet`?{...e,rel:`prefetch`,as:`style`}:{...e,rel:`prefetch`}))}function gn(e,t,n,r,i,a){let o=(e,t)=>!n[t]||e.route.id!==n[t].route.id,s=(e,t)=>n[t].pathname!==e.pathname||n[t].route.path?.endsWith(`*`)&&n[t].params[`*`]!==e.params[`*`];return a===`assets`?t.filter((e,t)=>o(e,t)||s(e,t)):a===`data`?t.filter((t,a)=>{let c=r.routes[t.route.id];if(!c||!c.hasLoader)return!1;if(o(t,a)||s(t,a))return!0;if(t.route.shouldRevalidate){let r=t.route.shouldRevalidate({currentUrl:new URL(i.pathname+i.search+i.hash,window.origin),currentParams:n[0]?.params||{},nextUrl:new URL(e,window.origin),nextParams:t.params,defaultShouldRevalidate:!0});if(typeof r==`boolean`)return r}return!0}):[]}function _n(e,t,{includeHydrateFallback:n}={}){return vn(e.map(e=>{let r=t.routes[e.route.id];if(!r)return[];let i=[r.module];return r.clientActionModule&&(i=i.concat(r.clientActionModule)),r.clientLoaderModule&&(i=i.concat(r.clientLoaderModule)),n&&r.hydrateFallbackModule&&(i=i.concat(r.hydrateFallbackModule)),r.imports&&(i=i.concat(r.imports)),i}).flat(1))}function vn(e){return[...new Set(e)]}function yn(e){let t={},n=Object.keys(e).sort();for(let r of n)t[r]=e[r];return t}function bn(e,t){let n=new Set,r=new Set(t);return e.reduce((e,i)=>{if(t&&!pn(i)&&i.as===`script`&&i.href&&r.has(i.href))return e;let a=JSON.stringify(yn(i));return n.has(a)||(n.add(a),e.push({key:a,link:i})),e},[])}function xn(){let e=x.useContext(nt);return un(e,`You must render this element inside a <DataRouterContext.Provider> element`),e}function Sn(){let e=x.useContext(rt);return un(e,`You must render this element inside a <DataRouterStateContext.Provider> element`),e}var Cn=x.createContext(void 0);Cn.displayName=`FrameworkContext`;function wn(){let e=x.useContext(Cn);return un(e,`You must render this element inside a <HydratedRouter> element`),e}function Tn(e,t){let n=x.useContext(Cn),[r,i]=x.useState(!1),[a,o]=x.useState(!1),{onFocus:s,onBlur:c,onMouseEnter:l,onMouseLeave:u,onTouchStart:d}=t,f=x.useRef(null);x.useEffect(()=>{if(e===`render`&&o(!0),e===`viewport`){let e=new IntersectionObserver(e=>{e.forEach(e=>{o(e.isIntersecting)})},{threshold:.5});return f.current&&e.observe(f.current),()=>{e.disconnect()}}},[e]),x.useEffect(()=>{if(r){let e=setTimeout(()=>{o(!0)},100);return()=>{clearTimeout(e)}}},[r]);let p=()=>{i(!0)},m=()=>{i(!1),o(!1)};return n?e===`intent`?[a,f,{onFocus:En(s,p),onBlur:En(c,m),onMouseEnter:En(l,p),onMouseLeave:En(u,m),onTouchStart:En(d,p)}]:[a,f,{}]:[!1,f,{}]}function En(e,t){return n=>{e&&e(n),n.defaultPrevented||t(n)}}function Dn({page:e,...t}){let n=at(),{nonce:r}=wn(),{router:i}=xn(),a=x.useMemo(()=>fe(i.routes,e,i.basename),[i.routes,e,i.basename]);return a?(t.nonce==null&&r&&(t={...t,nonce:r}),n?x.createElement(kn,{page:e,matches:a,...t}):x.createElement(An,{page:e,matches:a,...t})):null}function On(e){let{manifest:t,routeModules:n}=wn(),[r,i]=x.useState([]);return x.useEffect(()=>{let r=!1;return hn(e,t,n).then(e=>{r||i(e)}),()=>{r=!0}},[e,t,n]),r}function kn({page:e,matches:t,...n}){let r=bt(),{future:i}=wn(),{basename:a}=xn(),o=x.useMemo(()=>{if(e===r.pathname+r.search+r.hash)return[];let n=dn(e,a,i.v8_trailingSlashAwareDataRequests,`rsc`),o=!1,s=[];for(let e of t)typeof e.route.shouldRevalidate==`function`?o=!0:s.push(e.route.id);return o&&s.length>0&&n.searchParams.set(`_routes`,s.join(`,`)),[n.pathname+n.search]},[a,i.v8_trailingSlashAwareDataRequests,e,r,t]);return x.createElement(x.Fragment,null,o.map(e=>x.createElement(`link`,{key:e,rel:`prefetch`,as:`fetch`,href:e,...n})))}function An({page:e,matches:t,...n}){let r=bt(),{future:i,manifest:a,routeModules:o}=wn(),{basename:s}=xn(),{loaderData:c,matches:l}=Sn(),u=x.useMemo(()=>gn(e,t,l,a,r,`data`),[e,t,l,a,r]),d=x.useMemo(()=>gn(e,t,l,a,r,`assets`),[e,t,l,a,r]),f=x.useMemo(()=>{if(e===r.pathname+r.search+r.hash)return[];let n=new Set,l=!1;if(t.forEach(e=>{let t=a.routes[e.route.id];t&&t.hasLoader&&(!u.some(t=>t.route.id===e.route.id)&&e.route.id in c&&o[e.route.id]?.shouldRevalidate||t.hasClientLoader?l=!0:n.add(e.route.id))}),n.size===0)return[];let d=dn(e,s,i.v8_trailingSlashAwareDataRequests,`data`);return l&&n.size>0&&d.searchParams.set(`_routes`,t.filter(e=>n.has(e.route.id)).map(e=>e.route.id).join(`,`)),[d.pathname+d.search]},[s,i.v8_trailingSlashAwareDataRequests,c,r,a,u,t,e,o]),p=x.useMemo(()=>_n(d,a),[d,a]),m=On(d);return x.createElement(x.Fragment,null,f.map(e=>x.createElement(`link`,{key:e,rel:`prefetch`,as:`fetch`,href:e,...n})),p.map(e=>x.createElement(`link`,{key:e,rel:`modulepreload`,href:e,...n})),m.map(({key:e,link:t})=>x.createElement(`link`,{key:e,nonce:n.nonce,...t,crossOrigin:t.crossOrigin??n.crossOrigin})))}function jn(...e){return t=>{e.forEach(e=>{typeof e==`function`?e(t):e!=null&&(e.current=t)})}}x.Component;var Mn=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;try{Mn&&(window.__reactRouterVersion=`7.18.4`)}catch{}function Nn({basename:e,children:t,useTransitions:n,window:r}){let i=x.useRef();i.current??=ae({window:r,v5Compat:!0});let a=i.current,[o,s]=x.useState({action:a.action,location:a.location}),c=x.useCallback(e=>{n===!1?s(e):x.startTransition(()=>s(e))},[n]);return x.useLayoutEffect(()=>a.listen(c),[a,c]),x.createElement(Jt,{basename:e,children:t,location:o.location,navigationType:o.action,navigator:a,useTransitions:n})}var Pn=x.forwardRef(function({onClick:e,discover:t=`render`,prefetch:n=`none`,relative:r,reloadDocument:i,replace:a,mask:o,state:s,target:c,to:l,preventScrollReset:u,viewTransition:d,defaultShouldRevalidate:f,...p},m){let{basename:h,navigator:g,useTransitions:_}=x.useContext(lt),v=typeof l==`string`&&ne.test(l),y=Ke(l,h);l=y.to;let b=vt(l,{relative:r}),ee=bt(),te=null;if(o){let e=Fe(o,[],ee.mask?ee.mask.pathname:`/`,!0);h!==`/`&&(e.pathname=e.pathname===`/`?h:Le([h,e.pathname])),te=g.createHref(e)}let[S,re,C]=Tn(n,p),ie=zn(l,{replace:a,mask:o,state:s,target:c,preventScrollReset:u,relative:r,viewTransition:d,defaultShouldRevalidate:f,useTransitions:_});function ae(t){e&&e(t),t.defaultPrevented||ie(t)}let w=!(y.isExternal||i),T=x.createElement(`a`,{...p,...C,href:(w?te:void 0)||y.absoluteURL||b,onClick:w?ae:e,ref:jn(m,re),target:c,"data-discover":!v&&t===`render`?`true`:void 0});return S&&!v?x.createElement(x.Fragment,null,T,x.createElement(Dn,{page:b})):T});Pn.displayName=`Link`;var Fn=x.forwardRef(function({"aria-current":e=`page`,caseSensitive:t=!1,className:n=``,end:r=!1,style:i,to:a,viewTransition:o,children:s,...c},l){let u=Et(a,{relative:c.relative}),d=bt(),f=x.useContext(rt),{navigator:p,basename:m}=x.useContext(lt),h=f!=null&&Wn(u)&&o===!0,g=p.encodeLocation?p.encodeLocation(u).pathname:u.pathname,_=d.pathname,v=f&&f.navigation&&f.navigation.location?f.navigation.location.pathname:null;t||(_=_.toLowerCase(),v=v?v.toLowerCase():null,g=g.toLowerCase()),v&&m&&(v=ke(v,m)||v);let y=g!==`/`&&g.endsWith(`/`)?g.length-1:g.length,b=_===g||!r&&_.startsWith(g)&&_.charAt(y)===`/`,ee=v!=null&&(v===g||!r&&v.startsWith(g)&&v.charAt(g.length)===`/`),te={isActive:b,isPending:ee,isTransitioning:h},ne=b?e:void 0,S;S=typeof n==`function`?n(te):[n,b?`active`:null,ee?`pending`:null,h?`transitioning`:null].filter(Boolean).join(` `);let re=typeof i==`function`?i(te):i;return x.createElement(Pn,{...c,"aria-current":ne,className:S,ref:l,style:re,to:a,viewTransition:o},typeof s==`function`?s(te):s)});Fn.displayName=`NavLink`;var In=x.forwardRef(({discover:e=`render`,fetcherKey:t,navigate:n,reloadDocument:r,replace:i,state:a,method:o=Zt,action:s,onSubmit:c,relative:l,preventScrollReset:u,viewTransition:d,defaultShouldRevalidate:f,...p},m)=>{let{useTransitions:h}=x.useContext(lt),g=Hn(),_=Un(s,{relative:l}),v=o.toLowerCase()===`get`?`get`:`post`,y=typeof s==`string`&&ne.test(s);return x.createElement(`form`,{ref:m,method:v,action:_,onSubmit:r?c:e=>{if(c&&c(e),e.defaultPrevented)return;e.preventDefault();let r=e.nativeEvent.submitter,s=r?.getAttribute(`formmethod`)||o,p=()=>g(r||e.currentTarget,{fetcherKey:t,method:s,navigate:n,replace:i,state:a,relative:l,preventScrollReset:u,viewTransition:d,defaultShouldRevalidate:f});h&&n!==!1?x.startTransition(()=>p()):p()},...p,"data-discover":!y&&e===`render`?`true`:void 0})});In.displayName=`Form`;function Ln(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Rn(e){let t=x.useContext(nt);return w(t,Ln(e)),t}function zn(e,{target:t,replace:n,mask:r,state:i,preventScrollReset:a,relative:o,viewTransition:s,defaultShouldRevalidate:c,useTransitions:l}={}){let u=Ct(),d=bt(),f=Et(e,{relative:o});return x.useCallback(p=>{if(rn(p,t)){p.preventDefault();let t=n===void 0?E(d)===E(f):n,m=()=>u(e,{replace:t,mask:r,state:i,preventScrollReset:a,relative:o,viewTransition:s,defaultShouldRevalidate:c});l?x.startTransition(()=>m()):m()}},[d,u,f,n,r,i,t,e,a,o,s,c,l])}var Bn=0,Vn=()=>`__${String(++Bn)}__`;function Hn(){let{router:e}=Rn(`useSubmit`),{basename:t}=x.useContext(lt),n=Vt(),r=e.fetch,i=e.navigate;return x.useCallback(async(e,a={})=>{let{action:o,method:s,encType:c,formData:l,body:u}=ln(e,t);if(a.navigate===!1){let e=a.fetcherKey||Vn();await r(e,n,a.action||o,{defaultShouldRevalidate:a.defaultShouldRevalidate,preventScrollReset:a.preventScrollReset,formData:l,body:u,formMethod:a.method||s,formEncType:a.encType||c,flushSync:a.flushSync})}else await i(a.action||o,{defaultShouldRevalidate:a.defaultShouldRevalidate,preventScrollReset:a.preventScrollReset,formData:l,body:u,formMethod:a.method||s,formEncType:a.encType||c,replace:a.replace,state:a.state,fromRouteId:n,flushSync:a.flushSync,viewTransition:a.viewTransition})},[r,i,t,n])}function Un(e,{relative:t}={}){let{basename:n}=x.useContext(lt),r=x.useContext(dt);w(r,`useFormAction must be used inside a RouteContext`);let[i]=r.matches.slice(-1),a={...Et(e||`.`,{relative:t})},o=bt();if(e==null){a.search=o.search;let e=new URLSearchParams(a.search),t=e.getAll(`index`);if(t.some(e=>e===``)){e.delete(`index`),t.filter(e=>e).forEach(t=>e.append(`index`,t));let n=e.toString();a.search=n?`?${n}`:``}}return(!e||e===`.`)&&i.route.index&&(a.search=a.search?a.search.replace(/^\?/,`?index&`):`?index`),n!==`/`&&(a.pathname=a.pathname===`/`?n:Le([n,a.pathname])),E(a)}function Wn(e,{relative:t}={}){let n=x.useContext(ot);w(n!=null,"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. Did you accidentally import `RouterProvider` from `react-router`?");let{basename:r}=Rn(`useViewTransitionState`),i=Et(e,{relative:t});if(!n.isTransitioning)return!1;let a=ke(n.currentLocation.pathname,r)||n.currentLocation.pathname,o=ke(n.nextLocation.pathname,r)||n.nextLocation.pathname;return Ee(i.pathname,o)!=null||Ee(i.pathname,a)!=null}var Gn=g();async function Kn(e,t={}){let n=await fetch(`/api/command`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({op:e,args:t})});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error||n.statusText)}}async function qn(e){let t=await fetch(e,{method:`POST`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.error||t.statusText)}}var Jn=()=>qn(`/api/sensor-setup/start`),Yn=()=>qn(`/api/sensor-setup/finish`),Xn=()=>qn(`/api/sensor-setup/cancel`);function Zn(e){let t=new EventSource(`/api/events`);return t.onopen=()=>e.onOpen(),t.onerror=()=>e.onError(),t.addEventListener(`control.state`,t=>{e.onControl(JSON.parse(t.data))}),t.addEventListener(`temps.reading`,t=>{e.onTemps(JSON.parse(t.data))}),t.addEventListener(`temps.setup`,t=>{e.onSetup(JSON.parse(t.data))}),()=>t.close()}var Qn=o((e=>{var t=Symbol.for(`react.transitional.element`);function n(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.jsx=n,e.jsxs=n})),M=o(((e,t)=>{t.exports=Qn()}))();function $n({status:e,label:t}){return(0,M.jsxs)(`span`,{className:`status-light`,children:[(0,M.jsx)(`span`,{className:`light light-${e}`,"aria-hidden":`true`}),(0,M.jsx)(`span`,{children:t})]})}function er({connection:e}){let[t,n]={open:[`on`,`Live`],connecting:[`waiting`,`Connecting…`],reconnecting:[`error`,`Reconnecting…`]}[e];return(0,M.jsx)(`div`,{className:`connection`,children:(0,M.jsx)($n,{status:t,label:n})})}function tr({connection:e}){return(0,M.jsxs)(`header`,{className:`app-header`,children:[(0,M.jsxs)(`div`,{className:`brand`,children:[(0,M.jsx)(`span`,{className:`brand-icon`,"aria-hidden":`true`,children:`♨`}),` `,`Hot Tub`]}),(0,M.jsxs)(`nav`,{className:`nav`,children:[(0,M.jsx)(Fn,{className:({isActive:e})=>`nav-tab${e?` active`:``}`,to:`/control`,children:`Main Control`}),(0,M.jsx)(Fn,{className:({isActive:e})=>`nav-tab${e?` active`:``}`,to:`/diagnostic`,children:`Diagnostic`})]}),(0,M.jsx)(er,{connection:e})]})}var nr=[{key:`hot_in`,label:`Hot input`},{key:`tub1`,label:`Tub 1`},{key:`tub2`,label:`Tub 2`},{key:`tub3`,label:`Tub 3`},{key:`tub4`,label:`Tub 4`}],rr=[`tub1`,`tub2`,`tub3`,`tub4`],ir=e=>e!=null;function ar(e){return ir(e)?`${Number(e).toFixed(1)}°F`:`--`}function or(e){return`${Number.isInteger(e)?e:Number(e).toFixed(1)}°F`}function sr(e){return new Date(e).toLocaleTimeString([],{hour:`numeric`,minute:`2-digit`})}function cr(e){return e.heat_mode===`backflush`||e.heat_phase===`backflush`}function lr(e){return e.heat_phase.startsWith(`startup_`)?`Starting (attempt ${e.heat_phase.slice(-1)} of 3)`:{idle:e.heat_mode===`maintain`?`Waiting (in range)`:`Off`,heating:`Heating`,backflush:`Backflushing`,error_startup:`Failed to start`,error_safety:`Blocked by safety check`}[e.heat_phase]||e.heat_phase}function ur(e){let t=e.filter_pump===`on`;return e.filter_mode===`auto`?t?`Auto · running`:e.filter_next_start?`Auto · next ${sr(e.filter_next_start)}`:`Auto · idle`:t?`On`:`Off`}function dr(e){return e.roles??e.probes??{}}function fr(e,t=15){return!e||(Date.now()-new Date(e).getTime())/1e3>t}function pr({kind:e,label:t,running:n}){let r=[0,90,180,270].map(e=>(0,M.jsx)(`ellipse`,{cx:`60`,cy:`36`,rx:`11`,ry:`21`,transform:`rotate(${e} 60 60)`},e));return(0,M.jsxs)(`div`,{className:`pump pump-${e}${n?` running`:``}`,children:[(0,M.jsxs)(`svg`,{className:`pump-svg`,viewBox:`0 0 120 120`,"aria-hidden":`true`,children:[(0,M.jsx)(`rect`,{className:`pump-frame`,x:`4`,y:`4`,width:`112`,height:`112`,rx:`18`}),(0,M.jsx)(`circle`,{className:`pump-housing`,cx:`60`,cy:`60`,r:`48`}),(0,M.jsx)(`g`,{className:`fan-blades`,children:r}),(0,M.jsx)(`circle`,{className:`pump-hub`,cx:`60`,cy:`60`,r:`8`})]}),(0,M.jsx)(`div`,{className:`pump-label`,children:t}),(0,M.jsx)($n,{status:n?`on`:`off`,label:n?`Running`:`Off`})]})}var mr=`M40 64 L360 64 L338 204 Q335 214 324 214 L76 214 Q65 214 62 204 Z`;function hr(e){let t=`M-100 ${e}`;for(let e=0;e<6;e++)t+=` q25 -7 50 0 q25 7 50 0`;return`${t} L500 240 L-100 240 Z`}function gr(){return(0,M.jsxs)(`g`,{className:`duck-bob`,"aria-hidden":`true`,children:[(0,M.jsx)(`ellipse`,{cx:`14`,cy:`18`,rx:`16`,ry:`10`,fill:`#facc15`}),(0,M.jsx)(`path`,{d:`M4 16 q8 -6 14 2`,fill:`none`,stroke:`#eab308`,strokeWidth:`2`,strokeLinecap:`round`}),(0,M.jsx)(`circle`,{cx:`25`,cy:`6`,r:`8`,fill:`#facc15`}),(0,M.jsx)(`path`,{d:`M32 5 l8 2 l-8 3 z`,fill:`#f97316`}),(0,M.jsx)(`circle`,{cx:`27`,cy:`4`,r:`1.6`,fill:`#1e293b`})]})}function _r({tempF:e,steaming:t}){let n=ir(e)?`${e.toFixed(1)}°F`:`--.-°F`,r=[60,110,160,210,260,310,340].map(e=>(0,M.jsx)(`circle`,{className:`rivet`,cx:e,cy:`65`,r:`2.2`},e));return(0,M.jsxs)(`svg`,{className:`tub`,viewBox:`0 0 400 240`,role:`img`,"aria-label":`Tub water temperature`,children:[(0,M.jsxs)(`defs`,{children:[(0,M.jsx)(`clipPath`,{id:`tub-inside`,children:(0,M.jsx)(`path`,{d:mr})}),(0,M.jsxs)(`linearGradient`,{id:`water-grad`,x1:`0`,y1:`0`,x2:`0`,y2:`1`,children:[(0,M.jsx)(`stop`,{offset:`0`,stopColor:`#38bdf8`}),(0,M.jsx)(`stop`,{offset:`1`,stopColor:`#0369a1`})]}),(0,M.jsxs)(`linearGradient`,{id:`steel-grad`,x1:`0`,y1:`0`,x2:`1`,y2:`0`,children:[(0,M.jsx)(`stop`,{offset:`0`,stopColor:`#94a3b8`}),(0,M.jsx)(`stop`,{offset:`0.5`,stopColor:`#f1f5f9`}),(0,M.jsx)(`stop`,{offset:`1`,stopColor:`#94a3b8`})]})]}),(0,M.jsx)(`ellipse`,{className:`ground`,cx:`200`,cy:`222`,rx:`170`,ry:`10`}),(0,M.jsxs)(`g`,{className:`steam${t?` on`:``}`,"aria-hidden":`true`,children:[(0,M.jsx)(`path`,{className:`steam-puff`,d:`M140 52 q-8 -10 0 -20 t0 -20`}),(0,M.jsx)(`path`,{className:`steam-puff`,d:`M200 52 q-8 -10 0 -20 t0 -20`}),(0,M.jsx)(`path`,{className:`steam-puff`,d:`M260 52 q-8 -10 0 -20 t0 -20`})]}),(0,M.jsx)(`path`,{className:`tub-shell`,d:mr}),(0,M.jsxs)(`g`,{clipPath:`url(#tub-inside)`,children:[(0,M.jsx)(`g`,{className:`wave wave-back`,children:(0,M.jsx)(`path`,{d:hr(90),fill:`#7dd3fc`})}),(0,M.jsx)(`g`,{className:`wave wave-front`,children:(0,M.jsx)(`path`,{d:hr(96),fill:`url(#water-grad)`})})]}),(0,M.jsx)(`path`,{className:`tub-wall`,d:mr}),(0,M.jsx)(`rect`,{className:`tub-rim`,x:`22`,y:`58`,width:`356`,height:`14`,rx:`7`,fill:`url(#steel-grad)`}),r,(0,M.jsx)(`g`,{transform:`translate(286 72)`,children:(0,M.jsx)(gr,{})}),(0,M.jsx)(`text`,{className:`tub-temp`,x:`200`,y:`165`,textAnchor:`middle`,children:n}),(0,M.jsx)(`text`,{className:`tub-temp-caption`,x:`200`,y:`190`,textAnchor:`middle`,children:`water`})]})}function vr({error:e,onReset:t}){return(0,M.jsxs)(`div`,{className:`banner banner-error`,role:`alert`,children:[(0,M.jsxs)(`span`,{children:[(0,M.jsx)(`strong`,{children:`Heater problem.`}),` `,e.detail||e.code]}),(0,M.jsx)(`button`,{type:`button`,className:`btn btn-small`,onClick:t,children:`Reset (set Sleep)`})]})}function yr({control:e}){let t=e.tub_avg_f,n=e.heat_pump===`on`||ir(t)&&t>=95;return(0,M.jsxs)(`div`,{className:`control-layout`,children:[(0,M.jsxs)(`section`,{className:`card tub-card`,children:[(0,M.jsx)(`div`,{className:`banner-slot`,children:e.error?(0,M.jsx)(vr,{error:e.error,onReset:()=>Kn(`set_heat_mode`,{mode:`sleep`})}):null}),(0,M.jsx)(_r,{tempF:t,steaming:n}),(0,M.jsxs)(`div`,{className:`pumps`,children:[(0,M.jsx)(pr,{kind:`heat`,label:`Heat`,running:e.heat_pump===`on`}),(0,M.jsx)(pr,{kind:`filter`,label:`Filter`,running:e.filter_pump===`on`})]})]}),(0,M.jsxs)(`section`,{className:`card settings-card`,children:[(0,M.jsxs)(`div`,{className:`target`,children:[(0,M.jsx)(`span`,{className:`field-label`,children:`Target temperature`}),(0,M.jsxs)(`div`,{className:`target-row`,children:[(0,M.jsx)(`button`,{type:`button`,className:`round-btn`,"aria-label":`Decrease target`,disabled:e.target_f<=50,onClick:()=>Kn(`target_temp_decrement`),children:`−`}),(0,M.jsx)(`div`,{className:`target-value`,children:or(e.target_f)}),(0,M.jsx)(`button`,{type:`button`,className:`round-btn`,"aria-label":`Increase target`,disabled:e.target_f>=110,onClick:()=>Kn(`target_temp_increment`),children:`+`})]})]}),(0,M.jsxs)(`div`,{className:`modes`,children:[(0,M.jsxs)(`label`,{className:`field`,children:[(0,M.jsx)(`span`,{className:`field-label`,children:`Heat mode`}),(0,M.jsxs)(`select`,{value:e.heat_mode===`backflush`?`backflush`:e.heat_mode,onChange:e=>Kn(`set_heat_mode`,{mode:e.target.value}),children:[(0,M.jsx)(`option`,{value:`sleep`,children:`Sleep`}),(0,M.jsx)(`option`,{value:`maintain`,children:`Maintain temperature`}),e.heat_mode===`backflush`?(0,M.jsx)(`option`,{value:`backflush`,disabled:!0,children:`Backflush (running)`}):null]})]}),(0,M.jsxs)(`label`,{className:`field`,children:[(0,M.jsx)(`span`,{className:`field-label`,children:`Filter mode`}),(0,M.jsxs)(`select`,{value:e.filter_mode,onChange:e=>Kn(`set_filter_mode`,{mode:e.target.value}),children:[(0,M.jsx)(`option`,{value:`auto`,children:`Auto (daily schedule)`}),(0,M.jsx)(`option`,{value:`on`,children:`On`}),(0,M.jsx)(`option`,{value:`off`,children:`Off`})]})]})]}),(0,M.jsxs)(`dl`,{className:`summary`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`dt`,{children:`Heater`}),(0,M.jsx)(`dd`,{children:lr(e)})]}),(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`dt`,{children:`Hot input`}),(0,M.jsx)(`dd`,{children:ar(e.hot_in_f)})]}),(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`dt`,{children:`Filter`}),(0,M.jsx)(`dd`,{children:ur(e)})]})]})]})]})}function br(){return(0,M.jsxs)(`div`,{className:`backflush-anim`,role:`status`,children:[(0,M.jsxs)(`svg`,{viewBox:`0 0 120 120`,"aria-hidden":`true`,children:[(0,M.jsxs)(`g`,{className:`backflush-arrows`,children:[(0,M.jsx)(`path`,{d:`M60 10 A50 50 0 0 1 110 60`}),(0,M.jsx)(`path`,{d:`M60 110 A50 50 0 0 1 10 60`}),(0,M.jsx)(`path`,{className:`arrowhead`,d:`M102 54 L118 54 L110 66 Z`}),(0,M.jsx)(`path`,{className:`arrowhead`,d:`M2 66 L18 66 L10 54 Z`})]}),(0,M.jsx)(`path`,{className:`droplet`,d:`M60 28 C60 28 36 56 36 72 A24 24 0 0 0 84 72 C84 56 60 28 60 28 Z`}),(0,M.jsx)(`circle`,{className:`droplet-eye`,cx:`52`,cy:`70`,r:`3`}),(0,M.jsx)(`circle`,{className:`droplet-eye`,cx:`68`,cy:`70`,r:`3`}),(0,M.jsx)(`path`,{className:`droplet-smile`,d:`M52 80 q8 7 16 0`}),(0,M.jsx)(`circle`,{className:`bubble b1`,cx:`44`,cy:`102`,r:`4`}),(0,M.jsx)(`circle`,{className:`bubble b2`,cx:`60`,cy:`104`,r:`3`}),(0,M.jsx)(`circle`,{className:`bubble b3`,cx:`76`,cy:`102`,r:`4`})]}),(0,M.jsx)(`p`,{children:`Backflush in progress…`})]})}function xr({label:e,value:t,missingStatus:n}){return(0,M.jsxs)(`li`,{className:`probe-row`,children:[(0,M.jsx)($n,{status:ir(t)?`on`:n,label:e}),(0,M.jsx)(`span`,{className:`probe-value`,children:ar(t)})]})}function Sr({control:e,temps:t,backflushRequested:n,onBackflushRequested:r}){let i=Ct(),a=cr(e),o=dr(t);return(0,M.jsxs)(`div`,{className:`diagnostic-layout`,children:[(0,M.jsxs)(`section`,{className:`card`,children:[(0,M.jsx)(`button`,{type:`button`,className:`big-button big-button-backflush`,disabled:a||n,onClick:()=>{r(!0),Kn(`set_heat_mode`,{mode:`backflush`}).catch(()=>r(!1))},children:a?`Backflushing…`:`Backflush`}),(0,M.jsx)(`div`,{className:`backflush-slot`,children:a?(0,M.jsx)(br,{}):(0,M.jsx)(`p`,{className:`hint`,children:`Runs the heat pump briefly to purge the heater line, then returns to Sleep.`})})]}),(0,M.jsxs)(`section`,{className:`card`,children:[(0,M.jsx)(`button`,{type:`button`,className:`big-button big-button-setup`,onClick:()=>i(`/diagnostic/sensor-setup/1`),children:`Sensor Setup`}),(0,M.jsxs)(`div`,{className:`readings`,children:[(0,M.jsxs)(`div`,{className:`readings-header`,children:[(0,M.jsx)(`span`,{className:`field-label`,children:`Sensors`}),(0,M.jsx)(`span`,{className:`setup-state`,children:t.setup_state.replace(/_/g,` `)})]}),(0,M.jsx)(`ul`,{className:`probe-list`,children:nr.map(e=>(0,M.jsx)(xr,{label:e.label,value:o[e.key],missingStatus:`error`},e.key))})]})]})]})}function Cr({step:e}){return(0,M.jsx)(`ol`,{className:`stepper`,children:[`Disconnect`,`First set`,`Second set`].map((t,n)=>(0,M.jsx)(`li`,{className:n+1<e?`done`:n+1===e?`current`:``,children:t},t))})}function wr({label:e,value:t,missingStatus:n}){return(0,M.jsxs)(`li`,{className:`probe-row`,children:[(0,M.jsx)($n,{status:ir(t)?`on`:n,label:e}),(0,M.jsx)(`span`,{className:`probe-value`,children:ar(t)})]})}function Tr({temps:e,firstBundle:t,onFirstBundle:n}){let r=Ct(),{step:i}=Tt(),a=Math.min(3,Math.max(1,Number(i)||1)),o=dr(e),s=()=>r(`/diagnostic`);return(0,M.jsxs)(`section`,{className:`card setup-card`,children:[(0,M.jsx)(`h1`,{children:`Sensor setup`}),(0,M.jsx)(Cr,{step:a}),a===1?(0,M.jsx)(Er,{values:o,onCancel:()=>Xn().finally(s),onContinue:()=>Jn().then(()=>r(`/diagnostic/sensor-setup/2`))}):null,a===2?(0,M.jsx)(Or,{setupState:e.setup_state,values:o,onCancel:()=>Xn().finally(s),onContinue:()=>{let e=ir(o.hot_in),t=rr.some(e=>ir(o[e]));n(e&&t?null:e?`hot`:`water`),r(`/diagnostic/sensor-setup/3`)}}):null,a===3?(0,M.jsx)(kr,{setupState:e.setup_state,values:o,firstBundle:t,onCancel:()=>Xn().finally(s),onFinish:()=>Yn().then(s)}):null]})}function Er({values:e,onContinue:t,onCancel:n}){let r=nr.filter(t=>ir(e[t.key])).length;return(0,M.jsxs)(`div`,{className:`setup-step`,children:[(0,M.jsx)(`h2`,{children:`Disconnect all sensors`}),(0,M.jsx)(`p`,{children:`Unplug both sensor bundles from the enclosure: the hot-input probe and the four water probes.`}),(0,M.jsx)(`div`,{className:`setup-status`,children:r?(0,M.jsx)($n,{status:`waiting`,label:`${r} sensor${r===1?``:`s`} still connected`}):(0,M.jsx)($n,{status:`on`,label:`All sensors disconnected`})}),(0,M.jsxs)(`div`,{className:`actions`,children:[(0,M.jsx)(`button`,{type:`button`,className:`btn btn-secondary`,onClick:n,children:`Cancel`}),(0,M.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:t,children:`Continue`})]})]})}function Dr(e,t){return!!(e===`need_water`||e===`need_hot_in`&&rr.every(e=>ir(t[e])))}function Or({setupState:e,values:t,onContinue:n,onCancel:r}){let i=ir(t.hot_in),a=rr.some(e=>ir(t[e])),o=`Waiting for readings…`;i&&a?o=`Both sets detected`:i?o=`Hot-input sensor detected`:a&&(o=`Water sensors detected`);let s=Dr(e,t);return(0,M.jsxs)(`div`,{className:`setup-step`,children:[(0,M.jsx)(`h2`,{children:`Plug in one set of sensors`}),(0,M.jsx)(`p`,{children:`Connect either the hot-input bundle or the water bundle. Continue unlocks once readings arrive.`}),(0,M.jsx)(`div`,{className:`setup-status`,children:(0,M.jsx)($n,{status:s?`on`:`waiting`,label:o})}),(0,M.jsxs)(`div`,{className:`actions`,children:[(0,M.jsx)(`button`,{type:`button`,className:`btn btn-secondary`,onClick:r,children:`Cancel`}),(0,M.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!s,onClick:n,children:`Continue`})]})]})}function kr({setupState:e,values:t,firstBundle:n,onFinish:r,onCancel:i}){let a=n===`hot`?`the water sensor bundle (4 probes)`:n===`water`?`the hot-input sensor`:`any remaining sensors`,o=e===`ready`&&nr.every(e=>ir(t[e.key]));return(0,M.jsxs)(`div`,{className:`setup-step`,children:[(0,M.jsx)(`h2`,{children:`Plug in the other set`}),(0,M.jsxs)(`p`,{children:[`Now connect `,a,`. Finish unlocks once every sensor is reporting.`]}),(0,M.jsx)(`ul`,{className:`probe-list`,children:nr.map(e=>(0,M.jsx)(wr,{label:e.label,value:t[e.key],missingStatus:`waiting`},e.key))}),(0,M.jsxs)(`div`,{className:`actions`,children:[(0,M.jsx)(`button`,{type:`button`,className:`btn btn-secondary`,onClick:i,children:`Cancel`}),(0,M.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!o,onClick:r,children:`Finish`})]})]})}function Ar(){return(0,M.jsx)(`div`,{className:`card loading`,children:`Connecting to the hot tub…`})}function jr(){let[e,t]=(0,x.useState)(`connecting`),[n,r]=(0,x.useState)(null),[i,a]=(0,x.useState)(null),[o,s]=(0,x.useState)(!1),[c,l]=(0,x.useState)(null),u=bt();(0,x.useEffect)(()=>Zn({onOpen:()=>t(`open`),onError:()=>t(`reconnecting`),onControl:e=>{let t=e;r(t),s(e=>e&&!cr(t))},onTemps:e=>a(e),onSetup:()=>{}}),[]);let d=(e!==`open`||fr(n?.ts)||fr(i?.ts))&&e===`open`?`reconnecting`:e;return(0,M.jsxs)(`div`,{className:`app`,children:[(0,M.jsx)(tr,{connection:d}),(0,M.jsx)(`main`,{className:`page${d===`open`?``:` stale`}`,children:n&&i?(0,M.jsxs)(Yt,{children:[(0,M.jsx)(qt,{path:`/`,element:(0,M.jsx)(Kt,{to:`/control`,replace:!0})}),(0,M.jsx)(qt,{path:`/control`,element:(0,M.jsx)(yr,{control:n})}),(0,M.jsx)(qt,{path:`/diagnostic`,element:(0,M.jsx)(Sr,{control:n,temps:i,backflushRequested:o,onBackflushRequested:s})}),(0,M.jsx)(qt,{path:`/diagnostic/sensor-setup/:step`,element:(0,M.jsx)(Tr,{temps:i,firstBundle:c,onFirstBundle:l})}),(0,M.jsx)(qt,{path:`*`,element:(0,M.jsx)(Kt,{to:u.pathname.startsWith(`/diagnostic`)?`/diagnostic`:`/control`,replace:!0})})]}):(0,M.jsx)(Ar,{})})]})}(0,Gn.createRoot)(document.getElementById(`root`)).render((0,M.jsx)(x.StrictMode,{children:(0,M.jsx)(Nn,{children:(0,M.jsx)(jr,{})})})); |
|
| ... | ... | \ No newline at end of file |
projects/hot-tub/src/web/frontend/dist/assets/index-CESZI1ev.css
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +:root{--bg-top:#e0f2fe;--bg-bottom:#f8fafc;--card:#fff;--text:#0f172a;--muted:#64748b;--border:#e2e8f0;--surface:#f8fafc;--accent:#0e7490;--accent-strong:#155e75;--heat:#f97316;--filter:#0ea5e9;--green:#22c55e;--yellow:#eab308;--red:#ef4444;--off:#cbd5e1;--radius:18px;--shadow:0 6px 24px #0f172a14}*{box-sizing:border-box}body{min-height:100vh;color:var(--text);background:linear-gradient(var(--bg-top), var(--bg-bottom)) fixed;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif}.app-header{z-index:10;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-bottom:1px solid var(--border);background:#ffffffd9;flex-wrap:wrap;align-items:center;gap:12px 20px;padding:12px 20px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:8px;font-size:1.25rem;font-weight:700;display:flex}.brand-icon{color:var(--heat);font-size:1.4rem}.nav{background:#f1f5f9;border-radius:999px;gap:4px;padding:4px;display:flex}.nav-tab{color:var(--muted);border-radius:999px;padding:8px 16px;font-weight:600;text-decoration:none}.nav-tab.active{background:var(--card);color:var(--accent-strong);box-shadow:0 1px 4px #0f172a1f}.connection{margin-left:auto;font-size:.85rem}@media (width<=599px){.app-header{padding:10px 12px}.nav{order:3;width:100%}.nav-tab{text-align:center;flex:1}}.page{max-width:1100px;margin:0 auto;padding:20px;transition:opacity .3s}.page.stale{opacity:.55}@media (width<=599px){.page{padding:12px}}.card{border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow);padding:20px}.loading{color:var(--muted);text-align:center;padding:60px 20px}.control-layout,.diagnostic-layout{gap:20px;display:grid}@media (width>=900px){.control-layout{grid-template-columns:minmax(0,2fr) minmax(280px,1fr);align-items:start}}@media (width>=760px){.diagnostic-layout{grid-template-columns:1fr 1fr;align-items:start}}.status-light{color:var(--muted);align-items:center;gap:8px;display:inline-flex}.light{background:var(--off);border-radius:50%;flex-shrink:0;width:14px;height:14px}.light-on{background:var(--green);box-shadow:0 0 0 3px #22c55e33, 0 0 12px var(--green)}.light-waiting{background:var(--yellow);animation:1.2s ease-in-out infinite pulse}.light-error{background:var(--red)}.banner{border-radius:12px;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 12px;margin-bottom:12px;padding:12px 14px;display:flex}.banner-error{color:#991b1b;background:#fef2f2;border:1px solid #fecaca}.tub{width:100%;max-width:560px;margin:0 auto;display:block}.ground{fill:#0f172a14}.tub-shell{fill:#475569}.tub-wall{fill:none;stroke:#94a3b8;stroke-width:10px;stroke-linejoin:round}.tub-rim{stroke:#64748b;stroke-width:1.5px}.rivet{fill:#64748b}.tub-temp{fill:#fff;paint-order:stroke;stroke:#0369a199;stroke-width:4px;font-size:52px;font-weight:800}.tub-temp-caption{letter-spacing:.2em;text-transform:uppercase;fill:#e0f2fe;font-size:15px}.wave{animation:4s linear infinite wave}.wave-back{opacity:.8;animation-duration:6s;animation-direction:reverse}.duck-bob{transform-box:fill-box;transform-origin:50%;animation:2.4s ease-in-out infinite bob}.steam{opacity:0;transition:opacity 1s}.steam.on{opacity:1}.steam-puff{fill:none;stroke:#cbd5e1;stroke-width:5px;stroke-linecap:round;animation:3s ease-in-out infinite rise}.steam-puff:nth-child(2){animation-delay:1s}.steam-puff:nth-child(3){animation-delay:2s}.pumps{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;max-width:420px;margin:8px auto 0;display:grid}.pump{flex-direction:column;align-items:center;gap:6px;display:flex}.pump-svg{width:96px;height:96px}.pump-frame{fill:#f1f5f9;stroke:var(--border);stroke-width:2px}.pump-housing{fill:#1e293b}.pump-hub{fill:#e2e8f0}.fan-blades{opacity:.5;transform-box:view-box;transform-origin:60px 60px;transition:opacity .3s}.pump-heat .fan-blades{fill:var(--heat)}.pump-filter .fan-blades{fill:var(--filter)}.pump.running .fan-blades{opacity:1;animation:.9s linear infinite spin}.pump-label{font-weight:700}@media (width<=400px){.pump-svg{width:80px;height:80px}}.settings-card{flex-direction:column;gap:22px;display:flex}.field-label{color:var(--muted);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px;font-size:.8rem;font-weight:700;display:block}.target-row{justify-content:space-between;align-items:center;gap:12px;display:flex}.target-value{color:var(--accent-strong);font-variant-numeric:tabular-nums;font-size:3.25rem;font-weight:800}.round-btn{background:var(--accent);color:#fff;cursor:pointer;touch-action:manipulation;border:none;border-radius:50%;width:60px;height:60px;font-size:2rem;line-height:1}.round-btn:active:not(:disabled){transform:scale(.94)}.modes{flex-direction:column;gap:14px;display:flex}.field select{border:1px solid var(--border);background:var(--surface);width:100%;font:inherit;border-radius:12px;padding:12px 14px;font-size:1.05rem}.summary{gap:10px;margin:0;display:grid}.summary div{border-bottom:1px dashed var(--border);justify-content:space-between;gap:12px;padding-bottom:8px;display:flex}.summary dt{color:var(--muted)}.summary dd{text-align:right;margin:0;font-weight:600}.btn{font:inherit;cursor:pointer;border:none;border-radius:12px;justify-content:center;align-items:center;padding:12px 22px;font-weight:700;display:inline-flex}.btn-primary{background:var(--accent);color:#fff}.btn-secondary{color:var(--text);background:#f1f5f9}.btn-small{color:#fff;background:#991b1b;padding:6px 12px;font-size:.875rem}.big-button{border-radius:var(--radius);width:100%;min-height:110px;box-shadow:var(--shadow);color:#fff;font:inherit;cursor:pointer;border:none;font-size:1.75rem;font-weight:800}.big-button-backflush{background:linear-gradient(135deg,#0ea5e9,#0369a1)}.big-button-setup{background:linear-gradient(135deg,#14b8a6,#0f766e)}.big-button:active:not(:disabled){transform:translateY(1px)}.round-btn:disabled,.btn:disabled,.big-button:disabled{opacity:.4;cursor:not-allowed}.backflush-slot{place-items:center;min-height:190px;margin-top:16px;display:grid}.hint{max-width:320px;color:var(--muted);text-align:center}.backflush-anim{color:var(--accent-strong);text-align:center;font-weight:600}.backflush-anim svg{width:140px;height:140px}.backflush-anim p{margin:6px 0 0}.backflush-arrows{fill:none;stroke:var(--filter);stroke-width:6px;stroke-linecap:round;transform-box:view-box;transform-origin:60px 60px;animation:2s linear infinite spin}.backflush-arrows .arrowhead{fill:var(--filter);stroke:none}.droplet{fill:#38bdf8}.droplet-eye{fill:var(--text)}.droplet-smile{fill:none;stroke:var(--text);stroke-width:2.5px;stroke-linecap:round}.bubble{fill:#7dd3fc;transform-box:fill-box;animation:1.8s ease-in infinite bubble}.bubble.b2{animation-delay:.6s}.bubble.b3{animation-delay:1.2s}.readings{margin-top:20px}.readings-header{justify-content:space-between;align-items:baseline;display:flex}.setup-state{color:var(--muted);text-transform:capitalize;font-size:.85rem}.probe-list{gap:8px;margin:8px 0 0;padding:0;list-style:none;display:grid}.probe-row{background:var(--surface);border-radius:10px;justify-content:space-between;align-items:center;padding:10px 12px;display:flex}.probe-row .status-light{color:var(--text)}.probe-value{font-variant-numeric:tabular-nums;font-weight:600}.setup-card{max-width:640px;margin:0 auto}.setup-card h1{margin:0 0 16px;font-size:1.5rem}.stepper{gap:8px;margin:0;padding:0;list-style:none;display:flex}.stepper li{border-top:4px solid var(--border);color:var(--muted);text-align:center;flex:1;padding-top:10px;font-size:.8rem}.stepper li.done,.stepper li.current{border-color:var(--accent);color:var(--accent-strong);font-weight:700}.setup-step h2{margin:24px 0 8px;font-size:1.25rem}.setup-step p{color:var(--muted);line-height:1.5}.setup-status{background:var(--surface);border-radius:12px;margin:20px 0;padding:16px}.setup-status .status-light{color:var(--text);font-size:1.1rem}.setup-status .light{width:22px;height:22px}.actions{justify-content:space-between;gap:12px;margin-top:24px;display:flex}.mock-panel{z-index:50;max-width:250px;box-shadow:var(--shadow);background:#fffbeb;border:2px dashed #f59e0b;border-radius:12px;font-size:.85rem;position:fixed;bottom:12px;right:12px}.mock-panel summary{color:#92400e;cursor:pointer;padding:8px 12px;font-weight:700}.mock-panel-body{gap:8px;padding:0 12px 12px;display:grid}.mock-panel label{align-items:center;gap:6px;display:flex}.mock-panel button{font:inherit;text-align:left;cursor:pointer;background:#fff;border:1px solid #fcd34d;border-radius:8px;padding:6px 8px}@keyframes spin{to{transform:rotate(360deg)}}@keyframes wave{to{transform:translate(-100px)}}@keyframes bob{50%{transform:translateY(4px)rotate(-4deg)}}@keyframes rise{0%{opacity:0;transform:translateY(8px)}40%{opacity:.9}to{opacity:0;transform:translateY(-14px)}}@keyframes bubble{0%{opacity:0;transform:translateY(0)}20%{opacity:1}to{opacity:0;transform:translateY(-40px)}}@keyframes pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}} |
projects/hot-tub/src/web/frontend/dist/favicon.svg
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg> |
|
| ... | ... | \ No newline at end of file |
projects/hot-tub/src/web/frontend/dist/icons.svg
| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg"> |
|
| 2 | + <symbol id="bluesky-icon" viewBox="0 0 16 17"> |
|
| 3 | + <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g> |
|
| 4 | + <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs> |
|
| 5 | + </symbol> |
|
| 6 | + <symbol id="discord-icon" viewBox="0 0 20 19"> |
|
| 7 | + <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/> |
|
| 8 | + </symbol> |
|
| 9 | + <symbol id="documentation-icon" viewBox="0 0 21 20"> |
|
| 10 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/> |
|
| 11 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/> |
|
| 12 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/> |
|
| 13 | + </symbol> |
|
| 14 | + <symbol id="github-icon" viewBox="0 0 19 19"> |
|
| 15 | + <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/> |
|
| 16 | + </symbol> |
|
| 17 | + <symbol id="social-icon" viewBox="0 0 20 20"> |
|
| 18 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/> |
|
| 19 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/> |
|
| 20 | + </symbol> |
|
| 21 | + <symbol id="x-icon" viewBox="0 0 19 19"> |
|
| 22 | + <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/> |
|
| 23 | + </symbol> |
|
| 24 | +</svg> |
projects/hot-tub/src/web/frontend/dist/index.html
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +<!doctype html> |
|
| 2 | +<html lang="en"> |
|
| 3 | + <head> |
|
| 4 | + <meta charset="UTF-8" /> |
|
| 5 | + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> |
|
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
| 7 | + <title>frontend</title> |
|
| 8 | + <script type="module" crossorigin src="/assets/index-BUdY2Uw4.js"></script> |
|
| 9 | + <link rel="stylesheet" crossorigin href="/assets/index-CESZI1ev.css"> |
|
| 10 | + </head> |
|
| 11 | + <body> |
|
| 12 | + <div id="root"></div> |
|
| 13 | + </body> |
|
| 14 | +</html> |
projects/hot-tub/src/web/frontend/index.html
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +<!doctype html> |
|
| 2 | +<html lang="en"> |
|
| 3 | + <head> |
|
| 4 | + <meta charset="UTF-8" /> |
|
| 5 | + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> |
|
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
| 7 | + <title>frontend</title> |
|
| 8 | + </head> |
|
| 9 | + <body> |
|
| 10 | + <div id="root"></div> |
|
| 11 | + <script type="module" src="/src/main.tsx"></script> |
|
| 12 | + </body> |
|
| 13 | +</html> |
projects/hot-tub/src/web/frontend/package-lock.json
| ... | ... | @@ -0,0 +1,1714 @@ |
| 1 | +{ |
|
| 2 | + "name": "frontend", |
|
| 3 | + "version": "0.0.0", |
|
| 4 | + "lockfileVersion": 3, |
|
| 5 | + "requires": true, |
|
| 6 | + "packages": { |
|
| 7 | + "": { |
|
| 8 | + "name": "frontend", |
|
| 9 | + "version": "0.0.0", |
|
| 10 | + "dependencies": { |
|
| 11 | + "react": "^19.3.1", |
|
| 12 | + "react-dom": "^19.3.1", |
|
| 13 | + "react-router-dom": "^7.18.4" |
|
| 14 | + }, |
|
| 15 | + "devDependencies": { |
|
| 16 | + "@types/node": "^24.19.2", |
|
| 17 | + "@types/react": "^19.3.0", |
|
| 18 | + "@types/react-dom": "^19.3.0", |
|
| 19 | + "@vitejs/plugin-react": "^6.1.2", |
|
| 20 | + "oxlint": "^1.88.0", |
|
| 21 | + "typescript": "~7.0.2", |
|
| 22 | + "vite": "^8.3.4" |
|
| 23 | + } |
|
| 24 | + }, |
|
| 25 | + "node_modules/@oxc-project/types": { |
|
| 26 | + "version": "0.154.0", |
|
| 27 | + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", |
|
| 28 | + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", |
|
| 29 | + "dev": true, |
|
| 30 | + "license": "MIT", |
|
| 31 | + "funding": { |
|
| 32 | + "url": "https://github.com/sponsors/oxc-project" |
|
| 33 | + } |
|
| 34 | + }, |
|
| 35 | + "node_modules/@oxlint/binding-android-arm-eabi": { |
|
| 36 | + "version": "1.89.0", |
|
| 37 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.89.0.tgz", |
|
| 38 | + "integrity": "sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==", |
|
| 39 | + "cpu": [ |
|
| 40 | + "arm" |
|
| 41 | + ], |
|
| 42 | + "dev": true, |
|
| 43 | + "license": "MIT", |
|
| 44 | + "optional": true, |
|
| 45 | + "os": [ |
|
| 46 | + "android" |
|
| 47 | + ], |
|
| 48 | + "engines": { |
|
| 49 | + "node": "^20.19.0 || >=22.12.0" |
|
| 50 | + } |
|
| 51 | + }, |
|
| 52 | + "node_modules/@oxlint/binding-android-arm64": { |
|
| 53 | + "version": "1.89.0", |
|
| 54 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.89.0.tgz", |
|
| 55 | + "integrity": "sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==", |
|
| 56 | + "cpu": [ |
|
| 57 | + "arm64" |
|
| 58 | + ], |
|
| 59 | + "dev": true, |
|
| 60 | + "license": "MIT", |
|
| 61 | + "optional": true, |
|
| 62 | + "os": [ |
|
| 63 | + "android" |
|
| 64 | + ], |
|
| 65 | + "engines": { |
|
| 66 | + "node": "^20.19.0 || >=22.12.0" |
|
| 67 | + } |
|
| 68 | + }, |
|
| 69 | + "node_modules/@oxlint/binding-darwin-arm64": { |
|
| 70 | + "version": "1.89.0", |
|
| 71 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.89.0.tgz", |
|
| 72 | + "integrity": "sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==", |
|
| 73 | + "cpu": [ |
|
| 74 | + "arm64" |
|
| 75 | + ], |
|
| 76 | + "dev": true, |
|
| 77 | + "license": "MIT", |
|
| 78 | + "optional": true, |
|
| 79 | + "os": [ |
|
| 80 | + "darwin" |
|
| 81 | + ], |
|
| 82 | + "engines": { |
|
| 83 | + "node": "^20.19.0 || >=22.12.0" |
|
| 84 | + } |
|
| 85 | + }, |
|
| 86 | + "node_modules/@oxlint/binding-darwin-x64": { |
|
| 87 | + "version": "1.89.0", |
|
| 88 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.89.0.tgz", |
|
| 89 | + "integrity": "sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==", |
|
| 90 | + "cpu": [ |
|
| 91 | + "x64" |
|
| 92 | + ], |
|
| 93 | + "dev": true, |
|
| 94 | + "license": "MIT", |
|
| 95 | + "optional": true, |
|
| 96 | + "os": [ |
|
| 97 | + "darwin" |
|
| 98 | + ], |
|
| 99 | + "engines": { |
|
| 100 | + "node": "^20.19.0 || >=22.12.0" |
|
| 101 | + } |
|
| 102 | + }, |
|
| 103 | + "node_modules/@oxlint/binding-freebsd-x64": { |
|
| 104 | + "version": "1.89.0", |
|
| 105 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.89.0.tgz", |
|
| 106 | + "integrity": "sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==", |
|
| 107 | + "cpu": [ |
|
| 108 | + "x64" |
|
| 109 | + ], |
|
| 110 | + "dev": true, |
|
| 111 | + "license": "MIT", |
|
| 112 | + "optional": true, |
|
| 113 | + "os": [ |
|
| 114 | + "freebsd" |
|
| 115 | + ], |
|
| 116 | + "engines": { |
|
| 117 | + "node": "^20.19.0 || >=22.12.0" |
|
| 118 | + } |
|
| 119 | + }, |
|
| 120 | + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { |
|
| 121 | + "version": "1.89.0", |
|
| 122 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.89.0.tgz", |
|
| 123 | + "integrity": "sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==", |
|
| 124 | + "cpu": [ |
|
| 125 | + "arm" |
|
| 126 | + ], |
|
| 127 | + "dev": true, |
|
| 128 | + "license": "MIT", |
|
| 129 | + "optional": true, |
|
| 130 | + "os": [ |
|
| 131 | + "linux" |
|
| 132 | + ], |
|
| 133 | + "engines": { |
|
| 134 | + "node": "^20.19.0 || >=22.12.0" |
|
| 135 | + } |
|
| 136 | + }, |
|
| 137 | + "node_modules/@oxlint/binding-linux-arm-musleabihf": { |
|
| 138 | + "version": "1.89.0", |
|
| 139 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.89.0.tgz", |
|
| 140 | + "integrity": "sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==", |
|
| 141 | + "cpu": [ |
|
| 142 | + "arm" |
|
| 143 | + ], |
|
| 144 | + "dev": true, |
|
| 145 | + "license": "MIT", |
|
| 146 | + "optional": true, |
|
| 147 | + "os": [ |
|
| 148 | + "linux" |
|
| 149 | + ], |
|
| 150 | + "engines": { |
|
| 151 | + "node": "^20.19.0 || >=22.12.0" |
|
| 152 | + } |
|
| 153 | + }, |
|
| 154 | + "node_modules/@oxlint/binding-linux-arm64-gnu": { |
|
| 155 | + "version": "1.89.0", |
|
| 156 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.89.0.tgz", |
|
| 157 | + "integrity": "sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==", |
|
| 158 | + "cpu": [ |
|
| 159 | + "arm64" |
|
| 160 | + ], |
|
| 161 | + "dev": true, |
|
| 162 | + "license": "MIT", |
|
| 163 | + "optional": true, |
|
| 164 | + "os": [ |
|
| 165 | + "linux" |
|
| 166 | + ], |
|
| 167 | + "engines": { |
|
| 168 | + "node": "^20.19.0 || >=22.12.0" |
|
| 169 | + } |
|
| 170 | + }, |
|
| 171 | + "node_modules/@oxlint/binding-linux-arm64-musl": { |
|
| 172 | + "version": "1.89.0", |
|
| 173 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.89.0.tgz", |
|
| 174 | + "integrity": "sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==", |
|
| 175 | + "cpu": [ |
|
| 176 | + "arm64" |
|
| 177 | + ], |
|
| 178 | + "dev": true, |
|
| 179 | + "license": "MIT", |
|
| 180 | + "optional": true, |
|
| 181 | + "os": [ |
|
| 182 | + "linux" |
|
| 183 | + ], |
|
| 184 | + "engines": { |
|
| 185 | + "node": "^20.19.0 || >=22.12.0" |
|
| 186 | + } |
|
| 187 | + }, |
|
| 188 | + "node_modules/@oxlint/binding-linux-ppc64-gnu": { |
|
| 189 | + "version": "1.89.0", |
|
| 190 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.89.0.tgz", |
|
| 191 | + "integrity": "sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==", |
|
| 192 | + "cpu": [ |
|
| 193 | + "ppc64" |
|
| 194 | + ], |
|
| 195 | + "dev": true, |
|
| 196 | + "license": "MIT", |
|
| 197 | + "optional": true, |
|
| 198 | + "os": [ |
|
| 199 | + "linux" |
|
| 200 | + ], |
|
| 201 | + "engines": { |
|
| 202 | + "node": "^20.19.0 || >=22.12.0" |
|
| 203 | + } |
|
| 204 | + }, |
|
| 205 | + "node_modules/@oxlint/binding-linux-riscv64-gnu": { |
|
| 206 | + "version": "1.89.0", |
|
| 207 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.89.0.tgz", |
|
| 208 | + "integrity": "sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==", |
|
| 209 | + "cpu": [ |
|
| 210 | + "riscv64" |
|
| 211 | + ], |
|
| 212 | + "dev": true, |
|
| 213 | + "license": "MIT", |
|
| 214 | + "optional": true, |
|
| 215 | + "os": [ |
|
| 216 | + "linux" |
|
| 217 | + ], |
|
| 218 | + "engines": { |
|
| 219 | + "node": "^20.19.0 || >=22.12.0" |
|
| 220 | + } |
|
| 221 | + }, |
|
| 222 | + "node_modules/@oxlint/binding-linux-riscv64-musl": { |
|
| 223 | + "version": "1.89.0", |
|
| 224 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.89.0.tgz", |
|
| 225 | + "integrity": "sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==", |
|
| 226 | + "cpu": [ |
|
| 227 | + "riscv64" |
|
| 228 | + ], |
|
| 229 | + "dev": true, |
|
| 230 | + "license": "MIT", |
|
| 231 | + "optional": true, |
|
| 232 | + "os": [ |
|
| 233 | + "linux" |
|
| 234 | + ], |
|
| 235 | + "engines": { |
|
| 236 | + "node": "^20.19.0 || >=22.12.0" |
|
| 237 | + } |
|
| 238 | + }, |
|
| 239 | + "node_modules/@oxlint/binding-linux-s390x-gnu": { |
|
| 240 | + "version": "1.89.0", |
|
| 241 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.89.0.tgz", |
|
| 242 | + "integrity": "sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==", |
|
| 243 | + "cpu": [ |
|
| 244 | + "s390x" |
|
| 245 | + ], |
|
| 246 | + "dev": true, |
|
| 247 | + "license": "MIT", |
|
| 248 | + "optional": true, |
|
| 249 | + "os": [ |
|
| 250 | + "linux" |
|
| 251 | + ], |
|
| 252 | + "engines": { |
|
| 253 | + "node": "^20.19.0 || >=22.12.0" |
|
| 254 | + } |
|
| 255 | + }, |
|
| 256 | + "node_modules/@oxlint/binding-linux-x64-gnu": { |
|
| 257 | + "version": "1.89.0", |
|
| 258 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.89.0.tgz", |
|
| 259 | + "integrity": "sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==", |
|
| 260 | + "cpu": [ |
|
| 261 | + "x64" |
|
| 262 | + ], |
|
| 263 | + "dev": true, |
|
| 264 | + "license": "MIT", |
|
| 265 | + "optional": true, |
|
| 266 | + "os": [ |
|
| 267 | + "linux" |
|
| 268 | + ], |
|
| 269 | + "engines": { |
|
| 270 | + "node": "^20.19.0 || >=22.12.0" |
|
| 271 | + } |
|
| 272 | + }, |
|
| 273 | + "node_modules/@oxlint/binding-linux-x64-musl": { |
|
| 274 | + "version": "1.89.0", |
|
| 275 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.89.0.tgz", |
|
| 276 | + "integrity": "sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==", |
|
| 277 | + "cpu": [ |
|
| 278 | + "x64" |
|
| 279 | + ], |
|
| 280 | + "dev": true, |
|
| 281 | + "license": "MIT", |
|
| 282 | + "optional": true, |
|
| 283 | + "os": [ |
|
| 284 | + "linux" |
|
| 285 | + ], |
|
| 286 | + "engines": { |
|
| 287 | + "node": "^20.19.0 || >=22.12.0" |
|
| 288 | + } |
|
| 289 | + }, |
|
| 290 | + "node_modules/@oxlint/binding-openharmony-arm64": { |
|
| 291 | + "version": "1.89.0", |
|
| 292 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.89.0.tgz", |
|
| 293 | + "integrity": "sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==", |
|
| 294 | + "cpu": [ |
|
| 295 | + "arm64" |
|
| 296 | + ], |
|
| 297 | + "dev": true, |
|
| 298 | + "license": "MIT", |
|
| 299 | + "optional": true, |
|
| 300 | + "os": [ |
|
| 301 | + "openharmony" |
|
| 302 | + ], |
|
| 303 | + "engines": { |
|
| 304 | + "node": "^20.19.0 || >=22.12.0" |
|
| 305 | + } |
|
| 306 | + }, |
|
| 307 | + "node_modules/@oxlint/binding-win32-arm64-msvc": { |
|
| 308 | + "version": "1.89.0", |
|
| 309 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.89.0.tgz", |
|
| 310 | + "integrity": "sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==", |
|
| 311 | + "cpu": [ |
|
| 312 | + "arm64" |
|
| 313 | + ], |
|
| 314 | + "dev": true, |
|
| 315 | + "license": "MIT", |
|
| 316 | + "optional": true, |
|
| 317 | + "os": [ |
|
| 318 | + "win32" |
|
| 319 | + ], |
|
| 320 | + "engines": { |
|
| 321 | + "node": "^20.19.0 || >=22.12.0" |
|
| 322 | + } |
|
| 323 | + }, |
|
| 324 | + "node_modules/@oxlint/binding-win32-ia32-msvc": { |
|
| 325 | + "version": "1.89.0", |
|
| 326 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.89.0.tgz", |
|
| 327 | + "integrity": "sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==", |
|
| 328 | + "cpu": [ |
|
| 329 | + "ia32" |
|
| 330 | + ], |
|
| 331 | + "dev": true, |
|
| 332 | + "license": "MIT", |
|
| 333 | + "optional": true, |
|
| 334 | + "os": [ |
|
| 335 | + "win32" |
|
| 336 | + ], |
|
| 337 | + "engines": { |
|
| 338 | + "node": "^20.19.0 || >=22.12.0" |
|
| 339 | + } |
|
| 340 | + }, |
|
| 341 | + "node_modules/@oxlint/binding-win32-x64-msvc": { |
|
| 342 | + "version": "1.89.0", |
|
| 343 | + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.89.0.tgz", |
|
| 344 | + "integrity": "sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==", |
|
| 345 | + "cpu": [ |
|
| 346 | + "x64" |
|
| 347 | + ], |
|
| 348 | + "dev": true, |
|
| 349 | + "license": "MIT", |
|
| 350 | + "optional": true, |
|
| 351 | + "os": [ |
|
| 352 | + "win32" |
|
| 353 | + ], |
|
| 354 | + "engines": { |
|
| 355 | + "node": "^20.19.0 || >=22.12.0" |
|
| 356 | + } |
|
| 357 | + }, |
|
| 358 | + "node_modules/@rolldown/binding-android-arm-eabi": { |
|
| 359 | + "version": "1.2.14", |
|
| 360 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", |
|
| 361 | + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", |
|
| 362 | + "cpu": [ |
|
| 363 | + "arm" |
|
| 364 | + ], |
|
| 365 | + "dev": true, |
|
| 366 | + "license": "MIT", |
|
| 367 | + "optional": true, |
|
| 368 | + "os": [ |
|
| 369 | + "android" |
|
| 370 | + ], |
|
| 371 | + "engines": { |
|
| 372 | + "node": "^20.19.0 || >=22.12.0" |
|
| 373 | + } |
|
| 374 | + }, |
|
| 375 | + "node_modules/@rolldown/binding-android-arm64": { |
|
| 376 | + "version": "1.2.14", |
|
| 377 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", |
|
| 378 | + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", |
|
| 379 | + "cpu": [ |
|
| 380 | + "arm64" |
|
| 381 | + ], |
|
| 382 | + "dev": true, |
|
| 383 | + "license": "MIT", |
|
| 384 | + "optional": true, |
|
| 385 | + "os": [ |
|
| 386 | + "android" |
|
| 387 | + ], |
|
| 388 | + "engines": { |
|
| 389 | + "node": "^20.19.0 || >=22.12.0" |
|
| 390 | + } |
|
| 391 | + }, |
|
| 392 | + "node_modules/@rolldown/binding-darwin-arm64": { |
|
| 393 | + "version": "1.2.14", |
|
| 394 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", |
|
| 395 | + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", |
|
| 396 | + "cpu": [ |
|
| 397 | + "arm64" |
|
| 398 | + ], |
|
| 399 | + "dev": true, |
|
| 400 | + "license": "MIT", |
|
| 401 | + "optional": true, |
|
| 402 | + "os": [ |
|
| 403 | + "darwin" |
|
| 404 | + ], |
|
| 405 | + "engines": { |
|
| 406 | + "node": "^20.19.0 || >=22.12.0" |
|
| 407 | + } |
|
| 408 | + }, |
|
| 409 | + "node_modules/@rolldown/binding-darwin-x64": { |
|
| 410 | + "version": "1.2.14", |
|
| 411 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", |
|
| 412 | + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", |
|
| 413 | + "cpu": [ |
|
| 414 | + "x64" |
|
| 415 | + ], |
|
| 416 | + "dev": true, |
|
| 417 | + "license": "MIT", |
|
| 418 | + "optional": true, |
|
| 419 | + "os": [ |
|
| 420 | + "darwin" |
|
| 421 | + ], |
|
| 422 | + "engines": { |
|
| 423 | + "node": "^20.19.0 || >=22.12.0" |
|
| 424 | + } |
|
| 425 | + }, |
|
| 426 | + "node_modules/@rolldown/binding-freebsd-x64": { |
|
| 427 | + "version": "1.2.14", |
|
| 428 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", |
|
| 429 | + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", |
|
| 430 | + "cpu": [ |
|
| 431 | + "x64" |
|
| 432 | + ], |
|
| 433 | + "dev": true, |
|
| 434 | + "license": "MIT", |
|
| 435 | + "optional": true, |
|
| 436 | + "os": [ |
|
| 437 | + "freebsd" |
|
| 438 | + ], |
|
| 439 | + "engines": { |
|
| 440 | + "node": "^20.19.0 || >=22.12.0" |
|
| 441 | + } |
|
| 442 | + }, |
|
| 443 | + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { |
|
| 444 | + "version": "1.2.14", |
|
| 445 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", |
|
| 446 | + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", |
|
| 447 | + "cpu": [ |
|
| 448 | + "arm" |
|
| 449 | + ], |
|
| 450 | + "dev": true, |
|
| 451 | + "license": "MIT", |
|
| 452 | + "optional": true, |
|
| 453 | + "os": [ |
|
| 454 | + "linux" |
|
| 455 | + ], |
|
| 456 | + "engines": { |
|
| 457 | + "node": "^20.19.0 || >=22.12.0" |
|
| 458 | + } |
|
| 459 | + }, |
|
| 460 | + "node_modules/@rolldown/binding-linux-arm64-gnu": { |
|
| 461 | + "version": "1.2.14", |
|
| 462 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", |
|
| 463 | + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", |
|
| 464 | + "cpu": [ |
|
| 465 | + "arm64" |
|
| 466 | + ], |
|
| 467 | + "dev": true, |
|
| 468 | + "license": "MIT", |
|
| 469 | + "optional": true, |
|
| 470 | + "os": [ |
|
| 471 | + "linux" |
|
| 472 | + ], |
|
| 473 | + "engines": { |
|
| 474 | + "node": "^20.19.0 || >=22.12.0" |
|
| 475 | + } |
|
| 476 | + }, |
|
| 477 | + "node_modules/@rolldown/binding-linux-arm64-musl": { |
|
| 478 | + "version": "1.2.14", |
|
| 479 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", |
|
| 480 | + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", |
|
| 481 | + "cpu": [ |
|
| 482 | + "arm64" |
|
| 483 | + ], |
|
| 484 | + "dev": true, |
|
| 485 | + "license": "MIT", |
|
| 486 | + "optional": true, |
|
| 487 | + "os": [ |
|
| 488 | + "linux" |
|
| 489 | + ], |
|
| 490 | + "engines": { |
|
| 491 | + "node": "^20.19.0 || >=22.12.0" |
|
| 492 | + } |
|
| 493 | + }, |
|
| 494 | + "node_modules/@rolldown/binding-linux-ppc64-gnu": { |
|
| 495 | + "version": "1.2.14", |
|
| 496 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", |
|
| 497 | + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", |
|
| 498 | + "cpu": [ |
|
| 499 | + "ppc64" |
|
| 500 | + ], |
|
| 501 | + "dev": true, |
|
| 502 | + "license": "MIT", |
|
| 503 | + "optional": true, |
|
| 504 | + "os": [ |
|
| 505 | + "linux" |
|
| 506 | + ], |
|
| 507 | + "engines": { |
|
| 508 | + "node": "^20.19.0 || >=22.12.0" |
|
| 509 | + } |
|
| 510 | + }, |
|
| 511 | + "node_modules/@rolldown/binding-linux-s390x-gnu": { |
|
| 512 | + "version": "1.2.14", |
|
| 513 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", |
|
| 514 | + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", |
|
| 515 | + "cpu": [ |
|
| 516 | + "s390x" |
|
| 517 | + ], |
|
| 518 | + "dev": true, |
|
| 519 | + "license": "MIT", |
|
| 520 | + "optional": true, |
|
| 521 | + "os": [ |
|
| 522 | + "linux" |
|
| 523 | + ], |
|
| 524 | + "engines": { |
|
| 525 | + "node": "^20.19.0 || >=22.12.0" |
|
| 526 | + } |
|
| 527 | + }, |
|
| 528 | + "node_modules/@rolldown/binding-linux-x64-gnu": { |
|
| 529 | + "version": "1.2.14", |
|
| 530 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", |
|
| 531 | + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", |
|
| 532 | + "cpu": [ |
|
| 533 | + "x64" |
|
| 534 | + ], |
|
| 535 | + "dev": true, |
|
| 536 | + "license": "MIT", |
|
| 537 | + "optional": true, |
|
| 538 | + "os": [ |
|
| 539 | + "linux" |
|
| 540 | + ], |
|
| 541 | + "engines": { |
|
| 542 | + "node": "^20.19.0 || >=22.12.0" |
|
| 543 | + } |
|
| 544 | + }, |
|
| 545 | + "node_modules/@rolldown/binding-linux-x64-musl": { |
|
| 546 | + "version": "1.2.14", |
|
| 547 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", |
|
| 548 | + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", |
|
| 549 | + "cpu": [ |
|
| 550 | + "x64" |
|
| 551 | + ], |
|
| 552 | + "dev": true, |
|
| 553 | + "license": "MIT", |
|
| 554 | + "optional": true, |
|
| 555 | + "os": [ |
|
| 556 | + "linux" |
|
| 557 | + ], |
|
| 558 | + "engines": { |
|
| 559 | + "node": "^20.19.0 || >=22.12.0" |
|
| 560 | + } |
|
| 561 | + }, |
|
| 562 | + "node_modules/@rolldown/binding-openharmony-arm64": { |
|
| 563 | + "version": "1.2.14", |
|
| 564 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", |
|
| 565 | + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", |
|
| 566 | + "cpu": [ |
|
| 567 | + "arm64" |
|
| 568 | + ], |
|
| 569 | + "dev": true, |
|
| 570 | + "license": "MIT", |
|
| 571 | + "optional": true, |
|
| 572 | + "os": [ |
|
| 573 | + "openharmony" |
|
| 574 | + ], |
|
| 575 | + "engines": { |
|
| 576 | + "node": "^20.19.0 || >=22.12.0" |
|
| 577 | + } |
|
| 578 | + }, |
|
| 579 | + "node_modules/@rolldown/binding-win32-arm64-msvc": { |
|
| 580 | + "version": "1.2.14", |
|
| 581 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", |
|
| 582 | + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", |
|
| 583 | + "cpu": [ |
|
| 584 | + "arm64" |
|
| 585 | + ], |
|
| 586 | + "dev": true, |
|
| 587 | + "license": "MIT", |
|
| 588 | + "optional": true, |
|
| 589 | + "os": [ |
|
| 590 | + "win32" |
|
| 591 | + ], |
|
| 592 | + "engines": { |
|
| 593 | + "node": "^20.19.0 || >=22.12.0" |
|
| 594 | + } |
|
| 595 | + }, |
|
| 596 | + "node_modules/@rolldown/binding-win32-x64-msvc": { |
|
| 597 | + "version": "1.2.14", |
|
| 598 | + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", |
|
| 599 | + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", |
|
| 600 | + "cpu": [ |
|
| 601 | + "x64" |
|
| 602 | + ], |
|
| 603 | + "dev": true, |
|
| 604 | + "license": "MIT", |
|
| 605 | + "optional": true, |
|
| 606 | + "os": [ |
|
| 607 | + "win32" |
|
| 608 | + ], |
|
| 609 | + "engines": { |
|
| 610 | + "node": "^20.19.0 || >=22.12.0" |
|
| 611 | + } |
|
| 612 | + }, |
|
| 613 | + "node_modules/@rolldown/pluginutils": { |
|
| 614 | + "version": "1.0.1", |
|
| 615 | + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", |
|
| 616 | + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", |
|
| 617 | + "dev": true, |
|
| 618 | + "license": "MIT" |
|
| 619 | + }, |
|
| 620 | + "node_modules/@types/node": { |
|
| 621 | + "version": "24.19.2", |
|
| 622 | + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", |
|
| 623 | + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", |
|
| 624 | + "dev": true, |
|
| 625 | + "license": "MIT", |
|
| 626 | + "dependencies": { |
|
| 627 | + "undici-types": ">=7.24.0 <7.24.7" |
|
| 628 | + } |
|
| 629 | + }, |
|
| 630 | + "node_modules/@types/react": { |
|
| 631 | + "version": "19.3.0", |
|
| 632 | + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", |
|
| 633 | + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", |
|
| 634 | + "dev": true, |
|
| 635 | + "license": "MIT", |
|
| 636 | + "dependencies": { |
|
| 637 | + "csstype": "^3.2.2" |
|
| 638 | + } |
|
| 639 | + }, |
|
| 640 | + "node_modules/@types/react-dom": { |
|
| 641 | + "version": "19.3.0", |
|
| 642 | + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", |
|
| 643 | + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", |
|
| 644 | + "dev": true, |
|
| 645 | + "license": "MIT", |
|
| 646 | + "peerDependencies": { |
|
| 647 | + "@types/react": "^19.3.0" |
|
| 648 | + } |
|
| 649 | + }, |
|
| 650 | + "node_modules/@typescript/typescript-aix-ppc64": { |
|
| 651 | + "version": "7.0.2", |
|
| 652 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", |
|
| 653 | + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", |
|
| 654 | + "cpu": [ |
|
| 655 | + "ppc64" |
|
| 656 | + ], |
|
| 657 | + "dev": true, |
|
| 658 | + "license": "Apache-2.0", |
|
| 659 | + "optional": true, |
|
| 660 | + "os": [ |
|
| 661 | + "aix" |
|
| 662 | + ], |
|
| 663 | + "engines": { |
|
| 664 | + "node": ">=16.20.0" |
|
| 665 | + } |
|
| 666 | + }, |
|
| 667 | + "node_modules/@typescript/typescript-darwin-arm64": { |
|
| 668 | + "version": "7.0.2", |
|
| 669 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", |
|
| 670 | + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", |
|
| 671 | + "cpu": [ |
|
| 672 | + "arm64" |
|
| 673 | + ], |
|
| 674 | + "dev": true, |
|
| 675 | + "license": "Apache-2.0", |
|
| 676 | + "optional": true, |
|
| 677 | + "os": [ |
|
| 678 | + "darwin" |
|
| 679 | + ], |
|
| 680 | + "engines": { |
|
| 681 | + "node": ">=16.20.0" |
|
| 682 | + } |
|
| 683 | + }, |
|
| 684 | + "node_modules/@typescript/typescript-darwin-x64": { |
|
| 685 | + "version": "7.0.2", |
|
| 686 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", |
|
| 687 | + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", |
|
| 688 | + "cpu": [ |
|
| 689 | + "x64" |
|
| 690 | + ], |
|
| 691 | + "dev": true, |
|
| 692 | + "license": "Apache-2.0", |
|
| 693 | + "optional": true, |
|
| 694 | + "os": [ |
|
| 695 | + "darwin" |
|
| 696 | + ], |
|
| 697 | + "engines": { |
|
| 698 | + "node": ">=16.20.0" |
|
| 699 | + } |
|
| 700 | + }, |
|
| 701 | + "node_modules/@typescript/typescript-freebsd-arm64": { |
|
| 702 | + "version": "7.0.2", |
|
| 703 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", |
|
| 704 | + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", |
|
| 705 | + "cpu": [ |
|
| 706 | + "arm64" |
|
| 707 | + ], |
|
| 708 | + "dev": true, |
|
| 709 | + "license": "Apache-2.0", |
|
| 710 | + "optional": true, |
|
| 711 | + "os": [ |
|
| 712 | + "freebsd" |
|
| 713 | + ], |
|
| 714 | + "engines": { |
|
| 715 | + "node": ">=16.20.0" |
|
| 716 | + } |
|
| 717 | + }, |
|
| 718 | + "node_modules/@typescript/typescript-freebsd-x64": { |
|
| 719 | + "version": "7.0.2", |
|
| 720 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", |
|
| 721 | + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", |
|
| 722 | + "cpu": [ |
|
| 723 | + "x64" |
|
| 724 | + ], |
|
| 725 | + "dev": true, |
|
| 726 | + "license": "Apache-2.0", |
|
| 727 | + "optional": true, |
|
| 728 | + "os": [ |
|
| 729 | + "freebsd" |
|
| 730 | + ], |
|
| 731 | + "engines": { |
|
| 732 | + "node": ">=16.20.0" |
|
| 733 | + } |
|
| 734 | + }, |
|
| 735 | + "node_modules/@typescript/typescript-linux-arm": { |
|
| 736 | + "version": "7.0.2", |
|
| 737 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", |
|
| 738 | + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", |
|
| 739 | + "cpu": [ |
|
| 740 | + "arm" |
|
| 741 | + ], |
|
| 742 | + "dev": true, |
|
| 743 | + "license": "Apache-2.0", |
|
| 744 | + "optional": true, |
|
| 745 | + "os": [ |
|
| 746 | + "linux" |
|
| 747 | + ], |
|
| 748 | + "engines": { |
|
| 749 | + "node": ">=16.20.0" |
|
| 750 | + } |
|
| 751 | + }, |
|
| 752 | + "node_modules/@typescript/typescript-linux-arm64": { |
|
| 753 | + "version": "7.0.2", |
|
| 754 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", |
|
| 755 | + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", |
|
| 756 | + "cpu": [ |
|
| 757 | + "arm64" |
|
| 758 | + ], |
|
| 759 | + "dev": true, |
|
| 760 | + "license": "Apache-2.0", |
|
| 761 | + "optional": true, |
|
| 762 | + "os": [ |
|
| 763 | + "linux" |
|
| 764 | + ], |
|
| 765 | + "engines": { |
|
| 766 | + "node": ">=16.20.0" |
|
| 767 | + } |
|
| 768 | + }, |
|
| 769 | + "node_modules/@typescript/typescript-linux-loong64": { |
|
| 770 | + "version": "7.0.2", |
|
| 771 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", |
|
| 772 | + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", |
|
| 773 | + "cpu": [ |
|
| 774 | + "loong64" |
|
| 775 | + ], |
|
| 776 | + "dev": true, |
|
| 777 | + "license": "Apache-2.0", |
|
| 778 | + "optional": true, |
|
| 779 | + "os": [ |
|
| 780 | + "linux" |
|
| 781 | + ], |
|
| 782 | + "engines": { |
|
| 783 | + "node": ">=16.20.0" |
|
| 784 | + } |
|
| 785 | + }, |
|
| 786 | + "node_modules/@typescript/typescript-linux-mips64el": { |
|
| 787 | + "version": "7.0.2", |
|
| 788 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", |
|
| 789 | + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", |
|
| 790 | + "cpu": [ |
|
| 791 | + "mips64el" |
|
| 792 | + ], |
|
| 793 | + "dev": true, |
|
| 794 | + "license": "Apache-2.0", |
|
| 795 | + "optional": true, |
|
| 796 | + "os": [ |
|
| 797 | + "linux" |
|
| 798 | + ], |
|
| 799 | + "engines": { |
|
| 800 | + "node": ">=16.20.0" |
|
| 801 | + } |
|
| 802 | + }, |
|
| 803 | + "node_modules/@typescript/typescript-linux-ppc64": { |
|
| 804 | + "version": "7.0.2", |
|
| 805 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", |
|
| 806 | + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", |
|
| 807 | + "cpu": [ |
|
| 808 | + "ppc64" |
|
| 809 | + ], |
|
| 810 | + "dev": true, |
|
| 811 | + "license": "Apache-2.0", |
|
| 812 | + "optional": true, |
|
| 813 | + "os": [ |
|
| 814 | + "linux" |
|
| 815 | + ], |
|
| 816 | + "engines": { |
|
| 817 | + "node": ">=16.20.0" |
|
| 818 | + } |
|
| 819 | + }, |
|
| 820 | + "node_modules/@typescript/typescript-linux-riscv64": { |
|
| 821 | + "version": "7.0.2", |
|
| 822 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", |
|
| 823 | + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", |
|
| 824 | + "cpu": [ |
|
| 825 | + "riscv64" |
|
| 826 | + ], |
|
| 827 | + "dev": true, |
|
| 828 | + "license": "Apache-2.0", |
|
| 829 | + "optional": true, |
|
| 830 | + "os": [ |
|
| 831 | + "linux" |
|
| 832 | + ], |
|
| 833 | + "engines": { |
|
| 834 | + "node": ">=16.20.0" |
|
| 835 | + } |
|
| 836 | + }, |
|
| 837 | + "node_modules/@typescript/typescript-linux-s390x": { |
|
| 838 | + "version": "7.0.2", |
|
| 839 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", |
|
| 840 | + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", |
|
| 841 | + "cpu": [ |
|
| 842 | + "s390x" |
|
| 843 | + ], |
|
| 844 | + "dev": true, |
|
| 845 | + "license": "Apache-2.0", |
|
| 846 | + "optional": true, |
|
| 847 | + "os": [ |
|
| 848 | + "linux" |
|
| 849 | + ], |
|
| 850 | + "engines": { |
|
| 851 | + "node": ">=16.20.0" |
|
| 852 | + } |
|
| 853 | + }, |
|
| 854 | + "node_modules/@typescript/typescript-linux-x64": { |
|
| 855 | + "version": "7.0.2", |
|
| 856 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", |
|
| 857 | + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", |
|
| 858 | + "cpu": [ |
|
| 859 | + "x64" |
|
| 860 | + ], |
|
| 861 | + "dev": true, |
|
| 862 | + "license": "Apache-2.0", |
|
| 863 | + "optional": true, |
|
| 864 | + "os": [ |
|
| 865 | + "linux" |
|
| 866 | + ], |
|
| 867 | + "engines": { |
|
| 868 | + "node": ">=16.20.0" |
|
| 869 | + } |
|
| 870 | + }, |
|
| 871 | + "node_modules/@typescript/typescript-netbsd-arm64": { |
|
| 872 | + "version": "7.0.2", |
|
| 873 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", |
|
| 874 | + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", |
|
| 875 | + "cpu": [ |
|
| 876 | + "arm64" |
|
| 877 | + ], |
|
| 878 | + "dev": true, |
|
| 879 | + "license": "Apache-2.0", |
|
| 880 | + "optional": true, |
|
| 881 | + "os": [ |
|
| 882 | + "netbsd" |
|
| 883 | + ], |
|
| 884 | + "engines": { |
|
| 885 | + "node": ">=16.20.0" |
|
| 886 | + } |
|
| 887 | + }, |
|
| 888 | + "node_modules/@typescript/typescript-netbsd-x64": { |
|
| 889 | + "version": "7.0.2", |
|
| 890 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", |
|
| 891 | + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", |
|
| 892 | + "cpu": [ |
|
| 893 | + "x64" |
|
| 894 | + ], |
|
| 895 | + "dev": true, |
|
| 896 | + "license": "Apache-2.0", |
|
| 897 | + "optional": true, |
|
| 898 | + "os": [ |
|
| 899 | + "netbsd" |
|
| 900 | + ], |
|
| 901 | + "engines": { |
|
| 902 | + "node": ">=16.20.0" |
|
| 903 | + } |
|
| 904 | + }, |
|
| 905 | + "node_modules/@typescript/typescript-openbsd-arm64": { |
|
| 906 | + "version": "7.0.2", |
|
| 907 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", |
|
| 908 | + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", |
|
| 909 | + "cpu": [ |
|
| 910 | + "arm64" |
|
| 911 | + ], |
|
| 912 | + "dev": true, |
|
| 913 | + "license": "Apache-2.0", |
|
| 914 | + "optional": true, |
|
| 915 | + "os": [ |
|
| 916 | + "openbsd" |
|
| 917 | + ], |
|
| 918 | + "engines": { |
|
| 919 | + "node": ">=16.20.0" |
|
| 920 | + } |
|
| 921 | + }, |
|
| 922 | + "node_modules/@typescript/typescript-openbsd-x64": { |
|
| 923 | + "version": "7.0.2", |
|
| 924 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", |
|
| 925 | + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", |
|
| 926 | + "cpu": [ |
|
| 927 | + "x64" |
|
| 928 | + ], |
|
| 929 | + "dev": true, |
|
| 930 | + "license": "Apache-2.0", |
|
| 931 | + "optional": true, |
|
| 932 | + "os": [ |
|
| 933 | + "openbsd" |
|
| 934 | + ], |
|
| 935 | + "engines": { |
|
| 936 | + "node": ">=16.20.0" |
|
| 937 | + } |
|
| 938 | + }, |
|
| 939 | + "node_modules/@typescript/typescript-sunos-x64": { |
|
| 940 | + "version": "7.0.2", |
|
| 941 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", |
|
| 942 | + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", |
|
| 943 | + "cpu": [ |
|
| 944 | + "x64" |
|
| 945 | + ], |
|
| 946 | + "dev": true, |
|
| 947 | + "license": "Apache-2.0", |
|
| 948 | + "optional": true, |
|
| 949 | + "os": [ |
|
| 950 | + "sunos" |
|
| 951 | + ], |
|
| 952 | + "engines": { |
|
| 953 | + "node": ">=16.20.0" |
|
| 954 | + } |
|
| 955 | + }, |
|
| 956 | + "node_modules/@typescript/typescript-win32-arm64": { |
|
| 957 | + "version": "7.0.2", |
|
| 958 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", |
|
| 959 | + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", |
|
| 960 | + "cpu": [ |
|
| 961 | + "arm64" |
|
| 962 | + ], |
|
| 963 | + "dev": true, |
|
| 964 | + "license": "Apache-2.0", |
|
| 965 | + "optional": true, |
|
| 966 | + "os": [ |
|
| 967 | + "win32" |
|
| 968 | + ], |
|
| 969 | + "engines": { |
|
| 970 | + "node": ">=16.20.0" |
|
| 971 | + } |
|
| 972 | + }, |
|
| 973 | + "node_modules/@typescript/typescript-win32-x64": { |
|
| 974 | + "version": "7.0.2", |
|
| 975 | + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", |
|
| 976 | + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", |
|
| 977 | + "cpu": [ |
|
| 978 | + "x64" |
|
| 979 | + ], |
|
| 980 | + "dev": true, |
|
| 981 | + "license": "Apache-2.0", |
|
| 982 | + "optional": true, |
|
| 983 | + "os": [ |
|
| 984 | + "win32" |
|
| 985 | + ], |
|
| 986 | + "engines": { |
|
| 987 | + "node": ">=16.20.0" |
|
| 988 | + } |
|
| 989 | + }, |
|
| 990 | + "node_modules/@vitejs/plugin-react": { |
|
| 991 | + "version": "6.1.2", |
|
| 992 | + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", |
|
| 993 | + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", |
|
| 994 | + "dev": true, |
|
| 995 | + "license": "MIT", |
|
| 996 | + "dependencies": { |
|
| 997 | + "@rolldown/pluginutils": "^1.0.1" |
|
| 998 | + }, |
|
| 999 | + "engines": { |
|
| 1000 | + "node": "^20.19.0 || >=22.12.0" |
|
| 1001 | + }, |
|
| 1002 | + "peerDependencies": { |
|
| 1003 | + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", |
|
| 1004 | + "babel-plugin-react-compiler": "^1.0.0", |
|
| 1005 | + "oxc-transform-react": "^0.152.0", |
|
| 1006 | + "vite": "^8.0.0" |
|
| 1007 | + }, |
|
| 1008 | + "peerDependenciesMeta": { |
|
| 1009 | + "@rolldown/plugin-babel": { |
|
| 1010 | + "optional": true |
|
| 1011 | + }, |
|
| 1012 | + "babel-plugin-react-compiler": { |
|
| 1013 | + "optional": true |
|
| 1014 | + }, |
|
| 1015 | + "oxc-transform-react": { |
|
| 1016 | + "optional": true |
|
| 1017 | + } |
|
| 1018 | + } |
|
| 1019 | + }, |
|
| 1020 | + "node_modules/cookie": { |
|
| 1021 | + "version": "1.1.1", |
|
| 1022 | + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", |
|
| 1023 | + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", |
|
| 1024 | + "license": "MIT", |
|
| 1025 | + "engines": { |
|
| 1026 | + "node": ">=18" |
|
| 1027 | + }, |
|
| 1028 | + "funding": { |
|
| 1029 | + "type": "opencollective", |
|
| 1030 | + "url": "https://opencollective.com/express" |
|
| 1031 | + } |
|
| 1032 | + }, |
|
| 1033 | + "node_modules/csstype": { |
|
| 1034 | + "version": "3.2.3", |
|
| 1035 | + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", |
|
| 1036 | + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", |
|
| 1037 | + "dev": true, |
|
| 1038 | + "license": "MIT" |
|
| 1039 | + }, |
|
| 1040 | + "node_modules/detect-libc": { |
|
| 1041 | + "version": "2.1.2", |
|
| 1042 | + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", |
|
| 1043 | + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", |
|
| 1044 | + "dev": true, |
|
| 1045 | + "license": "Apache-2.0", |
|
| 1046 | + "engines": { |
|
| 1047 | + "node": ">=8" |
|
| 1048 | + } |
|
| 1049 | + }, |
|
| 1050 | + "node_modules/fdir": { |
|
| 1051 | + "version": "6.5.0", |
|
| 1052 | + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", |
|
| 1053 | + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", |
|
| 1054 | + "dev": true, |
|
| 1055 | + "license": "MIT", |
|
| 1056 | + "engines": { |
|
| 1057 | + "node": ">=12.0.0" |
|
| 1058 | + }, |
|
| 1059 | + "peerDependencies": { |
|
| 1060 | + "picomatch": "^3 || ^4" |
|
| 1061 | + }, |
|
| 1062 | + "peerDependenciesMeta": { |
|
| 1063 | + "picomatch": { |
|
| 1064 | + "optional": true |
|
| 1065 | + } |
|
| 1066 | + } |
|
| 1067 | + }, |
|
| 1068 | + "node_modules/fsevents": { |
|
| 1069 | + "version": "2.3.3", |
|
| 1070 | + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", |
|
| 1071 | + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", |
|
| 1072 | + "dev": true, |
|
| 1073 | + "hasInstallScript": true, |
|
| 1074 | + "license": "MIT", |
|
| 1075 | + "optional": true, |
|
| 1076 | + "os": [ |
|
| 1077 | + "darwin" |
|
| 1078 | + ], |
|
| 1079 | + "engines": { |
|
| 1080 | + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" |
|
| 1081 | + } |
|
| 1082 | + }, |
|
| 1083 | + "node_modules/lightningcss": { |
|
| 1084 | + "version": "1.33.0", |
|
| 1085 | + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", |
|
| 1086 | + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", |
|
| 1087 | + "dev": true, |
|
| 1088 | + "license": "MPL-2.0", |
|
| 1089 | + "dependencies": { |
|
| 1090 | + "detect-libc": "^2.0.3" |
|
| 1091 | + }, |
|
| 1092 | + "engines": { |
|
| 1093 | + "node": ">= 12.0.0" |
|
| 1094 | + }, |
|
| 1095 | + "funding": { |
|
| 1096 | + "type": "opencollective", |
|
| 1097 | + "url": "https://opencollective.com/parcel" |
|
| 1098 | + }, |
|
| 1099 | + "optionalDependencies": { |
|
| 1100 | + "lightningcss-android-arm64": "1.33.0", |
|
| 1101 | + "lightningcss-darwin-arm64": "1.33.0", |
|
| 1102 | + "lightningcss-darwin-x64": "1.33.0", |
|
| 1103 | + "lightningcss-freebsd-x64": "1.33.0", |
|
| 1104 | + "lightningcss-linux-arm-gnueabihf": "1.33.0", |
|
| 1105 | + "lightningcss-linux-arm64-gnu": "1.33.0", |
|
| 1106 | + "lightningcss-linux-arm64-musl": "1.33.0", |
|
| 1107 | + "lightningcss-linux-x64-gnu": "1.33.0", |
|
| 1108 | + "lightningcss-linux-x64-musl": "1.33.0", |
|
| 1109 | + "lightningcss-win32-arm64-msvc": "1.33.0", |
|
| 1110 | + "lightningcss-win32-x64-msvc": "1.33.0" |
|
| 1111 | + } |
|
| 1112 | + }, |
|
| 1113 | + "node_modules/lightningcss-android-arm64": { |
|
| 1114 | + "version": "1.33.0", |
|
| 1115 | + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", |
|
| 1116 | + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", |
|
| 1117 | + "cpu": [ |
|
| 1118 | + "arm64" |
|
| 1119 | + ], |
|
| 1120 | + "dev": true, |
|
| 1121 | + "license": "MPL-2.0", |
|
| 1122 | + "optional": true, |
|
| 1123 | + "os": [ |
|
| 1124 | + "android" |
|
| 1125 | + ], |
|
| 1126 | + "engines": { |
|
| 1127 | + "node": ">= 12.0.0" |
|
| 1128 | + }, |
|
| 1129 | + "funding": { |
|
| 1130 | + "type": "opencollective", |
|
| 1131 | + "url": "https://opencollective.com/parcel" |
|
| 1132 | + } |
|
| 1133 | + }, |
|
| 1134 | + "node_modules/lightningcss-darwin-arm64": { |
|
| 1135 | + "version": "1.33.0", |
|
| 1136 | + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", |
|
| 1137 | + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", |
|
| 1138 | + "cpu": [ |
|
| 1139 | + "arm64" |
|
| 1140 | + ], |
|
| 1141 | + "dev": true, |
|
| 1142 | + "license": "MPL-2.0", |
|
| 1143 | + "optional": true, |
|
| 1144 | + "os": [ |
|
| 1145 | + "darwin" |
|
| 1146 | + ], |
|
| 1147 | + "engines": { |
|
| 1148 | + "node": ">= 12.0.0" |
|
| 1149 | + }, |
|
| 1150 | + "funding": { |
|
| 1151 | + "type": "opencollective", |
|
| 1152 | + "url": "https://opencollective.com/parcel" |
|
| 1153 | + } |
|
| 1154 | + }, |
|
| 1155 | + "node_modules/lightningcss-darwin-x64": { |
|
| 1156 | + "version": "1.33.0", |
|
| 1157 | + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", |
|
| 1158 | + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", |
|
| 1159 | + "cpu": [ |
|
| 1160 | + "x64" |
|
| 1161 | + ], |
|
| 1162 | + "dev": true, |
|
| 1163 | + "license": "MPL-2.0", |
|
| 1164 | + "optional": true, |
|
| 1165 | + "os": [ |
|
| 1166 | + "darwin" |
|
| 1167 | + ], |
|
| 1168 | + "engines": { |
|
| 1169 | + "node": ">= 12.0.0" |
|
| 1170 | + }, |
|
| 1171 | + "funding": { |
|
| 1172 | + "type": "opencollective", |
|
| 1173 | + "url": "https://opencollective.com/parcel" |
|
| 1174 | + } |
|
| 1175 | + }, |
|
| 1176 | + "node_modules/lightningcss-freebsd-x64": { |
|
| 1177 | + "version": "1.33.0", |
|
| 1178 | + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", |
|
| 1179 | + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", |
|
| 1180 | + "cpu": [ |
|
| 1181 | + "x64" |
|
| 1182 | + ], |
|
| 1183 | + "dev": true, |
|
| 1184 | + "license": "MPL-2.0", |
|
| 1185 | + "optional": true, |
|
| 1186 | + "os": [ |
|
| 1187 | + "freebsd" |
|
| 1188 | + ], |
|
| 1189 | + "engines": { |
|
| 1190 | + "node": ">= 12.0.0" |
|
| 1191 | + }, |
|
| 1192 | + "funding": { |
|
| 1193 | + "type": "opencollective", |
|
| 1194 | + "url": "https://opencollective.com/parcel" |
|
| 1195 | + } |
|
| 1196 | + }, |
|
| 1197 | + "node_modules/lightningcss-linux-arm-gnueabihf": { |
|
| 1198 | + "version": "1.33.0", |
|
| 1199 | + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", |
|
| 1200 | + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", |
|
| 1201 | + "cpu": [ |
|
| 1202 | + "arm" |
|
| 1203 | + ], |
|
| 1204 | + "dev": true, |
|
| 1205 | + "license": "MPL-2.0", |
|
| 1206 | + "optional": true, |
|
| 1207 | + "os": [ |
|
| 1208 | + "linux" |
|
| 1209 | + ], |
|
| 1210 | + "engines": { |
|
| 1211 | + "node": ">= 12.0.0" |
|
| 1212 | + }, |
|
| 1213 | + "funding": { |
|
| 1214 | + "type": "opencollective", |
|
| 1215 | + "url": "https://opencollective.com/parcel" |
|
| 1216 | + } |
|
| 1217 | + }, |
|
| 1218 | + "node_modules/lightningcss-linux-arm64-gnu": { |
|
| 1219 | + "version": "1.33.0", |
|
| 1220 | + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", |
|
| 1221 | + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", |
|
| 1222 | + "cpu": [ |
|
| 1223 | + "arm64" |
|
| 1224 | + ], |
|
| 1225 | + "dev": true, |
|
| 1226 | + "license": "MPL-2.0", |
|
| 1227 | + "optional": true, |
|
| 1228 | + "os": [ |
|
| 1229 | + "linux" |
|
| 1230 | + ], |
|
| 1231 | + "engines": { |
|
| 1232 | + "node": ">= 12.0.0" |
|
| 1233 | + }, |
|
| 1234 | + "funding": { |
|
| 1235 | + "type": "opencollective", |
|
| 1236 | + "url": "https://opencollective.com/parcel" |
|
| 1237 | + } |
|
| 1238 | + }, |
|
| 1239 | + "node_modules/lightningcss-linux-arm64-musl": { |
|
| 1240 | + "version": "1.33.0", |
|
| 1241 | + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", |
|
| 1242 | + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", |
|
| 1243 | + "cpu": [ |
|
| 1244 | + "arm64" |
|
| 1245 | + ], |
|
| 1246 | + "dev": true, |
|
| 1247 | + "license": "MPL-2.0", |
|
| 1248 | + "optional": true, |
|
| 1249 | + "os": [ |
|
| 1250 | + "linux" |
|
| 1251 | + ], |
|
| 1252 | + "engines": { |
|
| 1253 | + "node": ">= 12.0.0" |
|
| 1254 | + }, |
|
| 1255 | + "funding": { |
|
| 1256 | + "type": "opencollective", |
|
| 1257 | + "url": "https://opencollective.com/parcel" |
|
| 1258 | + } |
|
| 1259 | + }, |
|
| 1260 | + "node_modules/lightningcss-linux-x64-gnu": { |
|
| 1261 | + "version": "1.33.0", |
|
| 1262 | + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", |
|
| 1263 | + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", |
|
| 1264 | + "cpu": [ |
|
| 1265 | + "x64" |
|
| 1266 | + ], |
|
| 1267 | + "dev": true, |
|
| 1268 | + "license": "MPL-2.0", |
|
| 1269 | + "optional": true, |
|
| 1270 | + "os": [ |
|
| 1271 | + "linux" |
|
| 1272 | + ], |
|
| 1273 | + "engines": { |
|
| 1274 | + "node": ">= 12.0.0" |
|
| 1275 | + }, |
|
| 1276 | + "funding": { |
|
| 1277 | + "type": "opencollective", |
|
| 1278 | + "url": "https://opencollective.com/parcel" |
|
| 1279 | + } |
|
| 1280 | + }, |
|
| 1281 | + "node_modules/lightningcss-linux-x64-musl": { |
|
| 1282 | + "version": "1.33.0", |
|
| 1283 | + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", |
|
| 1284 | + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", |
|
| 1285 | + "cpu": [ |
|
| 1286 | + "x64" |
|
| 1287 | + ], |
|
| 1288 | + "dev": true, |
|
| 1289 | + "license": "MPL-2.0", |
|
| 1290 | + "optional": true, |
|
| 1291 | + "os": [ |
|
| 1292 | + "linux" |
|
| 1293 | + ], |
|
| 1294 | + "engines": { |
|
| 1295 | + "node": ">= 12.0.0" |
|
| 1296 | + }, |
|
| 1297 | + "funding": { |
|
| 1298 | + "type": "opencollective", |
|
| 1299 | + "url": "https://opencollective.com/parcel" |
|
| 1300 | + } |
|
| 1301 | + }, |
|
| 1302 | + "node_modules/lightningcss-win32-arm64-msvc": { |
|
| 1303 | + "version": "1.33.0", |
|
| 1304 | + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", |
|
| 1305 | + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", |
|
| 1306 | + "cpu": [ |
|
| 1307 | + "arm64" |
|
| 1308 | + ], |
|
| 1309 | + "dev": true, |
|
| 1310 | + "license": "MPL-2.0", |
|
| 1311 | + "optional": true, |
|
| 1312 | + "os": [ |
|
| 1313 | + "win32" |
|
| 1314 | + ], |
|
| 1315 | + "engines": { |
|
| 1316 | + "node": ">= 12.0.0" |
|
| 1317 | + }, |
|
| 1318 | + "funding": { |
|
| 1319 | + "type": "opencollective", |
|
| 1320 | + "url": "https://opencollective.com/parcel" |
|
| 1321 | + } |
|
| 1322 | + }, |
|
| 1323 | + "node_modules/lightningcss-win32-x64-msvc": { |
|
| 1324 | + "version": "1.33.0", |
|
| 1325 | + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", |
|
| 1326 | + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", |
|
| 1327 | + "cpu": [ |
|
| 1328 | + "x64" |
|
| 1329 | + ], |
|
| 1330 | + "dev": true, |
|
| 1331 | + "license": "MPL-2.0", |
|
| 1332 | + "optional": true, |
|
| 1333 | + "os": [ |
|
| 1334 | + "win32" |
|
| 1335 | + ], |
|
| 1336 | + "engines": { |
|
| 1337 | + "node": ">= 12.0.0" |
|
| 1338 | + }, |
|
| 1339 | + "funding": { |
|
| 1340 | + "type": "opencollective", |
|
| 1341 | + "url": "https://opencollective.com/parcel" |
|
| 1342 | + } |
|
| 1343 | + }, |
|
| 1344 | + "node_modules/nanoid": { |
|
| 1345 | + "version": "3.3.20", |
|
| 1346 | + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", |
|
| 1347 | + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", |
|
| 1348 | + "dev": true, |
|
| 1349 | + "funding": [ |
|
| 1350 | + { |
|
| 1351 | + "type": "github", |
|
| 1352 | + "url": "https://github.com/sponsors/ai" |
|
| 1353 | + } |
|
| 1354 | + ], |
|
| 1355 | + "license": "MIT", |
|
| 1356 | + "bin": { |
|
| 1357 | + "nanoid": "bin/nanoid.cjs" |
|
| 1358 | + }, |
|
| 1359 | + "engines": { |
|
| 1360 | + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" |
|
| 1361 | + } |
|
| 1362 | + }, |
|
| 1363 | + "node_modules/oxlint": { |
|
| 1364 | + "version": "1.89.0", |
|
| 1365 | + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.89.0.tgz", |
|
| 1366 | + "integrity": "sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==", |
|
| 1367 | + "dev": true, |
|
| 1368 | + "license": "MIT", |
|
| 1369 | + "bin": { |
|
| 1370 | + "oxlint": "bin/oxlint" |
|
| 1371 | + }, |
|
| 1372 | + "engines": { |
|
| 1373 | + "node": "^20.19.0 || >=22.12.0" |
|
| 1374 | + }, |
|
| 1375 | + "funding": { |
|
| 1376 | + "url": "https://github.com/sponsors/oxc-project" |
|
| 1377 | + }, |
|
| 1378 | + "optionalDependencies": { |
|
| 1379 | + "@oxlint/binding-android-arm-eabi": "1.89.0", |
|
| 1380 | + "@oxlint/binding-android-arm64": "1.89.0", |
|
| 1381 | + "@oxlint/binding-darwin-arm64": "1.89.0", |
|
| 1382 | + "@oxlint/binding-darwin-x64": "1.89.0", |
|
| 1383 | + "@oxlint/binding-freebsd-x64": "1.89.0", |
|
| 1384 | + "@oxlint/binding-linux-arm-gnueabihf": "1.89.0", |
|
| 1385 | + "@oxlint/binding-linux-arm-musleabihf": "1.89.0", |
|
| 1386 | + "@oxlint/binding-linux-arm64-gnu": "1.89.0", |
|
| 1387 | + "@oxlint/binding-linux-arm64-musl": "1.89.0", |
|
| 1388 | + "@oxlint/binding-linux-ppc64-gnu": "1.89.0", |
|
| 1389 | + "@oxlint/binding-linux-riscv64-gnu": "1.89.0", |
|
| 1390 | + "@oxlint/binding-linux-riscv64-musl": "1.89.0", |
|
| 1391 | + "@oxlint/binding-linux-s390x-gnu": "1.89.0", |
|
| 1392 | + "@oxlint/binding-linux-x64-gnu": "1.89.0", |
|
| 1393 | + "@oxlint/binding-linux-x64-musl": "1.89.0", |
|
| 1394 | + "@oxlint/binding-openharmony-arm64": "1.89.0", |
|
| 1395 | + "@oxlint/binding-win32-arm64-msvc": "1.89.0", |
|
| 1396 | + "@oxlint/binding-win32-ia32-msvc": "1.89.0", |
|
| 1397 | + "@oxlint/binding-win32-x64-msvc": "1.89.0" |
|
| 1398 | + }, |
|
| 1399 | + "peerDependencies": { |
|
| 1400 | + "oxlint-tsgolint": ">=7.0.2003", |
|
| 1401 | + "vite-plus": "*" |
|
| 1402 | + }, |
|
| 1403 | + "peerDependenciesMeta": { |
|
| 1404 | + "oxlint-tsgolint": { |
|
| 1405 | + "optional": true |
|
| 1406 | + }, |
|
| 1407 | + "vite-plus": { |
|
| 1408 | + "optional": true |
|
| 1409 | + } |
|
| 1410 | + } |
|
| 1411 | + }, |
|
| 1412 | + "node_modules/picocolors": { |
|
| 1413 | + "version": "1.1.1", |
|
| 1414 | + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", |
|
| 1415 | + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", |
|
| 1416 | + "dev": true, |
|
| 1417 | + "license": "ISC" |
|
| 1418 | + }, |
|
| 1419 | + "node_modules/picomatch": { |
|
| 1420 | + "version": "4.0.7", |
|
| 1421 | + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", |
|
| 1422 | + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", |
|
| 1423 | + "dev": true, |
|
| 1424 | + "license": "MIT", |
|
| 1425 | + "engines": { |
|
| 1426 | + "node": ">=12" |
|
| 1427 | + }, |
|
| 1428 | + "funding": { |
|
| 1429 | + "url": "https://github.com/sponsors/jonschlinkert" |
|
| 1430 | + } |
|
| 1431 | + }, |
|
| 1432 | + "node_modules/postcss": { |
|
| 1433 | + "version": "8.5.31", |
|
| 1434 | + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", |
|
| 1435 | + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", |
|
| 1436 | + "dev": true, |
|
| 1437 | + "funding": [ |
|
| 1438 | + { |
|
| 1439 | + "type": "opencollective", |
|
| 1440 | + "url": "https://opencollective.com/postcss/" |
|
| 1441 | + }, |
|
| 1442 | + { |
|
| 1443 | + "type": "tidelift", |
|
| 1444 | + "url": "https://tidelift.com/funding/github/npm/postcss" |
|
| 1445 | + }, |
|
| 1446 | + { |
|
| 1447 | + "type": "github", |
|
| 1448 | + "url": "https://github.com/sponsors/ai" |
|
| 1449 | + } |
|
| 1450 | + ], |
|
| 1451 | + "license": "MIT", |
|
| 1452 | + "dependencies": { |
|
| 1453 | + "nanoid": "^3.3.20", |
|
| 1454 | + "picocolors": "^1.1.1", |
|
| 1455 | + "source-map-js": "^1.2.2" |
|
| 1456 | + }, |
|
| 1457 | + "engines": { |
|
| 1458 | + "node": "^10 || ^12 || >=14" |
|
| 1459 | + } |
|
| 1460 | + }, |
|
| 1461 | + "node_modules/react": { |
|
| 1462 | + "version": "19.3.1", |
|
| 1463 | + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", |
|
| 1464 | + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", |
|
| 1465 | + "license": "MIT", |
|
| 1466 | + "engines": { |
|
| 1467 | + "node": ">=0.10.0" |
|
| 1468 | + } |
|
| 1469 | + }, |
|
| 1470 | + "node_modules/react-dom": { |
|
| 1471 | + "version": "19.3.1", |
|
| 1472 | + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", |
|
| 1473 | + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", |
|
| 1474 | + "license": "MIT", |
|
| 1475 | + "dependencies": { |
|
| 1476 | + "scheduler": "^0.28.0" |
|
| 1477 | + }, |
|
| 1478 | + "peerDependencies": { |
|
| 1479 | + "react": "^19.3.1" |
|
| 1480 | + } |
|
| 1481 | + }, |
|
| 1482 | + "node_modules/react-router": { |
|
| 1483 | + "version": "7.18.4", |
|
| 1484 | + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", |
|
| 1485 | + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", |
|
| 1486 | + "license": "MIT", |
|
| 1487 | + "dependencies": { |
|
| 1488 | + "cookie": "^1.0.1", |
|
| 1489 | + "set-cookie-parser": "^2.6.0" |
|
| 1490 | + }, |
|
| 1491 | + "engines": { |
|
| 1492 | + "node": ">=20.0.0" |
|
| 1493 | + }, |
|
| 1494 | + "peerDependencies": { |
|
| 1495 | + "react": ">=18", |
|
| 1496 | + "react-dom": ">=18" |
|
| 1497 | + }, |
|
| 1498 | + "peerDependenciesMeta": { |
|
| 1499 | + "react-dom": { |
|
| 1500 | + "optional": true |
|
| 1501 | + } |
|
| 1502 | + } |
|
| 1503 | + }, |
|
| 1504 | + "node_modules/react-router-dom": { |
|
| 1505 | + "version": "7.18.4", |
|
| 1506 | + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", |
|
| 1507 | + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", |
|
| 1508 | + "license": "MIT", |
|
| 1509 | + "dependencies": { |
|
| 1510 | + "react-router": "7.18.4" |
|
| 1511 | + }, |
|
| 1512 | + "engines": { |
|
| 1513 | + "node": ">=20.0.0" |
|
| 1514 | + }, |
|
| 1515 | + "peerDependencies": { |
|
| 1516 | + "react": ">=18", |
|
| 1517 | + "react-dom": ">=18" |
|
| 1518 | + } |
|
| 1519 | + }, |
|
| 1520 | + "node_modules/rolldown": { |
|
| 1521 | + "version": "1.2.14", |
|
| 1522 | + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", |
|
| 1523 | + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", |
|
| 1524 | + "dev": true, |
|
| 1525 | + "license": "MIT", |
|
| 1526 | + "dependencies": { |
|
| 1527 | + "@oxc-project/types": "=0.154.0", |
|
| 1528 | + "@rolldown/pluginutils": "^1.0.0" |
|
| 1529 | + }, |
|
| 1530 | + "bin": { |
|
| 1531 | + "rolldown": "bin/cli.mjs" |
|
| 1532 | + }, |
|
| 1533 | + "engines": { |
|
| 1534 | + "node": "^20.19.0 || >=22.12.0" |
|
| 1535 | + }, |
|
| 1536 | + "optionalDependencies": { |
|
| 1537 | + "@rolldown/binding-android-arm-eabi": "1.2.14", |
|
| 1538 | + "@rolldown/binding-android-arm64": "1.2.14", |
|
| 1539 | + "@rolldown/binding-darwin-arm64": "1.2.14", |
|
| 1540 | + "@rolldown/binding-darwin-x64": "1.2.14", |
|
| 1541 | + "@rolldown/binding-freebsd-x64": "1.2.14", |
|
| 1542 | + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", |
|
| 1543 | + "@rolldown/binding-linux-arm64-gnu": "1.2.14", |
|
| 1544 | + "@rolldown/binding-linux-arm64-musl": "1.2.14", |
|
| 1545 | + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", |
|
| 1546 | + "@rolldown/binding-linux-s390x-gnu": "1.2.14", |
|
| 1547 | + "@rolldown/binding-linux-x64-gnu": "1.2.14", |
|
| 1548 | + "@rolldown/binding-linux-x64-musl": "1.2.14", |
|
| 1549 | + "@rolldown/binding-openharmony-arm64": "1.2.14", |
|
| 1550 | + "@rolldown/binding-win32-arm64-msvc": "1.2.14", |
|
| 1551 | + "@rolldown/binding-win32-x64-msvc": "1.2.14" |
|
| 1552 | + } |
|
| 1553 | + }, |
|
| 1554 | + "node_modules/scheduler": { |
|
| 1555 | + "version": "0.28.0", |
|
| 1556 | + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", |
|
| 1557 | + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", |
|
| 1558 | + "license": "MIT" |
|
| 1559 | + }, |
|
| 1560 | + "node_modules/set-cookie-parser": { |
|
| 1561 | + "version": "2.7.2", |
|
| 1562 | + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", |
|
| 1563 | + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", |
|
| 1564 | + "license": "MIT" |
|
| 1565 | + }, |
|
| 1566 | + "node_modules/source-map-js": { |
|
| 1567 | + "version": "1.2.2", |
|
| 1568 | + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", |
|
| 1569 | + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", |
|
| 1570 | + "dev": true, |
|
| 1571 | + "license": "BSD-3-Clause", |
|
| 1572 | + "engines": { |
|
| 1573 | + "node": ">=0.10.0" |
|
| 1574 | + } |
|
| 1575 | + }, |
|
| 1576 | + "node_modules/tinyglobby": { |
|
| 1577 | + "version": "0.2.17", |
|
| 1578 | + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", |
|
| 1579 | + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", |
|
| 1580 | + "dev": true, |
|
| 1581 | + "license": "MIT", |
|
| 1582 | + "dependencies": { |
|
| 1583 | + "fdir": "^6.5.0", |
|
| 1584 | + "picomatch": "^4.0.4" |
|
| 1585 | + }, |
|
| 1586 | + "engines": { |
|
| 1587 | + "node": ">=12.0.0" |
|
| 1588 | + }, |
|
| 1589 | + "funding": { |
|
| 1590 | + "url": "https://github.com/sponsors/SuperchupuDev" |
|
| 1591 | + } |
|
| 1592 | + }, |
|
| 1593 | + "node_modules/typescript": { |
|
| 1594 | + "version": "7.0.2", |
|
| 1595 | + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", |
|
| 1596 | + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", |
|
| 1597 | + "dev": true, |
|
| 1598 | + "license": "Apache-2.0", |
|
| 1599 | + "bin": { |
|
| 1600 | + "tsc": "bin/tsc" |
|
| 1601 | + }, |
|
| 1602 | + "engines": { |
|
| 1603 | + "node": ">=16.20.0" |
|
| 1604 | + }, |
|
| 1605 | + "optionalDependencies": { |
|
| 1606 | + "@typescript/typescript-aix-ppc64": "7.0.2", |
|
| 1607 | + "@typescript/typescript-darwin-arm64": "7.0.2", |
|
| 1608 | + "@typescript/typescript-darwin-x64": "7.0.2", |
|
| 1609 | + "@typescript/typescript-freebsd-arm64": "7.0.2", |
|
| 1610 | + "@typescript/typescript-freebsd-x64": "7.0.2", |
|
| 1611 | + "@typescript/typescript-linux-arm": "7.0.2", |
|
| 1612 | + "@typescript/typescript-linux-arm64": "7.0.2", |
|
| 1613 | + "@typescript/typescript-linux-loong64": "7.0.2", |
|
| 1614 | + "@typescript/typescript-linux-mips64el": "7.0.2", |
|
| 1615 | + "@typescript/typescript-linux-ppc64": "7.0.2", |
|
| 1616 | + "@typescript/typescript-linux-riscv64": "7.0.2", |
|
| 1617 | + "@typescript/typescript-linux-s390x": "7.0.2", |
|
| 1618 | + "@typescript/typescript-linux-x64": "7.0.2", |
|
| 1619 | + "@typescript/typescript-netbsd-arm64": "7.0.2", |
|
| 1620 | + "@typescript/typescript-netbsd-x64": "7.0.2", |
|
| 1621 | + "@typescript/typescript-openbsd-arm64": "7.0.2", |
|
| 1622 | + "@typescript/typescript-openbsd-x64": "7.0.2", |
|
| 1623 | + "@typescript/typescript-sunos-x64": "7.0.2", |
|
| 1624 | + "@typescript/typescript-win32-arm64": "7.0.2", |
|
| 1625 | + "@typescript/typescript-win32-x64": "7.0.2" |
|
| 1626 | + } |
|
| 1627 | + }, |
|
| 1628 | + "node_modules/undici-types": { |
|
| 1629 | + "version": "7.24.6", |
|
| 1630 | + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", |
|
| 1631 | + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", |
|
| 1632 | + "dev": true, |
|
| 1633 | + "license": "MIT" |
|
| 1634 | + }, |
|
| 1635 | + "node_modules/vite": { |
|
| 1636 | + "version": "8.3.4", |
|
| 1637 | + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", |
|
| 1638 | + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", |
|
| 1639 | + "dev": true, |
|
| 1640 | + "license": "MIT", |
|
| 1641 | + "dependencies": { |
|
| 1642 | + "lightningcss": "^1.33.0", |
|
| 1643 | + "picomatch": "^4.0.7", |
|
| 1644 | + "postcss": "^8.5.29", |
|
| 1645 | + "rolldown": "~1.2.12", |
|
| 1646 | + "tinyglobby": "^0.2.17" |
|
| 1647 | + }, |
|
| 1648 | + "bin": { |
|
| 1649 | + "vite": "bin/vite.js" |
|
| 1650 | + }, |
|
| 1651 | + "engines": { |
|
| 1652 | + "node": "^20.19.0 || >=22.12.0" |
|
| 1653 | + }, |
|
| 1654 | + "funding": { |
|
| 1655 | + "url": "https://github.com/vitejs/vite?sponsor=1" |
|
| 1656 | + }, |
|
| 1657 | + "optionalDependencies": { |
|
| 1658 | + "fsevents": "~2.3.3" |
|
| 1659 | + }, |
|
| 1660 | + "peerDependencies": { |
|
| 1661 | + "@types/node": "^20.19.0 || >=22.12.0", |
|
| 1662 | + "@vitejs/devtools": "^0.7.1", |
|
| 1663 | + "esbuild": "^0.27.0 || ^0.28.0", |
|
| 1664 | + "jiti": ">=1.21.0", |
|
| 1665 | + "less": "^4.0.0", |
|
| 1666 | + "sass": "^1.70.0", |
|
| 1667 | + "sass-embedded": "^1.70.0", |
|
| 1668 | + "stylus": ">=0.54.8", |
|
| 1669 | + "sugarss": "^5.0.0", |
|
| 1670 | + "terser": "^5.16.0", |
|
| 1671 | + "tsx": "^4.8.1", |
|
| 1672 | + "yaml": "^2.4.2" |
|
| 1673 | + }, |
|
| 1674 | + "peerDependenciesMeta": { |
|
| 1675 | + "@types/node": { |
|
| 1676 | + "optional": true |
|
| 1677 | + }, |
|
| 1678 | + "@vitejs/devtools": { |
|
| 1679 | + "optional": true |
|
| 1680 | + }, |
|
| 1681 | + "esbuild": { |
|
| 1682 | + "optional": true |
|
| 1683 | + }, |
|
| 1684 | + "jiti": { |
|
| 1685 | + "optional": true |
|
| 1686 | + }, |
|
| 1687 | + "less": { |
|
| 1688 | + "optional": true |
|
| 1689 | + }, |
|
| 1690 | + "sass": { |
|
| 1691 | + "optional": true |
|
| 1692 | + }, |
|
| 1693 | + "sass-embedded": { |
|
| 1694 | + "optional": true |
|
| 1695 | + }, |
|
| 1696 | + "stylus": { |
|
| 1697 | + "optional": true |
|
| 1698 | + }, |
|
| 1699 | + "sugarss": { |
|
| 1700 | + "optional": true |
|
| 1701 | + }, |
|
| 1702 | + "terser": { |
|
| 1703 | + "optional": true |
|
| 1704 | + }, |
|
| 1705 | + "tsx": { |
|
| 1706 | + "optional": true |
|
| 1707 | + }, |
|
| 1708 | + "yaml": { |
|
| 1709 | + "optional": true |
|
| 1710 | + } |
|
| 1711 | + } |
|
| 1712 | + } |
|
| 1713 | + } |
|
| 1714 | +} |
projects/hot-tub/src/web/frontend/package.json
| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | +{ |
|
| 2 | + "name": "frontend", |
|
| 3 | + "private": true, |
|
| 4 | + "version": "0.0.0", |
|
| 5 | + "type": "module", |
|
| 6 | + "scripts": { |
|
| 7 | + "dev": "vite", |
|
| 8 | + "build": "tsc -b && vite build", |
|
| 9 | + "lint": "oxlint", |
|
| 10 | + "preview": "vite preview" |
|
| 11 | + }, |
|
| 12 | + "dependencies": { |
|
| 13 | + "react": "^19.3.1", |
|
| 14 | + "react-dom": "^19.3.1", |
|
| 15 | + "react-router-dom": "^7.18.4" |
|
| 16 | + }, |
|
| 17 | + "devDependencies": { |
|
| 18 | + "@types/node": "^24.19.2", |
|
| 19 | + "@types/react": "^19.3.0", |
|
| 20 | + "@types/react-dom": "^19.3.0", |
|
| 21 | + "@vitejs/plugin-react": "^6.1.2", |
|
| 22 | + "oxlint": "^1.88.0", |
|
| 23 | + "typescript": "~7.0.2", |
|
| 24 | + "vite": "^8.3.4" |
|
| 25 | + } |
|
| 26 | +} |
projects/hot-tub/src/web/frontend/public/favicon.svg
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg> |
|
| ... | ... | \ No newline at end of file |
projects/hot-tub/src/web/frontend/public/icons.svg
| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg"> |
|
| 2 | + <symbol id="bluesky-icon" viewBox="0 0 16 17"> |
|
| 3 | + <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g> |
|
| 4 | + <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs> |
|
| 5 | + </symbol> |
|
| 6 | + <symbol id="discord-icon" viewBox="0 0 20 19"> |
|
| 7 | + <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/> |
|
| 8 | + </symbol> |
|
| 9 | + <symbol id="documentation-icon" viewBox="0 0 21 20"> |
|
| 10 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/> |
|
| 11 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/> |
|
| 12 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/> |
|
| 13 | + </symbol> |
|
| 14 | + <symbol id="github-icon" viewBox="0 0 19 19"> |
|
| 15 | + <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/> |
|
| 16 | + </symbol> |
|
| 17 | + <symbol id="social-icon" viewBox="0 0 20 20"> |
|
| 18 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/> |
|
| 19 | + <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/> |
|
| 20 | + </symbol> |
|
| 21 | + <symbol id="x-icon" viewBox="0 0 19 19"> |
|
| 22 | + <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/> |
|
| 23 | + </symbol> |
|
| 24 | +</svg> |
projects/hot-tub/src/web/frontend/src/App.css
| ... | ... | @@ -0,0 +1,184 @@ |
| 1 | +.counter { |
|
| 2 | + font-size: 16px; |
|
| 3 | + padding: 5px 10px; |
|
| 4 | + border-radius: 5px; |
|
| 5 | + color: var(--accent); |
|
| 6 | + background: var(--accent-bg); |
|
| 7 | + border: 2px solid transparent; |
|
| 8 | + transition: border-color 0.3s; |
|
| 9 | + margin-bottom: 24px; |
|
| 10 | + |
|
| 11 | + &:hover { |
|
| 12 | + border-color: var(--accent-border); |
|
| 13 | + } |
|
| 14 | + &:focus-visible { |
|
| 15 | + outline: 2px solid var(--accent); |
|
| 16 | + outline-offset: 2px; |
|
| 17 | + } |
|
| 18 | +} |
|
| 19 | + |
|
| 20 | +.hero { |
|
| 21 | + position: relative; |
|
| 22 | + |
|
| 23 | + .base, |
|
| 24 | + .framework, |
|
| 25 | + .vite { |
|
| 26 | + inset-inline: 0; |
|
| 27 | + margin: 0 auto; |
|
| 28 | + } |
|
| 29 | + |
|
| 30 | + .base { |
|
| 31 | + width: 170px; |
|
| 32 | + position: relative; |
|
| 33 | + z-index: 0; |
|
| 34 | + } |
|
| 35 | + |
|
| 36 | + .framework, |
|
| 37 | + .vite { |
|
| 38 | + position: absolute; |
|
| 39 | + } |
|
| 40 | + |
|
| 41 | + .framework { |
|
| 42 | + z-index: 1; |
|
| 43 | + top: 34px; |
|
| 44 | + height: 28px; |
|
| 45 | + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) |
|
| 46 | + scale(1.4); |
|
| 47 | + } |
|
| 48 | + |
|
| 49 | + .vite { |
|
| 50 | + z-index: 0; |
|
| 51 | + top: 107px; |
|
| 52 | + height: 26px; |
|
| 53 | + width: auto; |
|
| 54 | + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) |
|
| 55 | + scale(0.8); |
|
| 56 | + } |
|
| 57 | +} |
|
| 58 | + |
|
| 59 | +#center { |
|
| 60 | + display: flex; |
|
| 61 | + flex-direction: column; |
|
| 62 | + gap: 25px; |
|
| 63 | + place-content: center; |
|
| 64 | + place-items: center; |
|
| 65 | + flex-grow: 1; |
|
| 66 | + |
|
| 67 | + @media (max-width: 1024px) { |
|
| 68 | + padding: 32px 20px 24px; |
|
| 69 | + gap: 18px; |
|
| 70 | + } |
|
| 71 | +} |
|
| 72 | + |
|
| 73 | +#next-steps { |
|
| 74 | + display: flex; |
|
| 75 | + border-top: 1px solid var(--border); |
|
| 76 | + text-align: left; |
|
| 77 | + |
|
| 78 | + & > div { |
|
| 79 | + flex: 1 1 0; |
|
| 80 | + padding: 32px; |
|
| 81 | + @media (max-width: 1024px) { |
|
| 82 | + padding: 24px 20px; |
|
| 83 | + } |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + .icon { |
|
| 87 | + margin-bottom: 16px; |
|
| 88 | + width: 22px; |
|
| 89 | + height: 22px; |
|
| 90 | + } |
|
| 91 | + |
|
| 92 | + @media (max-width: 1024px) { |
|
| 93 | + flex-direction: column; |
|
| 94 | + text-align: center; |
|
| 95 | + } |
|
| 96 | +} |
|
| 97 | + |
|
| 98 | +#docs { |
|
| 99 | + border-right: 1px solid var(--border); |
|
| 100 | + |
|
| 101 | + @media (max-width: 1024px) { |
|
| 102 | + border-right: none; |
|
| 103 | + border-bottom: 1px solid var(--border); |
|
| 104 | + } |
|
| 105 | +} |
|
| 106 | + |
|
| 107 | +#next-steps ul { |
|
| 108 | + list-style: none; |
|
| 109 | + padding: 0; |
|
| 110 | + display: flex; |
|
| 111 | + gap: 8px; |
|
| 112 | + margin: 32px 0 0; |
|
| 113 | + |
|
| 114 | + .logo { |
|
| 115 | + height: 18px; |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + a { |
|
| 119 | + color: var(--text-h); |
|
| 120 | + font-size: 16px; |
|
| 121 | + border-radius: 6px; |
|
| 122 | + background: var(--social-bg); |
|
| 123 | + display: flex; |
|
| 124 | + padding: 6px 12px; |
|
| 125 | + align-items: center; |
|
| 126 | + gap: 8px; |
|
| 127 | + text-decoration: none; |
|
| 128 | + transition: box-shadow 0.3s; |
|
| 129 | + |
|
| 130 | + &:hover { |
|
| 131 | + box-shadow: var(--shadow); |
|
| 132 | + } |
|
| 133 | + .button-icon { |
|
| 134 | + height: 18px; |
|
| 135 | + width: 18px; |
|
| 136 | + } |
|
| 137 | + } |
|
| 138 | + |
|
| 139 | + @media (max-width: 1024px) { |
|
| 140 | + margin-top: 20px; |
|
| 141 | + flex-wrap: wrap; |
|
| 142 | + justify-content: center; |
|
| 143 | + |
|
| 144 | + li { |
|
| 145 | + flex: 1 1 calc(50% - 8px); |
|
| 146 | + } |
|
| 147 | + |
|
| 148 | + a { |
|
| 149 | + width: 100%; |
|
| 150 | + justify-content: center; |
|
| 151 | + box-sizing: border-box; |
|
| 152 | + } |
|
| 153 | + } |
|
| 154 | +} |
|
| 155 | + |
|
| 156 | +#spacer { |
|
| 157 | + height: 88px; |
|
| 158 | + border-top: 1px solid var(--border); |
|
| 159 | + @media (max-width: 1024px) { |
|
| 160 | + height: 48px; |
|
| 161 | + } |
|
| 162 | +} |
|
| 163 | + |
|
| 164 | +.ticks { |
|
| 165 | + position: relative; |
|
| 166 | + width: 100%; |
|
| 167 | + |
|
| 168 | + &::before, |
|
| 169 | + &::after { |
|
| 170 | + content: ''; |
|
| 171 | + position: absolute; |
|
| 172 | + top: -4.5px; |
|
| 173 | + border: 5px solid transparent; |
|
| 174 | + } |
|
| 175 | + |
|
| 176 | + &::before { |
|
| 177 | + left: 0; |
|
| 178 | + border-left-color: var(--border); |
|
| 179 | + } |
|
| 180 | + &::after { |
|
| 181 | + right: 0; |
|
| 182 | + border-right-color: var(--border); |
|
| 183 | + } |
|
| 184 | +} |
projects/hot-tub/src/web/frontend/src/App.tsx
| ... | ... | @@ -0,0 +1,79 @@ |
| 1 | +import { useEffect, useState } from 'react' |
|
| 2 | +import { Navigate, Route, Routes, useLocation } from 'react-router-dom' |
|
| 3 | +import { connectEvents } from './api' |
|
| 4 | +import { Header } from './components/Header' |
|
| 5 | +import { ControlPage } from './components/ControlPage' |
|
| 6 | +import { DiagnosticPage } from './components/DiagnosticPage' |
|
| 7 | +import { SensorSetupPage } from './components/SensorSetupPage' |
|
| 8 | +import { isBackflushing, isPayloadStale } from './helpers' |
|
| 9 | +import type { ConnectionState, ControlState, FirstBundle, TempsState } from './types' |
|
| 10 | + |
|
| 11 | +function Loading() { |
|
| 12 | + return <div className="card loading">Connecting to the hot tub…</div> |
|
| 13 | +} |
|
| 14 | + |
|
| 15 | +export default function App() { |
|
| 16 | + const [connection, setConnection] = useState<ConnectionState>('connecting') |
|
| 17 | + const [control, setControl] = useState<ControlState | null>(null) |
|
| 18 | + const [temps, setTemps] = useState<TempsState | null>(null) |
|
| 19 | + const [backflushRequested, setBackflushRequested] = useState(false) |
|
| 20 | + const [firstBundle, setFirstBundle] = useState<FirstBundle>(null) |
|
| 21 | + const location = useLocation() |
|
| 22 | + |
|
| 23 | + useEffect(() => { |
|
| 24 | + return connectEvents({ |
|
| 25 | + onOpen: () => setConnection('open'), |
|
| 26 | + onError: () => setConnection('reconnecting'), |
|
| 27 | + onControl: (data) => { |
|
| 28 | + const c = data as ControlState |
|
| 29 | + setControl(c) |
|
| 30 | + setBackflushRequested((prev) => prev && !isBackflushing(c)) |
|
| 31 | + }, |
|
| 32 | + onTemps: (data) => setTemps(data as TempsState), |
|
| 33 | + onSetup: () => {}, |
|
| 34 | + }) |
|
| 35 | + }, []) |
|
| 36 | + |
|
| 37 | + const stale = |
|
| 38 | + connection !== 'open' || |
|
| 39 | + isPayloadStale(control?.ts) || |
|
| 40 | + isPayloadStale(temps?.ts) |
|
| 41 | + |
|
| 42 | + const displayConnection: ConnectionState = stale && connection === 'open' ? 'reconnecting' : connection |
|
| 43 | + |
|
| 44 | + const ready = control && temps |
|
| 45 | + |
|
| 46 | + return ( |
|
| 47 | + <div className="app"> |
|
| 48 | + <Header connection={displayConnection} /> |
|
| 49 | + <main className={`page${displayConnection === 'open' ? '' : ' stale'}`}> |
|
| 50 | + {!ready ? ( |
|
| 51 | + <Loading /> |
|
| 52 | + ) : ( |
|
| 53 | + <Routes> |
|
| 54 | + <Route path="/" element={<Navigate to="/control" replace />} /> |
|
| 55 | + <Route path="/control" element={<ControlPage control={control} />} /> |
|
| 56 | + <Route |
|
| 57 | + path="/diagnostic" |
|
| 58 | + element={ |
|
| 59 | + <DiagnosticPage |
|
| 60 | + control={control} |
|
| 61 | + temps={temps} |
|
| 62 | + backflushRequested={backflushRequested} |
|
| 63 | + onBackflushRequested={setBackflushRequested} |
|
| 64 | + /> |
|
| 65 | + } |
|
| 66 | + /> |
|
| 67 | + <Route |
|
| 68 | + path="/diagnostic/sensor-setup/:step" |
|
| 69 | + element={ |
|
| 70 | + <SensorSetupPage temps={temps} firstBundle={firstBundle} onFirstBundle={setFirstBundle} /> |
|
| 71 | + } |
|
| 72 | + /> |
|
| 73 | + <Route path="*" element={<Navigate to={location.pathname.startsWith('/diagnostic') ? '/diagnostic' : '/control'} replace />} /> |
|
| 74 | + </Routes> |
|
| 75 | + )} |
|
| 76 | + </main> |
|
| 77 | + </div> |
|
| 78 | + ) |
|
| 79 | +} |
projects/hot-tub/src/web/frontend/src/api.ts
| ... | ... | @@ -0,0 +1,47 @@ |
| 1 | +export async function sendCommand(op: string, args: Record<string, unknown> = {}): Promise<void> { |
|
| 2 | + const res = await fetch('/api/command', { |
|
| 3 | + method: 'POST', |
|
| 4 | + headers: { 'Content-Type': 'application/json' }, |
|
| 5 | + body: JSON.stringify({ op, args }), |
|
| 6 | + }) |
|
| 7 | + if (!res.ok) { |
|
| 8 | + const body = await res.json().catch(() => ({})) |
|
| 9 | + throw new Error((body as { error?: string }).error || res.statusText) |
|
| 10 | + } |
|
| 11 | +} |
|
| 12 | + |
|
| 13 | +async function setupPost(path: string): Promise<void> { |
|
| 14 | + const res = await fetch(path, { method: 'POST' }) |
|
| 15 | + if (!res.ok) { |
|
| 16 | + const body = await res.json().catch(() => ({})) |
|
| 17 | + throw new Error((body as { error?: string }).error || res.statusText) |
|
| 18 | + } |
|
| 19 | +} |
|
| 20 | + |
|
| 21 | +export const startSensorSetup = () => setupPost('/api/sensor-setup/start') |
|
| 22 | +export const finishSensorSetup = () => setupPost('/api/sensor-setup/finish') |
|
| 23 | +export const cancelSensorSetup = () => setupPost('/api/sensor-setup/cancel') |
|
| 24 | + |
|
| 25 | +export type SseHandlers = { |
|
| 26 | + onOpen: () => void |
|
| 27 | + onError: () => void |
|
| 28 | + onControl: (data: unknown) => void |
|
| 29 | + onTemps: (data: unknown) => void |
|
| 30 | + onSetup: (data: unknown) => void |
|
| 31 | +} |
|
| 32 | + |
|
| 33 | +export function connectEvents(handlers: SseHandlers): () => void { |
|
| 34 | + const es = new EventSource('/api/events') |
|
| 35 | + es.onopen = () => handlers.onOpen() |
|
| 36 | + es.onerror = () => handlers.onError() |
|
| 37 | + es.addEventListener('control.state', (e) => { |
|
| 38 | + handlers.onControl(JSON.parse((e as MessageEvent).data)) |
|
| 39 | + }) |
|
| 40 | + es.addEventListener('temps.reading', (e) => { |
|
| 41 | + handlers.onTemps(JSON.parse((e as MessageEvent).data)) |
|
| 42 | + }) |
|
| 43 | + es.addEventListener('temps.setup', (e) => { |
|
| 44 | + handlers.onSetup(JSON.parse((e as MessageEvent).data)) |
|
| 45 | + }) |
|
| 46 | + return () => es.close() |
|
| 47 | +} |
projects/hot-tub/src/web/frontend/src/assets/hero.png
| ... | ... | Binary files /dev/null and b/projects/hot-tub/src/web/frontend/src/assets/hero.png differ |
projects/hot-tub/src/web/frontend/src/assets/react.svg
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> |
|
| ... | ... | \ No newline at end of file |
projects/hot-tub/src/web/frontend/src/assets/vite.svg
| ... | ... | @@ -0,0 +1 @@ |
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg> |
projects/hot-tub/src/web/frontend/src/components/ControlPage.tsx
| ... | ... | @@ -0,0 +1,108 @@ |
| 1 | +import { sendCommand } from '../api' |
|
| 2 | +import { TARGET_MAX, TARGET_MIN, filterLabel, fmtF, fmtTarget, has, phaseLabel } from '../helpers' |
|
| 3 | +import type { ControlState } from '../types' |
|
| 4 | +import { Pump } from './Pump' |
|
| 5 | +import { TubGraphic } from './TubGraphic' |
|
| 6 | + |
|
| 7 | +function ErrorBanner({ error, onReset }: { error: NonNullable<ControlState['error']>; onReset: () => void }) { |
|
| 8 | + return ( |
|
| 9 | + <div className="banner banner-error" role="alert"> |
|
| 10 | + <span> |
|
| 11 | + <strong>Heater problem.</strong> {error.detail || error.code} |
|
| 12 | + </span> |
|
| 13 | + <button type="button" className="btn btn-small" onClick={onReset}> |
|
| 14 | + Reset (set Sleep) |
|
| 15 | + </button> |
|
| 16 | + </div> |
|
| 17 | + ) |
|
| 18 | +} |
|
| 19 | + |
|
| 20 | +export function ControlPage({ control }: { control: ControlState }) { |
|
| 21 | + const tub = control.tub_avg_f |
|
| 22 | + const steaming = control.heat_pump === 'on' || (has(tub) && tub >= 95) |
|
| 23 | + |
|
| 24 | + return ( |
|
| 25 | + <div className="control-layout"> |
|
| 26 | + <section className="card tub-card"> |
|
| 27 | + <div className="banner-slot"> |
|
| 28 | + {control.error ? ( |
|
| 29 | + <ErrorBanner error={control.error} onReset={() => sendCommand('set_heat_mode', { mode: 'sleep' })} /> |
|
| 30 | + ) : null} |
|
| 31 | + </div> |
|
| 32 | + <TubGraphic tempF={tub} steaming={steaming} /> |
|
| 33 | + <div className="pumps"> |
|
| 34 | + <Pump kind="heat" label="Heat" running={control.heat_pump === 'on'} /> |
|
| 35 | + <Pump kind="filter" label="Filter" running={control.filter_pump === 'on'} /> |
|
| 36 | + </div> |
|
| 37 | + </section> |
|
| 38 | + <section className="card settings-card"> |
|
| 39 | + <div className="target"> |
|
| 40 | + <span className="field-label">Target temperature</span> |
|
| 41 | + <div className="target-row"> |
|
| 42 | + <button |
|
| 43 | + type="button" |
|
| 44 | + className="round-btn" |
|
| 45 | + aria-label="Decrease target" |
|
| 46 | + disabled={control.target_f <= TARGET_MIN} |
|
| 47 | + onClick={() => sendCommand('target_temp_decrement')} |
|
| 48 | + > |
|
| 49 | + − |
|
| 50 | + </button> |
|
| 51 | + <div className="target-value">{fmtTarget(control.target_f)}</div> |
|
| 52 | + <button |
|
| 53 | + type="button" |
|
| 54 | + className="round-btn" |
|
| 55 | + aria-label="Increase target" |
|
| 56 | + disabled={control.target_f >= TARGET_MAX} |
|
| 57 | + onClick={() => sendCommand('target_temp_increment')} |
|
| 58 | + > |
|
| 59 | + + |
|
| 60 | + </button> |
|
| 61 | + </div> |
|
| 62 | + </div> |
|
| 63 | + <div className="modes"> |
|
| 64 | + <label className="field"> |
|
| 65 | + <span className="field-label">Heat mode</span> |
|
| 66 | + <select |
|
| 67 | + value={control.heat_mode === 'backflush' ? 'backflush' : control.heat_mode} |
|
| 68 | + onChange={(e) => sendCommand('set_heat_mode', { mode: e.target.value })} |
|
| 69 | + > |
|
| 70 | + <option value="sleep">Sleep</option> |
|
| 71 | + <option value="maintain">Maintain temperature</option> |
|
| 72 | + {control.heat_mode === 'backflush' ? ( |
|
| 73 | + <option value="backflush" disabled> |
|
| 74 | + Backflush (running) |
|
| 75 | + </option> |
|
| 76 | + ) : null} |
|
| 77 | + </select> |
|
| 78 | + </label> |
|
| 79 | + <label className="field"> |
|
| 80 | + <span className="field-label">Filter mode</span> |
|
| 81 | + <select |
|
| 82 | + value={control.filter_mode} |
|
| 83 | + onChange={(e) => sendCommand('set_filter_mode', { mode: e.target.value })} |
|
| 84 | + > |
|
| 85 | + <option value="auto">Auto (daily schedule)</option> |
|
| 86 | + <option value="on">On</option> |
|
| 87 | + <option value="off">Off</option> |
|
| 88 | + </select> |
|
| 89 | + </label> |
|
| 90 | + </div> |
|
| 91 | + <dl className="summary"> |
|
| 92 | + <div> |
|
| 93 | + <dt>Heater</dt> |
|
| 94 | + <dd>{phaseLabel(control)}</dd> |
|
| 95 | + </div> |
|
| 96 | + <div> |
|
| 97 | + <dt>Hot input</dt> |
|
| 98 | + <dd>{fmtF(control.hot_in_f)}</dd> |
|
| 99 | + </div> |
|
| 100 | + <div> |
|
| 101 | + <dt>Filter</dt> |
|
| 102 | + <dd>{filterLabel(control)}</dd> |
|
| 103 | + </div> |
|
| 104 | + </dl> |
|
| 105 | + </section> |
|
| 106 | + </div> |
|
| 107 | + ) |
|
| 108 | +} |
projects/hot-tub/src/web/frontend/src/components/DiagnosticPage.tsx
| ... | ... | @@ -0,0 +1,107 @@ |
| 1 | +import { useNavigate } from 'react-router-dom' |
|
| 2 | +import { sendCommand } from '../api' |
|
| 3 | +import { PROBES, fmtF, has, isBackflushing, probeValues } from '../helpers' |
|
| 4 | +import type { ControlState, TempsState } from '../types' |
|
| 5 | +import { StatusLight } from './StatusLight' |
|
| 6 | + |
|
| 7 | +function BackflushAnimation() { |
|
| 8 | + return ( |
|
| 9 | + <div className="backflush-anim" role="status"> |
|
| 10 | + <svg viewBox="0 0 120 120" aria-hidden="true"> |
|
| 11 | + <g className="backflush-arrows"> |
|
| 12 | + <path d="M60 10 A50 50 0 0 1 110 60" /> |
|
| 13 | + <path d="M60 110 A50 50 0 0 1 10 60" /> |
|
| 14 | + <path className="arrowhead" d="M102 54 L118 54 L110 66 Z" /> |
|
| 15 | + <path className="arrowhead" d="M2 66 L18 66 L10 54 Z" /> |
|
| 16 | + </g> |
|
| 17 | + <path |
|
| 18 | + className="droplet" |
|
| 19 | + d="M60 28 C60 28 36 56 36 72 A24 24 0 0 0 84 72 C84 56 60 28 60 28 Z" |
|
| 20 | + /> |
|
| 21 | + <circle className="droplet-eye" cx="52" cy="70" r="3" /> |
|
| 22 | + <circle className="droplet-eye" cx="68" cy="70" r="3" /> |
|
| 23 | + <path className="droplet-smile" d="M52 80 q8 7 16 0" /> |
|
| 24 | + <circle className="bubble b1" cx="44" cy="102" r="4" /> |
|
| 25 | + <circle className="bubble b2" cx="60" cy="104" r="3" /> |
|
| 26 | + <circle className="bubble b3" cx="76" cy="102" r="4" /> |
|
| 27 | + </svg> |
|
| 28 | + <p>Backflush in progress…</p> |
|
| 29 | + </div> |
|
| 30 | + ) |
|
| 31 | +} |
|
| 32 | + |
|
| 33 | +function ProbeRow({ |
|
| 34 | + label, |
|
| 35 | + value, |
|
| 36 | + missingStatus, |
|
| 37 | +}: { |
|
| 38 | + label: string |
|
| 39 | + value: number | null | undefined |
|
| 40 | + missingStatus: string |
|
| 41 | +}) { |
|
| 42 | + return ( |
|
| 43 | + <li className="probe-row"> |
|
| 44 | + <StatusLight status={has(value) ? 'on' : missingStatus} label={label} /> |
|
| 45 | + <span className="probe-value">{fmtF(value)}</span> |
|
| 46 | + </li> |
|
| 47 | + ) |
|
| 48 | +} |
|
| 49 | + |
|
| 50 | +export function DiagnosticPage({ |
|
| 51 | + control, |
|
| 52 | + temps, |
|
| 53 | + backflushRequested, |
|
| 54 | + onBackflushRequested, |
|
| 55 | +}: { |
|
| 56 | + control: ControlState |
|
| 57 | + temps: TempsState |
|
| 58 | + backflushRequested: boolean |
|
| 59 | + onBackflushRequested: (v: boolean) => void |
|
| 60 | +}) { |
|
| 61 | + const navigate = useNavigate() |
|
| 62 | + const running = isBackflushing(control) |
|
| 63 | + const values = probeValues(temps) |
|
| 64 | + |
|
| 65 | + const startBackflush = () => { |
|
| 66 | + onBackflushRequested(true) |
|
| 67 | + sendCommand('set_heat_mode', { mode: 'backflush' }).catch(() => onBackflushRequested(false)) |
|
| 68 | + } |
|
| 69 | + |
|
| 70 | + return ( |
|
| 71 | + <div className="diagnostic-layout"> |
|
| 72 | + <section className="card"> |
|
| 73 | + <button |
|
| 74 | + type="button" |
|
| 75 | + className="big-button big-button-backflush" |
|
| 76 | + disabled={running || backflushRequested} |
|
| 77 | + onClick={startBackflush} |
|
| 78 | + > |
|
| 79 | + {running ? 'Backflushing…' : 'Backflush'} |
|
| 80 | + </button> |
|
| 81 | + <div className="backflush-slot"> |
|
| 82 | + {running ? ( |
|
| 83 | + <BackflushAnimation /> |
|
| 84 | + ) : ( |
|
| 85 | + <p className="hint">Runs the heat pump briefly to purge the heater line, then returns to Sleep.</p> |
|
| 86 | + )} |
|
| 87 | + </div> |
|
| 88 | + </section> |
|
| 89 | + <section className="card"> |
|
| 90 | + <button type="button" className="big-button big-button-setup" onClick={() => navigate('/diagnostic/sensor-setup/1')}> |
|
| 91 | + Sensor Setup |
|
| 92 | + </button> |
|
| 93 | + <div className="readings"> |
|
| 94 | + <div className="readings-header"> |
|
| 95 | + <span className="field-label">Sensors</span> |
|
| 96 | + <span className="setup-state">{temps.setup_state.replace(/_/g, ' ')}</span> |
|
| 97 | + </div> |
|
| 98 | + <ul className="probe-list"> |
|
| 99 | + {PROBES.map((p) => ( |
|
| 100 | + <ProbeRow key={p.key} label={p.label} value={values[p.key]} missingStatus="error" /> |
|
| 101 | + ))} |
|
| 102 | + </ul> |
|
| 103 | + </div> |
|
| 104 | + </section> |
|
| 105 | + </div> |
|
| 106 | + ) |
|
| 107 | +} |
projects/hot-tub/src/web/frontend/src/components/Header.tsx
| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | +import { NavLink } from 'react-router-dom' |
|
| 2 | +import type { ConnectionState } from '../types' |
|
| 3 | +import { StatusLight } from './StatusLight' |
|
| 4 | + |
|
| 5 | +function ConnectionBadge({ connection }: { connection: ConnectionState }) { |
|
| 6 | + const map: Record<ConnectionState, [string, string]> = { |
|
| 7 | + open: ['on', 'Live'], |
|
| 8 | + connecting: ['waiting', 'Connecting…'], |
|
| 9 | + reconnecting: ['error', 'Reconnecting…'], |
|
| 10 | + } |
|
| 11 | + const [status, label] = map[connection] |
|
| 12 | + return ( |
|
| 13 | + <div className="connection"> |
|
| 14 | + <StatusLight status={status} label={label} /> |
|
| 15 | + </div> |
|
| 16 | + ) |
|
| 17 | +} |
|
| 18 | + |
|
| 19 | +export function Header({ connection }: { connection: ConnectionState }) { |
|
| 20 | + return ( |
|
| 21 | + <header className="app-header"> |
|
| 22 | + <div className="brand"> |
|
| 23 | + <span className="brand-icon" aria-hidden="true"> |
|
| 24 | + ♨ |
|
| 25 | + </span>{' '} |
|
| 26 | + Hot Tub |
|
| 27 | + </div> |
|
| 28 | + <nav className="nav"> |
|
| 29 | + <NavLink className={({ isActive }) => `nav-tab${isActive ? ' active' : ''}`} to="/control"> |
|
| 30 | + Main Control |
|
| 31 | + </NavLink> |
|
| 32 | + <NavLink |
|
| 33 | + className={({ isActive }) => `nav-tab${isActive ? ' active' : ''}`} |
|
| 34 | + to="/diagnostic" |
|
| 35 | + end={false} |
|
| 36 | + > |
|
| 37 | + Diagnostic |
|
| 38 | + </NavLink> |
|
| 39 | + </nav> |
|
| 40 | + <ConnectionBadge connection={connection} /> |
|
| 41 | + </header> |
|
| 42 | + ) |
|
| 43 | +} |
projects/hot-tub/src/web/frontend/src/components/Pump.tsx
| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | +import { StatusLight } from './StatusLight' |
|
| 2 | + |
|
| 3 | +export function Pump({ kind, label, running }: { kind: string; label: string; running: boolean }) { |
|
| 4 | + const blades = [0, 90, 180, 270].map((a) => ( |
|
| 5 | + <ellipse key={a} cx="60" cy="36" rx="11" ry="21" transform={`rotate(${a} 60 60)`} /> |
|
| 6 | + )) |
|
| 7 | + return ( |
|
| 8 | + <div className={`pump pump-${kind}${running ? ' running' : ''}`}> |
|
| 9 | + <svg className="pump-svg" viewBox="0 0 120 120" aria-hidden="true"> |
|
| 10 | + <rect className="pump-frame" x="4" y="4" width="112" height="112" rx="18" /> |
|
| 11 | + <circle className="pump-housing" cx="60" cy="60" r="48" /> |
|
| 12 | + <g className="fan-blades">{blades}</g> |
|
| 13 | + <circle className="pump-hub" cx="60" cy="60" r="8" /> |
|
| 14 | + </svg> |
|
| 15 | + <div className="pump-label">{label}</div> |
|
| 16 | + <StatusLight status={running ? 'on' : 'off'} label={running ? 'Running' : 'Off'} /> |
|
| 17 | + </div> |
|
| 18 | + ) |
|
| 19 | +} |
projects/hot-tub/src/web/frontend/src/components/SensorSetupPage.tsx
| ... | ... | @@ -0,0 +1,202 @@ |
| 1 | +import { useNavigate, useParams } from 'react-router-dom' |
|
| 2 | +import { cancelSensorSetup, finishSensorSetup, startSensorSetup } from '../api' |
|
| 3 | +import { PROBES, WATER_KEYS, fmtF, has, probeValues } from '../helpers' |
|
| 4 | +import type { FirstBundle, TempsState } from '../types' |
|
| 5 | +import { StatusLight } from './StatusLight' |
|
| 6 | + |
|
| 7 | +function Stepper({ step }: { step: number }) { |
|
| 8 | + const labels = ['Disconnect', 'First set', 'Second set'] |
|
| 9 | + return ( |
|
| 10 | + <ol className="stepper"> |
|
| 11 | + {labels.map((l, i) => ( |
|
| 12 | + <li key={l} className={i + 1 < step ? 'done' : i + 1 === step ? 'current' : ''}> |
|
| 13 | + {l} |
|
| 14 | + </li> |
|
| 15 | + ))} |
|
| 16 | + </ol> |
|
| 17 | + ) |
|
| 18 | +} |
|
| 19 | + |
|
| 20 | +function ProbeRow({ |
|
| 21 | + label, |
|
| 22 | + value, |
|
| 23 | + missingStatus, |
|
| 24 | +}: { |
|
| 25 | + label: string |
|
| 26 | + value: number | null | undefined |
|
| 27 | + missingStatus: string |
|
| 28 | +}) { |
|
| 29 | + return ( |
|
| 30 | + <li className="probe-row"> |
|
| 31 | + <StatusLight status={has(value) ? 'on' : missingStatus} label={label} /> |
|
| 32 | + <span className="probe-value">{fmtF(value)}</span> |
|
| 33 | + </li> |
|
| 34 | + ) |
|
| 35 | +} |
|
| 36 | + |
|
| 37 | +export function SensorSetupPage({ |
|
| 38 | + temps, |
|
| 39 | + firstBundle, |
|
| 40 | + onFirstBundle, |
|
| 41 | +}: { |
|
| 42 | + temps: TempsState |
|
| 43 | + firstBundle: FirstBundle |
|
| 44 | + onFirstBundle: (v: FirstBundle) => void |
|
| 45 | +}) { |
|
| 46 | + const navigate = useNavigate() |
|
| 47 | + const { step: stepParam } = useParams() |
|
| 48 | + const step = Math.min(3, Math.max(1, Number(stepParam) || 1)) |
|
| 49 | + const values = probeValues(temps) |
|
| 50 | + |
|
| 51 | + const goDiagnostic = () => navigate('/diagnostic') |
|
| 52 | + |
|
| 53 | + return ( |
|
| 54 | + <section className="card setup-card"> |
|
| 55 | + <h1>Sensor setup</h1> |
|
| 56 | + <Stepper step={step} /> |
|
| 57 | + {step === 1 ? ( |
|
| 58 | + <SetupStep1 values={values} onCancel={() => cancelSensorSetup().finally(goDiagnostic)} onContinue={() => startSensorSetup().then(() => navigate('/diagnostic/sensor-setup/2'))} /> |
|
| 59 | + ) : null} |
|
| 60 | + {step === 2 ? ( |
|
| 61 | + <SetupStep2 |
|
| 62 | + setupState={temps.setup_state} |
|
| 63 | + values={values} |
|
| 64 | + onCancel={() => cancelSensorSetup().finally(goDiagnostic)} |
|
| 65 | + onContinue={() => { |
|
| 66 | + const hot = has(values.hot_in) |
|
| 67 | + const water = WATER_KEYS.some((k) => has(values[k])) |
|
| 68 | + onFirstBundle(hot && water ? null : hot ? 'hot' : 'water') |
|
| 69 | + navigate('/diagnostic/sensor-setup/3') |
|
| 70 | + }} |
|
| 71 | + /> |
|
| 72 | + ) : null} |
|
| 73 | + {step === 3 ? ( |
|
| 74 | + <SetupStep3 |
|
| 75 | + setupState={temps.setup_state} |
|
| 76 | + values={values} |
|
| 77 | + firstBundle={firstBundle} |
|
| 78 | + onCancel={() => cancelSensorSetup().finally(goDiagnostic)} |
|
| 79 | + onFinish={() => finishSensorSetup().then(goDiagnostic)} |
|
| 80 | + /> |
|
| 81 | + ) : null} |
|
| 82 | + </section> |
|
| 83 | + ) |
|
| 84 | +} |
|
| 85 | + |
|
| 86 | +function SetupStep1({ |
|
| 87 | + values, |
|
| 88 | + onContinue, |
|
| 89 | + onCancel, |
|
| 90 | +}: { |
|
| 91 | + values: Record<string, number | null | undefined> |
|
| 92 | + onContinue: () => void |
|
| 93 | + onCancel: () => void |
|
| 94 | +}) { |
|
| 95 | + const connected = PROBES.filter((p) => has(values[p.key])).length |
|
| 96 | + return ( |
|
| 97 | + <div className="setup-step"> |
|
| 98 | + <h2>Disconnect all sensors</h2> |
|
| 99 | + <p>Unplug both sensor bundles from the enclosure: the hot-input probe and the four water probes.</p> |
|
| 100 | + <div className="setup-status"> |
|
| 101 | + {connected ? ( |
|
| 102 | + <StatusLight status="waiting" label={`${connected} sensor${connected === 1 ? '' : 's'} still connected`} /> |
|
| 103 | + ) : ( |
|
| 104 | + <StatusLight status="on" label="All sensors disconnected" /> |
|
| 105 | + )} |
|
| 106 | + </div> |
|
| 107 | + <div className="actions"> |
|
| 108 | + <button type="button" className="btn btn-secondary" onClick={onCancel}> |
|
| 109 | + Cancel |
|
| 110 | + </button> |
|
| 111 | + <button type="button" className="btn btn-primary" onClick={onContinue}> |
|
| 112 | + Continue |
|
| 113 | + </button> |
|
| 114 | + </div> |
|
| 115 | + </div> |
|
| 116 | + ) |
|
| 117 | +} |
|
| 118 | + |
|
| 119 | +function step2CanContinue(setupState: string, values: Record<string, number | null | undefined>) { |
|
| 120 | + if (setupState === 'need_water') return true |
|
| 121 | + if (setupState === 'need_hot_in' && WATER_KEYS.every((k) => has(values[k]))) return true |
|
| 122 | + return false |
|
| 123 | +} |
|
| 124 | + |
|
| 125 | +function SetupStep2({ |
|
| 126 | + setupState, |
|
| 127 | + values, |
|
| 128 | + onContinue, |
|
| 129 | + onCancel, |
|
| 130 | +}: { |
|
| 131 | + setupState: string |
|
| 132 | + values: Record<string, number | null | undefined> |
|
| 133 | + onContinue: () => void |
|
| 134 | + onCancel: () => void |
|
| 135 | +}) { |
|
| 136 | + const hot = has(values.hot_in) |
|
| 137 | + const water = WATER_KEYS.some((k) => has(values[k])) |
|
| 138 | + let label = 'Waiting for readings…' |
|
| 139 | + if (hot && water) label = 'Both sets detected' |
|
| 140 | + else if (hot) label = 'Hot-input sensor detected' |
|
| 141 | + else if (water) label = 'Water sensors detected' |
|
| 142 | + const enabled = step2CanContinue(setupState, values) |
|
| 143 | + |
|
| 144 | + return ( |
|
| 145 | + <div className="setup-step"> |
|
| 146 | + <h2>Plug in one set of sensors</h2> |
|
| 147 | + <p>Connect either the hot-input bundle or the water bundle. Continue unlocks once readings arrive.</p> |
|
| 148 | + <div className="setup-status"> |
|
| 149 | + <StatusLight status={enabled ? 'on' : 'waiting'} label={label} /> |
|
| 150 | + </div> |
|
| 151 | + <div className="actions"> |
|
| 152 | + <button type="button" className="btn btn-secondary" onClick={onCancel}> |
|
| 153 | + Cancel |
|
| 154 | + </button> |
|
| 155 | + <button type="button" className="btn btn-primary" disabled={!enabled} onClick={onContinue}> |
|
| 156 | + Continue |
|
| 157 | + </button> |
|
| 158 | + </div> |
|
| 159 | + </div> |
|
| 160 | + ) |
|
| 161 | +} |
|
| 162 | + |
|
| 163 | +function SetupStep3({ |
|
| 164 | + setupState, |
|
| 165 | + values, |
|
| 166 | + firstBundle, |
|
| 167 | + onFinish, |
|
| 168 | + onCancel, |
|
| 169 | +}: { |
|
| 170 | + setupState: string |
|
| 171 | + values: Record<string, number | null | undefined> |
|
| 172 | + firstBundle: FirstBundle |
|
| 173 | + onFinish: () => void |
|
| 174 | + onCancel: () => void |
|
| 175 | +}) { |
|
| 176 | + const remaining = |
|
| 177 | + firstBundle === 'hot' |
|
| 178 | + ? 'the water sensor bundle (4 probes)' |
|
| 179 | + : firstBundle === 'water' |
|
| 180 | + ? 'the hot-input sensor' |
|
| 181 | + : 'any remaining sensors' |
|
| 182 | + const all = setupState === 'ready' && PROBES.every((p) => has(values[p.key])) |
|
| 183 | + return ( |
|
| 184 | + <div className="setup-step"> |
|
| 185 | + <h2>Plug in the other set</h2> |
|
| 186 | + <p>Now connect {remaining}. Finish unlocks once every sensor is reporting.</p> |
|
| 187 | + <ul className="probe-list"> |
|
| 188 | + {PROBES.map((p) => ( |
|
| 189 | + <ProbeRow key={p.key} label={p.label} value={values[p.key]} missingStatus="waiting" /> |
|
| 190 | + ))} |
|
| 191 | + </ul> |
|
| 192 | + <div className="actions"> |
|
| 193 | + <button type="button" className="btn btn-secondary" onClick={onCancel}> |
|
| 194 | + Cancel |
|
| 195 | + </button> |
|
| 196 | + <button type="button" className="btn btn-primary" disabled={!all} onClick={onFinish}> |
|
| 197 | + Finish |
|
| 198 | + </button> |
|
| 199 | + </div> |
|
| 200 | + </div> |
|
| 201 | + ) |
|
| 202 | +} |
projects/hot-tub/src/web/frontend/src/components/StatusLight.tsx
| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | +export function StatusLight({ status, label }: { status: string; label: React.ReactNode }) { |
|
| 2 | + return ( |
|
| 3 | + <span className="status-light"> |
|
| 4 | + <span className={`light light-${status}`} aria-hidden="true" /> |
|
| 5 | + <span>{label}</span> |
|
| 6 | + </span> |
|
| 7 | + ) |
|
| 8 | +} |
projects/hot-tub/src/web/frontend/src/components/TubGraphic.tsx
| ... | ... | @@ -0,0 +1,73 @@ |
| 1 | +import { has } from '../helpers' |
|
| 2 | + |
|
| 3 | +const TUB_INSIDE = 'M40 64 L360 64 L338 204 Q335 214 324 214 L76 214 Q65 214 62 204 Z' |
|
| 4 | + |
|
| 5 | +function wavePath(y: number) { |
|
| 6 | + let d = `M-100 ${y}` |
|
| 7 | + for (let i = 0; i < 6; i++) d += ' q25 -7 50 0 q25 7 50 0' |
|
| 8 | + return `${d} L500 240 L-100 240 Z` |
|
| 9 | +} |
|
| 10 | + |
|
| 11 | +function Duck() { |
|
| 12 | + return ( |
|
| 13 | + <g className="duck-bob" aria-hidden="true"> |
|
| 14 | + <ellipse cx="14" cy="18" rx="16" ry="10" fill="#facc15" /> |
|
| 15 | + <path d="M4 16 q8 -6 14 2" fill="none" stroke="#eab308" strokeWidth="2" strokeLinecap="round" /> |
|
| 16 | + <circle cx="25" cy="6" r="8" fill="#facc15" /> |
|
| 17 | + <path d="M32 5 l8 2 l-8 3 z" fill="#f97316" /> |
|
| 18 | + <circle cx="27" cy="4" r="1.6" fill="#1e293b" /> |
|
| 19 | + </g> |
|
| 20 | + ) |
|
| 21 | +} |
|
| 22 | + |
|
| 23 | +export function TubGraphic({ tempF, steaming }: { tempF: number | null; steaming: boolean }) { |
|
| 24 | + const label = has(tempF) ? `${tempF.toFixed(1)}°F` : '--.-°F' |
|
| 25 | + const rivets = [60, 110, 160, 210, 260, 310, 340].map((x) => ( |
|
| 26 | + <circle key={x} className="rivet" cx={x} cy="65" r="2.2" /> |
|
| 27 | + )) |
|
| 28 | + return ( |
|
| 29 | + <svg className="tub" viewBox="0 0 400 240" role="img" aria-label="Tub water temperature"> |
|
| 30 | + <defs> |
|
| 31 | + <clipPath id="tub-inside"> |
|
| 32 | + <path d={TUB_INSIDE} /> |
|
| 33 | + </clipPath> |
|
| 34 | + <linearGradient id="water-grad" x1="0" y1="0" x2="0" y2="1"> |
|
| 35 | + <stop offset="0" stopColor="#38bdf8" /> |
|
| 36 | + <stop offset="1" stopColor="#0369a1" /> |
|
| 37 | + </linearGradient> |
|
| 38 | + <linearGradient id="steel-grad" x1="0" y1="0" x2="1" y2="0"> |
|
| 39 | + <stop offset="0" stopColor="#94a3b8" /> |
|
| 40 | + <stop offset="0.5" stopColor="#f1f5f9" /> |
|
| 41 | + <stop offset="1" stopColor="#94a3b8" /> |
|
| 42 | + </linearGradient> |
|
| 43 | + </defs> |
|
| 44 | + <ellipse className="ground" cx="200" cy="222" rx="170" ry="10" /> |
|
| 45 | + <g className={`steam${steaming ? ' on' : ''}`} aria-hidden="true"> |
|
| 46 | + <path className="steam-puff" d="M140 52 q-8 -10 0 -20 t0 -20" /> |
|
| 47 | + <path className="steam-puff" d="M200 52 q-8 -10 0 -20 t0 -20" /> |
|
| 48 | + <path className="steam-puff" d="M260 52 q-8 -10 0 -20 t0 -20" /> |
|
| 49 | + </g> |
|
| 50 | + <path className="tub-shell" d={TUB_INSIDE} /> |
|
| 51 | + <g clipPath="url(#tub-inside)"> |
|
| 52 | + <g className="wave wave-back"> |
|
| 53 | + <path d={wavePath(90)} fill="#7dd3fc" /> |
|
| 54 | + </g> |
|
| 55 | + <g className="wave wave-front"> |
|
| 56 | + <path d={wavePath(96)} fill="url(#water-grad)" /> |
|
| 57 | + </g> |
|
| 58 | + </g> |
|
| 59 | + <path className="tub-wall" d={TUB_INSIDE} /> |
|
| 60 | + <rect className="tub-rim" x="22" y="58" width="356" height="14" rx="7" fill="url(#steel-grad)" /> |
|
| 61 | + {rivets} |
|
| 62 | + <g transform="translate(286 72)"> |
|
| 63 | + <Duck /> |
|
| 64 | + </g> |
|
| 65 | + <text className="tub-temp" x="200" y="165" textAnchor="middle"> |
|
| 66 | + {label} |
|
| 67 | + </text> |
|
| 68 | + <text className="tub-temp-caption" x="200" y="190" textAnchor="middle"> |
|
| 69 | + water |
|
| 70 | + </text> |
|
| 71 | + </svg> |
|
| 72 | + ) |
|
| 73 | +} |
projects/hot-tub/src/web/frontend/src/helpers.ts
| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | +import type { ControlState } from './types' |
|
| 2 | + |
|
| 3 | +export const TARGET_MIN = 50 |
|
| 4 | +export const TARGET_MAX = 110 |
|
| 5 | + |
|
| 6 | +export const PROBES = [ |
|
| 7 | + { key: 'hot_in', label: 'Hot input' }, |
|
| 8 | + { key: 'tub1', label: 'Tub 1' }, |
|
| 9 | + { key: 'tub2', label: 'Tub 2' }, |
|
| 10 | + { key: 'tub3', label: 'Tub 3' }, |
|
| 11 | + { key: 'tub4', label: 'Tub 4' }, |
|
| 12 | +] as const |
|
| 13 | + |
|
| 14 | +export const WATER_KEYS = ['tub1', 'tub2', 'tub3', 'tub4'] as const |
|
| 15 | + |
|
| 16 | +export const has = (v: unknown): v is number => v !== null && v !== undefined |
|
| 17 | + |
|
| 18 | +export function fmtF(v: number | null | undefined): string { |
|
| 19 | + return has(v) ? `${Number(v).toFixed(1)}°F` : '--' |
|
| 20 | +} |
|
| 21 | + |
|
| 22 | +export function fmtTarget(v: number): string { |
|
| 23 | + return `${Number.isInteger(v) ? v : Number(v).toFixed(1)}°F` |
|
| 24 | +} |
|
| 25 | + |
|
| 26 | +export function fmtTime(iso: string): string { |
|
| 27 | + return new Date(iso).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) |
|
| 28 | +} |
|
| 29 | + |
|
| 30 | +export function isBackflushing(c: ControlState): boolean { |
|
| 31 | + return c.heat_mode === 'backflush' || c.heat_phase === 'backflush' |
|
| 32 | +} |
|
| 33 | + |
|
| 34 | +export function phaseLabel(control: ControlState): string { |
|
| 35 | + if (control.heat_phase.startsWith('startup_')) { |
|
| 36 | + return `Starting (attempt ${control.heat_phase.slice(-1)} of 3)` |
|
| 37 | + } |
|
| 38 | + const labels: Record<string, string> = { |
|
| 39 | + idle: control.heat_mode === 'maintain' ? 'Waiting (in range)' : 'Off', |
|
| 40 | + heating: 'Heating', |
|
| 41 | + backflush: 'Backflushing', |
|
| 42 | + error_startup: 'Failed to start', |
|
| 43 | + error_safety: 'Blocked by safety check', |
|
| 44 | + } |
|
| 45 | + return labels[control.heat_phase] || control.heat_phase |
|
| 46 | +} |
|
| 47 | + |
|
| 48 | +export function filterLabel(c: ControlState): string { |
|
| 49 | + const running = c.filter_pump === 'on' |
|
| 50 | + if (c.filter_mode !== 'auto') return running ? 'On' : 'Off' |
|
| 51 | + if (running) return 'Auto · running' |
|
| 52 | + return c.filter_next_start ? `Auto · next ${fmtTime(c.filter_next_start)}` : 'Auto · idle' |
|
| 53 | +} |
|
| 54 | + |
|
| 55 | +export function probeValues(temps: { roles?: Record<string, number | null>; probes?: Record<string, number | null> }) { |
|
| 56 | + const src = temps.roles ?? temps.probes ?? {} |
|
| 57 | + return src |
|
| 58 | +} |
|
| 59 | + |
|
| 60 | +export function isPayloadStale(ts: string | undefined, staleS = 15): boolean { |
|
| 61 | + if (!ts) return true |
|
| 62 | + const age = (Date.now() - new Date(ts).getTime()) / 1000 |
|
| 63 | + return age > staleS |
|
| 64 | +} |
projects/hot-tub/src/web/frontend/src/index.css
| ... | ... | @@ -0,0 +1,111 @@ |
| 1 | +:root { |
|
| 2 | + --text: #6b6375; |
|
| 3 | + --text-h: #08060d; |
|
| 4 | + --bg: #fff; |
|
| 5 | + --border: #e5e4e7; |
|
| 6 | + --code-bg: #f4f3ec; |
|
| 7 | + --accent: #aa3bff; |
|
| 8 | + --accent-bg: rgba(170, 59, 255, 0.1); |
|
| 9 | + --accent-border: rgba(170, 59, 255, 0.5); |
|
| 10 | + --social-bg: rgba(244, 243, 236, 0.5); |
|
| 11 | + --shadow: |
|
| 12 | + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; |
|
| 13 | + |
|
| 14 | + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; |
|
| 15 | + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; |
|
| 16 | + --mono: ui-monospace, Consolas, monospace; |
|
| 17 | + |
|
| 18 | + font: 18px/145% var(--sans); |
|
| 19 | + letter-spacing: 0.18px; |
|
| 20 | + color-scheme: light dark; |
|
| 21 | + color: var(--text); |
|
| 22 | + background: var(--bg); |
|
| 23 | + font-synthesis: none; |
|
| 24 | + text-rendering: optimizeLegibility; |
|
| 25 | + -webkit-font-smoothing: antialiased; |
|
| 26 | + -moz-osx-font-smoothing: grayscale; |
|
| 27 | + |
|
| 28 | + @media (max-width: 1024px) { |
|
| 29 | + font-size: 16px; |
|
| 30 | + } |
|
| 31 | +} |
|
| 32 | + |
|
| 33 | +@media (prefers-color-scheme: dark) { |
|
| 34 | + :root { |
|
| 35 | + --text: #9ca3af; |
|
| 36 | + --text-h: #f3f4f6; |
|
| 37 | + --bg: #16171d; |
|
| 38 | + --border: #2e303a; |
|
| 39 | + --code-bg: #1f2028; |
|
| 40 | + --accent: #c084fc; |
|
| 41 | + --accent-bg: rgba(192, 132, 252, 0.15); |
|
| 42 | + --accent-border: rgba(192, 132, 252, 0.5); |
|
| 43 | + --social-bg: rgba(47, 48, 58, 0.5); |
|
| 44 | + --shadow: |
|
| 45 | + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; |
|
| 46 | + } |
|
| 47 | + |
|
| 48 | + #social .button-icon { |
|
| 49 | + filter: invert(1) brightness(2); |
|
| 50 | + } |
|
| 51 | +} |
|
| 52 | + |
|
| 53 | +#root { |
|
| 54 | + width: 1126px; |
|
| 55 | + max-width: 100%; |
|
| 56 | + margin: 0 auto; |
|
| 57 | + text-align: center; |
|
| 58 | + border-inline: 1px solid var(--border); |
|
| 59 | + min-height: 100svh; |
|
| 60 | + display: flex; |
|
| 61 | + flex-direction: column; |
|
| 62 | + box-sizing: border-box; |
|
| 63 | +} |
|
| 64 | + |
|
| 65 | +body { |
|
| 66 | + margin: 0; |
|
| 67 | +} |
|
| 68 | + |
|
| 69 | +h1, |
|
| 70 | +h2 { |
|
| 71 | + font-family: var(--heading); |
|
| 72 | + font-weight: 500; |
|
| 73 | + color: var(--text-h); |
|
| 74 | +} |
|
| 75 | + |
|
| 76 | +h1 { |
|
| 77 | + font-size: 56px; |
|
| 78 | + letter-spacing: -1.68px; |
|
| 79 | + margin: 32px 0; |
|
| 80 | + @media (max-width: 1024px) { |
|
| 81 | + font-size: 36px; |
|
| 82 | + margin: 20px 0; |
|
| 83 | + } |
|
| 84 | +} |
|
| 85 | +h2 { |
|
| 86 | + font-size: 24px; |
|
| 87 | + line-height: 118%; |
|
| 88 | + letter-spacing: -0.24px; |
|
| 89 | + margin: 0 0 8px; |
|
| 90 | + @media (max-width: 1024px) { |
|
| 91 | + font-size: 20px; |
|
| 92 | + } |
|
| 93 | +} |
|
| 94 | +p { |
|
| 95 | + margin: 0; |
|
| 96 | +} |
|
| 97 | + |
|
| 98 | +code, |
|
| 99 | +.counter { |
|
| 100 | + font-family: var(--mono); |
|
| 101 | + display: inline-flex; |
|
| 102 | + border-radius: 4px; |
|
| 103 | + color: var(--text-h); |
|
| 104 | +} |
|
| 105 | + |
|
| 106 | +code { |
|
| 107 | + font-size: 15px; |
|
| 108 | + line-height: 135%; |
|
| 109 | + padding: 4px 8px; |
|
| 110 | + background: var(--code-bg); |
|
| 111 | +} |
projects/hot-tub/src/web/frontend/src/main.tsx
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +import { StrictMode } from 'react' |
|
| 2 | +import { createRoot } from 'react-dom/client' |
|
| 3 | +import { HashRouter } from 'react-router-dom' |
|
| 4 | +import App from './App.tsx' |
|
| 5 | +import './styles.css' |
|
| 6 | + |
|
| 7 | +createRoot(document.getElementById('root')!).render( |
|
| 8 | + <StrictMode> |
|
| 9 | + <HashRouter> |
|
| 10 | + <App /> |
|
| 11 | + </HashRouter> |
|
| 12 | + </StrictMode>, |
|
| 13 | +) |
projects/hot-tub/src/web/frontend/src/styles.css
| ... | ... | @@ -0,0 +1,803 @@ |
| 1 | +:root { |
|
| 2 | + --bg-top: #e0f2fe; |
|
| 3 | + --bg-bottom: #f8fafc; |
|
| 4 | + --card: #ffffff; |
|
| 5 | + --text: #0f172a; |
|
| 6 | + --muted: #64748b; |
|
| 7 | + --border: #e2e8f0; |
|
| 8 | + --surface: #f8fafc; |
|
| 9 | + --accent: #0e7490; |
|
| 10 | + --accent-strong: #155e75; |
|
| 11 | + --heat: #f97316; |
|
| 12 | + --filter: #0ea5e9; |
|
| 13 | + --green: #22c55e; |
|
| 14 | + --yellow: #eab308; |
|
| 15 | + --red: #ef4444; |
|
| 16 | + --off: #cbd5e1; |
|
| 17 | + --radius: 18px; |
|
| 18 | + --shadow: 0 6px 24px rgba(15, 23, 42, 0.08); |
|
| 19 | +} |
|
| 20 | + |
|
| 21 | +* { |
|
| 22 | + box-sizing: border-box; |
|
| 23 | +} |
|
| 24 | + |
|
| 25 | +body { |
|
| 26 | + margin: 0; |
|
| 27 | + min-height: 100vh; |
|
| 28 | + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; |
|
| 29 | + color: var(--text); |
|
| 30 | + background: linear-gradient(var(--bg-top), var(--bg-bottom)) fixed; |
|
| 31 | +} |
|
| 32 | + |
|
| 33 | +/* ---------- header / nav ---------- */ |
|
| 34 | + |
|
| 35 | +.app-header { |
|
| 36 | + position: sticky; |
|
| 37 | + top: 0; |
|
| 38 | + z-index: 10; |
|
| 39 | + display: flex; |
|
| 40 | + flex-wrap: wrap; |
|
| 41 | + align-items: center; |
|
| 42 | + gap: 12px 20px; |
|
| 43 | + padding: 12px 20px; |
|
| 44 | + background: rgba(255, 255, 255, 0.85); |
|
| 45 | + backdrop-filter: blur(8px); |
|
| 46 | + border-bottom: 1px solid var(--border); |
|
| 47 | +} |
|
| 48 | + |
|
| 49 | +.brand { |
|
| 50 | + display: flex; |
|
| 51 | + align-items: center; |
|
| 52 | + gap: 8px; |
|
| 53 | + font-size: 1.25rem; |
|
| 54 | + font-weight: 700; |
|
| 55 | +} |
|
| 56 | + |
|
| 57 | +.brand-icon { |
|
| 58 | + color: var(--heat); |
|
| 59 | + font-size: 1.4rem; |
|
| 60 | +} |
|
| 61 | + |
|
| 62 | +.nav { |
|
| 63 | + display: flex; |
|
| 64 | + gap: 4px; |
|
| 65 | + padding: 4px; |
|
| 66 | + border-radius: 999px; |
|
| 67 | + background: #f1f5f9; |
|
| 68 | +} |
|
| 69 | + |
|
| 70 | +.nav-tab { |
|
| 71 | + padding: 8px 16px; |
|
| 72 | + border-radius: 999px; |
|
| 73 | + color: var(--muted); |
|
| 74 | + font-weight: 600; |
|
| 75 | + text-decoration: none; |
|
| 76 | +} |
|
| 77 | + |
|
| 78 | +.nav-tab.active { |
|
| 79 | + background: var(--card); |
|
| 80 | + color: var(--accent-strong); |
|
| 81 | + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12); |
|
| 82 | +} |
|
| 83 | + |
|
| 84 | +.connection { |
|
| 85 | + margin-left: auto; |
|
| 86 | + font-size: 0.85rem; |
|
| 87 | +} |
|
| 88 | + |
|
| 89 | +@media (max-width: 599px) { |
|
| 90 | + .app-header { |
|
| 91 | + padding: 10px 12px; |
|
| 92 | + } |
|
| 93 | + .nav { |
|
| 94 | + order: 3; |
|
| 95 | + width: 100%; |
|
| 96 | + } |
|
| 97 | + .nav-tab { |
|
| 98 | + flex: 1; |
|
| 99 | + text-align: center; |
|
| 100 | + } |
|
| 101 | +} |
|
| 102 | + |
|
| 103 | +/* ---------- layout ---------- */ |
|
| 104 | + |
|
| 105 | +.page { |
|
| 106 | + max-width: 1100px; |
|
| 107 | + margin: 0 auto; |
|
| 108 | + padding: 20px; |
|
| 109 | + transition: opacity 0.3s; |
|
| 110 | +} |
|
| 111 | + |
|
| 112 | +.page.stale { |
|
| 113 | + opacity: 0.55; |
|
| 114 | +} |
|
| 115 | + |
|
| 116 | +@media (max-width: 599px) { |
|
| 117 | + .page { |
|
| 118 | + padding: 12px; |
|
| 119 | + } |
|
| 120 | +} |
|
| 121 | + |
|
| 122 | +.card { |
|
| 123 | + padding: 20px; |
|
| 124 | + border-radius: var(--radius); |
|
| 125 | + background: var(--card); |
|
| 126 | + box-shadow: var(--shadow); |
|
| 127 | +} |
|
| 128 | + |
|
| 129 | +.loading { |
|
| 130 | + padding: 60px 20px; |
|
| 131 | + color: var(--muted); |
|
| 132 | + text-align: center; |
|
| 133 | +} |
|
| 134 | + |
|
| 135 | +.control-layout, |
|
| 136 | +.diagnostic-layout { |
|
| 137 | + display: grid; |
|
| 138 | + gap: 20px; |
|
| 139 | +} |
|
| 140 | + |
|
| 141 | +@media (min-width: 900px) { |
|
| 142 | + .control-layout { |
|
| 143 | + grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); |
|
| 144 | + align-items: start; |
|
| 145 | + } |
|
| 146 | +} |
|
| 147 | + |
|
| 148 | +@media (min-width: 760px) { |
|
| 149 | + .diagnostic-layout { |
|
| 150 | + grid-template-columns: 1fr 1fr; |
|
| 151 | + align-items: start; |
|
| 152 | + } |
|
| 153 | +} |
|
| 154 | + |
|
| 155 | +/* ---------- status lights ---------- */ |
|
| 156 | + |
|
| 157 | +.status-light { |
|
| 158 | + display: inline-flex; |
|
| 159 | + align-items: center; |
|
| 160 | + gap: 8px; |
|
| 161 | + color: var(--muted); |
|
| 162 | +} |
|
| 163 | + |
|
| 164 | +.light { |
|
| 165 | + flex-shrink: 0; |
|
| 166 | + width: 14px; |
|
| 167 | + height: 14px; |
|
| 168 | + border-radius: 50%; |
|
| 169 | + background: var(--off); |
|
| 170 | +} |
|
| 171 | + |
|
| 172 | +.light-on { |
|
| 173 | + background: var(--green); |
|
| 174 | + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2), 0 0 12px var(--green); |
|
| 175 | +} |
|
| 176 | + |
|
| 177 | +.light-waiting { |
|
| 178 | + background: var(--yellow); |
|
| 179 | + animation: pulse 1.2s ease-in-out infinite; |
|
| 180 | +} |
|
| 181 | + |
|
| 182 | +.light-error { |
|
| 183 | + background: var(--red); |
|
| 184 | +} |
|
| 185 | + |
|
| 186 | +/* ---------- tub ---------- */ |
|
| 187 | + |
|
| 188 | +.banner { |
|
| 189 | + display: flex; |
|
| 190 | + flex-wrap: wrap; |
|
| 191 | + align-items: center; |
|
| 192 | + justify-content: space-between; |
|
| 193 | + gap: 8px 12px; |
|
| 194 | + margin-bottom: 12px; |
|
| 195 | + padding: 12px 14px; |
|
| 196 | + border-radius: 12px; |
|
| 197 | +} |
|
| 198 | + |
|
| 199 | +.banner-error { |
|
| 200 | + border: 1px solid #fecaca; |
|
| 201 | + background: #fef2f2; |
|
| 202 | + color: #991b1b; |
|
| 203 | +} |
|
| 204 | + |
|
| 205 | +.tub { |
|
| 206 | + display: block; |
|
| 207 | + width: 100%; |
|
| 208 | + max-width: 560px; |
|
| 209 | + margin: 0 auto; |
|
| 210 | +} |
|
| 211 | + |
|
| 212 | +.ground { |
|
| 213 | + fill: rgba(15, 23, 42, 0.08); |
|
| 214 | +} |
|
| 215 | + |
|
| 216 | +.tub-shell { |
|
| 217 | + fill: #475569; |
|
| 218 | +} |
|
| 219 | + |
|
| 220 | +.tub-wall { |
|
| 221 | + fill: none; |
|
| 222 | + stroke: #94a3b8; |
|
| 223 | + stroke-width: 10; |
|
| 224 | + stroke-linejoin: round; |
|
| 225 | +} |
|
| 226 | + |
|
| 227 | +.tub-rim { |
|
| 228 | + stroke: #64748b; |
|
| 229 | + stroke-width: 1.5; |
|
| 230 | +} |
|
| 231 | + |
|
| 232 | +.rivet { |
|
| 233 | + fill: #64748b; |
|
| 234 | +} |
|
| 235 | + |
|
| 236 | +.tub-temp { |
|
| 237 | + font-size: 52px; |
|
| 238 | + font-weight: 800; |
|
| 239 | + fill: #fff; |
|
| 240 | + paint-order: stroke; |
|
| 241 | + stroke: rgba(3, 105, 161, 0.6); |
|
| 242 | + stroke-width: 4px; |
|
| 243 | +} |
|
| 244 | + |
|
| 245 | +.tub-temp-caption { |
|
| 246 | + font-size: 15px; |
|
| 247 | + letter-spacing: 0.2em; |
|
| 248 | + text-transform: uppercase; |
|
| 249 | + fill: #e0f2fe; |
|
| 250 | +} |
|
| 251 | + |
|
| 252 | +.wave { |
|
| 253 | + animation: wave 4s linear infinite; |
|
| 254 | +} |
|
| 255 | + |
|
| 256 | +.wave-back { |
|
| 257 | + opacity: 0.8; |
|
| 258 | + animation-duration: 6s; |
|
| 259 | + animation-direction: reverse; |
|
| 260 | +} |
|
| 261 | + |
|
| 262 | +.duck-bob { |
|
| 263 | + transform-box: fill-box; |
|
| 264 | + transform-origin: center; |
|
| 265 | + animation: bob 2.4s ease-in-out infinite; |
|
| 266 | +} |
|
| 267 | + |
|
| 268 | +.steam { |
|
| 269 | + opacity: 0; |
|
| 270 | + transition: opacity 1s; |
|
| 271 | +} |
|
| 272 | + |
|
| 273 | +.steam.on { |
|
| 274 | + opacity: 1; |
|
| 275 | +} |
|
| 276 | + |
|
| 277 | +.steam-puff { |
|
| 278 | + fill: none; |
|
| 279 | + stroke: #cbd5e1; |
|
| 280 | + stroke-width: 5; |
|
| 281 | + stroke-linecap: round; |
|
| 282 | + animation: rise 3s ease-in-out infinite; |
|
| 283 | +} |
|
| 284 | + |
|
| 285 | +.steam-puff:nth-child(2) { |
|
| 286 | + animation-delay: 1s; |
|
| 287 | +} |
|
| 288 | + |
|
| 289 | +.steam-puff:nth-child(3) { |
|
| 290 | + animation-delay: 2s; |
|
| 291 | +} |
|
| 292 | + |
|
| 293 | +/* ---------- pumps ---------- */ |
|
| 294 | + |
|
| 295 | +.pumps { |
|
| 296 | + display: grid; |
|
| 297 | + grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
| 298 | + gap: 16px; |
|
| 299 | + max-width: 420px; |
|
| 300 | + margin: 8px auto 0; |
|
| 301 | +} |
|
| 302 | + |
|
| 303 | +.pump { |
|
| 304 | + display: flex; |
|
| 305 | + flex-direction: column; |
|
| 306 | + align-items: center; |
|
| 307 | + gap: 6px; |
|
| 308 | +} |
|
| 309 | + |
|
| 310 | +.pump-svg { |
|
| 311 | + width: 96px; |
|
| 312 | + height: 96px; |
|
| 313 | +} |
|
| 314 | + |
|
| 315 | +.pump-frame { |
|
| 316 | + fill: #f1f5f9; |
|
| 317 | + stroke: var(--border); |
|
| 318 | + stroke-width: 2; |
|
| 319 | +} |
|
| 320 | + |
|
| 321 | +.pump-housing { |
|
| 322 | + fill: #1e293b; |
|
| 323 | +} |
|
| 324 | + |
|
| 325 | +.pump-hub { |
|
| 326 | + fill: #e2e8f0; |
|
| 327 | +} |
|
| 328 | + |
|
| 329 | +.fan-blades { |
|
| 330 | + opacity: 0.5; |
|
| 331 | + transform-box: view-box; |
|
| 332 | + transform-origin: 60px 60px; |
|
| 333 | + transition: opacity 0.3s; |
|
| 334 | +} |
|
| 335 | + |
|
| 336 | +.pump-heat .fan-blades { |
|
| 337 | + fill: var(--heat); |
|
| 338 | +} |
|
| 339 | + |
|
| 340 | +.pump-filter .fan-blades { |
|
| 341 | + fill: var(--filter); |
|
| 342 | +} |
|
| 343 | + |
|
| 344 | +.pump.running .fan-blades { |
|
| 345 | + opacity: 1; |
|
| 346 | + animation: spin 0.9s linear infinite; |
|
| 347 | +} |
|
| 348 | + |
|
| 349 | +.pump-label { |
|
| 350 | + font-weight: 700; |
|
| 351 | +} |
|
| 352 | + |
|
| 353 | +@media (max-width: 400px) { |
|
| 354 | + .pump-svg { |
|
| 355 | + width: 80px; |
|
| 356 | + height: 80px; |
|
| 357 | + } |
|
| 358 | +} |
|
| 359 | + |
|
| 360 | +/* ---------- settings ---------- */ |
|
| 361 | + |
|
| 362 | +.settings-card { |
|
| 363 | + display: flex; |
|
| 364 | + flex-direction: column; |
|
| 365 | + gap: 22px; |
|
| 366 | +} |
|
| 367 | + |
|
| 368 | +.field-label { |
|
| 369 | + display: block; |
|
| 370 | + margin-bottom: 6px; |
|
| 371 | + color: var(--muted); |
|
| 372 | + font-size: 0.8rem; |
|
| 373 | + font-weight: 700; |
|
| 374 | + letter-spacing: 0.06em; |
|
| 375 | + text-transform: uppercase; |
|
| 376 | +} |
|
| 377 | + |
|
| 378 | +.target-row { |
|
| 379 | + display: flex; |
|
| 380 | + align-items: center; |
|
| 381 | + justify-content: space-between; |
|
| 382 | + gap: 12px; |
|
| 383 | +} |
|
| 384 | + |
|
| 385 | +.target-value { |
|
| 386 | + color: var(--accent-strong); |
|
| 387 | + font-size: 3.25rem; |
|
| 388 | + font-weight: 800; |
|
| 389 | + font-variant-numeric: tabular-nums; |
|
| 390 | +} |
|
| 391 | + |
|
| 392 | +.round-btn { |
|
| 393 | + width: 60px; |
|
| 394 | + height: 60px; |
|
| 395 | + border: none; |
|
| 396 | + border-radius: 50%; |
|
| 397 | + background: var(--accent); |
|
| 398 | + color: #fff; |
|
| 399 | + font-size: 2rem; |
|
| 400 | + line-height: 1; |
|
| 401 | + cursor: pointer; |
|
| 402 | + touch-action: manipulation; |
|
| 403 | +} |
|
| 404 | + |
|
| 405 | +.round-btn:active:not(:disabled) { |
|
| 406 | + transform: scale(0.94); |
|
| 407 | +} |
|
| 408 | + |
|
| 409 | +.modes { |
|
| 410 | + display: flex; |
|
| 411 | + flex-direction: column; |
|
| 412 | + gap: 14px; |
|
| 413 | +} |
|
| 414 | + |
|
| 415 | +.field select { |
|
| 416 | + width: 100%; |
|
| 417 | + padding: 12px 14px; |
|
| 418 | + border: 1px solid var(--border); |
|
| 419 | + border-radius: 12px; |
|
| 420 | + background: var(--surface); |
|
| 421 | + font: inherit; |
|
| 422 | + font-size: 1.05rem; |
|
| 423 | +} |
|
| 424 | + |
|
| 425 | +.summary { |
|
| 426 | + display: grid; |
|
| 427 | + gap: 10px; |
|
| 428 | + margin: 0; |
|
| 429 | +} |
|
| 430 | + |
|
| 431 | +.summary div { |
|
| 432 | + display: flex; |
|
| 433 | + justify-content: space-between; |
|
| 434 | + gap: 12px; |
|
| 435 | + padding-bottom: 8px; |
|
| 436 | + border-bottom: 1px dashed var(--border); |
|
| 437 | +} |
|
| 438 | + |
|
| 439 | +.summary dt { |
|
| 440 | + color: var(--muted); |
|
| 441 | +} |
|
| 442 | + |
|
| 443 | +.summary dd { |
|
| 444 | + margin: 0; |
|
| 445 | + font-weight: 600; |
|
| 446 | + text-align: right; |
|
| 447 | +} |
|
| 448 | + |
|
| 449 | +/* ---------- buttons ---------- */ |
|
| 450 | + |
|
| 451 | +.btn { |
|
| 452 | + display: inline-flex; |
|
| 453 | + align-items: center; |
|
| 454 | + justify-content: center; |
|
| 455 | + padding: 12px 22px; |
|
| 456 | + border: none; |
|
| 457 | + border-radius: 12px; |
|
| 458 | + font: inherit; |
|
| 459 | + font-weight: 700; |
|
| 460 | + cursor: pointer; |
|
| 461 | +} |
|
| 462 | + |
|
| 463 | +.btn-primary { |
|
| 464 | + background: var(--accent); |
|
| 465 | + color: #fff; |
|
| 466 | +} |
|
| 467 | + |
|
| 468 | +.btn-secondary { |
|
| 469 | + background: #f1f5f9; |
|
| 470 | + color: var(--text); |
|
| 471 | +} |
|
| 472 | + |
|
| 473 | +.btn-small { |
|
| 474 | + padding: 6px 12px; |
|
| 475 | + background: #991b1b; |
|
| 476 | + color: #fff; |
|
| 477 | + font-size: 0.875rem; |
|
| 478 | +} |
|
| 479 | + |
|
| 480 | +.big-button { |
|
| 481 | + width: 100%; |
|
| 482 | + min-height: 110px; |
|
| 483 | + border: none; |
|
| 484 | + border-radius: var(--radius); |
|
| 485 | + box-shadow: var(--shadow); |
|
| 486 | + color: #fff; |
|
| 487 | + font: inherit; |
|
| 488 | + font-size: 1.75rem; |
|
| 489 | + font-weight: 800; |
|
| 490 | + cursor: pointer; |
|
| 491 | +} |
|
| 492 | + |
|
| 493 | +.big-button-backflush { |
|
| 494 | + background: linear-gradient(135deg, #0ea5e9, #0369a1); |
|
| 495 | +} |
|
| 496 | + |
|
| 497 | +.big-button-setup { |
|
| 498 | + background: linear-gradient(135deg, #14b8a6, #0f766e); |
|
| 499 | +} |
|
| 500 | + |
|
| 501 | +.big-button:active:not(:disabled) { |
|
| 502 | + transform: translateY(1px); |
|
| 503 | +} |
|
| 504 | + |
|
| 505 | +.round-btn:disabled, |
|
| 506 | +.btn:disabled, |
|
| 507 | +.big-button:disabled { |
|
| 508 | + opacity: 0.4; |
|
| 509 | + cursor: not-allowed; |
|
| 510 | +} |
|
| 511 | + |
|
| 512 | +/* ---------- diagnostic ---------- */ |
|
| 513 | + |
|
| 514 | +.backflush-slot { |
|
| 515 | + display: grid; |
|
| 516 | + place-items: center; |
|
| 517 | + min-height: 190px; |
|
| 518 | + margin-top: 16px; |
|
| 519 | +} |
|
| 520 | + |
|
| 521 | +.hint { |
|
| 522 | + max-width: 320px; |
|
| 523 | + color: var(--muted); |
|
| 524 | + text-align: center; |
|
| 525 | +} |
|
| 526 | + |
|
| 527 | +.backflush-anim { |
|
| 528 | + color: var(--accent-strong); |
|
| 529 | + font-weight: 600; |
|
| 530 | + text-align: center; |
|
| 531 | +} |
|
| 532 | + |
|
| 533 | +.backflush-anim svg { |
|
| 534 | + width: 140px; |
|
| 535 | + height: 140px; |
|
| 536 | +} |
|
| 537 | + |
|
| 538 | +.backflush-anim p { |
|
| 539 | + margin: 6px 0 0; |
|
| 540 | +} |
|
| 541 | + |
|
| 542 | +.backflush-arrows { |
|
| 543 | + fill: none; |
|
| 544 | + stroke: var(--filter); |
|
| 545 | + stroke-width: 6; |
|
| 546 | + stroke-linecap: round; |
|
| 547 | + transform-box: view-box; |
|
| 548 | + transform-origin: 60px 60px; |
|
| 549 | + animation: spin 2s linear infinite; |
|
| 550 | +} |
|
| 551 | + |
|
| 552 | +.backflush-arrows .arrowhead { |
|
| 553 | + fill: var(--filter); |
|
| 554 | + stroke: none; |
|
| 555 | +} |
|
| 556 | + |
|
| 557 | +.droplet { |
|
| 558 | + fill: #38bdf8; |
|
| 559 | +} |
|
| 560 | + |
|
| 561 | +.droplet-eye { |
|
| 562 | + fill: var(--text); |
|
| 563 | +} |
|
| 564 | + |
|
| 565 | +.droplet-smile { |
|
| 566 | + fill: none; |
|
| 567 | + stroke: var(--text); |
|
| 568 | + stroke-width: 2.5; |
|
| 569 | + stroke-linecap: round; |
|
| 570 | +} |
|
| 571 | + |
|
| 572 | +.bubble { |
|
| 573 | + fill: #7dd3fc; |
|
| 574 | + transform-box: fill-box; |
|
| 575 | + animation: bubble 1.8s ease-in infinite; |
|
| 576 | +} |
|
| 577 | + |
|
| 578 | +.bubble.b2 { |
|
| 579 | + animation-delay: 0.6s; |
|
| 580 | +} |
|
| 581 | + |
|
| 582 | +.bubble.b3 { |
|
| 583 | + animation-delay: 1.2s; |
|
| 584 | +} |
|
| 585 | + |
|
| 586 | +.readings { |
|
| 587 | + margin-top: 20px; |
|
| 588 | +} |
|
| 589 | + |
|
| 590 | +.readings-header { |
|
| 591 | + display: flex; |
|
| 592 | + justify-content: space-between; |
|
| 593 | + align-items: baseline; |
|
| 594 | +} |
|
| 595 | + |
|
| 596 | +.setup-state { |
|
| 597 | + color: var(--muted); |
|
| 598 | + font-size: 0.85rem; |
|
| 599 | + text-transform: capitalize; |
|
| 600 | +} |
|
| 601 | + |
|
| 602 | +.probe-list { |
|
| 603 | + display: grid; |
|
| 604 | + gap: 8px; |
|
| 605 | + margin: 8px 0 0; |
|
| 606 | + padding: 0; |
|
| 607 | + list-style: none; |
|
| 608 | +} |
|
| 609 | + |
|
| 610 | +.probe-row { |
|
| 611 | + display: flex; |
|
| 612 | + align-items: center; |
|
| 613 | + justify-content: space-between; |
|
| 614 | + padding: 10px 12px; |
|
| 615 | + border-radius: 10px; |
|
| 616 | + background: var(--surface); |
|
| 617 | +} |
|
| 618 | + |
|
| 619 | +.probe-row .status-light { |
|
| 620 | + color: var(--text); |
|
| 621 | +} |
|
| 622 | + |
|
| 623 | +.probe-value { |
|
| 624 | + font-weight: 600; |
|
| 625 | + font-variant-numeric: tabular-nums; |
|
| 626 | +} |
|
| 627 | + |
|
| 628 | +/* ---------- sensor setup ---------- */ |
|
| 629 | + |
|
| 630 | +.setup-card { |
|
| 631 | + max-width: 640px; |
|
| 632 | + margin: 0 auto; |
|
| 633 | +} |
|
| 634 | + |
|
| 635 | +.setup-card h1 { |
|
| 636 | + margin: 0 0 16px; |
|
| 637 | + font-size: 1.5rem; |
|
| 638 | +} |
|
| 639 | + |
|
| 640 | +.stepper { |
|
| 641 | + display: flex; |
|
| 642 | + gap: 8px; |
|
| 643 | + margin: 0; |
|
| 644 | + padding: 0; |
|
| 645 | + list-style: none; |
|
| 646 | +} |
|
| 647 | + |
|
| 648 | +.stepper li { |
|
| 649 | + flex: 1; |
|
| 650 | + padding-top: 10px; |
|
| 651 | + border-top: 4px solid var(--border); |
|
| 652 | + color: var(--muted); |
|
| 653 | + font-size: 0.8rem; |
|
| 654 | + text-align: center; |
|
| 655 | +} |
|
| 656 | + |
|
| 657 | +.stepper li.done, |
|
| 658 | +.stepper li.current { |
|
| 659 | + border-color: var(--accent); |
|
| 660 | + color: var(--accent-strong); |
|
| 661 | + font-weight: 700; |
|
| 662 | +} |
|
| 663 | + |
|
| 664 | +.setup-step h2 { |
|
| 665 | + margin: 24px 0 8px; |
|
| 666 | + font-size: 1.25rem; |
|
| 667 | +} |
|
| 668 | + |
|
| 669 | +.setup-step p { |
|
| 670 | + color: var(--muted); |
|
| 671 | + line-height: 1.5; |
|
| 672 | +} |
|
| 673 | + |
|
| 674 | +.setup-status { |
|
| 675 | + margin: 20px 0; |
|
| 676 | + padding: 16px; |
|
| 677 | + border-radius: 12px; |
|
| 678 | + background: var(--surface); |
|
| 679 | +} |
|
| 680 | + |
|
| 681 | +.setup-status .status-light { |
|
| 682 | + color: var(--text); |
|
| 683 | + font-size: 1.1rem; |
|
| 684 | +} |
|
| 685 | + |
|
| 686 | +.setup-status .light { |
|
| 687 | + width: 22px; |
|
| 688 | + height: 22px; |
|
| 689 | +} |
|
| 690 | + |
|
| 691 | +.actions { |
|
| 692 | + display: flex; |
|
| 693 | + justify-content: space-between; |
|
| 694 | + gap: 12px; |
|
| 695 | + margin-top: 24px; |
|
| 696 | +} |
|
| 697 | + |
|
| 698 | +/* ---------- mock backend panel (not part of the app) ---------- */ |
|
| 699 | + |
|
| 700 | +.mock-panel { |
|
| 701 | + position: fixed; |
|
| 702 | + right: 12px; |
|
| 703 | + bottom: 12px; |
|
| 704 | + z-index: 50; |
|
| 705 | + max-width: 250px; |
|
| 706 | + border: 2px dashed #f59e0b; |
|
| 707 | + border-radius: 12px; |
|
| 708 | + background: #fffbeb; |
|
| 709 | + box-shadow: var(--shadow); |
|
| 710 | + font-size: 0.85rem; |
|
| 711 | +} |
|
| 712 | + |
|
| 713 | +.mock-panel summary { |
|
| 714 | + padding: 8px 12px; |
|
| 715 | + color: #92400e; |
|
| 716 | + font-weight: 700; |
|
| 717 | + cursor: pointer; |
|
| 718 | +} |
|
| 719 | + |
|
| 720 | +.mock-panel-body { |
|
| 721 | + display: grid; |
|
| 722 | + gap: 8px; |
|
| 723 | + padding: 0 12px 12px; |
|
| 724 | +} |
|
| 725 | + |
|
| 726 | +.mock-panel label { |
|
| 727 | + display: flex; |
|
| 728 | + align-items: center; |
|
| 729 | + gap: 6px; |
|
| 730 | +} |
|
| 731 | + |
|
| 732 | +.mock-panel button { |
|
| 733 | + padding: 6px 8px; |
|
| 734 | + border: 1px solid #fcd34d; |
|
| 735 | + border-radius: 8px; |
|
| 736 | + background: #fff; |
|
| 737 | + font: inherit; |
|
| 738 | + text-align: left; |
|
| 739 | + cursor: pointer; |
|
| 740 | +} |
|
| 741 | + |
|
| 742 | +/* ---------- animation ---------- */ |
|
| 743 | + |
|
| 744 | +@keyframes spin { |
|
| 745 | + to { |
|
| 746 | + transform: rotate(360deg); |
|
| 747 | + } |
|
| 748 | +} |
|
| 749 | + |
|
| 750 | +@keyframes wave { |
|
| 751 | + to { |
|
| 752 | + transform: translateX(-100px); |
|
| 753 | + } |
|
| 754 | +} |
|
| 755 | + |
|
| 756 | +@keyframes bob { |
|
| 757 | + 50% { |
|
| 758 | + transform: translateY(4px) rotate(-4deg); |
|
| 759 | + } |
|
| 760 | +} |
|
| 761 | + |
|
| 762 | +@keyframes rise { |
|
| 763 | + 0% { |
|
| 764 | + opacity: 0; |
|
| 765 | + transform: translateY(8px); |
|
| 766 | + } |
|
| 767 | + 40% { |
|
| 768 | + opacity: 0.9; |
|
| 769 | + } |
|
| 770 | + 100% { |
|
| 771 | + opacity: 0; |
|
| 772 | + transform: translateY(-14px); |
|
| 773 | + } |
|
| 774 | +} |
|
| 775 | + |
|
| 776 | +@keyframes bubble { |
|
| 777 | + 0% { |
|
| 778 | + opacity: 0; |
|
| 779 | + transform: translateY(0); |
|
| 780 | + } |
|
| 781 | + 20% { |
|
| 782 | + opacity: 1; |
|
| 783 | + } |
|
| 784 | + 100% { |
|
| 785 | + opacity: 0; |
|
| 786 | + transform: translateY(-40px); |
|
| 787 | + } |
|
| 788 | +} |
|
| 789 | + |
|
| 790 | +@keyframes pulse { |
|
| 791 | + 50% { |
|
| 792 | + opacity: 0.35; |
|
| 793 | + } |
|
| 794 | +} |
|
| 795 | + |
|
| 796 | +@media (prefers-reduced-motion: reduce) { |
|
| 797 | + *, |
|
| 798 | + *::before, |
|
| 799 | + *::after { |
|
| 800 | + animation: none !important; |
|
| 801 | + transition: none !important; |
|
| 802 | + } |
|
| 803 | +} |
projects/hot-tub/src/web/frontend/src/types.ts
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +export type ConnectionState = 'connecting' | 'open' | 'reconnecting' |
|
| 2 | + |
|
| 3 | +export interface ControlState { |
|
| 4 | + ts: string |
|
| 5 | + heat_mode: 'sleep' | 'maintain' | 'backflush' |
|
| 6 | + filter_mode: 'on' | 'auto' | 'off' |
|
| 7 | + target_f: number |
|
| 8 | + tub_avg_f: number | null |
|
| 9 | + hot_in_f: number | null |
|
| 10 | + heat_pump: 'on' | 'off' |
|
| 11 | + filter_pump: 'on' | 'off' |
|
| 12 | + heat_phase: string |
|
| 13 | + error?: { code: string; detail?: string } | null |
|
| 14 | + filter_next_start?: string | null |
|
| 15 | +} |
|
| 16 | + |
|
| 17 | +export interface TempsState { |
|
| 18 | + ts: string |
|
| 19 | + setup_state: string |
|
| 20 | + probes: Record<string, number | null> |
|
| 21 | + roles?: Record<string, number | null> |
|
| 22 | + tub_avg_f?: number | null |
|
| 23 | +} |
|
| 24 | + |
|
| 25 | +export type FirstBundle = 'hot' | 'water' | null |
projects/hot-tub/src/web/frontend/tsconfig.app.json
| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | +{ |
|
| 2 | + "compilerOptions": { |
|
| 3 | + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |
|
| 4 | + "target": "es2023", |
|
| 5 | + "lib": ["ES2023", "DOM"], |
|
| 6 | + "module": "esnext", |
|
| 7 | + "types": ["vite/client"], |
|
| 8 | + "allowArbitraryExtensions": true, |
|
| 9 | + "skipLibCheck": true, |
|
| 10 | + |
|
| 11 | + /* Bundler mode */ |
|
| 12 | + "moduleResolution": "bundler", |
|
| 13 | + "allowImportingTsExtensions": true, |
|
| 14 | + "verbatimModuleSyntax": true, |
|
| 15 | + "moduleDetection": "force", |
|
| 16 | + "noEmit": true, |
|
| 17 | + "jsx": "react-jsx", |
|
| 18 | + |
|
| 19 | + /* Linting */ |
|
| 20 | + "noUnusedLocals": true, |
|
| 21 | + "noUnusedParameters": true, |
|
| 22 | + "erasableSyntaxOnly": true, |
|
| 23 | + "noFallthroughCasesInSwitch": true |
|
| 24 | + }, |
|
| 25 | + "include": ["src"] |
|
| 26 | +} |
projects/hot-tub/src/web/frontend/tsconfig.json
| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | +{ |
|
| 2 | + "files": [], |
|
| 3 | + "references": [ |
|
| 4 | + { "path": "./tsconfig.app.json" }, |
|
| 5 | + { "path": "./tsconfig.node.json" } |
|
| 6 | + ] |
|
| 7 | +} |
projects/hot-tub/src/web/frontend/tsconfig.node.json
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +{ |
|
| 2 | + "compilerOptions": { |
|
| 3 | + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |
|
| 4 | + "target": "es2023", |
|
| 5 | + "lib": ["ES2023"], |
|
| 6 | + "types": ["node"], |
|
| 7 | + "skipLibCheck": true, |
|
| 8 | + |
|
| 9 | + /* Bundler mode */ |
|
| 10 | + "module": "nodenext", |
|
| 11 | + "allowImportingTsExtensions": true, |
|
| 12 | + "verbatimModuleSyntax": true, |
|
| 13 | + "moduleDetection": "force", |
|
| 14 | + "noEmit": true, |
|
| 15 | + |
|
| 16 | + /* Linting */ |
|
| 17 | + "noUnusedLocals": true, |
|
| 18 | + "noUnusedParameters": true, |
|
| 19 | + "erasableSyntaxOnly": true, |
|
| 20 | + "noFallthroughCasesInSwitch": true |
|
| 21 | + }, |
|
| 22 | + "include": ["vite.config.ts"] |
|
| 23 | +} |
projects/hot-tub/src/web/frontend/vite.config.ts
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +import react from '@vitejs/plugin-react' |
|
| 2 | +import { defineConfig } from 'vite' |
|
| 3 | + |
|
| 4 | +export default defineConfig({ |
|
| 5 | + plugins: [react()], |
|
| 6 | + server: { |
|
| 7 | + proxy: { |
|
| 8 | + '/api': 'http://127.0.0.1:8080', |
|
| 9 | + }, |
|
| 10 | + }, |
|
| 11 | +}) |
projects/hot-tub/src/web/health.py
| ... | ... | @@ -0,0 +1,79 @@ |
| 1 | +"""Web service health heartbeat.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import logging |
|
| 6 | +import os |
|
| 7 | +import threading |
|
| 8 | +import time |
|
| 9 | +from typing import TYPE_CHECKING |
|
| 10 | + |
|
| 11 | +from .redis_io import ( |
|
| 12 | + KEY_CONTROL_LATEST, |
|
| 13 | + KEY_HEALTH_CONTROL, |
|
| 14 | + KEY_HEALTH_TEMPS, |
|
| 15 | + KEY_TEMPS_LATEST, |
|
| 16 | + WebRedis, |
|
| 17 | + payload_age_s, |
|
| 18 | +) |
|
| 19 | + |
|
| 20 | +if TYPE_CHECKING: |
|
| 21 | + from .config import WebConfig |
|
| 22 | + |
|
| 23 | +logger = logging.getLogger("hot_tub.web") |
|
| 24 | + |
|
| 25 | + |
|
| 26 | +def compute_health(redis_io: WebRedis, cfg: WebConfig) -> tuple[str, str]: |
|
| 27 | + client_ok = redis_io._ensure() is not None # noqa: SLF001 |
|
| 28 | + if not client_ok: |
|
| 29 | + return "error", "redis down" |
|
| 30 | + |
|
| 31 | + control = redis_io.get_json(KEY_CONTROL_LATEST) |
|
| 32 | + temps = redis_io.get_json(KEY_TEMPS_LATEST) |
|
| 33 | + stale = cfg.stale_s |
|
| 34 | + control_age = payload_age_s(control) |
|
| 35 | + temps_age = payload_age_s(temps) |
|
| 36 | + if control_age is not None and control_age > stale: |
|
| 37 | + return "degraded", "control:latest stale" |
|
| 38 | + if temps_age is not None and temps_age > stale: |
|
| 39 | + return "degraded", "temps:latest stale" |
|
| 40 | + if control is None or temps is None: |
|
| 41 | + return "degraded", "missing upstream snapshots" |
|
| 42 | + return "ok", "serving; redis ok" |
|
| 43 | + |
|
| 44 | + |
|
| 45 | +def health_snapshot(redis_io: WebRedis, cfg: WebConfig) -> dict: |
|
| 46 | + status, detail = compute_health(redis_io, cfg) |
|
| 47 | + from datetime import datetime, timezone |
|
| 48 | + |
|
| 49 | + ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
|
| 50 | + return { |
|
| 51 | + "service": "web", |
|
| 52 | + "ts": ts, |
|
| 53 | + "status": status, |
|
| 54 | + "detail": detail, |
|
| 55 | + "pid": os.getpid(), |
|
| 56 | + } |
|
| 57 | + |
|
| 58 | + |
|
| 59 | +def start_health_thread(redis_io: WebRedis, cfg: WebConfig, stop: threading.Event) -> threading.Thread: |
|
| 60 | + def _loop() -> None: |
|
| 61 | + while not stop.is_set(): |
|
| 62 | + payload = health_snapshot(redis_io, cfg) |
|
| 63 | + redis_io.publish_health(payload) |
|
| 64 | + stop.wait(cfg.heartbeat_s) |
|
| 65 | + |
|
| 66 | + thread = threading.Thread(target=_loop, daemon=True) |
|
| 67 | + thread.start() |
|
| 68 | + return thread |
|
| 69 | + |
|
| 70 | + |
|
| 71 | +def api_health_payload(redis_io: WebRedis, cfg: WebConfig) -> dict: |
|
| 72 | + web = health_snapshot(redis_io, cfg) |
|
| 73 | + return { |
|
| 74 | + "web": web, |
|
| 75 | + "control_age_s": payload_age_s(redis_io.get_json(KEY_CONTROL_LATEST)), |
|
| 76 | + "temps_age_s": payload_age_s(redis_io.get_json(KEY_TEMPS_LATEST)), |
|
| 77 | + "health_control": redis_io.get_json(KEY_HEALTH_CONTROL), |
|
| 78 | + "health_temps": redis_io.get_json(KEY_HEALTH_TEMPS), |
|
| 79 | + } |
projects/hot-tub/src/web/interface-requirements.md
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +# Hot Tub Control Web Interface |
|
| 2 | + |
|
| 3 | +- Should be responsive. Should look good on large, medium and phone-sized screens |
|
| 4 | +- should respond to events from the backend using something like SSE or websockets |
|
| 5 | +- Should have 2 main nav items: |
|
| 6 | + - Main Control: |
|
| 7 | + - Should include a simple, fun graphic, prominently featured in the center of a horse-trough shaped hot tub. In the water of the hot tub, the current temperature in fahrenheit should be dispayed |
|
| 8 | + - two simple pump / fan graphics should be shown below the tub, one for heat and one for the filter. When one of the pumps is running, a green light should be displayed below it, and the fan / pump should show an animation. If a fan is used, the blades could rotate, for instance |
|
| 9 | + - a number showing the target temp in fahrenheit |
|
| 10 | + - Plus and minus buttons to increment / decrement the target temp |
|
| 11 | + - a dropdown menu allowing the user to set the mode |
|
| 12 | + - Diagnostic: |
|
| 13 | + - A big button labeled "Backflush". |
|
| 14 | + - When pressed |
|
| 15 | + - it should initiate a backflush |
|
| 16 | + - it the backflush button should be disabled |
|
| 17 | + - a cute graphic should indicate that a backflush is in progress |
|
| 18 | + - when idle or backflush is finished, the button should re-enable |
|
| 19 | + - A big button labeled "Sensor Setup" |
|
| 20 | + - when clicked, user navigates to sensor_setup_1: |
|
| 21 | + - User is prompted to connect disconnect all sensors, if they aren't disconnected arleady. Continue button navigates to sensor_setup_2 |
|
| 22 | + - User is prompted to plug in one set of sensors. Disabled "Continue" button becomes enabled when temperature readings start coming in from either hot input or water temp sensors. Perhaps a yellow or red light that turns green. Upon clicking "Continue", user navigates to sensor_setup_3 |
|
| 23 | + - User is prompted to plug in the other set of sensors. Disabled "Finish" button becomes enabled when temperature readings start coming in from all temp sensors. Perhaps a yellow or red light that turns green. Upon clicking "Finish", user navigates to main Diagnostic screen |
projects/hot-tub/src/web/logging_setup.py
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +"""File logging for the web service.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import logging |
|
| 6 | +from pathlib import Path |
|
| 7 | + |
|
| 8 | + |
|
| 9 | +def setup_logging(log_path: Path, *, verbose: bool = False) -> logging.Logger: |
|
| 10 | + log_path.parent.mkdir(parents=True, exist_ok=True) |
|
| 11 | + logger = logging.getLogger("hot_tub.web") |
|
| 12 | + logger.setLevel(logging.DEBUG if verbose else logging.INFO) |
|
| 13 | + logger.handlers.clear() |
|
| 14 | + |
|
| 15 | + handler = logging.FileHandler(log_path, encoding="utf-8") |
|
| 16 | + handler.setFormatter( |
|
| 17 | + logging.Formatter("%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") |
|
| 18 | + ) |
|
| 19 | + logger.addHandler(handler) |
|
| 20 | + return logger |
projects/hot-tub/src/web/mockup/app.js
| ... | ... | @@ -0,0 +1,523 @@ |
| 1 | +/* |
|
| 2 | + * Hot tub web UI mockup, structured like the planned React app: |
|
| 3 | + * - `state` + `setState` stand in for a top-level useReducer/useState |
|
| 4 | + * - each PascalCase function takes props and returns markup -> one React component |
|
| 5 | + * - `data-action` attributes stand in for onClick / onChange props |
|
| 6 | + * - hash routes map to react-router paths |
|
| 7 | + * - `window.HotTubApi` is the API client (mocked in mock-backend.js) |
|
| 8 | + */ |
|
| 9 | +(function () { |
|
| 10 | + const api = window.HotTubApi; |
|
| 11 | + const root = document.getElementById("root"); |
|
| 12 | + |
|
| 13 | + const TARGET_MIN = 50; |
|
| 14 | + const TARGET_MAX = 110; |
|
| 15 | + const PROBES = [ |
|
| 16 | + { key: "hot_in", label: "Hot input" }, |
|
| 17 | + { key: "tub1", label: "Tub 1" }, |
|
| 18 | + { key: "tub2", label: "Tub 2" }, |
|
| 19 | + { key: "tub3", label: "Tub 3" }, |
|
| 20 | + { key: "tub4", label: "Tub 4" }, |
|
| 21 | + ]; |
|
| 22 | + const WATER_KEYS = ["tub1", "tub2", "tub3", "tub4"]; |
|
| 23 | + const TUB_INSIDE = "M40 64 L360 64 L338 204 Q335 214 324 214 L76 214 Q65 214 62 204 Z"; |
|
| 24 | + |
|
| 25 | + /* ---------- state + routing ---------- */ |
|
| 26 | + |
|
| 27 | + if (!location.hash) history.replaceState(null, "", "#/control"); |
|
| 28 | + |
|
| 29 | + let state = { |
|
| 30 | + route: parseRoute(location.hash), |
|
| 31 | + connection: "connecting", |
|
| 32 | + control: null, |
|
| 33 | + temps: null, |
|
| 34 | + backflushRequested: false, |
|
| 35 | + firstBundle: null, |
|
| 36 | + }; |
|
| 37 | + |
|
| 38 | + function setState(patchObj) { |
|
| 39 | + state = { ...state, ...patchObj }; |
|
| 40 | + render(); |
|
| 41 | + } |
|
| 42 | + |
|
| 43 | + function parseRoute(hash) { |
|
| 44 | + const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean); |
|
| 45 | + if (parts[0] === "diagnostic" && parts[1] === "sensor-setup") { |
|
| 46 | + const step = Number(parts[2]); |
|
| 47 | + return { page: "sensor-setup", step: [1, 2, 3].includes(step) ? step : 1 }; |
|
| 48 | + } |
|
| 49 | + if (parts[0] === "diagnostic") return { page: "diagnostic" }; |
|
| 50 | + return { page: "control" }; |
|
| 51 | + } |
|
| 52 | + |
|
| 53 | + const navigate = (path) => { |
|
| 54 | + location.hash = path; |
|
| 55 | + }; |
|
| 56 | + |
|
| 57 | + window.addEventListener("hashchange", () => setState({ route: parseRoute(location.hash) })); |
|
| 58 | + |
|
| 59 | + /* ---------- helpers ---------- */ |
|
| 60 | + |
|
| 61 | + const esc = (v) => |
|
| 62 | + String(v).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); |
|
| 63 | + const has = (v) => v !== null && v !== undefined; |
|
| 64 | + const fmtF = (v) => (has(v) ? `${Number(v).toFixed(1)}°F` : "--"); |
|
| 65 | + const fmtTarget = (v) => `${Number.isInteger(v) ? v : Number(v).toFixed(1)}°F`; |
|
| 66 | + const fmtTime = (iso) => new Date(iso).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); |
|
| 67 | + const attr = (cond, name) => (cond ? ` ${name}` : ""); |
|
| 68 | + const isBackflushing = (c) => c.heat_mode === "backflush" || c.heat_phase === "backflush"; |
|
| 69 | + |
|
| 70 | + function phaseLabel({ heat_phase, heat_mode }) { |
|
| 71 | + if (heat_phase.startsWith("startup_")) return `Starting (attempt ${heat_phase.slice(-1)} of 3)`; |
|
| 72 | + const labels = { |
|
| 73 | + idle: heat_mode === "maintain" ? "Waiting (in range)" : "Off", |
|
| 74 | + heating: "Heating", |
|
| 75 | + backflush: "Backflushing", |
|
| 76 | + error_startup: "Failed to start", |
|
| 77 | + error_safety: "Blocked by safety check", |
|
| 78 | + }; |
|
| 79 | + return labels[heat_phase] || heat_phase; |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + function filterLabel(c) { |
|
| 83 | + const running = c.filter_pump === "on"; |
|
| 84 | + if (c.filter_mode !== "auto") return running ? "On" : "Off"; |
|
| 85 | + if (running) return "Auto · running"; |
|
| 86 | + return c.filter_next_start ? `Auto · next ${fmtTime(c.filter_next_start)}` : "Auto · idle"; |
|
| 87 | + } |
|
| 88 | + |
|
| 89 | + /* ---------- components ---------- */ |
|
| 90 | + |
|
| 91 | + function App(s) { |
|
| 92 | + return `<div class="app"> |
|
| 93 | + ${Header(s)} |
|
| 94 | + <main class="page${s.connection === "open" ? "" : " stale"}">${Page(s)}</main> |
|
| 95 | + </div>`; |
|
| 96 | + } |
|
| 97 | + |
|
| 98 | + function Page(s) { |
|
| 99 | + if (!s.control || !s.temps) return Loading(); |
|
| 100 | + if (s.route.page === "diagnostic") return DiagnosticPage(s); |
|
| 101 | + if (s.route.page === "sensor-setup") return SensorSetupPage(s); |
|
| 102 | + return ControlPage(s); |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + function Loading() { |
|
| 106 | + return `<div class="card loading">Connecting to the hot tub…</div>`; |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + function Header({ route, connection }) { |
|
| 110 | + const onControl = route.page === "control"; |
|
| 111 | + const tab = (href, label, active) => |
|
| 112 | + `<a class="nav-tab${active ? " active" : ""}" href="${href}">${label}</a>`; |
|
| 113 | + return `<header class="app-header"> |
|
| 114 | + <div class="brand"><span class="brand-icon" aria-hidden="true">♨</span> Hot Tub</div> |
|
| 115 | + <nav class="nav"> |
|
| 116 | + ${tab("#/control", "Main Control", onControl)} |
|
| 117 | + ${tab("#/diagnostic", "Diagnostic", !onControl)} |
|
| 118 | + </nav> |
|
| 119 | + ${ConnectionBadge({ connection })} |
|
| 120 | + </header>`; |
|
| 121 | + } |
|
| 122 | + |
|
| 123 | + function ConnectionBadge({ connection }) { |
|
| 124 | + const map = { |
|
| 125 | + open: ["on", "Live"], |
|
| 126 | + connecting: ["waiting", "Connecting…"], |
|
| 127 | + reconnecting: ["error", "Reconnecting…"], |
|
| 128 | + }; |
|
| 129 | + const [status, label] = map[connection]; |
|
| 130 | + return `<div class="connection">${StatusLight({ status, label })}</div>`; |
|
| 131 | + } |
|
| 132 | + |
|
| 133 | + function StatusLight({ status, label }) { |
|
| 134 | + return `<span class="status-light"><span class="light light-${status}" aria-hidden="true"></span><span>${label}</span></span>`; |
|
| 135 | + } |
|
| 136 | + |
|
| 137 | + /* ----- Main Control ----- */ |
|
| 138 | + |
|
| 139 | + function ControlPage({ control }) { |
|
| 140 | + const tub = control.tub_avg_f; |
|
| 141 | + return `<div class="control-layout"> |
|
| 142 | + <section class="card tub-card"> |
|
| 143 | + <div class="banner-slot">${control.error ? ErrorBanner({ error: control.error }) : ""}</div> |
|
| 144 | + ${TubGraphic({ tempF: tub, steaming: control.heat_pump === "on" || (has(tub) && tub >= 95) })} |
|
| 145 | + <div class="pumps"> |
|
| 146 | + ${Pump({ kind: "heat", label: "Heat", running: control.heat_pump === "on" })} |
|
| 147 | + ${Pump({ kind: "filter", label: "Filter", running: control.filter_pump === "on" })} |
|
| 148 | + </div> |
|
| 149 | + </section> |
|
| 150 | + <section class="card settings-card"> |
|
| 151 | + ${TargetTemp({ targetF: control.target_f })} |
|
| 152 | + ${ModeSelects({ control })} |
|
| 153 | + ${StatusSummary({ control })} |
|
| 154 | + </section> |
|
| 155 | + </div>`; |
|
| 156 | + } |
|
| 157 | + |
|
| 158 | + function ErrorBanner({ error }) { |
|
| 159 | + return `<div class="banner banner-error" role="alert"> |
|
| 160 | + <span><strong>Heater problem.</strong> ${esc(error.detail || error.code)}</span> |
|
| 161 | + <button class="btn btn-small" data-action="reset-error">Reset (set Sleep)</button> |
|
| 162 | + </div>`; |
|
| 163 | + } |
|
| 164 | + |
|
| 165 | + function wavePath(y) { |
|
| 166 | + let d = `M-100 ${y}`; |
|
| 167 | + for (let i = 0; i < 6; i++) d += " q25 -7 50 0 q25 7 50 0"; |
|
| 168 | + return `${d} L500 240 L-100 240 Z`; |
|
| 169 | + } |
|
| 170 | + |
|
| 171 | + function TubGraphic({ tempF, steaming }) { |
|
| 172 | + const label = has(tempF) ? `${tempF.toFixed(1)}°F` : "--.-°F"; |
|
| 173 | + const rivets = [60, 110, 160, 210, 260, 310, 340] |
|
| 174 | + .map((x) => `<circle class="rivet" cx="${x}" cy="65" r="2.2"/>`) |
|
| 175 | + .join(""); |
|
| 176 | + return `<svg class="tub" viewBox="0 0 400 240" role="img" aria-label="Tub water temperature"> |
|
| 177 | + <defs> |
|
| 178 | + <clipPath id="tub-inside"><path d="${TUB_INSIDE}"/></clipPath> |
|
| 179 | + <linearGradient id="water-grad" x1="0" y1="0" x2="0" y2="1"> |
|
| 180 | + <stop offset="0" stop-color="#38bdf8"/> |
|
| 181 | + <stop offset="1" stop-color="#0369a1"/> |
|
| 182 | + </linearGradient> |
|
| 183 | + <linearGradient id="steel-grad" x1="0" y1="0" x2="1" y2="0"> |
|
| 184 | + <stop offset="0" stop-color="#94a3b8"/> |
|
| 185 | + <stop offset="0.5" stop-color="#f1f5f9"/> |
|
| 186 | + <stop offset="1" stop-color="#94a3b8"/> |
|
| 187 | + </linearGradient> |
|
| 188 | + </defs> |
|
| 189 | + <ellipse class="ground" cx="200" cy="222" rx="170" ry="10"/> |
|
| 190 | + <g class="steam${steaming ? " on" : ""}" aria-hidden="true"> |
|
| 191 | + <path class="steam-puff" d="M140 52 q-8 -10 0 -20 t0 -20"/> |
|
| 192 | + <path class="steam-puff" d="M200 52 q-8 -10 0 -20 t0 -20"/> |
|
| 193 | + <path class="steam-puff" d="M260 52 q-8 -10 0 -20 t0 -20"/> |
|
| 194 | + </g> |
|
| 195 | + <path class="tub-shell" d="${TUB_INSIDE}"/> |
|
| 196 | + <g clip-path="url(#tub-inside)"> |
|
| 197 | + <g class="wave wave-back"><path d="${wavePath(90)}" fill="#7dd3fc"/></g> |
|
| 198 | + <g class="wave wave-front"><path d="${wavePath(96)}" fill="url(#water-grad)"/></g> |
|
| 199 | + </g> |
|
| 200 | + <path class="tub-wall" d="${TUB_INSIDE}"/> |
|
| 201 | + <rect class="tub-rim" x="22" y="58" width="356" height="14" rx="7" fill="url(#steel-grad)"/> |
|
| 202 | + ${rivets} |
|
| 203 | + <g transform="translate(286 72)">${Duck()}</g> |
|
| 204 | + <text class="tub-temp" x="200" y="165" text-anchor="middle">${label}</text> |
|
| 205 | + <text class="tub-temp-caption" x="200" y="190" text-anchor="middle">water</text> |
|
| 206 | + </svg>`; |
|
| 207 | + } |
|
| 208 | + |
|
| 209 | + function Duck() { |
|
| 210 | + return `<g class="duck-bob" aria-hidden="true"> |
|
| 211 | + <ellipse cx="14" cy="18" rx="16" ry="10" fill="#facc15"/> |
|
| 212 | + <path d="M4 16 q8 -6 14 2" fill="none" stroke="#eab308" stroke-width="2" stroke-linecap="round"/> |
|
| 213 | + <circle cx="25" cy="6" r="8" fill="#facc15"/> |
|
| 214 | + <path d="M32 5 l8 2 l-8 3 z" fill="#f97316"/> |
|
| 215 | + <circle cx="27" cy="4" r="1.6" fill="#1e293b"/> |
|
| 216 | + </g>`; |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + function Pump({ kind, label, running }) { |
|
| 220 | + const blades = [0, 90, 180, 270] |
|
| 221 | + .map((a) => `<ellipse cx="60" cy="36" rx="11" ry="21" transform="rotate(${a} 60 60)"/>`) |
|
| 222 | + .join(""); |
|
| 223 | + return `<div class="pump pump-${kind}${running ? " running" : ""}"> |
|
| 224 | + <svg class="pump-svg" viewBox="0 0 120 120" aria-hidden="true"> |
|
| 225 | + <rect class="pump-frame" x="4" y="4" width="112" height="112" rx="18"/> |
|
| 226 | + <circle class="pump-housing" cx="60" cy="60" r="48"/> |
|
| 227 | + <g class="fan-blades">${blades}</g> |
|
| 228 | + <circle class="pump-hub" cx="60" cy="60" r="8"/> |
|
| 229 | + </svg> |
|
| 230 | + <div class="pump-label">${label}</div> |
|
| 231 | + ${StatusLight({ status: running ? "on" : "off", label: running ? "Running" : "Off" })} |
|
| 232 | + </div>`; |
|
| 233 | + } |
|
| 234 | + |
|
| 235 | + function TargetTemp({ targetF }) { |
|
| 236 | + return `<div class="target"> |
|
| 237 | + <span class="field-label">Target temperature</span> |
|
| 238 | + <div class="target-row"> |
|
| 239 | + <button class="round-btn" data-action="target-dec" aria-label="Decrease target"${attr(targetF <= TARGET_MIN, "disabled")}>−</button> |
|
| 240 | + <div class="target-value">${fmtTarget(targetF)}</div> |
|
| 241 | + <button class="round-btn" data-action="target-inc" aria-label="Increase target"${attr(targetF >= TARGET_MAX, "disabled")}>+</button> |
|
| 242 | + </div> |
|
| 243 | + </div>`; |
|
| 244 | + } |
|
| 245 | + |
|
| 246 | + function ModeSelects({ control }) { |
|
| 247 | + const opt = (value, label, current, disabled = false) => |
|
| 248 | + `<option value="${value}"${attr(value === current, "selected")}${attr(disabled, "disabled")}>${label}</option>`; |
|
| 249 | + const heatOptions = [ |
|
| 250 | + opt("sleep", "Sleep", control.heat_mode), |
|
| 251 | + opt("maintain", "Maintain temperature", control.heat_mode), |
|
| 252 | + ]; |
|
| 253 | + if (control.heat_mode === "backflush") { |
|
| 254 | + heatOptions.push(opt("backflush", "Backflush (running)", control.heat_mode, true)); |
|
| 255 | + } |
|
| 256 | + return `<div class="modes"> |
|
| 257 | + <label class="field"> |
|
| 258 | + <span class="field-label">Heat mode</span> |
|
| 259 | + <select data-action="set-heat-mode">${heatOptions.join("")}</select> |
|
| 260 | + </label> |
|
| 261 | + <label class="field"> |
|
| 262 | + <span class="field-label">Filter mode</span> |
|
| 263 | + <select data-action="set-filter-mode"> |
|
| 264 | + ${opt("auto", "Auto (daily schedule)", control.filter_mode)} |
|
| 265 | + ${opt("on", "On", control.filter_mode)} |
|
| 266 | + ${opt("off", "Off", control.filter_mode)} |
|
| 267 | + </select> |
|
| 268 | + </label> |
|
| 269 | + </div>`; |
|
| 270 | + } |
|
| 271 | + |
|
| 272 | + function StatusSummary({ control }) { |
|
| 273 | + return `<dl class="summary"> |
|
| 274 | + <div><dt>Heater</dt><dd>${esc(phaseLabel(control))}</dd></div> |
|
| 275 | + <div><dt>Hot input</dt><dd>${fmtF(control.hot_in_f)}</dd></div> |
|
| 276 | + <div><dt>Filter</dt><dd>${filterLabel(control)}</dd></div> |
|
| 277 | + </dl>`; |
|
| 278 | + } |
|
| 279 | + |
|
| 280 | + /* ----- Diagnostic ----- */ |
|
| 281 | + |
|
| 282 | + function DiagnosticPage({ control, temps, backflushRequested }) { |
|
| 283 | + const running = isBackflushing(control); |
|
| 284 | + return `<div class="diagnostic-layout"> |
|
| 285 | + <section class="card"> |
|
| 286 | + <button class="big-button big-button-backflush" data-action="backflush"${attr(running || backflushRequested, "disabled")}> |
|
| 287 | + ${running ? "Backflushing…" : "Backflush"} |
|
| 288 | + </button> |
|
| 289 | + <div class="backflush-slot">${running ? BackflushAnimation() : `<p class="hint">Runs the heat pump briefly to purge the heater line, then returns to Sleep.</p>`}</div> |
|
| 290 | + </section> |
|
| 291 | + <section class="card"> |
|
| 292 | + <button class="big-button big-button-setup" data-action="start-sensor-setup">Sensor Setup</button> |
|
| 293 | + ${SensorReadings({ temps })} |
|
| 294 | + </section> |
|
| 295 | + </div>`; |
|
| 296 | + } |
|
| 297 | + |
|
| 298 | + function BackflushAnimation() { |
|
| 299 | + return `<div class="backflush-anim" role="status"> |
|
| 300 | + <svg viewBox="0 0 120 120" aria-hidden="true"> |
|
| 301 | + <g class="backflush-arrows"> |
|
| 302 | + <path d="M60 10 A50 50 0 0 1 110 60"/> |
|
| 303 | + <path d="M60 110 A50 50 0 0 1 10 60"/> |
|
| 304 | + <path class="arrowhead" d="M102 54 L118 54 L110 66 Z"/> |
|
| 305 | + <path class="arrowhead" d="M2 66 L18 66 L10 54 Z"/> |
|
| 306 | + </g> |
|
| 307 | + <path class="droplet" d="M60 28 C60 28 36 56 36 72 A24 24 0 0 0 84 72 C84 56 60 28 60 28 Z"/> |
|
| 308 | + <circle class="droplet-eye" cx="52" cy="70" r="3"/> |
|
| 309 | + <circle class="droplet-eye" cx="68" cy="70" r="3"/> |
|
| 310 | + <path class="droplet-smile" d="M52 80 q8 7 16 0"/> |
|
| 311 | + <circle class="bubble b1" cx="44" cy="102" r="4"/> |
|
| 312 | + <circle class="bubble b2" cx="60" cy="104" r="3"/> |
|
| 313 | + <circle class="bubble b3" cx="76" cy="102" r="4"/> |
|
| 314 | + </svg> |
|
| 315 | + <p>Backflush in progress…</p> |
|
| 316 | + </div>`; |
|
| 317 | + } |
|
| 318 | + |
|
| 319 | + function SensorReadings({ temps }) { |
|
| 320 | + return `<div class="readings"> |
|
| 321 | + <div class="readings-header"> |
|
| 322 | + <span class="field-label">Sensors</span> |
|
| 323 | + <span class="setup-state">${esc(temps.setup_state.replace(/_/g, " "))}</span> |
|
| 324 | + </div> |
|
| 325 | + <ul class="probe-list"> |
|
| 326 | + ${PROBES.map((p) => ProbeRow({ label: p.label, value: temps.probes[p.key], missingStatus: "error" })).join("")} |
|
| 327 | + </ul> |
|
| 328 | + </div>`; |
|
| 329 | + } |
|
| 330 | + |
|
| 331 | + function ProbeRow({ label, value, missingStatus }) { |
|
| 332 | + return `<li class="probe-row"> |
|
| 333 | + ${StatusLight({ status: has(value) ? "on" : missingStatus, label })} |
|
| 334 | + <span class="probe-value">${fmtF(value)}</span> |
|
| 335 | + </li>`; |
|
| 336 | + } |
|
| 337 | + |
|
| 338 | + /* ----- Sensor setup wizard ----- */ |
|
| 339 | + |
|
| 340 | + function SensorSetupPage(s) { |
|
| 341 | + const steps = { 1: SetupStep1, 2: SetupStep2, 3: SetupStep3 }; |
|
| 342 | + return `<section class="card setup-card"> |
|
| 343 | + <h1>Sensor setup</h1> |
|
| 344 | + ${Stepper({ step: s.route.step })} |
|
| 345 | + ${steps[s.route.step](s)} |
|
| 346 | + </section>`; |
|
| 347 | + } |
|
| 348 | + |
|
| 349 | + function Stepper({ step }) { |
|
| 350 | + const labels = ["Disconnect", "First set", "Second set"]; |
|
| 351 | + return `<ol class="stepper"> |
|
| 352 | + ${labels.map((l, i) => `<li class="${i + 1 < step ? "done" : i + 1 === step ? "current" : ""}">${l}</li>`).join("")} |
|
| 353 | + </ol>`; |
|
| 354 | + } |
|
| 355 | + |
|
| 356 | + function SetupStep1({ temps }) { |
|
| 357 | + const connected = PROBES.filter((p) => has(temps.probes[p.key])).length; |
|
| 358 | + const status = connected |
|
| 359 | + ? StatusLight({ status: "waiting", label: `${connected} sensor${connected === 1 ? "" : "s"} still connected` }) |
|
| 360 | + : StatusLight({ status: "on", label: "All sensors disconnected" }); |
|
| 361 | + return `<div class="setup-step"> |
|
| 362 | + <h2>Disconnect all sensors</h2> |
|
| 363 | + <p>Unplug both sensor bundles from the enclosure: the hot-input probe and the four water probes.</p> |
|
| 364 | + <div class="setup-status">${status}</div> |
|
| 365 | + <div class="actions"> |
|
| 366 | + <button class="btn btn-secondary" data-action="setup-cancel">Cancel</button> |
|
| 367 | + <button class="btn btn-primary" data-action="setup-continue-1">Continue</button> |
|
| 368 | + </div> |
|
| 369 | + </div>`; |
|
| 370 | + } |
|
| 371 | + |
|
| 372 | + function SetupStep2({ temps }) { |
|
| 373 | + const hot = has(temps.probes.hot_in); |
|
| 374 | + const water = WATER_KEYS.some((k) => has(temps.probes[k])); |
|
| 375 | + let label = "Waiting for readings…"; |
|
| 376 | + if (hot && water) label = "Both sets detected"; |
|
| 377 | + else if (hot) label = "Hot-input sensor detected"; |
|
| 378 | + else if (water) label = "Water sensors detected"; |
|
| 379 | + return `<div class="setup-step"> |
|
| 380 | + <h2>Plug in one set of sensors</h2> |
|
| 381 | + <p>Connect either the hot-input bundle or the water bundle. Continue unlocks once readings arrive.</p> |
|
| 382 | + <div class="setup-status">${StatusLight({ status: hot || water ? "on" : "waiting", label })}</div> |
|
| 383 | + <div class="actions"> |
|
| 384 | + <button class="btn btn-secondary" data-action="setup-cancel">Cancel</button> |
|
| 385 | + <button class="btn btn-primary" data-action="setup-continue-2"${attr(!(hot || water), "disabled")}>Continue</button> |
|
| 386 | + </div> |
|
| 387 | + </div>`; |
|
| 388 | + } |
|
| 389 | + |
|
| 390 | + function SetupStep3({ temps, firstBundle }) { |
|
| 391 | + const remaining = { |
|
| 392 | + hot: "the water sensor bundle (4 probes)", |
|
| 393 | + water: "the hot-input sensor", |
|
| 394 | + }[firstBundle] || "any remaining sensors"; |
|
| 395 | + const all = PROBES.every((p) => has(temps.probes[p.key])); |
|
| 396 | + return `<div class="setup-step"> |
|
| 397 | + <h2>Plug in the other set</h2> |
|
| 398 | + <p>Now connect ${remaining}. Finish unlocks once every sensor is reporting.</p> |
|
| 399 | + <ul class="probe-list"> |
|
| 400 | + ${PROBES.map((p) => ProbeRow({ label: p.label, value: temps.probes[p.key], missingStatus: "waiting" })).join("")} |
|
| 401 | + </ul> |
|
| 402 | + <div class="actions"> |
|
| 403 | + <button class="btn btn-secondary" data-action="setup-cancel">Cancel</button> |
|
| 404 | + <button class="btn btn-primary" data-action="setup-finish"${attr(!all, "disabled")}>Finish</button> |
|
| 405 | + </div> |
|
| 406 | + </div>`; |
|
| 407 | + } |
|
| 408 | + |
|
| 409 | + /* ---------- events ---------- */ |
|
| 410 | + |
|
| 411 | + function command(op, args) { |
|
| 412 | + return api.sendCommand(op, args).then( |
|
| 413 | + () => true, |
|
| 414 | + (err) => { |
|
| 415 | + console.warn(`command ${op} failed`, err); |
|
| 416 | + return false; |
|
| 417 | + }, |
|
| 418 | + ); |
|
| 419 | + } |
|
| 420 | + |
|
| 421 | + root.addEventListener("click", (e) => { |
|
| 422 | + const el = e.target.closest("[data-action]"); |
|
| 423 | + if (!el || el.disabled || el.tagName === "SELECT") return; |
|
| 424 | + switch (el.dataset.action) { |
|
| 425 | + case "target-inc": |
|
| 426 | + command("target_temp_increment"); |
|
| 427 | + break; |
|
| 428 | + case "target-dec": |
|
| 429 | + command("target_temp_decrement"); |
|
| 430 | + break; |
|
| 431 | + case "reset-error": |
|
| 432 | + command("set_heat_mode", { mode: "sleep" }); |
|
| 433 | + break; |
|
| 434 | + case "backflush": |
|
| 435 | + setState({ backflushRequested: true }); |
|
| 436 | + command("set_heat_mode", { mode: "backflush" }).then((ok) => { |
|
| 437 | + if (!ok) setState({ backflushRequested: false }); |
|
| 438 | + }); |
|
| 439 | + break; |
|
| 440 | + case "start-sensor-setup": |
|
| 441 | + navigate("#/diagnostic/sensor-setup/1"); |
|
| 442 | + break; |
|
| 443 | + case "setup-continue-1": |
|
| 444 | + api.startSensorSetup().then(() => navigate("#/diagnostic/sensor-setup/2")); |
|
| 445 | + break; |
|
| 446 | + case "setup-continue-2": { |
|
| 447 | + const { probes } = state.temps; |
|
| 448 | + const hot = has(probes.hot_in); |
|
| 449 | + const water = WATER_KEYS.some((k) => has(probes[k])); |
|
| 450 | + setState({ firstBundle: hot && water ? null : hot ? "hot" : "water" }); |
|
| 451 | + navigate("#/diagnostic/sensor-setup/3"); |
|
| 452 | + break; |
|
| 453 | + } |
|
| 454 | + case "setup-finish": |
|
| 455 | + api.finishSensorSetup().then(() => navigate("#/diagnostic")); |
|
| 456 | + break; |
|
| 457 | + case "setup-cancel": |
|
| 458 | + api.cancelSensorSetup().finally(() => navigate("#/diagnostic")); |
|
| 459 | + break; |
|
| 460 | + } |
|
| 461 | + }); |
|
| 462 | + |
|
| 463 | + root.addEventListener("change", (e) => { |
|
| 464 | + const { action } = e.target.dataset; |
|
| 465 | + if (action === "set-heat-mode") command("set_heat_mode", { mode: e.target.value }); |
|
| 466 | + if (action === "set-filter-mode") command("set_filter_mode", { mode: e.target.value }); |
|
| 467 | + }); |
|
| 468 | + |
|
| 469 | + api.connect({ |
|
| 470 | + onOpen: () => setState({ connection: "open" }), |
|
| 471 | + onError: () => setState({ connection: "reconnecting" }), |
|
| 472 | + onMessage: (event, data) => { |
|
| 473 | + if (event === "temps.reading") setState({ temps: data }); |
|
| 474 | + if (event === "control.state") { |
|
| 475 | + setState({ |
|
| 476 | + control: data, |
|
| 477 | + backflushRequested: state.backflushRequested && !isBackflushing(data), |
|
| 478 | + }); |
|
| 479 | + } |
|
| 480 | + }, |
|
| 481 | + }); |
|
| 482 | + |
|
| 483 | + /* ---------- rendering ---------- */ |
|
| 484 | + |
|
| 485 | + // Stand-in for React reconciliation: unchanged nodes stay in place so CSS |
|
| 486 | + // animations (fans, waves, duck) don't restart on every SSE update. |
|
| 487 | + function patch(oldNode, newNode) { |
|
| 488 | + if (oldNode.nodeType !== newNode.nodeType || oldNode.nodeName !== newNode.nodeName) { |
|
| 489 | + oldNode.replaceWith(newNode); |
|
| 490 | + return; |
|
| 491 | + } |
|
| 492 | + if (oldNode.nodeType !== Node.ELEMENT_NODE) { |
|
| 493 | + if (oldNode.nodeValue !== newNode.nodeValue) oldNode.nodeValue = newNode.nodeValue; |
|
| 494 | + return; |
|
| 495 | + } |
|
| 496 | + const selectedValue = |
|
| 497 | + oldNode.tagName === "SELECT" ? newNode.querySelector("option[selected]")?.value : undefined; |
|
| 498 | + |
|
| 499 | + for (const { name } of [...oldNode.attributes]) { |
|
| 500 | + if (!newNode.hasAttribute(name)) oldNode.removeAttribute(name); |
|
| 501 | + } |
|
| 502 | + for (const { name, value } of [...newNode.attributes]) { |
|
| 503 | + if (oldNode.getAttribute(name) !== value) oldNode.setAttribute(name, value); |
|
| 504 | + } |
|
| 505 | + |
|
| 506 | + const oldKids = [...oldNode.childNodes]; |
|
| 507 | + const newKids = [...newNode.childNodes]; |
|
| 508 | + if (oldKids.length !== newKids.length) oldNode.replaceChildren(...newKids); |
|
| 509 | + else oldKids.forEach((child, i) => patch(child, newKids[i])); |
|
| 510 | + |
|
| 511 | + if (selectedValue !== undefined && oldNode.value !== selectedValue) oldNode.value = selectedValue; |
|
| 512 | + } |
|
| 513 | + |
|
| 514 | + function render() { |
|
| 515 | + const tpl = document.createElement("template"); |
|
| 516 | + tpl.innerHTML = App(state).trim(); |
|
| 517 | + const next = tpl.content.firstElementChild; |
|
| 518 | + if (root.firstElementChild) patch(root.firstElementChild, next); |
|
| 519 | + else root.appendChild(next); |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + render(); |
|
| 523 | +})(); |
projects/hot-tub/src/web/mockup/index.html
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +<!doctype html> |
|
| 2 | +<html lang="en"> |
|
| 3 | +<head> |
|
| 4 | + <meta charset="utf-8"> |
|
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1"> |
|
| 6 | + <title>Hot Tub</title> |
|
| 7 | + <link rel="stylesheet" href="styles.css"> |
|
| 8 | +</head> |
|
| 9 | +<body> |
|
| 10 | + <div id="root"></div> |
|
| 11 | + <script src="mock-backend.js"></script> |
|
| 12 | + <script src="app.js"></script> |
|
| 13 | +</body> |
|
| 14 | +</html> |
projects/hot-tub/src/web/mockup/mock-backend.js
| ... | ... | @@ -0,0 +1,294 @@ |
| 1 | +/* |
|
| 2 | + * Fake Web/API backend for the mockup. Exposes the surface the React app will |
|
| 3 | + * use against the real service: |
|
| 4 | + * HotTubApi.connect({ onOpen, onError, onMessage }) -> SSE GET /api/events |
|
| 5 | + * HotTubApi.sendCommand(op, args) -> POST /api/command |
|
| 6 | + * HotTubApi.startSensorSetup() / finishSensorSetup() / cancelSensorSetup() |
|
| 7 | + * -> POST /api/sensor-setup/* |
|
| 8 | + * Event payloads match Redis control:latest ("control.state") and |
|
| 9 | + * temps:latest ("temps.reading"). |
|
| 10 | + */ |
|
| 11 | +(function () { |
|
| 12 | + const TARGET_MIN = 50; |
|
| 13 | + const TARGET_MAX = 110; |
|
| 14 | + const BAND_F = 5; |
|
| 15 | + const BACKFLUSH_S = 10; |
|
| 16 | + const STARTUP_S = 3; |
|
| 17 | + const STARTUP_ATTEMPTS = 3; |
|
| 18 | + const FILTER_RUN_H = 2; |
|
| 19 | + const TUB_OFFSETS = [-0.4, 0.1, 0.3, -0.1]; |
|
| 20 | + |
|
| 21 | + const world = { |
|
| 22 | + hotBundle: true, |
|
| 23 | + waterBundle: true, |
|
| 24 | + tubF: 97.0, |
|
| 25 | + hotF: 97.4, |
|
| 26 | + noPropane: false, |
|
| 27 | + online: true, |
|
| 28 | + }; |
|
| 29 | + |
|
| 30 | + const control = { |
|
| 31 | + heat_mode: "maintain", |
|
| 32 | + filter_mode: "auto", |
|
| 33 | + target_f: 102, |
|
| 34 | + tub_avg_f: null, |
|
| 35 | + hot_in_f: null, |
|
| 36 | + heat_pump: "off", |
|
| 37 | + filter_pump: "off", |
|
| 38 | + heat_phase: "idle", |
|
| 39 | + startup_attempt: 0, |
|
| 40 | + error: null, |
|
| 41 | + last_command_id: null, |
|
| 42 | + filter_window_active: false, |
|
| 43 | + filter_next_start: null, |
|
| 44 | + }; |
|
| 45 | + |
|
| 46 | + let phaseTicks = 0; |
|
| 47 | + let commandSeq = 0; |
|
| 48 | + const subscribers = new Set(); |
|
| 49 | + |
|
| 50 | + const round1 = (n) => Math.round(n * 10) / 10; |
|
| 51 | + const nowIso = () => new Date().toISOString().replace(/\.\d+Z$/, "Z"); |
|
| 52 | + const clone = (o) => JSON.parse(JSON.stringify(o)); |
|
| 53 | + const clampTarget = (t) => Math.max(TARGET_MIN, Math.min(TARGET_MAX, t)); |
|
| 54 | + |
|
| 55 | + function tempsReading() { |
|
| 56 | + const tubs = TUB_OFFSETS.map((o) => (world.waterBundle ? round1(world.tubF + o) : null)); |
|
| 57 | + const hotIn = world.hotBundle ? round1(world.hotF) : null; |
|
| 58 | + const valid = tubs.filter((t) => t !== null); |
|
| 59 | + let setupState = "ready"; |
|
| 60 | + if (!world.hotBundle) setupState = "need_hot_in"; |
|
| 61 | + else if (!world.waterBundle) setupState = "need_water"; |
|
| 62 | + return { |
|
| 63 | + ts: nowIso(), |
|
| 64 | + setup_state: setupState, |
|
| 65 | + probes: { hot_in: hotIn, tub1: tubs[0], tub2: tubs[1], tub3: tubs[2], tub4: tubs[3] }, |
|
| 66 | + tub_avg_f: valid.length ? round1(valid.reduce((a, b) => a + b, 0) / valid.length) : null, |
|
| 67 | + hot_in_f: hotIn, |
|
| 68 | + ok_count: valid.length, |
|
| 69 | + fail_count: TUB_OFFSETS.length - valid.length, |
|
| 70 | + }; |
|
| 71 | + } |
|
| 72 | + |
|
| 73 | + function setPhase(phase) { |
|
| 74 | + control.heat_phase = phase; |
|
| 75 | + phaseTicks = 0; |
|
| 76 | + } |
|
| 77 | + |
|
| 78 | + function goIdle() { |
|
| 79 | + control.heat_pump = "off"; |
|
| 80 | + if (control.heat_phase !== "idle") setPhase("idle"); |
|
| 81 | + } |
|
| 82 | + |
|
| 83 | + function stepHeat(temps) { |
|
| 84 | + const tub = temps.tub_avg_f; |
|
| 85 | + |
|
| 86 | + if (control.heat_mode === "sleep") { |
|
| 87 | + goIdle(); |
|
| 88 | + control.startup_attempt = 0; |
|
| 89 | + return; |
|
| 90 | + } |
|
| 91 | + |
|
| 92 | + if (control.heat_mode === "backflush") { |
|
| 93 | + if (control.heat_phase !== "backflush") { |
|
| 94 | + setPhase("backflush"); |
|
| 95 | + control.heat_pump = "on"; |
|
| 96 | + return; |
|
| 97 | + } |
|
| 98 | + phaseTicks += 1; |
|
| 99 | + if (phaseTicks >= BACKFLUSH_S) { |
|
| 100 | + goIdle(); |
|
| 101 | + control.heat_mode = "sleep"; |
|
| 102 | + } |
|
| 103 | + return; |
|
| 104 | + } |
|
| 105 | + |
|
| 106 | + if (control.heat_phase.startsWith("error_")) { |
|
| 107 | + control.heat_pump = "off"; |
|
| 108 | + return; |
|
| 109 | + } |
|
| 110 | + if (tub === null || tub >= control.target_f) { |
|
| 111 | + goIdle(); |
|
| 112 | + control.startup_attempt = 0; |
|
| 113 | + return; |
|
| 114 | + } |
|
| 115 | + if (control.heat_phase === "idle") { |
|
| 116 | + if (tub <= control.target_f - BAND_F) { |
|
| 117 | + control.startup_attempt = 1; |
|
| 118 | + setPhase("startup_1"); |
|
| 119 | + control.heat_pump = "on"; |
|
| 120 | + } |
|
| 121 | + return; |
|
| 122 | + } |
|
| 123 | + if (control.heat_phase.startsWith("startup_")) { |
|
| 124 | + phaseTicks += 1; |
|
| 125 | + if (phaseTicks < STARTUP_S) return; |
|
| 126 | + if (!world.noPropane && temps.hot_in_f !== null) { |
|
| 127 | + setPhase("heating"); |
|
| 128 | + control.startup_attempt = 0; |
|
| 129 | + return; |
|
| 130 | + } |
|
| 131 | + if (control.startup_attempt >= STARTUP_ATTEMPTS) { |
|
| 132 | + control.heat_pump = "off"; |
|
| 133 | + setPhase("error_startup"); |
|
| 134 | + control.error = { |
|
| 135 | + code: "startup_failed", |
|
| 136 | + detail: "Heater did not warm up after 3 tries. Propane may be empty, or a backflush may be needed.", |
|
| 137 | + }; |
|
| 138 | + return; |
|
| 139 | + } |
|
| 140 | + control.startup_attempt += 1; |
|
| 141 | + setPhase(`startup_${control.startup_attempt}`); |
|
| 142 | + } |
|
| 143 | + } |
|
| 144 | + |
|
| 145 | + function stepFilter() { |
|
| 146 | + if (control.filter_mode !== "auto") { |
|
| 147 | + control.filter_pump = control.filter_mode === "on" ? "on" : "off"; |
|
| 148 | + control.filter_window_active = false; |
|
| 149 | + control.filter_next_start = null; |
|
| 150 | + return; |
|
| 151 | + } |
|
| 152 | + const now = new Date(); |
|
| 153 | + const start = new Date(now); |
|
| 154 | + start.setHours(0, 0, 0, 0); |
|
| 155 | + const end = new Date(start.getTime() + FILTER_RUN_H * 3600e3); |
|
| 156 | + const active = now >= start && now < end; |
|
| 157 | + const next = new Date(start); |
|
| 158 | + if (now >= start) next.setDate(next.getDate() + 1); |
|
| 159 | + control.filter_pump = active ? "on" : "off"; |
|
| 160 | + control.filter_window_active = active; |
|
| 161 | + control.filter_next_start = active ? null : next.toISOString(); |
|
| 162 | + } |
|
| 163 | + |
|
| 164 | + function publish(temps = tempsReading()) { |
|
| 165 | + if (!world.online) return; |
|
| 166 | + control.tub_avg_f = temps.tub_avg_f; |
|
| 167 | + control.hot_in_f = temps.hot_in_f; |
|
| 168 | + const snapshot = { ts: nowIso(), ...control }; |
|
| 169 | + subscribers.forEach((s) => { |
|
| 170 | + s.onMessage("temps.reading", clone(temps)); |
|
| 171 | + s.onMessage("control.state", clone(snapshot)); |
|
| 172 | + }); |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + function tick() { |
|
| 176 | + const flame = |
|
| 177 | + control.heat_pump === "on" && !world.noPropane && control.heat_phase !== "backflush"; |
|
| 178 | + world.tubF += flame ? 0.15 : -0.02; |
|
| 179 | + const hotTarget = flame ? world.tubF + 14 : world.tubF; |
|
| 180 | + world.hotF += (hotTarget - world.hotF) * 0.35; |
|
| 181 | + |
|
| 182 | + const temps = tempsReading(); |
|
| 183 | + stepHeat(temps); |
|
| 184 | + stepFilter(); |
|
| 185 | + publish(temps); |
|
| 186 | + } |
|
| 187 | + |
|
| 188 | + function delay(fn) { |
|
| 189 | + return new Promise((resolve, reject) => { |
|
| 190 | + setTimeout(() => { |
|
| 191 | + if (!world.online) { |
|
| 192 | + reject(new Error("offline")); |
|
| 193 | + return; |
|
| 194 | + } |
|
| 195 | + try { |
|
| 196 | + resolve(fn()); |
|
| 197 | + } catch (err) { |
|
| 198 | + reject(err); |
|
| 199 | + } |
|
| 200 | + }, 150); |
|
| 201 | + }); |
|
| 202 | + } |
|
| 203 | + |
|
| 204 | + function sendCommand(op, args = {}) { |
|
| 205 | + return delay(() => { |
|
| 206 | + control.last_command_id = `mock-${++commandSeq}`; |
|
| 207 | + switch (op) { |
|
| 208 | + case "set_heat_mode": |
|
| 209 | + control.heat_mode = args.mode; |
|
| 210 | + if (args.mode === "sleep") { |
|
| 211 | + control.error = null; |
|
| 212 | + if (control.heat_phase.startsWith("error_")) setPhase("idle"); |
|
| 213 | + } |
|
| 214 | + stepHeat(tempsReading()); |
|
| 215 | + break; |
|
| 216 | + case "set_target_temp": |
|
| 217 | + control.target_f = clampTarget(Number(args.target_f)); |
|
| 218 | + break; |
|
| 219 | + case "target_temp_increment": |
|
| 220 | + control.target_f = clampTarget(control.target_f + (args.delta_f ?? 1)); |
|
| 221 | + break; |
|
| 222 | + case "target_temp_decrement": |
|
| 223 | + control.target_f = clampTarget(control.target_f - (args.delta_f ?? 1)); |
|
| 224 | + break; |
|
| 225 | + case "set_filter_mode": |
|
| 226 | + control.filter_mode = args.mode; |
|
| 227 | + stepFilter(); |
|
| 228 | + break; |
|
| 229 | + default: |
|
| 230 | + throw new Error(`unknown op ${op}`); |
|
| 231 | + } |
|
| 232 | + publish(); |
|
| 233 | + return { id: control.last_command_id }; |
|
| 234 | + }); |
|
| 235 | + } |
|
| 236 | + |
|
| 237 | + function connect(handlers) { |
|
| 238 | + subscribers.add(handlers); |
|
| 239 | + if (world.online) { |
|
| 240 | + handlers.onOpen?.(); |
|
| 241 | + publish(); |
|
| 242 | + } |
|
| 243 | + return () => subscribers.delete(handlers); |
|
| 244 | + } |
|
| 245 | + |
|
| 246 | + function setOnline(online) { |
|
| 247 | + world.online = online; |
|
| 248 | + subscribers.forEach((s) => (online ? s.onOpen?.() : s.onError?.())); |
|
| 249 | + if (online) publish(); |
|
| 250 | + } |
|
| 251 | + |
|
| 252 | + function mountPanel() { |
|
| 253 | + const panel = document.createElement("details"); |
|
| 254 | + panel.className = "mock-panel"; |
|
| 255 | + panel.innerHTML = ` |
|
| 256 | + <summary>Mock backend</summary> |
|
| 257 | + <div class="mock-panel-body"> |
|
| 258 | + <label><input type="checkbox" data-mock="hot" checked> Hot-input bundle plugged in</label> |
|
| 259 | + <label><input type="checkbox" data-mock="water" checked> Water bundle plugged in</label> |
|
| 260 | + <label><input type="checkbox" data-mock="no-propane"> Empty propane</label> |
|
| 261 | + <label><input type="checkbox" data-mock="offline"> Drop connection</label> |
|
| 262 | + <button type="button" data-mock="cool">Cool tub 5°F</button> |
|
| 263 | + </div>`; |
|
| 264 | + panel.addEventListener("change", (e) => { |
|
| 265 | + const { mock } = e.target.dataset; |
|
| 266 | + const on = e.target.checked; |
|
| 267 | + if (mock === "hot") world.hotBundle = on; |
|
| 268 | + if (mock === "water") world.waterBundle = on; |
|
| 269 | + if (mock === "no-propane") world.noPropane = on; |
|
| 270 | + if (mock === "offline") setOnline(!on); |
|
| 271 | + else publish(); |
|
| 272 | + }); |
|
| 273 | + panel.addEventListener("click", (e) => { |
|
| 274 | + if (e.target.dataset.mock === "cool") { |
|
| 275 | + world.tubF -= 5; |
|
| 276 | + world.hotF -= 5; |
|
| 277 | + publish(); |
|
| 278 | + } |
|
| 279 | + }); |
|
| 280 | + document.body.appendChild(panel); |
|
| 281 | + } |
|
| 282 | + |
|
| 283 | + stepFilter(); |
|
| 284 | + setInterval(tick, 1000); |
|
| 285 | + mountPanel(); |
|
| 286 | + |
|
| 287 | + window.HotTubApi = { |
|
| 288 | + connect, |
|
| 289 | + sendCommand, |
|
| 290 | + startSensorSetup: () => delay(() => ({ ok: true })), |
|
| 291 | + finishSensorSetup: () => delay(() => ({ ok: true })), |
|
| 292 | + cancelSensorSetup: () => delay(() => ({ ok: true })), |
|
| 293 | + }; |
|
| 294 | +})(); |
projects/hot-tub/src/web/mockup/styles.css
| ... | ... | @@ -0,0 +1,803 @@ |
| 1 | +:root { |
|
| 2 | + --bg-top: #e0f2fe; |
|
| 3 | + --bg-bottom: #f8fafc; |
|
| 4 | + --card: #ffffff; |
|
| 5 | + --text: #0f172a; |
|
| 6 | + --muted: #64748b; |
|
| 7 | + --border: #e2e8f0; |
|
| 8 | + --surface: #f8fafc; |
|
| 9 | + --accent: #0e7490; |
|
| 10 | + --accent-strong: #155e75; |
|
| 11 | + --heat: #f97316; |
|
| 12 | + --filter: #0ea5e9; |
|
| 13 | + --green: #22c55e; |
|
| 14 | + --yellow: #eab308; |
|
| 15 | + --red: #ef4444; |
|
| 16 | + --off: #cbd5e1; |
|
| 17 | + --radius: 18px; |
|
| 18 | + --shadow: 0 6px 24px rgba(15, 23, 42, 0.08); |
|
| 19 | +} |
|
| 20 | + |
|
| 21 | +* { |
|
| 22 | + box-sizing: border-box; |
|
| 23 | +} |
|
| 24 | + |
|
| 25 | +body { |
|
| 26 | + margin: 0; |
|
| 27 | + min-height: 100vh; |
|
| 28 | + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; |
|
| 29 | + color: var(--text); |
|
| 30 | + background: linear-gradient(var(--bg-top), var(--bg-bottom)) fixed; |
|
| 31 | +} |
|
| 32 | + |
|
| 33 | +/* ---------- header / nav ---------- */ |
|
| 34 | + |
|
| 35 | +.app-header { |
|
| 36 | + position: sticky; |
|
| 37 | + top: 0; |
|
| 38 | + z-index: 10; |
|
| 39 | + display: flex; |
|
| 40 | + flex-wrap: wrap; |
|
| 41 | + align-items: center; |
|
| 42 | + gap: 12px 20px; |
|
| 43 | + padding: 12px 20px; |
|
| 44 | + background: rgba(255, 255, 255, 0.85); |
|
| 45 | + backdrop-filter: blur(8px); |
|
| 46 | + border-bottom: 1px solid var(--border); |
|
| 47 | +} |
|
| 48 | + |
|
| 49 | +.brand { |
|
| 50 | + display: flex; |
|
| 51 | + align-items: center; |
|
| 52 | + gap: 8px; |
|
| 53 | + font-size: 1.25rem; |
|
| 54 | + font-weight: 700; |
|
| 55 | +} |
|
| 56 | + |
|
| 57 | +.brand-icon { |
|
| 58 | + color: var(--heat); |
|
| 59 | + font-size: 1.4rem; |
|
| 60 | +} |
|
| 61 | + |
|
| 62 | +.nav { |
|
| 63 | + display: flex; |
|
| 64 | + gap: 4px; |
|
| 65 | + padding: 4px; |
|
| 66 | + border-radius: 999px; |
|
| 67 | + background: #f1f5f9; |
|
| 68 | +} |
|
| 69 | + |
|
| 70 | +.nav-tab { |
|
| 71 | + padding: 8px 16px; |
|
| 72 | + border-radius: 999px; |
|
| 73 | + color: var(--muted); |
|
| 74 | + font-weight: 600; |
|
| 75 | + text-decoration: none; |
|
| 76 | +} |
|
| 77 | + |
|
| 78 | +.nav-tab.active { |
|
| 79 | + background: var(--card); |
|
| 80 | + color: var(--accent-strong); |
|
| 81 | + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12); |
|
| 82 | +} |
|
| 83 | + |
|
| 84 | +.connection { |
|
| 85 | + margin-left: auto; |
|
| 86 | + font-size: 0.85rem; |
|
| 87 | +} |
|
| 88 | + |
|
| 89 | +@media (max-width: 599px) { |
|
| 90 | + .app-header { |
|
| 91 | + padding: 10px 12px; |
|
| 92 | + } |
|
| 93 | + .nav { |
|
| 94 | + order: 3; |
|
| 95 | + width: 100%; |
|
| 96 | + } |
|
| 97 | + .nav-tab { |
|
| 98 | + flex: 1; |
|
| 99 | + text-align: center; |
|
| 100 | + } |
|
| 101 | +} |
|
| 102 | + |
|
| 103 | +/* ---------- layout ---------- */ |
|
| 104 | + |
|
| 105 | +.page { |
|
| 106 | + max-width: 1100px; |
|
| 107 | + margin: 0 auto; |
|
| 108 | + padding: 20px; |
|
| 109 | + transition: opacity 0.3s; |
|
| 110 | +} |
|
| 111 | + |
|
| 112 | +.page.stale { |
|
| 113 | + opacity: 0.55; |
|
| 114 | +} |
|
| 115 | + |
|
| 116 | +@media (max-width: 599px) { |
|
| 117 | + .page { |
|
| 118 | + padding: 12px; |
|
| 119 | + } |
|
| 120 | +} |
|
| 121 | + |
|
| 122 | +.card { |
|
| 123 | + padding: 20px; |
|
| 124 | + border-radius: var(--radius); |
|
| 125 | + background: var(--card); |
|
| 126 | + box-shadow: var(--shadow); |
|
| 127 | +} |
|
| 128 | + |
|
| 129 | +.loading { |
|
| 130 | + padding: 60px 20px; |
|
| 131 | + color: var(--muted); |
|
| 132 | + text-align: center; |
|
| 133 | +} |
|
| 134 | + |
|
| 135 | +.control-layout, |
|
| 136 | +.diagnostic-layout { |
|
| 137 | + display: grid; |
|
| 138 | + gap: 20px; |
|
| 139 | +} |
|
| 140 | + |
|
| 141 | +@media (min-width: 900px) { |
|
| 142 | + .control-layout { |
|
| 143 | + grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); |
|
| 144 | + align-items: start; |
|
| 145 | + } |
|
| 146 | +} |
|
| 147 | + |
|
| 148 | +@media (min-width: 760px) { |
|
| 149 | + .diagnostic-layout { |
|
| 150 | + grid-template-columns: 1fr 1fr; |
|
| 151 | + align-items: start; |
|
| 152 | + } |
|
| 153 | +} |
|
| 154 | + |
|
| 155 | +/* ---------- status lights ---------- */ |
|
| 156 | + |
|
| 157 | +.status-light { |
|
| 158 | + display: inline-flex; |
|
| 159 | + align-items: center; |
|
| 160 | + gap: 8px; |
|
| 161 | + color: var(--muted); |
|
| 162 | +} |
|
| 163 | + |
|
| 164 | +.light { |
|
| 165 | + flex-shrink: 0; |
|
| 166 | + width: 14px; |
|
| 167 | + height: 14px; |
|
| 168 | + border-radius: 50%; |
|
| 169 | + background: var(--off); |
|
| 170 | +} |
|
| 171 | + |
|
| 172 | +.light-on { |
|
| 173 | + background: var(--green); |
|
| 174 | + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2), 0 0 12px var(--green); |
|
| 175 | +} |
|
| 176 | + |
|
| 177 | +.light-waiting { |
|
| 178 | + background: var(--yellow); |
|
| 179 | + animation: pulse 1.2s ease-in-out infinite; |
|
| 180 | +} |
|
| 181 | + |
|
| 182 | +.light-error { |
|
| 183 | + background: var(--red); |
|
| 184 | +} |
|
| 185 | + |
|
| 186 | +/* ---------- tub ---------- */ |
|
| 187 | + |
|
| 188 | +.banner { |
|
| 189 | + display: flex; |
|
| 190 | + flex-wrap: wrap; |
|
| 191 | + align-items: center; |
|
| 192 | + justify-content: space-between; |
|
| 193 | + gap: 8px 12px; |
|
| 194 | + margin-bottom: 12px; |
|
| 195 | + padding: 12px 14px; |
|
| 196 | + border-radius: 12px; |
|
| 197 | +} |
|
| 198 | + |
|
| 199 | +.banner-error { |
|
| 200 | + border: 1px solid #fecaca; |
|
| 201 | + background: #fef2f2; |
|
| 202 | + color: #991b1b; |
|
| 203 | +} |
|
| 204 | + |
|
| 205 | +.tub { |
|
| 206 | + display: block; |
|
| 207 | + width: 100%; |
|
| 208 | + max-width: 560px; |
|
| 209 | + margin: 0 auto; |
|
| 210 | +} |
|
| 211 | + |
|
| 212 | +.ground { |
|
| 213 | + fill: rgba(15, 23, 42, 0.08); |
|
| 214 | +} |
|
| 215 | + |
|
| 216 | +.tub-shell { |
|
| 217 | + fill: #475569; |
|
| 218 | +} |
|
| 219 | + |
|
| 220 | +.tub-wall { |
|
| 221 | + fill: none; |
|
| 222 | + stroke: #94a3b8; |
|
| 223 | + stroke-width: 10; |
|
| 224 | + stroke-linejoin: round; |
|
| 225 | +} |
|
| 226 | + |
|
| 227 | +.tub-rim { |
|
| 228 | + stroke: #64748b; |
|
| 229 | + stroke-width: 1.5; |
|
| 230 | +} |
|
| 231 | + |
|
| 232 | +.rivet { |
|
| 233 | + fill: #64748b; |
|
| 234 | +} |
|
| 235 | + |
|
| 236 | +.tub-temp { |
|
| 237 | + font-size: 52px; |
|
| 238 | + font-weight: 800; |
|
| 239 | + fill: #fff; |
|
| 240 | + paint-order: stroke; |
|
| 241 | + stroke: rgba(3, 105, 161, 0.6); |
|
| 242 | + stroke-width: 4px; |
|
| 243 | +} |
|
| 244 | + |
|
| 245 | +.tub-temp-caption { |
|
| 246 | + font-size: 15px; |
|
| 247 | + letter-spacing: 0.2em; |
|
| 248 | + text-transform: uppercase; |
|
| 249 | + fill: #e0f2fe; |
|
| 250 | +} |
|
| 251 | + |
|
| 252 | +.wave { |
|
| 253 | + animation: wave 4s linear infinite; |
|
| 254 | +} |
|
| 255 | + |
|
| 256 | +.wave-back { |
|
| 257 | + opacity: 0.8; |
|
| 258 | + animation-duration: 6s; |
|
| 259 | + animation-direction: reverse; |
|
| 260 | +} |
|
| 261 | + |
|
| 262 | +.duck-bob { |
|
| 263 | + transform-box: fill-box; |
|
| 264 | + transform-origin: center; |
|
| 265 | + animation: bob 2.4s ease-in-out infinite; |
|
| 266 | +} |
|
| 267 | + |
|
| 268 | +.steam { |
|
| 269 | + opacity: 0; |
|
| 270 | + transition: opacity 1s; |
|
| 271 | +} |
|
| 272 | + |
|
| 273 | +.steam.on { |
|
| 274 | + opacity: 1; |
|
| 275 | +} |
|
| 276 | + |
|
| 277 | +.steam-puff { |
|
| 278 | + fill: none; |
|
| 279 | + stroke: #cbd5e1; |
|
| 280 | + stroke-width: 5; |
|
| 281 | + stroke-linecap: round; |
|
| 282 | + animation: rise 3s ease-in-out infinite; |
|
| 283 | +} |
|
| 284 | + |
|
| 285 | +.steam-puff:nth-child(2) { |
|
| 286 | + animation-delay: 1s; |
|
| 287 | +} |
|
| 288 | + |
|
| 289 | +.steam-puff:nth-child(3) { |
|
| 290 | + animation-delay: 2s; |
|
| 291 | +} |
|
| 292 | + |
|
| 293 | +/* ---------- pumps ---------- */ |
|
| 294 | + |
|
| 295 | +.pumps { |
|
| 296 | + display: grid; |
|
| 297 | + grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
| 298 | + gap: 16px; |
|
| 299 | + max-width: 420px; |
|
| 300 | + margin: 8px auto 0; |
|
| 301 | +} |
|
| 302 | + |
|
| 303 | +.pump { |
|
| 304 | + display: flex; |
|
| 305 | + flex-direction: column; |
|
| 306 | + align-items: center; |
|
| 307 | + gap: 6px; |
|
| 308 | +} |
|
| 309 | + |
|
| 310 | +.pump-svg { |
|
| 311 | + width: 96px; |
|
| 312 | + height: 96px; |
|
| 313 | +} |
|
| 314 | + |
|
| 315 | +.pump-frame { |
|
| 316 | + fill: #f1f5f9; |
|
| 317 | + stroke: var(--border); |
|
| 318 | + stroke-width: 2; |
|
| 319 | +} |
|
| 320 | + |
|
| 321 | +.pump-housing { |
|
| 322 | + fill: #1e293b; |
|
| 323 | +} |
|
| 324 | + |
|
| 325 | +.pump-hub { |
|
| 326 | + fill: #e2e8f0; |
|
| 327 | +} |
|
| 328 | + |
|
| 329 | +.fan-blades { |
|
| 330 | + opacity: 0.5; |
|
| 331 | + transform-box: view-box; |
|
| 332 | + transform-origin: 60px 60px; |
|
| 333 | + transition: opacity 0.3s; |
|
| 334 | +} |
|
| 335 | + |
|
| 336 | +.pump-heat .fan-blades { |
|
| 337 | + fill: var(--heat); |
|
| 338 | +} |
|
| 339 | + |
|
| 340 | +.pump-filter .fan-blades { |
|
| 341 | + fill: var(--filter); |
|
| 342 | +} |
|
| 343 | + |
|
| 344 | +.pump.running .fan-blades { |
|
| 345 | + opacity: 1; |
|
| 346 | + animation: spin 0.9s linear infinite; |
|
| 347 | +} |
|
| 348 | + |
|
| 349 | +.pump-label { |
|
| 350 | + font-weight: 700; |
|
| 351 | +} |
|
| 352 | + |
|
| 353 | +@media (max-width: 400px) { |
|
| 354 | + .pump-svg { |
|
| 355 | + width: 80px; |
|
| 356 | + height: 80px; |
|
| 357 | + } |
|
| 358 | +} |
|
| 359 | + |
|
| 360 | +/* ---------- settings ---------- */ |
|
| 361 | + |
|
| 362 | +.settings-card { |
|
| 363 | + display: flex; |
|
| 364 | + flex-direction: column; |
|
| 365 | + gap: 22px; |
|
| 366 | +} |
|
| 367 | + |
|
| 368 | +.field-label { |
|
| 369 | + display: block; |
|
| 370 | + margin-bottom: 6px; |
|
| 371 | + color: var(--muted); |
|
| 372 | + font-size: 0.8rem; |
|
| 373 | + font-weight: 700; |
|
| 374 | + letter-spacing: 0.06em; |
|
| 375 | + text-transform: uppercase; |
|
| 376 | +} |
|
| 377 | + |
|
| 378 | +.target-row { |
|
| 379 | + display: flex; |
|
| 380 | + align-items: center; |
|
| 381 | + justify-content: space-between; |
|
| 382 | + gap: 12px; |
|
| 383 | +} |
|
| 384 | + |
|
| 385 | +.target-value { |
|
| 386 | + color: var(--accent-strong); |
|
| 387 | + font-size: 3.25rem; |
|
| 388 | + font-weight: 800; |
|
| 389 | + font-variant-numeric: tabular-nums; |
|
| 390 | +} |
|
| 391 | + |
|
| 392 | +.round-btn { |
|
| 393 | + width: 60px; |
|
| 394 | + height: 60px; |
|
| 395 | + border: none; |
|
| 396 | + border-radius: 50%; |
|
| 397 | + background: var(--accent); |
|
| 398 | + color: #fff; |
|
| 399 | + font-size: 2rem; |
|
| 400 | + line-height: 1; |
|
| 401 | + cursor: pointer; |
|
| 402 | + touch-action: manipulation; |
|
| 403 | +} |
|
| 404 | + |
|
| 405 | +.round-btn:active:not(:disabled) { |
|
| 406 | + transform: scale(0.94); |
|
| 407 | +} |
|
| 408 | + |
|
| 409 | +.modes { |
|
| 410 | + display: flex; |
|
| 411 | + flex-direction: column; |
|
| 412 | + gap: 14px; |
|
| 413 | +} |
|
| 414 | + |
|
| 415 | +.field select { |
|
| 416 | + width: 100%; |
|
| 417 | + padding: 12px 14px; |
|
| 418 | + border: 1px solid var(--border); |
|
| 419 | + border-radius: 12px; |
|
| 420 | + background: var(--surface); |
|
| 421 | + font: inherit; |
|
| 422 | + font-size: 1.05rem; |
|
| 423 | +} |
|
| 424 | + |
|
| 425 | +.summary { |
|
| 426 | + display: grid; |
|
| 427 | + gap: 10px; |
|
| 428 | + margin: 0; |
|
| 429 | +} |
|
| 430 | + |
|
| 431 | +.summary div { |
|
| 432 | + display: flex; |
|
| 433 | + justify-content: space-between; |
|
| 434 | + gap: 12px; |
|
| 435 | + padding-bottom: 8px; |
|
| 436 | + border-bottom: 1px dashed var(--border); |
|
| 437 | +} |
|
| 438 | + |
|
| 439 | +.summary dt { |
|
| 440 | + color: var(--muted); |
|
| 441 | +} |
|
| 442 | + |
|
| 443 | +.summary dd { |
|
| 444 | + margin: 0; |
|
| 445 | + font-weight: 600; |
|
| 446 | + text-align: right; |
|
| 447 | +} |
|
| 448 | + |
|
| 449 | +/* ---------- buttons ---------- */ |
|
| 450 | + |
|
| 451 | +.btn { |
|
| 452 | + display: inline-flex; |
|
| 453 | + align-items: center; |
|
| 454 | + justify-content: center; |
|
| 455 | + padding: 12px 22px; |
|
| 456 | + border: none; |
|
| 457 | + border-radius: 12px; |
|
| 458 | + font: inherit; |
|
| 459 | + font-weight: 700; |
|
| 460 | + cursor: pointer; |
|
| 461 | +} |
|
| 462 | + |
|
| 463 | +.btn-primary { |
|
| 464 | + background: var(--accent); |
|
| 465 | + color: #fff; |
|
| 466 | +} |
|
| 467 | + |
|
| 468 | +.btn-secondary { |
|
| 469 | + background: #f1f5f9; |
|
| 470 | + color: var(--text); |
|
| 471 | +} |
|
| 472 | + |
|
| 473 | +.btn-small { |
|
| 474 | + padding: 6px 12px; |
|
| 475 | + background: #991b1b; |
|
| 476 | + color: #fff; |
|
| 477 | + font-size: 0.875rem; |
|
| 478 | +} |
|
| 479 | + |
|
| 480 | +.big-button { |
|
| 481 | + width: 100%; |
|
| 482 | + min-height: 110px; |
|
| 483 | + border: none; |
|
| 484 | + border-radius: var(--radius); |
|
| 485 | + box-shadow: var(--shadow); |
|
| 486 | + color: #fff; |
|
| 487 | + font: inherit; |
|
| 488 | + font-size: 1.75rem; |
|
| 489 | + font-weight: 800; |
|
| 490 | + cursor: pointer; |
|
| 491 | +} |
|
| 492 | + |
|
| 493 | +.big-button-backflush { |
|
| 494 | + background: linear-gradient(135deg, #0ea5e9, #0369a1); |
|
| 495 | +} |
|
| 496 | + |
|
| 497 | +.big-button-setup { |
|
| 498 | + background: linear-gradient(135deg, #14b8a6, #0f766e); |
|
| 499 | +} |
|
| 500 | + |
|
| 501 | +.big-button:active:not(:disabled) { |
|
| 502 | + transform: translateY(1px); |
|
| 503 | +} |
|
| 504 | + |
|
| 505 | +.round-btn:disabled, |
|
| 506 | +.btn:disabled, |
|
| 507 | +.big-button:disabled { |
|
| 508 | + opacity: 0.4; |
|
| 509 | + cursor: not-allowed; |
|
| 510 | +} |
|
| 511 | + |
|
| 512 | +/* ---------- diagnostic ---------- */ |
|
| 513 | + |
|
| 514 | +.backflush-slot { |
|
| 515 | + display: grid; |
|
| 516 | + place-items: center; |
|
| 517 | + min-height: 190px; |
|
| 518 | + margin-top: 16px; |
|
| 519 | +} |
|
| 520 | + |
|
| 521 | +.hint { |
|
| 522 | + max-width: 320px; |
|
| 523 | + color: var(--muted); |
|
| 524 | + text-align: center; |
|
| 525 | +} |
|
| 526 | + |
|
| 527 | +.backflush-anim { |
|
| 528 | + color: var(--accent-strong); |
|
| 529 | + font-weight: 600; |
|
| 530 | + text-align: center; |
|
| 531 | +} |
|
| 532 | + |
|
| 533 | +.backflush-anim svg { |
|
| 534 | + width: 140px; |
|
| 535 | + height: 140px; |
|
| 536 | +} |
|
| 537 | + |
|
| 538 | +.backflush-anim p { |
|
| 539 | + margin: 6px 0 0; |
|
| 540 | +} |
|
| 541 | + |
|
| 542 | +.backflush-arrows { |
|
| 543 | + fill: none; |
|
| 544 | + stroke: var(--filter); |
|
| 545 | + stroke-width: 6; |
|
| 546 | + stroke-linecap: round; |
|
| 547 | + transform-box: view-box; |
|
| 548 | + transform-origin: 60px 60px; |
|
| 549 | + animation: spin 2s linear infinite; |
|
| 550 | +} |
|
| 551 | + |
|
| 552 | +.backflush-arrows .arrowhead { |
|
| 553 | + fill: var(--filter); |
|
| 554 | + stroke: none; |
|
| 555 | +} |
|
| 556 | + |
|
| 557 | +.droplet { |
|
| 558 | + fill: #38bdf8; |
|
| 559 | +} |
|
| 560 | + |
|
| 561 | +.droplet-eye { |
|
| 562 | + fill: var(--text); |
|
| 563 | +} |
|
| 564 | + |
|
| 565 | +.droplet-smile { |
|
| 566 | + fill: none; |
|
| 567 | + stroke: var(--text); |
|
| 568 | + stroke-width: 2.5; |
|
| 569 | + stroke-linecap: round; |
|
| 570 | +} |
|
| 571 | + |
|
| 572 | +.bubble { |
|
| 573 | + fill: #7dd3fc; |
|
| 574 | + transform-box: fill-box; |
|
| 575 | + animation: bubble 1.8s ease-in infinite; |
|
| 576 | +} |
|
| 577 | + |
|
| 578 | +.bubble.b2 { |
|
| 579 | + animation-delay: 0.6s; |
|
| 580 | +} |
|
| 581 | + |
|
| 582 | +.bubble.b3 { |
|
| 583 | + animation-delay: 1.2s; |
|
| 584 | +} |
|
| 585 | + |
|
| 586 | +.readings { |
|
| 587 | + margin-top: 20px; |
|
| 588 | +} |
|
| 589 | + |
|
| 590 | +.readings-header { |
|
| 591 | + display: flex; |
|
| 592 | + justify-content: space-between; |
|
| 593 | + align-items: baseline; |
|
| 594 | +} |
|
| 595 | + |
|
| 596 | +.setup-state { |
|
| 597 | + color: var(--muted); |
|
| 598 | + font-size: 0.85rem; |
|
| 599 | + text-transform: capitalize; |
|
| 600 | +} |
|
| 601 | + |
|
| 602 | +.probe-list { |
|
| 603 | + display: grid; |
|
| 604 | + gap: 8px; |
|
| 605 | + margin: 8px 0 0; |
|
| 606 | + padding: 0; |
|
| 607 | + list-style: none; |
|
| 608 | +} |
|
| 609 | + |
|
| 610 | +.probe-row { |
|
| 611 | + display: flex; |
|
| 612 | + align-items: center; |
|
| 613 | + justify-content: space-between; |
|
| 614 | + padding: 10px 12px; |
|
| 615 | + border-radius: 10px; |
|
| 616 | + background: var(--surface); |
|
| 617 | +} |
|
| 618 | + |
|
| 619 | +.probe-row .status-light { |
|
| 620 | + color: var(--text); |
|
| 621 | +} |
|
| 622 | + |
|
| 623 | +.probe-value { |
|
| 624 | + font-weight: 600; |
|
| 625 | + font-variant-numeric: tabular-nums; |
|
| 626 | +} |
|
| 627 | + |
|
| 628 | +/* ---------- sensor setup ---------- */ |
|
| 629 | + |
|
| 630 | +.setup-card { |
|
| 631 | + max-width: 640px; |
|
| 632 | + margin: 0 auto; |
|
| 633 | +} |
|
| 634 | + |
|
| 635 | +.setup-card h1 { |
|
| 636 | + margin: 0 0 16px; |
|
| 637 | + font-size: 1.5rem; |
|
| 638 | +} |
|
| 639 | + |
|
| 640 | +.stepper { |
|
| 641 | + display: flex; |
|
| 642 | + gap: 8px; |
|
| 643 | + margin: 0; |
|
| 644 | + padding: 0; |
|
| 645 | + list-style: none; |
|
| 646 | +} |
|
| 647 | + |
|
| 648 | +.stepper li { |
|
| 649 | + flex: 1; |
|
| 650 | + padding-top: 10px; |
|
| 651 | + border-top: 4px solid var(--border); |
|
| 652 | + color: var(--muted); |
|
| 653 | + font-size: 0.8rem; |
|
| 654 | + text-align: center; |
|
| 655 | +} |
|
| 656 | + |
|
| 657 | +.stepper li.done, |
|
| 658 | +.stepper li.current { |
|
| 659 | + border-color: var(--accent); |
|
| 660 | + color: var(--accent-strong); |
|
| 661 | + font-weight: 700; |
|
| 662 | +} |
|
| 663 | + |
|
| 664 | +.setup-step h2 { |
|
| 665 | + margin: 24px 0 8px; |
|
| 666 | + font-size: 1.25rem; |
|
| 667 | +} |
|
| 668 | + |
|
| 669 | +.setup-step p { |
|
| 670 | + color: var(--muted); |
|
| 671 | + line-height: 1.5; |
|
| 672 | +} |
|
| 673 | + |
|
| 674 | +.setup-status { |
|
| 675 | + margin: 20px 0; |
|
| 676 | + padding: 16px; |
|
| 677 | + border-radius: 12px; |
|
| 678 | + background: var(--surface); |
|
| 679 | +} |
|
| 680 | + |
|
| 681 | +.setup-status .status-light { |
|
| 682 | + color: var(--text); |
|
| 683 | + font-size: 1.1rem; |
|
| 684 | +} |
|
| 685 | + |
|
| 686 | +.setup-status .light { |
|
| 687 | + width: 22px; |
|
| 688 | + height: 22px; |
|
| 689 | +} |
|
| 690 | + |
|
| 691 | +.actions { |
|
| 692 | + display: flex; |
|
| 693 | + justify-content: space-between; |
|
| 694 | + gap: 12px; |
|
| 695 | + margin-top: 24px; |
|
| 696 | +} |
|
| 697 | + |
|
| 698 | +/* ---------- mock backend panel (not part of the app) ---------- */ |
|
| 699 | + |
|
| 700 | +.mock-panel { |
|
| 701 | + position: fixed; |
|
| 702 | + right: 12px; |
|
| 703 | + bottom: 12px; |
|
| 704 | + z-index: 50; |
|
| 705 | + max-width: 250px; |
|
| 706 | + border: 2px dashed #f59e0b; |
|
| 707 | + border-radius: 12px; |
|
| 708 | + background: #fffbeb; |
|
| 709 | + box-shadow: var(--shadow); |
|
| 710 | + font-size: 0.85rem; |
|
| 711 | +} |
|
| 712 | + |
|
| 713 | +.mock-panel summary { |
|
| 714 | + padding: 8px 12px; |
|
| 715 | + color: #92400e; |
|
| 716 | + font-weight: 700; |
|
| 717 | + cursor: pointer; |
|
| 718 | +} |
|
| 719 | + |
|
| 720 | +.mock-panel-body { |
|
| 721 | + display: grid; |
|
| 722 | + gap: 8px; |
|
| 723 | + padding: 0 12px 12px; |
|
| 724 | +} |
|
| 725 | + |
|
| 726 | +.mock-panel label { |
|
| 727 | + display: flex; |
|
| 728 | + align-items: center; |
|
| 729 | + gap: 6px; |
|
| 730 | +} |
|
| 731 | + |
|
| 732 | +.mock-panel button { |
|
| 733 | + padding: 6px 8px; |
|
| 734 | + border: 1px solid #fcd34d; |
|
| 735 | + border-radius: 8px; |
|
| 736 | + background: #fff; |
|
| 737 | + font: inherit; |
|
| 738 | + text-align: left; |
|
| 739 | + cursor: pointer; |
|
| 740 | +} |
|
| 741 | + |
|
| 742 | +/* ---------- animation ---------- */ |
|
| 743 | + |
|
| 744 | +@keyframes spin { |
|
| 745 | + to { |
|
| 746 | + transform: rotate(360deg); |
|
| 747 | + } |
|
| 748 | +} |
|
| 749 | + |
|
| 750 | +@keyframes wave { |
|
| 751 | + to { |
|
| 752 | + transform: translateX(-100px); |
|
| 753 | + } |
|
| 754 | +} |
|
| 755 | + |
|
| 756 | +@keyframes bob { |
|
| 757 | + 50% { |
|
| 758 | + transform: translateY(4px) rotate(-4deg); |
|
| 759 | + } |
|
| 760 | +} |
|
| 761 | + |
|
| 762 | +@keyframes rise { |
|
| 763 | + 0% { |
|
| 764 | + opacity: 0; |
|
| 765 | + transform: translateY(8px); |
|
| 766 | + } |
|
| 767 | + 40% { |
|
| 768 | + opacity: 0.9; |
|
| 769 | + } |
|
| 770 | + 100% { |
|
| 771 | + opacity: 0; |
|
| 772 | + transform: translateY(-14px); |
|
| 773 | + } |
|
| 774 | +} |
|
| 775 | + |
|
| 776 | +@keyframes bubble { |
|
| 777 | + 0% { |
|
| 778 | + opacity: 0; |
|
| 779 | + transform: translateY(0); |
|
| 780 | + } |
|
| 781 | + 20% { |
|
| 782 | + opacity: 1; |
|
| 783 | + } |
|
| 784 | + 100% { |
|
| 785 | + opacity: 0; |
|
| 786 | + transform: translateY(-40px); |
|
| 787 | + } |
|
| 788 | +} |
|
| 789 | + |
|
| 790 | +@keyframes pulse { |
|
| 791 | + 50% { |
|
| 792 | + opacity: 0.35; |
|
| 793 | + } |
|
| 794 | +} |
|
| 795 | + |
|
| 796 | +@media (prefers-reduced-motion: reduce) { |
|
| 797 | + *, |
|
| 798 | + *::before, |
|
| 799 | + *::after { |
|
| 800 | + animation: none !important; |
|
| 801 | + transition: none !important; |
|
| 802 | + } |
|
| 803 | +} |
projects/hot-tub/src/web/redis_io.py
| ... | ... | @@ -0,0 +1,217 @@ |
| 1 | +"""Redis access for the web service.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import json |
|
| 6 | +import logging |
|
| 7 | +import queue |
|
| 8 | +import threading |
|
| 9 | +import time |
|
| 10 | +import uuid |
|
| 11 | +from datetime import datetime, timezone |
|
| 12 | +from typing import Any, Callable |
|
| 13 | + |
|
| 14 | +try: |
|
| 15 | + import redis |
|
| 16 | +except ImportError: |
|
| 17 | + redis = None # type: ignore[assignment] |
|
| 18 | + |
|
| 19 | +logger = logging.getLogger("hot_tub.web") |
|
| 20 | + |
|
| 21 | +KEY_CONTROL_LATEST = "control:latest" |
|
| 22 | +CHAN_CONTROL_STATE = "control.state" |
|
| 23 | +KEY_TEMPS_LATEST = "temps:latest" |
|
| 24 | +CHAN_TEMPS_READING = "temps.reading" |
|
| 25 | +KEY_TEMPS_SETUP = "temps:setup" |
|
| 26 | +CHAN_TEMPS_SETUP = "temps.setup" |
|
| 27 | +KEY_HEALTH_WEB = "health:web" |
|
| 28 | +CHAN_HEALTH_WEB = "health.web" |
|
| 29 | +KEY_HEALTH_CONTROL = "health:control" |
|
| 30 | +KEY_HEALTH_TEMPS = "health:temps" |
|
| 31 | +CHAN_COMMAND = "control.command" |
|
| 32 | +CHAN_TEMPS_COMMAND = "temps.command" |
|
| 33 | + |
|
| 34 | +SSE_CHANNELS = (CHAN_CONTROL_STATE, CHAN_TEMPS_READING, CHAN_TEMPS_SETUP) |
|
| 35 | + |
|
| 36 | + |
|
| 37 | +def _utc_now() -> str: |
|
| 38 | + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
|
| 39 | + |
|
| 40 | + |
|
| 41 | +def _parse_ts(ts: str | None) -> datetime | None: |
|
| 42 | + if not ts: |
|
| 43 | + return None |
|
| 44 | + try: |
|
| 45 | + if ts.endswith("Z"): |
|
| 46 | + return datetime.fromisoformat(ts.replace("Z", "+00:00")) |
|
| 47 | + return datetime.fromisoformat(ts) |
|
| 48 | + except ValueError: |
|
| 49 | + return None |
|
| 50 | + |
|
| 51 | + |
|
| 52 | +def payload_age_s(payload: dict[str, Any] | None, now: datetime | None = None) -> float | None: |
|
| 53 | + if payload is None: |
|
| 54 | + return None |
|
| 55 | + ts = _parse_ts(payload.get("ts")) |
|
| 56 | + if ts is None: |
|
| 57 | + return None |
|
| 58 | + if ts.tzinfo is None: |
|
| 59 | + ts = ts.replace(tzinfo=timezone.utc) |
|
| 60 | + now = now or datetime.now(timezone.utc) |
|
| 61 | + return (now - ts.astimezone(timezone.utc)).total_seconds() |
|
| 62 | + |
|
| 63 | + |
|
| 64 | +class WebRedis: |
|
| 65 | + def __init__(self, redis_url: str) -> None: |
|
| 66 | + if redis is None: |
|
| 67 | + raise RuntimeError("redis package not installed (apt: python3-redis)") |
|
| 68 | + self._url = redis_url |
|
| 69 | + self._client: redis.Redis | None = None |
|
| 70 | + self._pubsub: redis.client.PubSub | None = None |
|
| 71 | + self._backoff_s = 1.0 |
|
| 72 | + self._fanout_lock = threading.Lock() |
|
| 73 | + self._subscribers: list[queue.Queue[tuple[str, str]]] = [] |
|
| 74 | + self._fanout_thread: threading.Thread | None = None |
|
| 75 | + self._fanout_stop = threading.Event() |
|
| 76 | + |
|
| 77 | + def _connect(self) -> redis.Redis: |
|
| 78 | + if self._client is None: |
|
| 79 | + self._client = redis.from_url(self._url, decode_responses=True) |
|
| 80 | + return self._client |
|
| 81 | + |
|
| 82 | + def _ensure(self) -> redis.Redis | None: |
|
| 83 | + try: |
|
| 84 | + client = self._connect() |
|
| 85 | + client.ping() |
|
| 86 | + self._backoff_s = 1.0 |
|
| 87 | + return client |
|
| 88 | + except Exception as exc: |
|
| 89 | + logger.warning("Redis unavailable: %s (retry in %.0fs)", exc, self._backoff_s) |
|
| 90 | + self._client = None |
|
| 91 | + self._pubsub = None |
|
| 92 | + time.sleep(min(self._backoff_s, 30.0)) |
|
| 93 | + self._backoff_s = min(self._backoff_s * 2, 30.0) |
|
| 94 | + return None |
|
| 95 | + |
|
| 96 | + def get_json(self, key: str) -> dict[str, Any] | None: |
|
| 97 | + client = self._ensure() |
|
| 98 | + if client is None: |
|
| 99 | + return None |
|
| 100 | + try: |
|
| 101 | + raw = client.get(key) |
|
| 102 | + if not raw: |
|
| 103 | + return None |
|
| 104 | + data = json.loads(raw) |
|
| 105 | + return data if isinstance(data, dict) else None |
|
| 106 | + except Exception as exc: |
|
| 107 | + logger.warning("GET %s failed: %s", key, exc) |
|
| 108 | + return None |
|
| 109 | + |
|
| 110 | + def publish_control_command(self, op: str, args: dict[str, Any]) -> str: |
|
| 111 | + cmd_id = str(uuid.uuid4()) |
|
| 112 | + envelope = { |
|
| 113 | + "ts": _utc_now(), |
|
| 114 | + "source": "web", |
|
| 115 | + "id": cmd_id, |
|
| 116 | + "op": op, |
|
| 117 | + "args": args, |
|
| 118 | + } |
|
| 119 | + client = self._ensure() |
|
| 120 | + if client is None: |
|
| 121 | + raise RuntimeError("redis unavailable") |
|
| 122 | + body = json.dumps(envelope) |
|
| 123 | + client.publish(CHAN_COMMAND, body) |
|
| 124 | + return cmd_id |
|
| 125 | + |
|
| 126 | + def publish_temp_setup_command(self, op: str) -> str: |
|
| 127 | + cmd_id = str(uuid.uuid4()) |
|
| 128 | + envelope = {"ts": _utc_now(), "source": "web", "id": cmd_id, "op": op} |
|
| 129 | + client = self._ensure() |
|
| 130 | + if client is None: |
|
| 131 | + raise RuntimeError("redis unavailable") |
|
| 132 | + body = json.dumps(envelope) |
|
| 133 | + client.publish(CHAN_TEMPS_COMMAND, body) |
|
| 134 | + return cmd_id |
|
| 135 | + |
|
| 136 | + def publish_health(self, payload: dict[str, Any]) -> bool: |
|
| 137 | + client = self._ensure() |
|
| 138 | + if client is None: |
|
| 139 | + return False |
|
| 140 | + body = json.dumps(payload) |
|
| 141 | + client.set(KEY_HEALTH_WEB, body) |
|
| 142 | + client.publish(CHAN_HEALTH_WEB, body) |
|
| 143 | + return True |
|
| 144 | + |
|
| 145 | + def _ensure_fanout(self) -> None: |
|
| 146 | + with self._fanout_lock: |
|
| 147 | + if self._fanout_thread is not None and self._fanout_thread.is_alive(): |
|
| 148 | + return |
|
| 149 | + self._fanout_stop.clear() |
|
| 150 | + self._fanout_thread = threading.Thread(target=self._fanout_loop, daemon=True) |
|
| 151 | + self._fanout_thread.start() |
|
| 152 | + |
|
| 153 | + def _fanout_loop(self) -> None: |
|
| 154 | + while not self._fanout_stop.is_set(): |
|
| 155 | + client = self._ensure() |
|
| 156 | + if client is None: |
|
| 157 | + time.sleep(1.0) |
|
| 158 | + continue |
|
| 159 | + try: |
|
| 160 | + if self._pubsub is None: |
|
| 161 | + self._pubsub = client.pubsub(ignore_subscribe_messages=True) |
|
| 162 | + self._pubsub.subscribe(*SSE_CHANNELS) |
|
| 163 | + msg = self._pubsub.get_message(timeout=1.0) |
|
| 164 | + if msg is None or msg.get("type") != "message": |
|
| 165 | + continue |
|
| 166 | + channel = msg.get("channel") |
|
| 167 | + data = msg.get("data") |
|
| 168 | + if not isinstance(channel, str) or not isinstance(data, str): |
|
| 169 | + continue |
|
| 170 | + with self._fanout_lock: |
|
| 171 | + dead: list[queue.Queue[tuple[str, str]]] = [] |
|
| 172 | + for q in self._subscribers: |
|
| 173 | + try: |
|
| 174 | + q.put_nowait((channel, data)) |
|
| 175 | + except queue.Full: |
|
| 176 | + dead.append(q) |
|
| 177 | + for q in dead: |
|
| 178 | + self._subscribers.remove(q) |
|
| 179 | + except Exception as exc: |
|
| 180 | + logger.warning("SSE fanout error: %s", exc) |
|
| 181 | + if self._pubsub is not None: |
|
| 182 | + try: |
|
| 183 | + self._pubsub.close() |
|
| 184 | + except Exception: |
|
| 185 | + pass |
|
| 186 | + self._pubsub = None |
|
| 187 | + time.sleep(0.5) |
|
| 188 | + |
|
| 189 | + def subscribe_sse(self) -> queue.Queue[tuple[str, str]]: |
|
| 190 | + q: queue.Queue[tuple[str, str]] = queue.Queue(maxsize=64) |
|
| 191 | + with self._fanout_lock: |
|
| 192 | + self._subscribers.append(q) |
|
| 193 | + self._ensure_fanout() |
|
| 194 | + return q |
|
| 195 | + |
|
| 196 | + def unsubscribe_sse(self, q: queue.Queue[tuple[str, str]]) -> None: |
|
| 197 | + with self._fanout_lock: |
|
| 198 | + if q in self._subscribers: |
|
| 199 | + self._subscribers.remove(q) |
|
| 200 | + |
|
| 201 | + def close(self) -> None: |
|
| 202 | + self._fanout_stop.set() |
|
| 203 | + if self._fanout_thread is not None: |
|
| 204 | + self._fanout_thread.join(timeout=2.0) |
|
| 205 | + if self._pubsub is not None: |
|
| 206 | + try: |
|
| 207 | + self._pubsub.unsubscribe() |
|
| 208 | + self._pubsub.close() |
|
| 209 | + except Exception: |
|
| 210 | + pass |
|
| 211 | + self._pubsub = None |
|
| 212 | + if self._client is not None: |
|
| 213 | + try: |
|
| 214 | + self._client.close() |
|
| 215 | + except Exception: |
|
| 216 | + pass |
|
| 217 | + self._client = None |
projects/hot-tub/src/web/server.py
| ... | ... | @@ -0,0 +1,150 @@ |
| 1 | +"""Flask HTTP server: static UI, REST API, SSE.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import json |
|
| 6 | +import logging |
|
| 7 | +import queue |
|
| 8 | +import time |
|
| 9 | +from pathlib import Path |
|
| 10 | +from typing import Any |
|
| 11 | + |
|
| 12 | +from flask import Flask, Response, jsonify, request, send_from_directory |
|
| 13 | + |
|
| 14 | +from .commands import CommandValidationError, validate_control_command, validate_temp_setup_op |
|
| 15 | +from .config import WebConfig |
|
| 16 | +from .health import api_health_payload |
|
| 17 | +from .redis_io import ( |
|
| 18 | + CHAN_CONTROL_STATE, |
|
| 19 | + CHAN_TEMPS_READING, |
|
| 20 | + CHAN_TEMPS_SETUP, |
|
| 21 | + KEY_CONTROL_LATEST, |
|
| 22 | + KEY_HEALTH_CONTROL, |
|
| 23 | + KEY_HEALTH_TEMPS, |
|
| 24 | + KEY_HEALTH_WEB, |
|
| 25 | + KEY_TEMPS_LATEST, |
|
| 26 | + KEY_TEMPS_SETUP, |
|
| 27 | + WebRedis, |
|
| 28 | +) |
|
| 29 | + |
|
| 30 | +logger = logging.getLogger("hot_tub.web") |
|
| 31 | + |
|
| 32 | + |
|
| 33 | +def create_app(cfg: WebConfig, redis_io: WebRedis) -> Flask: |
|
| 34 | + app = Flask(__name__, static_folder=None) |
|
| 35 | + static_dir = cfg.static_dir.resolve() |
|
| 36 | + |
|
| 37 | + def _sse_event(name: str, data: str) -> str: |
|
| 38 | + return f"event: {name}\ndata: {data}\n\n" |
|
| 39 | + |
|
| 40 | + @app.get("/api/events") |
|
| 41 | + def api_events() -> Response: |
|
| 42 | + def generate(): |
|
| 43 | + for key, channel in ( |
|
| 44 | + (KEY_CONTROL_LATEST, CHAN_CONTROL_STATE), |
|
| 45 | + (KEY_TEMPS_LATEST, CHAN_TEMPS_READING), |
|
| 46 | + (KEY_TEMPS_SETUP, CHAN_TEMPS_SETUP), |
|
| 47 | + ): |
|
| 48 | + snap = redis_io.get_json(key) |
|
| 49 | + if snap is not None: |
|
| 50 | + yield _sse_event(channel, json.dumps(snap)) |
|
| 51 | + |
|
| 52 | + q = redis_io.subscribe_sse() |
|
| 53 | + last_keepalive = time.monotonic() |
|
| 54 | + try: |
|
| 55 | + while True: |
|
| 56 | + try: |
|
| 57 | + channel, data = q.get(timeout=1.0) |
|
| 58 | + yield _sse_event(channel, data) |
|
| 59 | + except queue.Empty: |
|
| 60 | + pass |
|
| 61 | + if time.monotonic() - last_keepalive >= cfg.sse_keepalive_s: |
|
| 62 | + yield ": keepalive\n\n" |
|
| 63 | + last_keepalive = time.monotonic() |
|
| 64 | + finally: |
|
| 65 | + redis_io.unsubscribe_sse(q) |
|
| 66 | + |
|
| 67 | + return Response( |
|
| 68 | + generate(), |
|
| 69 | + mimetype="text/event-stream", |
|
| 70 | + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, |
|
| 71 | + ) |
|
| 72 | + |
|
| 73 | + @app.post("/api/command") |
|
| 74 | + def api_command() -> tuple[Any, int]: |
|
| 75 | + body = request.get_json(silent=True) |
|
| 76 | + if not isinstance(body, dict): |
|
| 77 | + return jsonify({"error": "JSON body required"}), 400 |
|
| 78 | + try: |
|
| 79 | + op, args = validate_control_command(body) |
|
| 80 | + cmd_id = redis_io.publish_control_command(op, args) |
|
| 81 | + except CommandValidationError as exc: |
|
| 82 | + return jsonify({"error": str(exc)}), 400 |
|
| 83 | + except RuntimeError as exc: |
|
| 84 | + return jsonify({"error": str(exc)}), 503 |
|
| 85 | + return jsonify({"id": cmd_id}), 202 |
|
| 86 | + |
|
| 87 | + def _setup_op(op: str) -> tuple[Any, int]: |
|
| 88 | + try: |
|
| 89 | + validate_temp_setup_op(op) |
|
| 90 | + cmd_id = redis_io.publish_temp_setup_command(op) |
|
| 91 | + except CommandValidationError as exc: |
|
| 92 | + return jsonify({"error": str(exc)}), 400 |
|
| 93 | + except RuntimeError as exc: |
|
| 94 | + return jsonify({"error": str(exc)}), 503 |
|
| 95 | + return jsonify({"id": cmd_id}), 202 |
|
| 96 | + |
|
| 97 | + @app.post("/api/sensor-setup/start") |
|
| 98 | + def sensor_setup_start() -> tuple[Any, int]: |
|
| 99 | + return _setup_op("setup_start") |
|
| 100 | + |
|
| 101 | + @app.post("/api/sensor-setup/finish") |
|
| 102 | + def sensor_setup_finish() -> tuple[Any, int]: |
|
| 103 | + return _setup_op("setup_finish") |
|
| 104 | + |
|
| 105 | + @app.post("/api/sensor-setup/cancel") |
|
| 106 | + def sensor_setup_cancel() -> tuple[Any, int]: |
|
| 107 | + return _setup_op("setup_cancel") |
|
| 108 | + |
|
| 109 | + @app.get("/api/status") |
|
| 110 | + def api_status() -> Any: |
|
| 111 | + return jsonify( |
|
| 112 | + { |
|
| 113 | + "control": redis_io.get_json(KEY_CONTROL_LATEST), |
|
| 114 | + "temps": redis_io.get_json(KEY_TEMPS_LATEST), |
|
| 115 | + "health": { |
|
| 116 | + "control": redis_io.get_json(KEY_HEALTH_CONTROL), |
|
| 117 | + "temps": redis_io.get_json(KEY_HEALTH_TEMPS), |
|
| 118 | + "web": redis_io.get_json(KEY_HEALTH_WEB), |
|
| 119 | + }, |
|
| 120 | + } |
|
| 121 | + ) |
|
| 122 | + |
|
| 123 | + @app.get("/api/health") |
|
| 124 | + def api_health() -> Any: |
|
| 125 | + return jsonify(api_health_payload(redis_io, cfg)) |
|
| 126 | + |
|
| 127 | + @app.route("/", defaults={"path": ""}) |
|
| 128 | + @app.route("/<path:path>") |
|
| 129 | + def static_files(path: str) -> Any: |
|
| 130 | + if path.startswith("api/"): |
|
| 131 | + return jsonify({"error": "not found"}), 404 |
|
| 132 | + target = static_dir / path |
|
| 133 | + if path and target.is_file(): |
|
| 134 | + return send_from_directory(static_dir, path) |
|
| 135 | + index = static_dir / "index.html" |
|
| 136 | + if index.is_file(): |
|
| 137 | + return send_from_directory(static_dir, "index.html") |
|
| 138 | + return jsonify({"error": "frontend not built; see src/web/frontend"}), 503 |
|
| 139 | + |
|
| 140 | + return app |
|
| 141 | + |
|
| 142 | + |
|
| 143 | +def run_server(cfg: WebConfig, redis_io: WebRedis) -> None: |
|
| 144 | + app = create_app(cfg, redis_io) |
|
| 145 | + app.run( |
|
| 146 | + host=cfg.bind_host, |
|
| 147 | + port=cfg.bind_port, |
|
| 148 | + threaded=True, |
|
| 149 | + use_reloader=False, |
|
| 150 | + ) |
projects/hot-tub/src/web/tests/__init__.py
| ... | ... | @@ -0,0 +1 @@ |
| 1 | + |
projects/hot-tub/src/web/tests/test_commands.py
| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | +"""Command validation tests.""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import unittest |
|
| 6 | + |
|
| 7 | +from web.commands import CommandValidationError, validate_control_command, validate_temp_setup_op |
|
| 8 | + |
|
| 9 | + |
|
| 10 | +class TestWebCommands(unittest.TestCase): |
|
| 11 | + def test_set_heat_mode(self) -> None: |
|
| 12 | + op, args = validate_control_command({"op": "set_heat_mode", "args": {"mode": "maintain"}}) |
|
| 13 | + self.assertEqual(op, "set_heat_mode") |
|
| 14 | + self.assertEqual(args["mode"], "maintain") |
|
| 15 | + |
|
| 16 | + def test_invalid_mode(self) -> None: |
|
| 17 | + with self.assertRaises(CommandValidationError): |
|
| 18 | + validate_control_command({"op": "set_heat_mode", "args": {"mode": "bogus"}}) |
|
| 19 | + |
|
| 20 | + def test_target_clamp(self) -> None: |
|
| 21 | + _, args = validate_control_command({"op": "set_target_temp", "args": {"target_f": 200}}) |
|
| 22 | + self.assertEqual(args["target_f"], 110.0) |
|
| 23 | + |
|
| 24 | + def test_setup_ops(self) -> None: |
|
| 25 | + self.assertEqual(validate_temp_setup_op("setup_start"), "setup_start") |
|
| 26 | + |
|
| 27 | + |
|
| 28 | +if __name__ == "__main__": |
|
| 29 | + unittest.main() |
projects/hot-tub/src/web/tests/test_server.py
| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | +"""Flask route smoke tests (no Redis).""" |
|
| 2 | + |
|
| 3 | +from __future__ import annotations |
|
| 4 | + |
|
| 5 | +import unittest |
|
| 6 | +from unittest.mock import MagicMock |
|
| 7 | + |
|
| 8 | +from web.config import WebConfig |
|
| 9 | +from web.server import create_app |
|
| 10 | + |
|
| 11 | + |
|
| 12 | +class TestServer(unittest.TestCase): |
|
| 13 | + def test_status_route(self) -> None: |
|
| 14 | + redis_io = MagicMock() |
|
| 15 | + redis_io.get_json.return_value = None |
|
| 16 | + cfg = WebConfig() |
|
| 17 | + app = create_app(cfg, redis_io) |
|
| 18 | + client = app.test_client() |
|
| 19 | + res = client.get("/api/status") |
|
| 20 | + self.assertEqual(res.status_code, 200) |
|
| 21 | + |
|
| 22 | + def test_command_validation_400(self) -> None: |
|
| 23 | + redis_io = MagicMock() |
|
| 24 | + cfg = WebConfig() |
|
| 25 | + app = create_app(cfg, redis_io) |
|
| 26 | + client = app.test_client() |
|
| 27 | + res = client.post("/api/command", json={"op": "bad_op"}) |
|
| 28 | + self.assertEqual(res.status_code, 400) |
|
| 29 | + |
|
| 30 | + |
|
| 31 | +if __name__ == "__main__": |
|
| 32 | + unittest.main() |