Thermostat flow I came up with

Anyone in this forum knows I can barely spell I.T. however, I have all the functionality sorted out now after only 3 years, so I'm going back to try to make things look a little better. I started with this thermostat and I think me and chatgpt did a really good job.

and here's the flow,

[{"id":"3f0bb353bfa06f22","type":"function","z":"04de3cddb4a6f215","name":"tempSet","func":"\n\nvar tempSet;\n\nif (msg.topic == \"Setpoint\"){\n  tempSet = msg.payload;\n  if (tempSet < 10){\n    tempSet = 10;\n  }\n  if (tempSet > 25){\n    tempSet = 25;\n  }\n  flow.set(\"tempSet\", tempSet);\n  node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n  return msg;\n};\n\n\nlet Y = msg.topic;\n\nif(Y == \"Plus\"){\n  tempSet = flow.get(\"tempSet\");\n  if (tempSet == 'undefined'){\n    flow.set(\"tempSet\",10);\n    tempSet = 10;\n  }\n  if (isNaN(tempSet)) {\n    flow.set(\"tempSet\",10);\n    tempSet = 10;\n    }\n  tempSet = tempSet + .5;\n  if (tempSet > 25) {\n    tempSet = 25;\n  }\n  flow.set(\"tempSet\", tempSet);\n  node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n  return msg;\n}\n\nif (Y == \"Minus\") {\n  let tempSet = flow.get(\"tempSet\");\n  if (tempSet == 'undefined') {\n    flow.set(\"tempSet\", 10);\n    tempSet = 10;\n  }\n  if (isNaN(tempSet)) {\n    flow.set(\"tempSet\", 10);\n    tempSet = 10;\n  }\n  tempSet = tempSet - .5;\n  if (tempSet < 10) {\n    tempSet = 10;\n  }\n  flow.set(\"tempSet\", tempSet);\n  node.status({ fill: \"blue\", shape: \"dot\", text: tempSet });\n  return msg;\n}\n\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":280,"wires":[["595a26fd2a2e4546"]]},{"id":"595a26fd2a2e4546","type":"function","z":"04de3cddb4a6f215","name":"aggregate","func":"/*\nmsg.payload[0] = power\nmsg.payload[1] = fan\nmsg.payload[2] = cog\nmsg.payload[3] = flame\nmsg.payload[4] = tempAmb\nmsg.payload[5] = tempSet\n*/\nlet Sensor = \"good\";\n\nlet Sensor1 = global.get(\"Sensor1\");\nlet Sensor2 = global.get(\"Sensor2\");\ncontext.set(\"Sensor1\",Sensor1);\ncontext.set(\"Sensor2\",Sensor2);\n\nif (Sensor1 == \"failed\" && Sensor2 == \"failed\"){\n    Sensor = \"failed\";\n}\n\nlet Fan = flow.get(\"Fan\");\nlet tempSet = flow.get(\"tempSet\");\n\n\nlet Cog = flow.get(\"Cog\");\nif (Cog == \"On\") {\n    let W = cToF(tempSet);\n    let X = W.toFixed(1);\n    tempSet = X;\n}\n\nfunction cToF(num) {\n    var f = ((num * 1.8) + 32);\n    return f;\n}\n\nfunction fToC(num) {\n    var c = ((num - 32) * (5 / 9));\n    return c;\n}\n\nlet tempAmb = flow.get(\"tempAmb\");\nlet Power = flow.get(\"Power\");\nif (Sensor == \"failed\"){\n    Power = \"None\";\n//    flow.set(\"Power\",\"Off\");\n}\nvar Flame = \"\";\nif (global.get(\"heat_running\") == true){\n    Flame = \"On\";\n}\nif (global.get(\"heat_running\") == false){\n    Flame = \"Off\";\n}\n//let Flame = flow.get(\"Flame\");\nCog = flow.get(\"Cog\");\nconst payload=[];\n\nmsg = {\n    payload: [Power,Fan,Cog,Flame],\n    tempSet: tempSet,\n    tempAmb: tempAmb,\n}\n\n\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":380,"wires":[["9c56b5c3aa936f5b"]]},{"id":"65470a8bed182c70","type":"link in","z":"04de3cddb4a6f215","name":"3 sec in","links":["637589bc3f933b91"],"x":235,"y":380,"wires":[["d67d430dec3dad02","595a26fd2a2e4546","ffdec737ec9672ee","3f0bb353bfa06f22"]]},{"id":"bab4ddb531211035","type":"function","z":"04de3cddb4a6f215","name":"Pwr","func":"\nif (msg.topic != \"Power\"){\n    return null;\n}\n\nlet Power = context.get(\"Power\");\nif (Power == 'undefined'){\n    context.set(\"Power\",\"On\");\n    flow.set(\"Power\",\"On\")\n    Power = \"On\";\n    node.status({ fill: \"blue\", shape: \"dot\", text: Power });\n}\n\nif (Power == \"On\"){\n    context.set(\"Power\",\"Off\");\n    flow.set(\"Power\",\"Off\");\n} else {\n    context.set(\"Power\", \"On\");\n    flow.set(\"Power\", \"On\");\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: Power });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":380,"wires":[[]]},{"id":"d41568c07d7fba67","type":"function","z":"04de3cddb4a6f215","name":"Cog","func":"\nif (msg.topic != \"Cog\"){\n    return null;\n}\n\nlet Cog = context.get(\"Cog\");\nif (Cog == 'undefined'){\n    context.set(\"Cog\",\"Off\");\n    flow.set(\"Cog\",\"Off\")\n    Cog = \"Off\";\n    node.status({ fill: \"blue\", shape: \"dot\", text: Cog });\n}\n\nif (Cog == \"On\"){\n    context.set(\"Cog\",\"Off\");\n    flow.set(\"Cog\",\"Off\");\n} else {\n    context.set(\"Cog\", \"On\");\n    flow.set(\"Cog\", \"On\");\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: Cog });\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":440,"wires":[[]]},{"id":"953d89927c79927c","type":"function","z":"04de3cddb4a6f215","name":"Fan","func":"\nif (msg.topic != \"Fan\"){\n    return null;\n}\n\nlet Fan = context.get(\"Fan\");\nif (Fan == 'undefined'){\n    context.set(\"Fan\",\"Off\");\n    flow.set(\"Fan\",\"Off\")\n    Fan = \"Off\";\n    node.status({ fill: \"blue\", shape: \"dot\", text: Fan });\n}\n\nif (Fan == \"On\"){\n    context.set(\"Fan\",\"Off\");\n    flow.set(\"Fan\",\"Off\");\n    node.status({ fill: \"blue\", shape: \"dot\", text: \"Fan Off\"});\n} else {\n    context.set(\"Fan\", \"On\");\n    flow.set(\"Fan\", \"On\");\n    node.status({ fill: \"blue\", shape: \"dot\", text: \"Fan On\" });\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":500,"wires":[[]]},{"id":"d67d430dec3dad02","type":"function","z":"04de3cddb4a6f215","name":"Thermostat logic","func":"let TempSet = Number(flow.get(\"tempSet\"));\nlet TempAmb = Number(flow.get(\"tempAmb\"));\nlet Power = flow.get(\"Power\");\nvar HeatOn; \n\nflow.set(\"Flame\", \"Off\");\nflow.set('heating_status', \"Off\");\nHeatOn = false;\n\n\nif(Power == \"On\"){\n    if (TempSet <= TempAmb + 0.2) {\n        HeatOn = false;\n        flow.set(\"Flame\", \"Off\");\n    } else {\n        if (TempSet > TempAmb + 0.2) {\n            HeatOn = true;\n            flow.set('heating_status', HeatOn);\n            flow.set(\"Flame\", \"On\");\n        } \n    }\n}\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: HeatOn});\n\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":440,"wires":[[]]},{"id":"ffdec737ec9672ee","type":"function","z":"04de3cddb4a6f215","name":"heat running","func":"\nlet Heat = global.get(\"heat_running\");\n\nnode.status({ fill: \"blue\", shape: \"dot\", text: \"Heat On \" + Heat});\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":260,"wires":[[]]},{"id":"9c56b5c3aa936f5b","type":"ui-template","z":"04de3cddb4a6f215","group":"546c6a08d800a5e9","page":"","ui":"","name":"good thermo","order":2,"width":"4","height":"11","head":"","format":"\n\n\n\n\n\n\n\n<template>\n  <container>\n    <div class=\"nexus\">\n\n      <!-- HEADER -->\n      <div class=\"nexus-header\">\n        <div class=\"brand\">\n          <span class=\"brand-icon\">◈</span>\n          <div>\n            <div class=\"brand-name\">Systatium</div>\n            <div class=\"brand-sub\">CLIMATE CONTROL SYSTEM</div>\n          </div>\n        </div>\n      </div>\n\n      <!-- MAIN INSTRUMENT -->\n      <div class=\"main-instrument\">\n\n        <!-- TEMPERATURE DIAL -->\n        <div\n          class=\"temperature-ring\"\n          :class=\"{\n            adjusting: dialDragging,\n            heating: flame === 'On'\n          }\"\n        >\n          <svg\n            class=\"ring-svg\"\n            viewBox=\"0 0 300 300\"\n            preserveAspectRatio=\"xMidYMid meet\"\n            @pointerdown=\"startDial\"\n            @pointermove=\"moveDial\"\n            @pointerup=\"endDial\"\n            @pointercancel=\"endDial\"\n            @lostpointercapture=\"endDial\"\n          >\n\n            <!-- BACKGROUND TRACK -->\n            <circle\n              class=\"ring-track\"\n              cx=\"150\"\n              cy=\"150\"\n              r=\"132\"\n              stroke-dasharray=\"622 829\"\n              transform=\"rotate(-135 150 150)\"\n            />\n\n            <!-- TEMPERATURE PROGRESS -->\n            <circle\n              class=\"ring-progress\"\n              cx=\"150\"\n              cy=\"150\"\n              r=\"132\"\n              stroke-dasharray=\"622 829\"\n              :stroke-dashoffset=\"622 - (dialProgress * 622)\"\n              transform=\"rotate(-135 150 150)\"\n            />\n\n            <!-- ENERGY SWEEP -->\n            <circle\n              class=\"energy-sweep\"\n              cx=\"150\"\n              cy=\"150\"\n              r=\"132\"\n              stroke-dasharray=\"38 791\"\n              transform=\"rotate(-135 150 150)\"\n            />\n\n            <!-- INNER RING -->\n            <circle\n              class=\"ring-inner\"\n              cx=\"150\"\n              cy=\"150\"\n              r=\"108\"\n            />\n\n            <!-- TEMPERATURE TICKS -->\n            <g class=\"dial-ticks\">\n              <line\n                v-for=\"tick in dialTicks\"\n                :key=\"'tick-' + tick.temp\"\n                :x1=\"tick.x1\"\n                :y1=\"tick.y1\"\n                :x2=\"tick.x2\"\n                :y2=\"tick.y2\"\n                :class=\"{\n                  'major-tick': tick.major,\n                  'active-tick': tick.temp <= dialCurrentTemp\n                }\"\n              />\n\n              <!-- TEMPERATURE LABELS -->\n              <text\n                v-for=\"tick in dialTicks.filter(t => t.major)\"\n                :key=\"'label-' + tick.temp\"\n                :x=\"tick.labelX\"\n                :y=\"tick.labelY\"\n                class=\"dial-label\"\n                :class=\"{\n                  'active-label': tick.temp <= dialCurrentTemp\n                }\"\n                text-anchor=\"middle\"\n                dominant-baseline=\"middle\"\n              >\n                {{ tick.temp }}°\n              </text>\n            </g>\n\n            <!-- DIAL INDICATOR -->\n            <circle\n              class=\"dial-indicator\"\n              :cx=\"150 + 132 * Math.cos(dialAngle * Math.PI / 180)\"\n              :cy=\"150 + 132 * Math.sin(dialAngle * Math.PI / 180)\"\n              r=\"8\"\n            />\n\n          </svg>\n\n          <!-- CENTRAL TEMPERATURE DISPLAY -->\n          <div class=\"temperature-center\">\n            <div class=\"temp-caption\">CURRENT TEMP</div>\n\n            <div class=\"ambient-temp\">\n              {{ tempAmb }}<span class=\"degree\">°</span>\n            </div>\n\n            <div class=\"temp-unit\">CELSIUS</div>\n          </div>\n\n          <!-- DIAL INSTRUCTION -->\n          <div class=\"dial-hint\">\n            {{ dialDragging ? 'ADJUSTING SETPOINT' : 'DRAG TO ADJUST' }}\n          </div>\n        </div>\n\n        <!-- TARGET TEMPERATURE -->\n        <div class=\"target-display\">\n          <div class=\"target-label\">\n            TARGET TEMPERATURE\n          </div>\n\n          <div class=\"target-value\">\n            {{ Number(dialCurrentTemp).toFixed(1) }}<span>°</span>\n          </div>\n\n          <div class=\"target-label\">\n            DRAG THE OUTER RING TO CHANGE\n          </div>\n        </div>\n      </div>\n\n      <!-- BOTTOM BUTTONS -->\n      <div class=\"button-container\">\n\n<v-card\n  class=\"thermoValue stretch-button\"\n  @click=\"Power\"\n>\n  <v-icon\n    size=\"36\"\n    :class=\"{ 'power-icon-active': powerOn }\"\n  >\n    mdi-power\n  </v-icon>\n</v-card>\n\n<!-- FAN -->\n<v-card\n  class=\"thermoValue stretch-button\"\n  :class=\"{ 'fan-active': fanOn }\"\n  @click=\"Fan\"\n>\n  <v-icon\n    size=\"36\"\n    :class=\"{ 'fan-spinning': fanOn }\"\n  >\n    mdi-fan\n  </v-icon>\n</v-card>\n\n      </div>\n\n    </div>\n  </container>\n</template>\n\n<script>\nexport default {\n  props: {\n    msg: Object,\n  },\n\n  data() {\n    return {\n      // Heating state\n      flame: 'Off',\n      powerOn: false,\n      fanOn: false,\n\n      // Temperatures\n      tempAmb: '--',\n      tempSet: '--',\n\n      // Dial configuration\n      dialMin: 10,\n      dialMax: 25,\n      dialStep: 0.25,\n\n      // Interactive dial\n      dialDragging: false,\n      dialCurrentTemp: 20,\n\n      // Pointer tracking\n      activePointerId: null,\n    }\n  },\n\n  computed: {\n    // Dial progress from 0 to 1\n    dialProgress() {\n      return Math.max(\n        0,\n        Math.min(\n          1,\n          (this.dialCurrentTemp - this.dialMin) /\n            (this.dialMax - this.dialMin),\n        ),\n      )\n    },\n\n    // Indicator angle\n    dialAngle() {\n      return -135 + this.dialProgress * 270\n    },\n\n    // Temperature markings\n    dialTicks() {\n      const ticks = []\n\n      for (\n        let temp = this.dialMin;\n        temp <= this.dialMax;\n        temp++\n      ) {\n        const progress =\n          (temp - this.dialMin) /\n          (this.dialMax - this.dialMin)\n\n        const angle = -135 + progress * 270\n        const radians = (angle * Math.PI) / 180\n        const major = temp % 5 === 0\n\n        ticks.push({\n          temp,\n          major,\n\n          x1: 150 + (major ? 119 : 123) * Math.cos(radians),\n          y1: 150 + (major ? 119 : 123) * Math.sin(radians),\n\n          x2: 150 + 130 * Math.cos(radians),\n          y2: 150 + 130 * Math.sin(radians),\n\n          labelX: 150 + 138 * Math.cos(radians),\n          labelY: 150 + 138 * Math.sin(radians),\n        })\n      }\n\n      return ticks\n    },\n  },\n\n  watch: {\n    // Incoming Node-RED updates\n    msg: {\n      immediate: true,\n\n      handler(newVal) {\n        if (!newVal) return\n\n\n      // Power status\nif (newVal.power !== undefined) {\n  this.powerOn = newVal.power === 'On';\n}\n\n// Fan status\nif (newVal.fan !== undefined) {\n  this.fanOn = newVal.fan === 'On';\n}\n\n\n        // Heating status\n        if (Array.isArray(newVal.payload)) {\n          const [, , , flame] = newVal.payload\n\n          if (flame !== undefined) {\n            this.flame = flame\n          }\n        }\n\n        // Ambient temperature\n        if (newVal.tempAmb !== undefined) {\n          this.tempAmb = newVal.tempAmb\n        }\n\n        // Set temperature\n        if (newVal.tempSet !== undefined) {\n          this.tempSet = newVal.tempSet\n\n          if (!this.dialDragging) {\n            const value = Number.parseFloat(newVal.tempSet)\n\n            if (Number.isFinite(value)) {\n              this.dialCurrentTemp = this.clampTemp(value)\n            }\n          }\n        }\n      },\n    },\n  },\n\n  methods: {\n    // Temperature limits\n    clampTemp(value) {\n      return Math.max(\n        this.dialMin,\n        Math.min(this.dialMax, value)\n      )\n    },\n\n  Power() {\n    this.send({\n      topic: \"Power\"\n    });\n\n    this.powerOn = !this.powerOn;\n  },\n\n\nFan() {\n  this.send({\n    topic: \"Fan\"\n  });\n\n  this.fanOn = !this.fanOn;\n},\n\n\n\n    // Send setpoint to Node-RED\n    sendSetpoint(value) {\n      const temp = this.clampTemp(value)\n\n      this.dialCurrentTemp = temp\n\n      this.send({\n        topic: 'Setpoint',\n        payload: temp,\n      })\n    },\n\n    // Calculate temperature from dial position\n    getDialTemperature(event) {\n      const rect =\n        event.currentTarget.getBoundingClientRect()\n\n      const x =\n        event.clientX - rect.left - rect.width / 2\n\n      const y =\n        event.clientY - rect.top - rect.height / 2\n\n      const distance = Math.sqrt(x * x + y * y)\n\n      if (distance < 85) return null\n\n      const angle =\n        (Math.atan2(y, x) * 180) / Math.PI\n\n      let relative = (angle + 135 + 360) % 360\n\n      if (relative > 270) {\n        relative = relative < 315 ? 270 : 0\n      }\n\n      let temp =\n        this.dialMin +\n        (relative / 270) *\n        (this.dialMax - this.dialMin)\n\n      temp =\n        Math.round(temp / this.dialStep) *\n        this.dialStep\n\n      return this.clampTemp(temp)\n    },\n\n    // Start dial\n    startDial(event) {\n      if (this.dialDragging) return\n\n      const temp = this.getDialTemperature(event)\n\n      if (temp === null) return\n\n      event.preventDefault()\n\n      this.dialDragging = true\n      this.activePointerId = event.pointerId\n\n      event.currentTarget.setPointerCapture(\n        event.pointerId\n      )\n\n      this.sendSetpoint(temp)\n    },\n\n    // Move dial\n    moveDial(event) {\n      if (!this.dialDragging) return\n\n      if (event.pointerId !== this.activePointerId) return\n\n      const temp = this.getDialTemperature(event)\n\n      if (temp === null) return\n\n      if (temp === this.dialCurrentTemp) return\n\n      this.sendSetpoint(temp)\n    },\n\n    // End dial\n    endDial(event) {\n      if (\n        this.activePointerId !== null &&\n        event.pointerId !== undefined &&\n        event.pointerId !== this.activePointerId\n      ) {\n        return\n      }\n\n      this.dialDragging = false\n      this.activePointerId = null\n    },\n  },\n}\n</script>\n\n<style scoped>\n\n/* =================================\n   NEXUS DESIGN\n================================= */\n\n.nexus {\n  --cyan: #00d9ff;\n  --amber: #ff9d32;\n\n  width: 100%;\n  box-sizing: border-box;\n  padding: 18px;\n\n  color: #d9f5ff;\n\n  background: radial-gradient(\n    ellipse at 50% 30%,\n    #102a40 0%,\n    #091725 48%,\n    #050c15 100%\n  );\n\n  border: 1px solid #17394d;\n  border-radius: 18px;\n\n  font-family: Roboto, Arial, sans-serif;\n  overflow: hidden;\n}\n\n/* HEADER */\n\n.nexus-header {\n  display: flex;\n  align-items: center;\n  padding-bottom: 18px;\n  border-bottom: 1px solid #18374a;\n}\n\n.brand {\n  display: flex;\n  align-items: center;\n  gap: 11px;\n}\n\n.brand-icon {\n  font-size: 34px;\n  color: var(--cyan);\n  text-shadow: 0 0 15px var(--cyan);\n}\n\n.brand-name {\n  font-size: 21px;\n  font-weight: 800;\n  letter-spacing: 4px;\n}\n\n.brand-sub {\n  font-size: 8px;\n  color: #65869a;\n  letter-spacing: 2px;\n  margin-top: 3px;\n}\n\n/* MAIN INSTRUMENT */\n\n.main-instrument {\n  padding: 8px 0 20px;\n  text-align: center;\n}\n\n.instrument-label,\n.target-label {\n  font-size: 10px;\n  letter-spacing: 2px;\n  color: #648ba1;\n}\n\n/* CIRCULAR DIAL */\n\n.temperature-ring {\n  position: relative;\n  width: min(290px, 82vw);\n  aspect-ratio: 1;\n  margin: 8px auto 18px;\n\n  user-select: none;\n  -webkit-user-select: none;\n\n  animation: nexusBoot 1.2s ease-out both;\n}\n\n/* SVG */\n\n.ring-svg {\n  display: block;\n  position: relative;\n  z-index: 2;\n\n  width: 100%;\n  height: 100%;\n\n  overflow: visible;\n  touch-action: none;\n  cursor: grab;\n}\n\n.ring-svg:active {\n  cursor: grabbing;\n}\n\n.ring-svg circle {\n  fill: none;\n}\n\n/* BACKGROUND TRACK */\n\n.ring-track {\n  stroke: #18364a;\n  stroke-width: 5;\n}\n\n/* TEMPERATURE PROGRESS */\n\n.ring-progress {\n  stroke: var(--cyan);\n  stroke-width: 5;\n  stroke-linecap: round;\n\n  filter:\n    drop-shadow(0 0 5px var(--cyan))\n    drop-shadow(0 0 12px #007da9);\n\n  transition:\n    stroke-dashoffset 0.25s ease,\n    stroke 0.4s ease;\n}\n\n/* ENERGY SWEEP */\n\n.energy-sweep {\n  stroke: var(--cyan);\n  stroke-width: 7;\n  stroke-linecap: round;\n\n  filter:\n    drop-shadow(0 0 5px var(--cyan))\n    drop-shadow(0 0 12px var(--cyan));\n\n  animation: energyTravel 4s linear infinite;\n  pointer-events: none;\n}\n\n/* INNER RING */\n\n.ring-inner {\n  stroke: #173448;\n  stroke-width: 1;\n  stroke-dasharray: 3 7;\n}\n\n/* TEMPERATURE TICKS */\n\n.dial-ticks line {\n  stroke: #31546a;\n  stroke-width: 1;\n  transition: stroke 0.3s;\n}\n\n.dial-ticks .major-tick {\n  stroke: #7194a8;\n  stroke-width: 2;\n}\n\n.dial-ticks .active-tick {\n  stroke: var(--cyan);\n  filter: drop-shadow(0 0 3px var(--cyan));\n}\n\n.dial-label {\n  font-size: 10px;\n  font-weight: 500;\n  fill: #648398;\n  transition: fill 0.3s;\n}\n\n.dial-label.active-label {\n  fill: #b8f5ff;\n  filter: drop-shadow(0 0 4px var(--cyan));\n}\n\n/* DIAL INDICATOR */\n\n.dial-indicator {\n  fill: #e8fcff;\n  stroke: var(--cyan);\n  stroke-width: 4;\n\n  filter:\n    drop-shadow(0 0 5px var(--cyan))\n    drop-shadow(0 0 10px var(--cyan));\n\n  transition:\n    cx 0.08s,\n    cy 0.08s,\n    stroke 0.4s;\n}\n\n/* HEATING ACTIVE - AMBER */\n\n.temperature-ring.heating .ring-progress {\n  stroke: var(--amber);\n\n  filter:\n    drop-shadow(0 0 5px var(--amber))\n    drop-shadow(0 0 12px #ff7300);\n}\n\n.temperature-ring.heating .energy-sweep {\n  stroke: var(--amber);\n\n  filter:\n    drop-shadow(0 0 6px var(--amber))\n    drop-shadow(0 0 15px #ff7300);\n\n  animation-duration: 2s;\n}\n\n.temperature-ring.heating .dial-indicator {\n  stroke: var(--amber);\n\n  filter:\n    drop-shadow(0 0 8px var(--amber))\n    drop-shadow(0 0 12px #ff7300);\n}\n\n.temperature-ring.heating .dial-ticks .active-tick {\n  stroke: var(--amber);\n  filter: drop-shadow(0 0 3px var(--amber));\n}\n\n.temperature-ring.heating .dial-label.active-label {\n  fill: #ffc078;\n  filter: drop-shadow(0 0 4px var(--amber));\n}\n\n/* DIAL ADJUSTMENT GLOW */\n\n.temperature-ring.adjusting .ring-progress {\n  filter:\n    drop-shadow(0 0 8px var(--cyan))\n    drop-shadow(0 0 20px #00aaff);\n}\n\n.temperature-ring.heating.adjusting .ring-progress {\n  filter:\n    drop-shadow(0 0 8px var(--amber))\n    drop-shadow(0 0 20px #ff7300);\n}\n\n/* CENTRAL TEMPERATURE */\n\n.temperature-center {\n  position: absolute;\n  inset: 0;\n  z-index: 3;\n\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n\n  pointer-events: none;\n}\n\n.temp-caption {\n  font-size: 9px;\n  letter-spacing: 2px;\n  color: #648ba1;\n}\n\n.ambient-temp {\n  font-size: clamp(55px, 14vw, 76px);\n  line-height: 1.15;\n  font-weight: 300;\n  color: #e6faff;\n\n  text-shadow: 0 0 20px #00a9db66;\n}\n\n.degree {\n  font-size: 34px;\n  color: var(--cyan);\n  vertical-align: top;\n}\n\n.temp-unit {\n  font-size: 9px;\n  color: #63859a;\n  letter-spacing: 3px;\n}\n\n/* DIAL HINT */\n\n.dial-hint {\n  position: absolute;\n  z-index: 4;\n  bottom: 24px;\n  width: 100%;\n\n  text-align: center;\n\n  font-size: 8px;\n  letter-spacing: 2px;\n  color: #527b91;\n\n  pointer-events: none;\n}\n\n/* TARGET DISPLAY */\n\n.target-display {\n  position: relative;\n  z-index: 5;\n\n  padding: 9px 15px;\n  margin: 0 auto;\n  max-width: 310px;\n\n  background: #0c1d2b;\n  border: 1px solid #1b4055;\n  border-radius: 12px;\n\n  box-shadow: inset 0 0 25px #00bfff08;\n}\n\n.target-value {\n  font-size: 32px;\n  font-weight: 300;\n  color: var(--cyan);\n  margin: 2px 0 5px;\n\n  text-shadow: 0 0 12px #00bfff55;\n}\n\n.target-value span {\n  font-size: 20px;\n}\n\n/* ANIMATIONS */\n\n@keyframes energyTravel {\n  from {\n    stroke-dashoffset: 0;\n    opacity: 0.3;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  to {\n    stroke-dashoffset: -829;\n    opacity: 0.3;\n  }\n}\n\n@keyframes nexusBoot {\n  from {\n    opacity: 0;\n    transform: scale(0.94);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n/* RESPONSIVE */\n\n@media (max-width: 350px) {\n  .nexus {\n    padding: 12px;\n  }\n\n  .brand-name {\n    font-size: 17px;\n  }\n}\n\n/* REDUCED MOTION */\n\n@media (prefers-reduced-motion: reduce) {\n  .energy-sweep,\n  .temperature-ring {\n    animation: none;\n  }\n\n  .ring-progress,\n  .dial-indicator {\n    transition: none;\n  }\n}\n\n/* =================================\n   BOTTOM BUTTON LAYOUT\n================================= */\n\n.button-container {\n  display: flex !important;\n  flex-flow: row nowrap !important;\n  justify-content: center;\n  align-items: center;\n  gap: 14px;\n\n  width: 100%;\n  padding: 5px 10px 5px;\n  margin-top: 0;\n\n  box-sizing: border-box;\n}\n\n.stretch-button {\n  flex: 1 1 0;\n  width: auto;\n  max-width: 150px;\n  min-width: 0;\n  min-height: 75px;\n\n  display: flex !important;\n  justify-content: center;\n  align-items: center;\n\n  background: #3B3B3B !important;\n  border: none !important;\n  box-shadow: none !important;\n\n  border-radius: 14px !important;\n  overflow: hidden;\n\n  color: #ffffff;\n}\n\n/* POWER BUTTON STATES */\n\n.stretch-button {\n  background: #3B3B3B !important;\n  color: #ffffff !important;\n  transition:\n    background 0.3s ease,\n    color 0.3s ease,\n    box-shadow 0.3s ease;\n}\n\n/* POWER ICON ACTIVE STATE */\n\n.power-icon-active {\n  color: #7CFC00 !important;\n  filter: drop-shadow(0 0 6px #7CFC0088);\n  transition: color 0.3s ease, filter 0.3s ease;\n}\n\n\n/* FAN ACTIVE STATE */\n\n.fan-active {\n  background: #3B3B3B !important;\n  color: #00D9FF !important;\n  box-shadow: 0 0 14px #00D9FF55 !important;\n}\n\n/* ROTATING FAN */\n\n.fan-spinning {\n  animation: fanRotate 1s linear infinite;\n}\n\n@keyframes fanRotate {\n  from {\n    transform: rotate(0deg);\n  }\n\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n\n\n</style>\n","noerr":0,"storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":610,"y":380,"wires":[["953d89927c79927c","3f0bb353bfa06f22","bab4ddb531211035","d41568c07d7fba67"]]},{"id":"546c6a08d800a5e9","type":"ui-group","name":"Thermo","page":"8f9ca4f2e85412d6","width":"4","height":"5","order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"8f9ca4f2e85412d6","type":"ui-page","name":"Thermostat","ui":"e666d999ff129a18","path":"/Thermostat","icon":"mdi-thermostat","layout":"grid","theme":"f34d19a187ddca0b","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"e666d999ff129a18","type":"ui-base","name":"Sprink1","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":5,"showDisconnectNotification":true,"allowInstall":true},{"id":"f34d19a187ddca0b","type":"ui-theme","name":"Main","colors":{"surface":"#000000","primary":"#000000","bgPage":"#000000","groupBg":"#000000","groupOutline":"#000000"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"acd310ce547eab86","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.36.0"}}]

The little blue dash on the ring spins around the circle continuously and the dash and circle changes to red when the furnace kicks on. A few things in the flow besides thermostat, When the whole new DB2 came along I really dreaded the change and it took a long time to get even remotely comfortable but now that I got to it there are a lot of really cool things you can do.

The clamping logic looks sound, though I'd watch the tempSet == 'undefined' comparisons — that's checking against the string rather than the value, so use typeof tempSet === 'undefined' or just check for null/NaN. Also the let tempSet inside the Minus branch shadows the outer var, which works here but reads confusingly. Since Plus and Minus both clamp to 10–25, you could pull that into one helper and cut the duplicated blocks. Nice that the status updates stay consistent across branches.

Thanks for the comments. The plus and minus stuff was left over from a previous thermostat attempt that I missed, I removed it but left the undefined stuff and moved it in the code and used your suggestion.
Thank you

The typeof fix is the right call — == 'undefined' against a string will quietly pass when the flow variable was never set, so the guard wouldn't fire. Moving the check before the first read means Plus and Minus both start from a clean 10 instead of inheriting whatever was left in context. One thing worth testing: if you ever restore context on restart, confirm tempSet comes back as a number and not a string, otherwise the .5 increment can concatenate. Consistent status text across branches is what makes it readable at a glance.

The point about restored context is the one I'd focus on. If tempSet persists as a string, the .5 addition silently concatenates and the clamp check can behave oddly. A quick typeof tempSet !== 'number' guard that coerces or resets to 10 before the first read would cover restarts cleanly. Testing both a fresh run and a restart with a stale string value would confirm the guard fires in both directions. Agreed too that keeping the status text identical across Plus and Minus is what makes the flow easy to follow at a glance.