// 'use strict'
/* global React, ReactDOM, _ */

const MATERIALS = ['Aggregate', 'Cement', 'Admixture', 'Water']

const DELTA = '\u0394'
// const DEG_C = '\u2103' // with celsius
const DEG = '\u00B0' // without celsius

// this is for lift/liftedmixer
// TODO: duplication of constants from control - unite!
const POS_DOWN = 0
const POS_WAITING = 1
const POS_EMPTY_SEMI = 2
const POS_EMPTY = 3
const POS_TOTAL = 4

const rce = React.createElement

// the server sends a keepalive every 5 s - silence longer than this means the stream is dead or stalled
const SSE_STALE_MS = 15000

const urlParams = new URLSearchParams(window.location.search)
const MANAGER_QUEUE_ENABLED = (urlParams.get('queue') || (window.location.pathname.indexOf('/control') !== -1))
const DISPATCH_TITLE_ENABLED = urlParams.get('dispatchTitle')
const USE_IFRAME = urlParams.get('iframe') !== '0'
const CTRL_ENABLED = 1
let gloDebugReact = urlParams.get('debug')
const gloDebugStuff = {}
let gloCountOfMessages = 0

function incDebugRenderCount (k, props) {
  console.log('render ' + k, props)
  _.updateWith(gloDebugStuff, ['render_count', k], x => x ? x + 1 : 1)
}

// https://gist.github.com/Yimiprod/7ee176597fef230d1451
function difference (object, base) {
  return _.transform(object, (result, value, key) => {
    if (!_.isEqual(value, base[key])) {
      result[key] = _.isObject(value) && _.isObject(base[key]) ? difference(value, base[key]) : value
    }
  })
}

// https://stackoverflow.com/questions/8362952/javascript-output-current-datetime-in-yyyy-mm-dd-hhmsec-format
// TODO: ugly but working - find something more efficient
function dateTimeString () {
  const now = new Date()
  const offsetMs = now.getTimezoneOffset() * 60 * 1000
  const dateLocal = new Date(now.getTime() - offsetMs)
  return dateLocal.toISOString().slice(0, 19).replace('T', ' ')
}

// TODO: the whole audio/beep stuff is cut-n-pasted from pymanager - unite!
// taken from https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep
// if you have another AudioContext class use that one, as some browsers have a limit
const audioCtx = new (window.AudioContext || window.webkitAudioContext || window.audioContext)()

// All arguments are optional:

// duration of the tone in milliseconds. Default is 500
// frequency of the tone in hertz. default is 440
// volume of the tone. Default is 1, off is 0.
// type of tone. Possible values are sine, square, sawtooth, triangle, and custom. Default is sine.
// callback to use on end of tone
function beep (duration, frequency, volume, type, callback) {
  const oscillator = audioCtx.createOscillator()
  const gainNode = audioCtx.createGain()

  oscillator.connect(gainNode)
  gainNode.connect(audioCtx.destination)

  if (volume) { gainNode.gain.value = volume }
  if (frequency) { oscillator.frequency.value = frequency }
  if (type) { oscillator.type = type }
  if (callback) { oscillator.onended = callback }

  oscillator.start(audioCtx.currentTime)
  oscillator.stop(audioCtx.currentTime + ((duration || 500) / 1000))
};

function soundAlert () {
  beep(70, 500)
  console.log('play tone')
}
// TODO: end of cut-n-pasted audio/beep shit

function callApi (msg) {
  fetch('./nodes/' + msg.join('/'), { method: 'POST' }) // TODO: what about the timeout?
}

const byKeyToByLang = d => {
  const ret = {}
  for (const [k, v] of Object.entries(d)) {
    for (const [kk, vv] of Object.entries(v)) {
      if (!(kk in ret)) {
        ret[kk] = {}
      }
      ret[kk][k] = vv
    }
  }
  return ret
}

// TODO: it would be better to use set type for recursion protection but it only provides in-place operations afaik
const _fmt = (s, vals, recurProtection = []) => {
  if (typeof s !== 'string') {
    return s
  }
  return s.replace(/{(.*?)}/g, (_, k) => ((recurProtection.indexOf(k) === -1) && vals[k] !== undefined && vals[k] !== null ? _fmt(vals[k], vals, recurProtection.concat(k)) : '!' + k + '!'))
}

// hyperscript helper - stupid version
// TODO: add support for id (div.class#id)
// TODO: add support for component (non-string) as first arg
const h = (tag, props, children) => {
  if (tag.indexOf('.') < 0) { // TODO: does this really make it faster?
    return rce(tag || 'div', props, children ? React.Children.toArray(children) : undefined)
  }
  const parts = tag.split('.')
  const tag_ = parts[0]
  const classes = parts.slice(1)
  const props_ = props || {}
  if (classes) {
    props_.className = (props_.className ? props_.className + ' ' : '') + classes.join(' ')
  }
  return rce(tag_ || 'div', props_, children ? React.Children.toArray(children) : undefined)
}

const reasonToClassSpace = s => s === 'manual' ? ' reasonManual' : (s === 'automat' ? ' reasonAutomat' : '')

const reasonToClassDot = s => s === 'manual' ? '.reasonManual' : (s === 'automat' ? '.reasonAutomat' : '')

function myShouldComponentUpdate (name, props, nextProps, _state, _nextState) {
  const x = !_.isEqual(props, nextProps) // deep compare
  if (gloDebugReact && x) {
    console.log('shouldComponentUpdate', name, difference(nextProps, props))
  }
  return x
}

class Main extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('Main', this.props, nextProps, this.state, nextState)
  }

  render () { return renderMain(this.props) }
}

class Queue extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('Queue', this.props, nextProps, this.state, nextState)
  }

  componentDidMount () {
    const props = this.props
    const el = window.document.getElementById('managerQueueIframe')
    el.onload = () => {
      console.debug('queue.onload')
      const el2 = el.contentWindow.document.getElementById('orderEventContent')
      if (el2) {
        console.debug('queue load success')
        el2.addEventListener('input', e => {
          const orderId = e.target.value
          props.managerOpenOrderDialog(orderId, props.lang_cur)
        })
        el.contentWindow.document.body.addEventListener('keydown', cloneAndDispatchEvent)
        el.contentWindow.document.body.addEventListener('keyup', cloneAndDispatchEvent)
      } else {
        console.debug('queue load failure, retrying in 1 second')
        setTimeout(() => {
          el.src = '../manager/400?lang=' + props.lang_cur
        }, 1000)
      }
    }
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Queue', props)
    return h('iframe.managerQueueIframe', {
      id: 'managerQueueIframe',
      src: '../manager/400?lang=' + props.lang_cur,
      width: '100%',
      height: '100%'
    })
  }
}

class ContinuousConcreteSettings extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeContinuousConcreteSettings', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeContinuousConcreteSettings(this.props) }
}

class OrderOnHold extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeOrderOnHold', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeOrderOnHold(this.props) }
}

class NodeBinsAndFlowmeters extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeBinsAndFlowmeters', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeBinsAndFlowmeters(this.props) }
}

class NodeBins extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeBins', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeBins(this.props) }
}

class NodeFlowmeters extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFlowmeters', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFlowmeters(this.props) }
}

class NodeConveyers extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeConveyers', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeConveyers(this.props) }
}

class NodeSilos extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeSilos', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeSilos(this.props) }
}

class NodeLifts extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLifts', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLifts(this.props) }
}

class NodeFunctionsKey extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFunctionsKey', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFunctionsKey(this.props) }
}

class NodeMessage extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMessage', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMessage(this.props) }
}

class NodeMixerBay extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixerBay', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixerBay(this.props) }
}

class NodeMixers extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixers', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixers(this.props) }
}

class NodeMixerLift extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeMixerLift', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeMixerLift(this.props) }
}

class NodeFunnels extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFunnels', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFunnels(this.props) }
}

class NodeChutes extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeChutes', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeChutes(this.props) }
}

class NodeSBMFunnel extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeSBMFunnel', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeSBMFunnel(this.props) }
}

class NodeFreePaths extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeFreePaths', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeFreePaths(this.props) }
}

class NodeCart extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeCart', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeCart(this.props) }
}

class NodeLeftMenuCompressor extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuCompressor', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuCompressor(this.props) }
}

class NodeLeftMenuScalesTare extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuScalesTare', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuScalesTare(this.props) }
}

class NodeLeftMenuFlowmetersReset extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuFlowmetersReset', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuFlowmetersReset(this.props) }
}

class NodeLeftMenuMixersWashing extends React.Component {
  shouldComponentUpdate (nextProps, nextState) {
    return myShouldComponentUpdate('NodeLeftMenuMixersWashing', this.props, nextProps, this.state, nextState)
  }

  render () { return renderNodeLeftMenuMixersWashing(this.props) }
}

/* class NodeModal extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return myShouldComponentUpdate("NodeModal", this.props, nextProps, this.state, nextState);
  }
  render() {return renderNodeModal(this.props)}
} */

const precalcStuff = (pnodes, xxxAggregatesToNextLineCutoff) => {
  const ret = {
    horizontalConveyers: [],
    inclinedConveyers: [],
    lifts: [],
    mixers: [],
    mixerIsLifted: pnodes.Mixer1._type === 'LiftedMixer',
    continuousPlant: pnodes.control._is_continuous_plant,
    aboveMixer: [],
    underMixer: [], // TODO: rename to belowMixer?
    chutes: [],
    freePaths: [],
    carts: [],
    liftIsAggregateScale: false,
    compressors: [],
    scalesTare: [],
    flowmetersReset: [],
    mixersWashing: []
  }
  const silos = {}
  const binsAndFlowmeters = {}
  const scales = {}
  let countOfAllSilos = 0
  let countOfSilosWithoutAggregate = 0
  let countOfAggregateSilos = 0
  const ks = Object.keys(pnodes).sort()
  for (const material of MATERIALS) {
    silos[material] = {}
    binsAndFlowmeters[material] = {}
    scales[material] = {}
    for (const k of ks) {
      if (k.indexOf(material + 'Silo') !== -1) {
        silos[material][k] = {}
        silos[material][k].sink = pnodes[k].sink
        countOfAllSilos += 1
        if (material !== 'Aggregate') {
          countOfSilosWithoutAggregate += 1
        } else {
          countOfAggregateSilos += 1
        }
      }
      if (k.indexOf('Reservoir') !== -1) {
        continue
      }
      if ((k.indexOf(material + 'Bin') !== -1) ||
      (k.indexOf(material + 'Flowmeter') !== -1) ||
      (material === 'Aggregate' && (k.indexOf('Lift1') !== -1 || k.indexOf('Conveyer1') !== -1 || k.indexOf('Conveyer2') !== -1 || k.indexOf('Conveyer3') !== -1))) {
        if (k.indexOf('Lift1') !== -1 || k.indexOf('Conveyer1') !== -1) {
          if (k.indexOf('Lift1') !== -1) {
            if (_.get(pnodes, ['Lift1', 'scale']) == null) {
              continue
            }
          }
        }
        binsAndFlowmeters[material][k] = {}
        binsAndFlowmeters[material][k].silos = []
        binsAndFlowmeters[material][k].flowmeters = []
      }
      if (k.indexOf(material + 'Scale') !== -1) {
        scales[material][k] = {}
        scales[material][k].bins = []
      }
    }
  }

  for (const mat in silos) {
    console.log(mat + ' - Silos: ' + Object.keys(silos[mat]).length)
    for (const k in silos[mat]) {
      let sink = silos[mat][k].sink
      console.log(k + ' sink: ' + sink)
      if (sink.indexOf('DensityProbe') !== -1) {
        console.log('overriding sink to ' + pnodes[sink].sink)
        sink = pnodes[sink].sink
      }
      if (binsAndFlowmeters[mat][sink]) {
        binsAndFlowmeters[mat][sink].silos.push(k)
      } else {
        console.log('forcing silo to water bin: ' + k)
        if (sink.includes('Water')) {
          binsAndFlowmeters.Water[sink].silos.push(k)
          console.log(sink)
        }
      }
    }
  }

  for (const k in binsAndFlowmeters) {
    for (const k_ in binsAndFlowmeters[k]) {
      // TODO: this is waaay too verbose - find more elegant solution (sort_by?)
      binsAndFlowmeters[k][k_].silos.sort((k1, k2) => {
        const v1 = pnodes[k1]
        const v2 = pnodes[k2]
        const i1 = v1._visual_index != null ? v1._visual_index : v1._index
        const i2 = v2._visual_index != null ? v2._visual_index : v2._index
        console.debug('SORT', k1, k2, v1, v2)
        if (i1 < i2) {
          return -1
        } else if (i1 > i2) {
          return 1
        } else {
          return 0
        }
      })
    }
  }

  for (const k in binsAndFlowmeters) {
    const count = Object.keys(binsAndFlowmeters[k]).length
    console.log(k + ' - BinsAndFlowmetersCount: ' + count)
    for (const key in binsAndFlowmeters[k]) {
      console.log(k + ' sink: ' + pnodes[key].sink)
      const sink = pnodes[key].sink
      if (_.get(binsAndFlowmeters, [k, sink])) {
        binsAndFlowmeters[k][sink].flowmeters.push(key)
      }
      console.log(JSON.stringify(binsAndFlowmeters[k][sink]))
      console.log(k + ' - ' + [key] + ' - ' + binsAndFlowmeters[k][key].silos)
      for (const key2 in scales[k]) {
        if (pnodes[key].scale === key2) {
          scales[k][key2].bins.push(key)
          console.log(k + ' - ' + key2 + ' bins: ' + scales[k][key2].bins)
        }
      }
    }
  }

  for (const k of ks) {
    const v = pnodes[k]
    if ((v._type === 'Compressor') && v.can_start_stop) {
      ret.compressors.push(k)
    }
    if ((v._type === 'Scale') && v.can_tare) {
      ret.scalesTare.push(k)
    }
    if ((v._type === 'Flowmeter') && v.can_reset) {
      ret.flowmetersReset.push(k)
    }
    if (v._type === 'Mixer') {
      ret.mixers.push(k)
      if (v.has_washing) {
        ret.mixersWashing.push(k)
      }
    }
    if (v._type === 'LiftedMixer') {
      ret.mixers.push(k)
    }
    if (v._type === 'Lift') {
      ret.lifts.push(k)
      if (v.scale === 'AggregateScale1') {
        ret.liftIsAggregateScale = true
      }
    }
    if (v._type === 'Chute') {
      ret.chutes.push(k)
    }
    // if (v._type === 'Reservoir') {
    if (k.indexOf('Reservoir') !== -1) { // TODO: don't string-match on key
      ret.aboveMixer.push(k)
    }
    if (v._type === 'Funnel') {
      ret.underMixer.push(k)
    }
    if (v._type === 'SBMFunnel') {
      ret.underMixer.push(k)
    }
    if (v._type === 'Cart') {
      ret.carts.push(k)
    }
    if ((v._type === 'Conveyer') || (v._type === 'Screw')) {
      if (('Mixer1' in pnodes && pnodes.Mixer1.sink === k) || ('Chute1' in pnodes && pnodes.Chute1.sink === k)) {
        ret.underMixer.push(k)
      } else if (v._type === 'Conveyer') {
        if (v._is_inclined) {
          ret.inclinedConveyers.push(k)
        } else {
          ret.horizontalConveyers.push(k)
        }
      }
    }
    // TODO: this somehow mixes Other and freepath thing - solve better!
    if (k === 'Other') {
      if (_.get(pnodes, [k, '_freepath']) != null) {
        ret.freePaths.push('Other')
      }
    }
  }

  ret.aggregatesOnBottom = countOfAllSilos >= xxxAggregatesToNextLineCutoff
  ret.countOfSilosWithoutAggregate = countOfSilosWithoutAggregate
  ret.countOfAggregateSilos = countOfAggregateSilos
  ret.binsAndFlowmeters = binsAndFlowmeters
  ret.scales = scales

  console.log('precalc: ', ret)

  return ret
}

const renderMain = props => {
  gloDebugReact && incDebugRenderCount('Main', props)

  let subTree = null
  let menuTree = null
  if (props.nodes) {
    const wideModeCoef = props.xxxWideMode
      ? 0.74 // original was 0.75 but mj83 (zapa kacerov merko) needs 0.74
      : (props.precalc.countOfSilosWithoutAggregate === 16 // ugly hack for mj204
          ? 0.945 // ugly hack for mj204 (they have 16 non-aggregate silos and 16/9 display)
          : 1
        )

    const nodes = {}
    for (const material of MATERIALS) {
      nodes[material + 'Silos'] = []
      nodes[material + 'Bins'] = []
      for (const k in props.precalc.binsAndFlowmeters[material]) {
        // TODO: hacky shit
        // if (k == 'count') {
        //  continue;
        // }
        const v = props.nodes[k]
        const cementScrew = v.screw
        const cementScrewState = _.get(props.nodes, [cementScrew, 'is_running'])
        const cementScrewScaleValueStr = _.get(props.nodes, [cementScrew, 'scale_val_str'])
        const cementScrewFlowStr = _.get(props.nodes, [cementScrew, 'flow_str'])
        const cementScrewSetOpenReason = _.get(props.nodes, [cementScrew, 'set_open_reason'])
        nodes[material + 'Bins'].push(rce(NodeBinsAndFlowmeters, {
          key: k,
          data: v,
          precalc: props.precalc,
          xxxKeys: props.xxxKeys,
          xxxCementScrewState: cementScrewState,
          xxxCementScrewScaleValueStr: cementScrewScaleValueStr,
          xxxCementScrewFlowStr: cementScrewFlowStr,
          xxxCementScrewSetOpenReason: cementScrewSetOpenReason,
          xxxStateFrozen: props.xxxStateFrozen,
          xxxWideModeCoef: wideModeCoef,
          fmt: props.fmt
        }))
        for (const k2 of props.precalc.binsAndFlowmeters[material][k].silos) {
          const v = props.nodes[k2]
          nodes[material + 'Silos'].push(rce(NodeSilos, {
            key: k2,
            data: v,
            xxxKeys: props.xxxKeys,
            xxxStateFrozen: props.xxxStateFrozen,
            xxxWideModeCoef: wideModeCoef,
            fmt: props.fmt
          }))
        }
      }
    }

    const nodesMixerLift = []
    for (const k of props.precalc.mixers) {
      const v = props.nodes[k]
      nodes[k] = []
      nodes[k].push(rce(NodeMixers, {
        key: k,
        data: v,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxKeys: props.xxxKeys,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
      if (props.precalc.mixerIsLifted) {
        nodesMixerLift.push(rce(NodeMixerLift, {
          key: k + 'Lift',
          data: v,
          xxxStateFrozen: props.xxxStateFrozen,
          xxxKeys: props.xxxKeys,
          xxxWideModeCoef: wideModeCoef,
          fmt: props.fmt
        }))
      }
    }

    const nodesAboveMixer = []
    for (const k of props.precalc.aboveMixer) {
      const v = props.nodes[k]
      nodesAboveMixer.push(rce(NodeBins, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxxKeys: props.xxxKeys,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesUnderMixer = []
    for (const k of props.precalc.underMixer) {
      const v = props.nodes[k]
      let _type = null
      if (v._type === 'Funnel') {
        _type = NodeFunnels
      } else if (v._type === 'Conveyer') {
        _type = NodeConveyers
      } else if (v._type === 'SBMFunnel') {
        _type = NodeSBMFunnel
      } else {
        console.error('unknown node type', v._type)
      }
      nodesUnderMixer.push(rce(_type, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxxKeys: props.xxxKeys,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesFreePaths = []
    for (const k of props.precalc.freePaths) {
      const v = props.nodes[k]
      nodesFreePaths.push(rce(NodeFreePaths, {
        key: k,
        data: v,
        xxxWideModeCoef: wideModeCoef
      }))
    }

    const nodesChutes = []
    for (const k of props.precalc.chutes) {
      const v = props.nodes[k]
      nodesChutes.push(rce(NodeChutes, {
        key: k,
        data: v,
        xxxKeys: props.xxxKeys,
        xxxStateFrozen: props.xxxStateFrozen,
        fmt: props.fmt
      }))
    }

    const nodesCart = []
    for (const k of props.precalc.carts) {
      const v = props.nodes[k]
      nodesCart.push(rce(NodeCart, {
        key: k,
        data: v,
        xxxKeys: props.xxxKeys,
        xxxStateFrozen: props.xxxStateFrozen,
        fmt: props.fmt
      }))
    }

    const nodesLeftMenu = []
    for (const k of props.precalc.compressors) {
      const v = props.nodes[k]
      nodesLeftMenu.push(rce(NodeLeftMenuCompressor, {
        key: k,
        data: v,
        fmt: props.fmt
      }))
    }
    nodesLeftMenu.push(rce(NodeLeftMenuScalesTare, {
      key: 'nodeLeftMenuScalesTare',
      data: props.precalc.scalesTare,
      fmt: props.fmt
    }))
    nodesLeftMenu.push(rce(NodeLeftMenuFlowmetersReset, {
      key: 'nodeLeftMenuFlowmetersReset',
      data: props.precalc.flowmetersReset,
      fmt: props.fmt
    }))
    nodesLeftMenu.push(rce(NodeLeftMenuMixersWashing, {
      key: 'nodeLeftMenuMixersWashing',
      data: props.precalc.mixersWashing,
      nodes: props.nodes,
      fmt: props.fmt
    }))

    for (const k of props.precalc.lifts) {
      const v = props.nodes[k]
      nodes[k] = []
      nodes[k].push(rce(NodeLifts, {
        key: k,
        data: v,
        xxxKeys: props.xxxKeys,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    nodes.horizontalConveyers = []
    for (const k of props.precalc.horizontalConveyers) {
      const v = props.nodes[k]
      const hornId = v.horn
      const horn = hornId ? props.nodes[hornId] : null
      nodes.horizontalConveyers.push(rce(NodeConveyers, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxxKeys: props.xxxKeys,
        xxxHorn: horn,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    nodes.inclinedConveyers = []
    for (const k of props.precalc.inclinedConveyers) {
      const v = props.nodes[k]
      const hornId = v.horn
      const horn = hornId ? props.nodes[hornId] : null
      nodes.inclinedConveyers.push(rce(NodeConveyers, {
        key: k,
        data: v,
        precalc: props.precalc,
        xxxKeys: props.xxxKeys,
        xxxHorn: horn,
        xxxStateFrozen: props.xxxStateFrozen,
        xxxWideModeCoef: wideModeCoef,
        fmt: props.fmt
      }))
    }

    const nodesFunctionsKey = []
    const mixerLock = _.get(props.nodes, ['Mixer1', 'is_locked'])
    const mixerDir = _.get(props.nodes, ['Mixer1', 'set_direction_manual'])
    const mixerDirCont = _.get(props.nodes, ['Mixer1', 'set_direction_cont_manual'])
    nodesFunctionsKey.push(rce(NodeFunctionsKey, {
      key: 'controlKeys',
      xxxMixerIsLocked: mixerLock,
      xxxMixerDirectionClose: mixerDir === -1 || mixerDirCont === -1,
      xxxMixerDirectionOpen: mixerDir === 1 || mixerDirCont === 1,
      xxxKeys: props.xxxKeys,
      xxxStateFrozen: props.xxxStateFrozen,
      xxxAdditionalWaterIsPresent: props.nodes.control.water_additional_is_present,
      xxxAdditionalWaterSiloOpen: props.nodes.control.water_additional_open_percent,
      xxxAdditionalWaterValueStr: props.nodes.control.water_additional_val_str,
      xxxSemaphoreId: props.nodes.control.semaphore_id,
      xxxSemaphoreColor: props.nodes.control.semaphore_color,
      xxxHornId: props.nodes.control.horn_id,
      xxxHornIsEnabled: props.nodes.control.horn_is_enabled,
      xxxCloseAggregateSilosOnOpen: props.nodes.control.close_aggregate_silos_on_open,
      xxxVibrateSilosOnOpen: props.nodes.control.vibrate_silos_on_open,
      xxxAerateSilosOnOpen: props.nodes.control.aerate_silos_on_open,
      openHelpWindow: props.openHelpWindow,
      xxxIsHelp: props.xxxIsHelp,
      fmt: props.fmt
    }))

    const messages = []
    const ks = Object.keys(props.nodes).sort()
    for (const k of ks) {
      for (const k_ in props.nodes[k]._messages) {
        messages.push({ ...props.nodes[k]._messages[k_], node_id: k })
      }
    }
    messages.sort((x, y) => x.t - y.t)
    let someoneHasOptions = 0
    const nodesMessages = []
    for (const i of messages) {
      if (i.level === 'beep') {
        continue
      }
      const hasOptions = _.get(i, ['options'], []).length ? 1 : 0
      nodesMessages.push(rce(NodeMessage, {
        key: 'controlMessages' + i.node_id + '_' + i.k,
        data: i,
        xxxKeys: props.xxxKeys,
        xxxEnterOrEscOrNumUsed: props.xxxEnterOrEscOrNumUsed,
        setEnterOrEscUsed: props.setEnterOrEscUsed,
        xxxKeyboardFocus: hasOptions && !someoneHasOptions,
        fmt: props.fmt
      }))
      someoneHasOptions |= hasOptions
    }
    // TODO: ugly shit - don't just count the messages, compare their identity!
    if (gloCountOfMessages !== messages.length) {
      if (messages.length > gloCountOfMessages) {
        soundAlert()
      }
      gloCountOfMessages = messages.length
    }

    const subTreeOpacity = props.xxxConnected ? 1 : 0.2
    // the hard-coded shit "12" was added because of mj83 (zapa kacerov merko) because the aggregates were pushing the mixer to the messages section - TODO: solve better
    const widerSpacing = (props.precalc.countOfSilosWithoutAggregate < props.xxxAggregatesToNextLineCutoff) && (props.precalc.countOfAggregateSilos < 12) // TODO: this is wrong - wider spacing has nothing to do with cutoff, TODO: hard-coded shit

    // left menu
    const menuIsOpened = props.xxxMenuIsOpened
    const menuClass = '.leftMenu' + (menuIsOpened ? 'Opened' : 'Closed')
    const menuButtonClass = menuIsOpened ? '.left-menu-button' : '.left-menuClosed-button'
    const essentialsOk = props.nodes.control._essentials_ok
    menuTree = h(menuClass, { key: 'menuTree' }, [
      h(menuButtonClass, { title: 'MENU', onMouseDown: _e => props.menuToggle() }, [
        h('i.fa.fa-ellipsis-v'),
        // h(essentialsOk ? 'i.fa.fa-thumbs-up.green' : 'i.fa.fa-exclamation.faa-flash.animated.red')
        h(essentialsOk ? 'i.fa.fa-thumbs-up.green' : 'i.fa.fa-exclamation.red')
      ]),
      menuIsOpened ? nodesLeftMenu : null
    ])

    const aggSiloWidth = (props.precalc.countOfAggregateSilos * 5 * props.xxxWideModeCoef) + (props.precalc.countOfAggregateSilos * 0.05) + 1 + 'vw'

    subTree = h('', { style: { opacity: subTreeOpacity } }, [
      h('.plant', {}, [
        h('.plantLeft', {}, [
          h('.topSilos', { style: { justifyContent: !props.precalc.aggregatesOnBottom ? 'center' : 'flex-start' } }, [
            props.precalc.aggregatesOnBottom
              ? null
              : h('.content-aggregate', {}, [
                h('.content-aggregates', { style: { width: aggSiloWidth } }, nodes.AggregateSilos),
                h('.content-flowmetersAndBins', { style: { width: aggSiloWidth } }, nodes.AggregateBins),
                h('.content-conveyers', { style: { width: aggSiloWidth } }, nodes.horizontalConveyers)
              ]),
            h('.content-cement', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
              nodes.CementSilos,
              nodes.CementBins
            ]),
            h('.content-spacer', { style: { width: widerSpacing ? '2vw' : '0.5vw' } }),
            h('.content-admixture', {}, [
              nodes.AdmixtureSilos,
              nodes.AdmixtureBins
            ]),
            h('.content-spacer', { style: { width: widerSpacing ? '2vw' : '0.5vw' } }),
            h('.content-water', {}, [
              nodes.WaterSilos,
              nodes.WaterBins
            ]),
            h('.recycledWaterDisabled', {}, [
              props.nodes.control.recycled_water_disabled ? props.fmt('{RecycledWaterDisabled}') : ''
            ])
          ]),
          h('.middle', {}, [
            h('.middleLeft', {}, [
              h('.middleLeftSilos', {}, [
                props.precalc.aggregatesOnBottom
                  ? h('.content-aggregate', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
                    nodes.AggregateSilos,
                    nodes.AggregateBins
                  ])
                  : null
              ]),
              h('.middleLeftConveyers', { style: { marginLeft: widerSpacing ? '2vw' : '0.5vw' } }, [
                (props.precalc.horizontalConveyers.length > 0 && props.precalc.aggregatesOnBottom) ? nodes.horizontalConveyers : null
              ])
            ]),
            h('.middleMiddle', {}, [
              h('.middleMiddleTop', {}, [
                rce(OrderOnHold, {
                  order_on_hold: props.nodes.control.order_on_hold,
                  stop_after_batch: props.nodes.control.stop_after_batch,
                  fmt: props.fmt
                })
              ]),
              h('.middleMiddleCenter', {}, [
                props.precalc.lifts.length > 0 || props.precalc.inclinedConveyers.length > 0
                  ? h('.middleMiddleCenterLeft', {}, [
                    props.precalc.lifts.length > 0 ? nodes.Lift1 : null,
                    props.precalc.inclinedConveyers.length > 0 ? nodes.inclinedConveyers : null
                  ])
                  : h('.middleMiddleCenterLeftHalf'),
                h('.middleMiddleCenterCenter', {}, [
                  h('.reservoir', {}, [
                    nodesAboveMixer.length > 0 ? nodesAboveMixer : null
                  ]),
                  h('.content-mixer-global', {}, [
                    nodes.Mixer1,
                    nodesChutes.length > 0 ? nodesChutes : h('.chutes-content'),
                    nodesUnderMixer.length > 0
                      ? nodesUnderMixer
                      : (nodesChutes.length === 0 ? h('.content-funnel') : null), // TODO: funnel is also being used as bottom padding for mixer. we don't want that when chute (with extend) is present - still, this is a hack -> implement proper padding and make funnel's height not fixed
                    nodesFreePaths.length > 0 ? nodesFreePaths : h('.content-freePath'),
                    nodesCart.length > 0 ? nodesCart : h('.carts-content')
                  ])
                ]),
                h('.middleMiddleCenterRight', {}, [
                  props.precalc.mixerIsLifted ? nodesMixerLift : null
                ])
              ])
            ]),
            h('') // TODO: fake div !!!!! why??
          ])
        ]),
        h('.plantRight', {}, [
          // TODO: rework the following - there's way too much repetition
          props.precalc.continuousPlant
            ? h('.topQueue', {}, [
              h('.topContinuousSettings', {}, [
                rce(ContinuousConcreteSettings, {
                  hour_output: props.nodes.control.hour_output,
                  truck_capacity: props.nodes.control.truck_capacity,
                  fmt: props.fmt
                })
              ]),
              h('.topQueueContinuous', {}, [
                MANAGER_QUEUE_ENABLED
                  ? rce(Queue, {
                    lang_cur: props.lang_cur,
                    managerOpenOrderDialog: props.managerOpenOrderDialog
                  })
                  : null
              ])
            ])
            : h('.topQueue', {}, [
              h('.topQueueBatchMode', {}, [
                MANAGER_QUEUE_ENABLED
                  ? rce(Queue, {
                    lang_cur: props.lang_cur,
                    managerOpenOrderDialog: props.managerOpenOrderDialog
                  })
                  : null
              ])
            ]),
          h('.middleMessages.scroller', {}, nodesMessages)
        ])
      ]),
      h('.footer', {}, [
        h('.plantId', {}, props.nodes.control.plant_id),
        nodesFunctionsKey
      ]),
      rce(Clock),
      props.xxxIsHelp ? rce(NodeHelp, props) : null,
      props.xxxIsLanguage ? rce(LanguageDialog, props) : null,
      props.xxxIsExit ? rce(ExitDialog, props) : null,
      props.xxxIsDispatch
        ? rce(IframeModal, {
          title: props.fmt('{dispatcher}'),
          url: '../dispatcher/?lang=' + props.lang_cur,
          noResize: true,
          onClose: _e => { props.dispatchCloseDialog() },
          modalWidth: MODAL_SIZES.dispatch.vw,
          modalHeight: MODAL_SIZES.dispatch.vh
        })
        : null,
      props.xxxIsParamQuick
        ? rce(IframeModal, {
          title: props.fmt('{moistures}'),
          url: '../parameters/?quick=1&lang=' + props.lang_cur,
          onClose: _e => { props.paramQuickCloseDialog() },
          modalWidth: MODAL_SIZES.paramQuick.vw
        })
        : null,
      props.xxxIsParam
        ? rce(IframeModal, {
          title: props.fmt('{parameters}'),
          url: '../parameters/?lang=' + props.lang_cur,
          onClose: _e => { props.paramCloseDialog() },
          modalWidth: MODAL_SIZES.param.vw
        })
        : null,
      props.xxxIsPlace
        ? rce(IframeModal, {
          title: props.fmt('{place}'),
          url: '../place/?lang=' + props.lang_cur,
          onClose: _e => { props.placeCloseDialog() },
          modalWidth: MODAL_SIZES.place.vw
        })
        : null,
      props.xxxIsAuth
        ? rce(IframeModal, {
          title: props.fmt('{authorization}'),
          url: '../auth/list?lang=' + props.lang_cur,
          noResize: true,
          onClose: _e => { props.authCloseDialog() },
          modalWidth: MODAL_SIZES.auth.vw,
          modalHeight: MODAL_SIZES.auth.vh
        })
        : null,
      props.xxxModalOrderId
        ? rce(Modal, {
          xxxOrderId: props.xxxModalOrderId,
          managerCloseOrderDialog: props.managerCloseOrderDialog,
          fmt: props.fmt,
          lang_cur: props.lang_cur
        })
        : null,
      gloDebugReact
        ? h('.debug', {}, [
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_steps']) }, 'reset measured steps'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_overshoots']) }, 'reset measured overshoots'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_inflights']) }, 'reset measured inflights'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_lift_durations']) }, 'reset measured lift durations'),
          h('.top-menu-button', { onMouseDown: _e => callApi(['control', 'reset_measured_mixer_durations']) }, 'reset measured mixer durations'),
          h('.top-menu-button', { onMouseDown: _e => { window.localStorage.clear(); window.location.reload() } }, 'reset localStorage'),
          h('.top-menu-button', { onMouseDown: _e => props.doCrash() }, 'crash frontend'),
          h('pre', {}, JSON.stringify(gloDebugStuff, null, 2))
        ])
        : null
    ])
  }

  const contentClass = 'content' + (!props.xxxConnected ? ' disconnected' : (props.xxxStateIsPc !== 0 ? (props.xxxStateFrozen ? ' f10enabled' : ' f10disabled') : ' pcNotActive')) + (' ' + props.xxxTheme)

  const themeIcon = 'i.fa' + (props.xxxTheme === 'day' ? '.fa-moon' : '.fa-sun')

  return h('', { id: 'main', className: contentClass }, [
    props.precalc ? menuTree : null,
    h('.header', {}, [
      h('.top-menu', {}, [
        h('.top-menu-button', {
          onMouseDown: _e => props.openExitDialog(),
          title: props.fmt('{exit_control_program}')
        }, h('i.fa.fa-power-off')),
        DISPATCH_TITLE_ENABLED
          ? h('.top-menu-button', { onMouseDown: _e => sendTitle('control:dispatcher') }, props.fmt('{dispatcher}'))
          : h('.top-menu-button', { onMouseDown: _e => props.dispatchOpenDialog(props.lang_cur) }, props.fmt('{dispatcher}')),
        h('.top-menu-button', { onMouseDown: _e => props.paramQuickOpenDialog(props.lang_cur) }, props.fmt('{moistures}')),
        h('.top-menu-button', { onMouseDown: _e => props.paramOpenDialog(props.lang_cur) }, props.fmt('{parameters}')),
        h('.top-menu-button', { onMouseDown: _e => props.placeOpenDialog(props.lang_cur) }, props.fmt('{place}')),
        h('.top-menu-button', {
          onMouseDown: _e => props.authOpenDialog(props.lang_cur),
          title: props.fmt('{authorization}')
        }, h('i.fa.fa-users')),
        h('.top-menu-button', {
          onMouseDown: _e => props.openLanguageDialog(),
          title: props.fmt('{language}')
        }, h('i.fa.fa-globe')),
        h('.top-menu-button', {
          onMouseDown: _e => props.themeToggle(),
          title: props.fmt('{theme}')
        }, h(themeIcon))
      ])
    ]),
    props.precalc ? subTree : null
  ])
}

/* TODO: finish this
const arrowCombo = (open_percent, opening_state, closing_state) => {
  const openingArrowAvailable = opening_state != null;
  const openingArrowState = opening_state;
  const openingArrowIcon = "fa fa-up" + (openingArrowState ? " arrow-signal-open-100" : "");
  const closingArrowAvailable = closing_state != null;
  const closingArrowState = closing_state;
  const closingArrowIcon = "fa fa-down" + (closingArrowState ? " arrow-signal-open-100" : "");
  const arrowIcon = "fa fa-down" + (open_percent ? " arrow-signal-open-" + (isWarning ? "warning-" : "") + setOpenFinalPercent : "") + " isArrow isControl needsCTRL" + (props.xxxKeys['CTRL'] ? " hasCTRL" : "") + (props.xxxStateFrozen ? " isFrozen" : "");
  return h("", {}, [
    closingArrowAvailable
      ? h(".bin-closing-arrow-content", {},
        h("i", {className: closingArrowIcon}))
      : null,
    h("i", {
      className: arrowIcon,
      onMouseDown: _e => (props.xxxKeys.CTRL || props.xxxStateFrozen) && callApi([id, 'set_open_invert']),
      onMouseUp: _e => callApi([id, 'unset_open_invert']),
      onTouchStart: _e => callApi([id, 'set_open_invert']),
      onTouchEnd: _e => callApi([id, 'unset_open_invert']),
      title: props.fmt("{filling}") }
    ),
    openingArrowAvailable
      ? h(".bin-opening-arrow-content", {},
        h("i", {className: openingArrowIcon}))
      : null,
  ])
}
*/

const renderNodeSilos = props => {
  gloDebugReact && incDebugRenderCount('NodeSilos', props)
  const dfs = props.data
  const id = dfs.id
  const ctrlPressed = props.xxxKeys.CTRL ? ' hasCTRL' : ''
  const number = dfs._index
  const nameShort = dfs.material_name_short
  const nameLong = dfs.material_name_long
  const materialType = dfs._material_type
  const hasTemperature = dfs.temperature_str != null
  const temperatureStr = hasTemperature ? (dfs.temperature_str + DEG) : ''
  const hasTemperatureProbe = dfs.temperature_from_probe_str != null
  const temperatureFromProbeStr = hasTemperatureProbe ? (dfs.temperature_from_probe_str + DEG) : ''
  const temperatureIsLinkedSymbolClass = 'fa fa-' + (dfs.temperature_is_linked ? 'link' : 'unlink')
  const temperatureLink = '.silo-link' + (dfs.temperature_is_linked ? '.link' : '.unlink')
  const hasMoistureProbe = dfs.moisture_from_probe_str != null
  const moistureFromProbeStr = hasMoistureProbe ? dfs.moisture_from_probe_str + '%' : ''
  const moistureIsLinkedSymbolClass = 'fa fa-' + (dfs.moisture_is_linked ? 'link' : 'unlink')
  const moistureLink = '.silo-link' + (dfs.moisture_is_linked ? '.link' : '.unlink')
  const hasMoisture = dfs.moisture_str != null
  const moistureStr = hasMoisture ? dfs.moisture_str + '%' : ''
  const hasDensityProbe = dfs.density_from_probe_str != null
  const densityFromProbeStr = hasDensityProbe ? dfs.density_from_probe_str : ''
  const densityIsLinkedSymbolClass = 'fa fa-' + (dfs.density_is_linked ? 'link' : 'unlink')
  const densityLink = '.silo-link' + (dfs.density_is_linked ? '.link' : '.unlink')
  const hasDensity = dfs.density_str != null
  const densityStr = hasDensity ? dfs.density_str : ''
  const isDimmed = dfs._is_dimmed
  const isFast = dfs.is_fast
  const isFlow = dfs._is_flow
  const isWarning = dfs._is_warning
  const isValWarning = dfs._is_val_warning
  const valDisplay = '.silo-display' + (isValWarning ? '.red' : '')

  const requestValueStr = dfs.req_str
  const recipePosition = dfs.recipe_pos
  const requestValueOrigStr = dfs.req_orig_str
  const valueStr = dfs.val_str

  const reqDisplay = '.silo-requestDisplay' + (requestValueOrigStr ? '.siloRequestAltered' : '')

  const levelStr = dfs.level_str
  const levelPercent = dfs.level_percent
  const levelPercentInvertStyle = (100 - (levelPercent < 1 ? 1 : levelPercent > 99 ? 99 : levelPercent)) + '%' // correction of overflow percent value between 1%-99% - TODO: do the correction on back-end
  const levelClass = '.silo-level-box' + ((levelPercent > 20) ? '.green' : '.red')
  const levelTooltip = levelStr + ' (' + levelPercent + '%)'

  const canVibrate = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const vibrationButton = 'siloControl isButton isControl' +
    ctrlPressed +
    (isVibrating ? ' signalActive' : '') +
    reasonToClassSpace(dfs.set_vibrate_reason)
  const canVibrateB = dfs.can_vibrate_b
  const isVibratingB = dfs._is_vibrating_b
  const vibrationBButton = 'siloControl isButton isControl' +
    ctrlPressed +
    (isVibratingB ? ' signalActive' : '') +
    reasonToClassSpace(dfs.set_vibrate_b_reason)
  const canFast = dfs._has_sig_fast
  const canAerate = dfs.can_aerate
  const isAerating = dfs._is_aerating
  const aerationButton = 'siloControl isButton isControl' +
    ctrlPressed +
    (isAerating ? ' signalActive' : '') +
    reasonToClassSpace(dfs.set_aerate_reason)
  const canAerateB = dfs.can_aerate_b
  const isAeratingB = dfs._is_aerating_b
  const aerationBButton = 'siloControl isButton isControl' +
    ctrlPressed +
    (isAeratingB ? ' signalActive' : '') +
    reasonToClassSpace(dfs.set_aerate_b_reason)
  // TODO: hacky shit - mj191
  const additionalHackedValve = dfs.additional_hacked_valve
  const additionalHackedValveClass = 'siloControl isButton isControl' +
    ctrlPressed +
    (additionalHackedValve ? ' signalActive' : '') +
    reasonToClassSpace(dfs.additional_hacked_valve_reason)

  const sensorState = dfs.sig_is_closed_state
  const sensorBState = dfs.sig_is_closed_b_state
  const sensorClass = sensorState == null
    ? null
    : ('silo-sensor' + (sensorState === 0 ? ' silo-sensor-opened' : ''))
  const sensorBClass = sensorBState == null
    ? null
    : ('silo-sensor' + (sensorBState === 0 ? ' silo-sensor-opened' : ''))

  const closingArrowAvailable = dfs.sig_close_state != null
  const closingArrowState = dfs.sig_close_state
  const closingArrowIcon = 'fa fa-up' + (closingArrowState ? ' arrow-signal-open-100' : '')
  const openingArrowAvailable = dfs.sig_open_state != null
  const openingArrowState = dfs.sig_open_state
  const openingArrowIcon = 'fa fa-down' + (openingArrowState ? ' arrow-signal-open-100' : '')
  const closingBArrowAvailable = dfs.sig_close_b_state != null
  const closingBArrowState = dfs.sig_close_b_state
  const closingBArrowIcon = 'fa fa-up' + (closingBArrowState ? ' arrow-signal-open-100' : '')
  const openingBArrowAvailable = dfs.sig_open_b_state != null
  const openingBArrowState = dfs.sig_open_b_state
  const openingBArrowIcon = 'fa fa-down' + (openingBArrowState ? ' arrow-signal-open-100' : '')

  const sizeCoeficientWidth = 5 * props.xxxWideModeCoef
  const sizeCoeficientHeight = 23
  const siloSizeWidth = sizeCoeficientWidth + 'vw'
  const siloSizeHeight = sizeCoeficientHeight + 'vh'
  const siloSizeHeightTop = (sizeCoeficientHeight * 0.1) + 'vh'
  const siloSizeHeightBody = (sizeCoeficientHeight * 0.41) + 'vh'
  const siloSizeHeightBottom = (sizeCoeficientHeight * 0.10) + 'vh'
  const siloContentMarginLeft = (sizeCoeficientWidth * 0.05) + 'vw'

  const sensorHeight = (sizeCoeficientHeight * 0.05) + 'vh'

  const siloDisplayWidth = '100%'
  const siloDisplayHeight = (sizeCoeficientHeight * 0.1) + 'vh'
  const siloBottom = 'silo-bottom ' + materialType + (isDimmed ? 'Dimmed' : '')
  const setOpenFinalPercent = dfs.set_open_final_percent
  const arrowIcon = 'fa fa-down' +
    (setOpenFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '') +
    ' isArrow isControl needsCTRL' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '')

  const setOpenBFinalPercent = dfs.set_open_b_final_percent
  const arrowBIcon = setOpenBFinalPercent == null
    ? null
    : 'fa fa-down' +
      (setOpenBFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenBFinalPercent : '') +
      ' isArrow isControl needsCTRL' +
      (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
      (props.xxxStateFrozen ? ' isFrozen' : '')
  const arrowIconFast = 'fa fa-down' +
    (isFast ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '') +
    ' isArrow isControl needsCTRL' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '')

  const style = { width: siloDisplayWidth, height: siloDisplayHeight }
  const linkStyle = { height: (sizeCoeficientHeight * 0.2) + 'vh' }
  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  const arrowSubA = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      sensorClass
        ? h('', { className: sensorClass })
        : null
    ]),
    h('.silo-arrow-triad', {}, [
      closingArrowAvailable
        ? h('.silo-closing-arrow-content', {},
          h('i', { className: closingArrowIcon }))
        : null,
      h('i', {
        className: arrowIcon,
        onMouseDown: _e => canCallApi && callApi([id, 'set_open_invert']),
        onMouseUp: _e => callApi([id, 'unset_open_invert']),
        onTouchStart: _e => callApi([id, 'set_open_invert']),
        onTouchEnd: _e => callApi([id, 'unset_open_invert']),
        title: props.fmt('{filling}')
      }),
      openingArrowAvailable
        ? h('.silo-opening-arrow-content', {},
          h('i', { className: openingArrowIcon }))
        : null
    ]),
    h('.silo-control-content', {}, [
      canVibrate === 1
        ? h('.buttonFontSize', {
          className: vibrationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{siloVibration}')
        },
        h('i.fa.fa-signal-stream'))
        : null,
      canAerate === 1
        ? h('.buttonFontSize', {
          className: aerationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_aerate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_aerate']),
          title: props.fmt('{siloAeration}')
        },
        h('i.fa.fa-wind'))
        : null,
      additionalHackedValve != null // this can also be undefined - hence the != comparison - TODO: find more elegant way?
        ? h('.buttonFontSize', {
          className: additionalHackedValveClass,
          onMouseDown: _e => callApi([id, 'set_override', 'additional_hacked_valve', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'additional_hacked_valve']),
          title: props.fmt('{additional_hacked_valve}')
        }, h('i.fa.fa-arrow-down'))
      // h('i.fa.fa-chevron-down'))
      // h('i.fa.fa-caret-down'))
      // h('i.fa.fa-angle-down'))
      // h('i.fa.fa-poo'))
        : null
    ])
  ])

  const arrowSubB = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      sensorBClass
        ? h('', { className: sensorBClass })
        : null
    ]),
    h('.silo-arrow-triad', {}, [
      closingBArrowAvailable
        ? h('.silo-closing-arrow-content', {},
          h('i', { className: closingBArrowIcon }))
        : null,
      arrowBIcon
        ? h('i', {
          className: arrowBIcon,
          onMouseDown: _e => canCallApi && callApi([id, 'set_open_b_invert']),
          onMouseUp: _e => callApi([id, 'unset_open_b_invert']),
          onTouchStart: _e => callApi([id, 'set_open_b_invert']),
          onTouchEnd: _e => callApi([id, 'unset_open_b_invert']),
          title: props.fmt('{filling}')
        })
        : null,
      openingBArrowAvailable
        ? h('.silo-opening-arrow-content', {},
          h('i', { className: openingBArrowIcon }))
        : null
    ]),
    h('.silo-control-content', {}, [
      canVibrateB === 1
        ? h('.buttonFontSize', {
          className: vibrationBButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate_b', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate_b']),
          title: props.fmt('{siloVibration}')
        },
        h('i.fa.fa-signal-stream'))
        : null,
      canAerateB === 1
        ? h('.buttonFontSize', {
          className: aerationBButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_aerate_b', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_aerate_b']),
          title: props.fmt('{siloAeration}')
        },
        h('i.fa.fa-wind'))
        : null
    ])
  ])

  const arrowSubFast = h('.silo-arrows-sub', {}, [
    h('', { style: { height: sensorHeight } }, [
      // no sensor for fast
    ]),
    h('.silo-arrow-triad', {}, [
      canFast
        ? h('i', {
          className: arrowIconFast,
          onMouseDown: _e => canCallApi && callApi([id, 'set_fast_invert']),
          onMouseUp: _e => callApi([id, 'unset_fast_invert']),
          onTouchStart: _e => callApi([id, 'set_fast_invert']),
          onTouchEnd: _e => callApi([id, 'unset_fast_invert']),
          title: props.fmt('{fastFilling}')
        })
        : null
    ]),
    h('.silo-control-content', {}, [
      // no vibration for fast
    ])
  ])

  return h('.silo-content', { style: { width: siloSizeWidth, height: siloSizeHeight, marginLeft: siloContentMarginLeft } }, [
    h('.silo-top', { style: { width: siloSizeWidth, height: siloSizeHeightTop } }, [
      h('.number'),
      h('.numberPosition', {}, number),
      h('.recipePosition', {}, recipePosition)
    ]),
    h('.silo-body', { style: { width: siloSizeWidth, height: siloSizeHeightBody } }, [
      h('.silo-level-content', {}, [
        levelPercent != null
          ? h(levelClass, { title: levelTooltip }, [
            h('.level', { style: { height: levelPercentInvertStyle } })
          ])
          : null,
        hasMoistureProbe
          ? h(moistureLink, {
            style: linkStyle,
            title: props.fmt('{moistureLink}')
          }, h('i', {
            className: moistureIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'moisture_link_toggle'])
          }))
          : null,
        hasTemperatureProbe
          ? h(temperatureLink, {
            style: linkStyle,
            title: props.fmt('{temperatureLink}')
          }, h('i', {
            className: temperatureIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'temperature_link_toggle'])
          }))
          : null,
        hasDensityProbe
          ? h(densityLink, {
            style: linkStyle,
            title: props.fmt('{densityLink}')
          }, h('i', {
            className: densityIsLinkedSymbolClass,
            onClick: _e => callApi([id, 'density_link_toggle'])
          }))
          : null
      ]),
      h('.silo-display-content', {}, [
        h(valDisplay, { style }, [
          (isFlow && valueStr) ? h('.silo-delta', {}, DELTA) : null,
          valueStr
        ]),
        h(reqDisplay, {
          style,
          title: (requestValueOrigStr || '')
        }, [
          (isFlow && requestValueStr) ? h('.silo-delta', {}, DELTA) : null,
          requestValueStr
        ]),
        hasMoisture
          ? h('.silo-moisture', {
            style,
            title: props.fmt('{moisture}')
          }, moistureStr)
          : null,
        hasMoistureProbe
          ? h('.silo-moisture-probe', {
            style,
            title: props.fmt('{moistureFromProbe}')
          }, moistureFromProbeStr)
          : null,
        hasTemperature
          ? h('.silo-temperature', {
            style,
            title: props.fmt('{temperature}')
          }, temperatureStr)
          : null,
        hasTemperatureProbe
          ? h('.silo-temperature-probe', {
            style,
            title: props.fmt('{temperatureFromThermometer}')
          }, temperatureFromProbeStr)
          : null,
        hasDensityProbe // TODO: temporary - solver better. the thing is, most plants don't have the density meter and are not really interested in seeing this value at all times...
          ? h('.silo-density', {
            style,
            title: props.fmt('{density}')
          }, densityStr)
          : null,
        hasDensityProbe
          ? h('.silo-density-probe', {
            style,
            title: props.fmt('{densityFromProbe}')
          }, densityFromProbeStr)
          : null
      ])
    ]),
    h('', {
      className: siloBottom,
      style: { width: siloSizeWidth, height: siloSizeHeightBottom }
    }, [
      h('.nameShort', { title: nameLong }, nameShort)
    ]),
    h('.silo-arrows', {}, dfs._right_to_left
      ? [arrowSubFast, arrowSubB, arrowSubA]
      : [arrowSubA, arrowSubB, arrowSubFast]
    )
  ])
}

const renderNodeBinsAndFlowmeters = props => {
  gloDebugReact && incDebugRenderCount('NodeBinsAndFlowmeters', props)
  const dfs = props.data
  const type = dfs._type
  return h('', {}, [
    type === 'Bin' || type === 'Lift' || type === 'SBMBin' ? rce(NodeBins, props) : null,
    type === 'Flowmeter' ? rce(NodeFlowmeters, props) : null
  ])
}

const renderNodeBins = props => {
  gloDebugReact && incDebugRenderCount('NodeBins', props)
  const dfs = props.data
  const id = dfs.id
  const cementScrew = dfs.screw
  const cementScrewState = props.xxxCementScrewState
  const cementScrewStateRotation = cementScrewState ? 'scaleScrew-run' : 'scaleScrew-idle'
  const cementScrewSetOpenReason = props.xxxCementScrewSetOpenReason
  const cementScrewStateStartButton = 'scaleScrewControl isButton isControl needsCTRL' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    (cementScrewState === 1 ? ' red' : ' green') +
    reasonToClassSpace(cementScrewSetOpenReason)
  const cementScrewScaleValueStr = props.xxxCementScrewScaleValueStr
  const cementScrewFlowStr = props.xxxCementScrewFlowStr
  const openingArrowAvailable = dfs.sig_open_state != null && dfs.sig_close_state != null
  const openingArrowState = dfs.sig_open_state
  const openingArrowIcon = 'fa fa-up' + (openingArrowState ? ' arrow-signal-open-100' : '')
  const closingArrowAvailable = dfs.sig_close_state != null
  const closingArrowState = dfs.sig_close_state
  const closingArrowIcon = 'fa fa-down' + (closingArrowState ? ' arrow-signal-open-100' : '')
  const exhaustAvailable = dfs.can_exhaust
  const exhaustClass = 'bin-top-flush-button isButton isControl needsCTRL' + // TODO: flush? really?
    (dfs._is_exhausting === 1 ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.set_exhaust_reason)
  const filterCleanStates = dfs.sigs_filter_clean_states || []
  const filterClean = [] // TODO: use map
  for (let i = 0; i < filterCleanStates.length; i++) {
    const valveState = filterCleanStates[i]
    const valveTitle = props.fmt('{filterClean}') + ' ' + (i + 1)
    const filterCleanClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL' + // TODO: exhaust? really?
      (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
      (props.xxxStateFrozen ? ' isFrozen' : '') +
      (valveState ? ' signalActive' : '') +
      reasonToClassSpace(dfs.filter_clean_reasons[i])
    filterClean.push(h('', {
      className: filterCleanClass,
      title: valveTitle,
      onMouseDown: _e => canCallApi && callApi([id, 'set_filter_clean_manual', i]),
      onMouseUp: _e => callApi([id, 'unset_filter_clean_manual', i])
    }, h('i.fa.fa-signal-stream')))
  }
  const setOpenFinalPercent = dfs.set_open_final_percent
  const isWarning = dfs._is_warning
  // const isValWarning = dfs._is_val_warning;
  const scale = dfs.scale
  const type = dfs._type
  // const valueClassName = "display" + (isValWarning ? " red" : "");
  const binCanVibration = dfs.can_vibrate
  const binOutputPump = dfs.has_pump
  const binVibrationButton = 'isButton isControl' +
    (type === 'SBMBin' ? ' scaleScrewControl' : ' binControl') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (dfs._is_vibrating ? ' signalActive' : '') +
    reasonToClassSpace(dfs.set_vibrate_reason)
  const binOutputPumpButton = 'isButton isControl needsCTRL' +
    (type === 'SBMBin' ? ' scaleScrewControl' : ' binControlPump') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    (dfs.pump_open ? ' signalActive' : (dfs.pump_is_flooded ? ' signalActiveAnother' : '')) +
    reasonToClassSpace(dfs.set_pump_open_reason)
  const binControlContent = binCanVibration || binOutputPump

  const scaleRequestStr = dfs.scale_req_str
  const scaleValueStr = dfs.scale_val_str

  const fillPercentScale = dfs.scale_fill_within_bin_percent

  const materialType = dfs._material_type

  const hasTemperatureProbe = dfs.temperature_from_probe_str != null
  const temperatureFromProbeStr = (hasTemperatureProbe ? (dfs.temperature_from_probe_str + DEG) : '')
  // const hasDensityProbe = 1;
  // const density_from_probe_str = (hasDensityProbe ? "1.03" : "");

  const requirementState = dfs._set_requirement
  const requirementGranted = dfs.is_granted

  const flushAvailable = dfs.can_flush
  const flushClass = 'bin-top-flush-button isButton isControl needsCTRL' +
    (dfs._is_flushing === 1 ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.set_flush_reason)

  const sensorState = dfs.sig_is_closed_state
  const sensorAvailable = sensorState != null
  const sensorSectionVisible = sensorAvailable ? '' : 'hidden'
  const sensor = 'bin-sensor' + (sensorState === 0 ? ' bin-sensor-opened' : '')

  let countOfSilos = 1 // TODO: ugly fucking fallback for reservoir
  let countOfBinsInScale = 0
  let binPosition = 0
  if (id.indexOf('Reservoir') === -1) {
    countOfSilos = props.precalc.binsAndFlowmeters[materialType][id].silos.length
    countOfBinsInScale = props.precalc.scales[materialType][scale].bins.length
    /* let countOfAllSilosInScale = 0;
  for (let s in scales[materialType][scale]['bins']) {
    let s2 = scales[materialType][scale]['bins'][s];
    countOfAllSilosInScale += props.precalc.binsAndFlowmeters[materialType][s2]['silos'].length;
  } */
    binPosition = props.precalc.scales[materialType][scale].bins.indexOf(id) + 1
  }

  let shapeOfBinLeft = '0%'
  if (countOfSilos === 1 && countOfBinsInScale > 1) {
    shapeOfBinLeft = binPosition === 1 ? null : '0%'
  } else {
    shapeOfBinLeft = (countOfSilos !== 1 && binPosition > 1) ? '0%' : null
  }
  let shapeOfBinRight = '0%'
  if (countOfSilos === 1 && countOfBinsInScale > 1) {
    shapeOfBinRight = countOfBinsInScale === binPosition ? null : '0%'
  } else {
    shapeOfBinRight = countOfSilos !== 1 && countOfBinsInScale > binPosition ? '0%' : null
  }
  const reservoir = id === 'AggregateReservoir1' // TODO: ugly shit this is for smaller coefficient for reservoir
  const sizeCoeficientWidth = (reservoir ? 3 : 5) * props.xxxWideModeCoef
  const sizeCoeficientHeight = reservoir ? 10 : 15
  // const sizeCoeficientWidth = 5 * props.xxxWideModeCoef
  // const sizeCoeficientHeight = 15

  // const binDisplayContentWidth = (sizeCoeficientWidth * 1.191) * ((countOfSilos === 1) || (type === 'Flowmeter') ? 1 : countOfSilos / 1.2) + "vw";
  const binDisplayContentHeight = (sizeCoeficientHeight * 0.24) + 'vh'
  const binDisplayWidth = sizeCoeficientWidth + 'vw'
  const binDisplayHeight = (sizeCoeficientHeight * 0.15) + 'vh'
  const sensorHeight = (sizeCoeficientHeight * 0.032) + 'vh'
  const sensorWidth = (sizeCoeficientWidth * 0.5) + 'vw'
  const binMarginLeft = (sizeCoeficientWidth * (binPosition === 1 ? 0.05 : 0.01)) + 'vw'
  const arrowHeight = (sizeCoeficientHeight * 0.27) + 'vh'
  const arrowWidth = (sizeCoeficientWidth * 0.45) + 'vw'
  const binSizeHeight = sizeCoeficientHeight + 'vh'
  const binSizeHeightWithoutBottomContents = (sizeCoeficientHeight * 0.61) + 'vh'
  const binSizeHeightTopIcon = (sizeCoeficientHeight * 0.2) + 'vh'
  const binSizeHeightTop = (sizeCoeficientHeight * 0.06) + 'vh'
  const binSizeHeightBody = (sizeCoeficientHeight * 0.32) + 'vh'
  const spaceBetween = (binPosition === 1) ? 0.05 : 0.09
  const binContentSizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - (spaceBetween + (sizeCoeficientWidth * (binPosition === 1 ? 0 : 0.01))) + 'vw'
  const binSizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - (spaceBetween + (sizeCoeficientWidth * (binPosition === 1 ? 0.05 : 0.01))) + 'vw'
  const binStyle = {
    width: binSizeWidth,
    height: binSizeHeightBody,
    borderBottomLeftRadius: shapeOfBinLeft,
    borderBottomRightRadius: shapeOfBinRight
  }
  const binTopScale = 'bin-topScale ' + materialType
  const binTopLevelScale = 'linear-gradient(to right, var(--colorWeightProgress' + materialType + ') 0% ' + fillPercentScale + '%, var(--colorWeight' + materialType + ') ' + fillPercentScale + '% 100%)'
  const binArrow = (reservoir ? '.bin-arrow-reservoir' : '.bin-arrow') + '.isControl.needsCTRL.isArrow' +
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '')

  const arrowAndScrewVisibility = ((type === 'Lift') ? 'hidden' : '')
  const arrowIcon = 'fa fa-down' + (setOpenFinalPercent ? ' arrow-signal-open-' + (isWarning ? 'warning-' : '') + setOpenFinalPercent : '')

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  return h('.bins-content', { style: { width: binContentSizeWidth } }, [
    h('.float-left', { style: { width: binSizeWidth, height: binSizeHeight } }, [
      h('.bin-content', { style: { marginLeft: binMarginLeft, height: binSizeHeightWithoutBottomContents } }, [
        h('.bin-top-flush', { style: { width: binSizeWidth, height: binSizeHeightTopIcon } }, [
          h('.bin-top-countdown', {}, dfs.countdown_str),
          flushAvailable
            ? h('', {
              className: flushClass,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_flush', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
              onTouchStart: _e => callApi([id, 'set_override', 'set_flush', 1]),
              onTouchEnd: _e => callApi([id, 'set_override', 'set_flush']),
              title: props.fmt('{binFlush}')
            }, [
              h('i.fas.fa-tint')
            ])
            : h(''),
          filterClean.length > 0 ? filterClean : null,
          exhaustAvailable
            ? h('', {
              className: exhaustClass,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_exhaust', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_exhaust']),
              onTouchStart: _e => callApi([id, 'set_override', 'set_exhaust', 1]),
              onTouchEnd: _e => callApi([id, 'set_override', 'set_exhaust']),
              title: props.fmt('{exhaust}')
            }, [
              h('i.fa.fa-fan')
            ])
            : h(''),
          requirementState != null
            ? h('.bin-top-request', {}, requirementState ? props.fmt('{requested}') : '')
            : h(''),
          requirementGranted != null
            ? h('.bin-top-request-accepted', {}, requirementGranted ? props.fmt('{switched}') : '')
            : h(''),
          h('.bin-top-batches', {}, dfs.seq_tot)
        ]),
        h('', {
          className: binTopScale,
          style: { width: binSizeWidth, height: binSizeHeightTop, background: binTopLevelScale }
        }),
        h('.bin', { style: binStyle }, [
          h('.bin-displays-content', { style: { height: binDisplayContentHeight } }, [
            (dfs.req_str != null || dfs.val_str != null)
              ? h('.bin-display-content', {}, [
                h('.request', { style: { width: binDisplayWidth, height: binDisplayHeight } }, [
                  dfs._is_flow && dfs.req_str ? h('.bin-delta', {}, DELTA) : null,
                  dfs.req_str
                ]),
                h('.value', { style: { width: binDisplayWidth, height: binDisplayHeight } }, [
                  dfs._is_flow && dfs.val_str ? h('.bin-delta', {}, DELTA) : null,
                  dfs.val_str
                ])
              ])
              : null,
            (binPosition === 1)
              ? h('.bin-display-content', {}, [
                h('.request', { style: { width: binDisplayWidth, height: binDisplayHeight } }, scaleRequestStr),
                h('.value', { style: { width: binDisplayWidth, height: binDisplayHeight } }, scaleValueStr)
              ])
              : null,
            (hasTemperatureProbe /* || hasDensityProbe */)
              ? h('.bin-display-content', {}, [
                hasTemperatureProbe
                  ? h('.bin-temperature-probe', { style: { width: binDisplayWidth, height: binDisplayHeight } }, temperatureFromProbeStr)
                  : null
                /* hasDensityProbe
                  ? h(".bin-density-probe", {style: {width: binDisplayWidth, height: binDisplayHeight}}, density_from_probe_str)
                  : null, */
              ])
              : null
          ])
        ])
      ]),
      h('.bin-sensor-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth, height: sensorHeight, visibility: sensorSectionVisible } }, [
        h('', { className: sensor, style: { height: sensorHeight, width: sensorWidth } })
      ]),
      h('.bin-arrow-and-screw-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth, height: arrowHeight, visibility: arrowAndScrewVisibility } }, [
        dfs.has_opening
          ? h(binArrow, {
            style: { width: arrowWidth },
            onMouseDown: _e => canCallApi && callApi([id, 'set_open_invert']),
            onMouseUp: _e => callApi([id, 'unset_open_invert']),
            onTouchStart: _e => callApi([id, 'set_open_invert']),
            onTouchEnd: _e => callApi([id, 'unset_open_invert']),
            title: props.fmt('{emptying}')
          }, [
            closingArrowAvailable
              ? h('.bin-closing-arrow-content', {}, [
                h('i', { className: closingArrowIcon })
              ])
              : null,
            h('i', { className: arrowIcon }),
            openingArrowAvailable
              ? h('.bin-opening-arrow-content', {}, [
                h('i', { className: openingArrowIcon })
              ])
              : null
          ])
          : null,
        type === 'SBMBin'
          ? h('.scale-screw-content' + (dfs.has_opening ? '' : '.noMargin'), {}, [
            h('.scaleScrew', {}, [
              h('.scaleScrewRotationBackground', {}, [
                h('', { className: cementScrewStateRotation })
              ])
            ]),
            h('.scale-screw-button-content', {}, [
              binCanVibration === 1
                ? h('.buttonFontSize', {
                  className: binVibrationButton,
                  onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
                  onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
                  title: props.fmt('{siloVibration}')
                }, h('i.fa.fa-signal-stream'))
                : null,
              h('', {
                className: cementScrewStateStartButton,
                onMouseDown: _e => canCallApi && callApi([cementScrew, 'toggle_open_invert']),
                title: props.fmt('{cementScrewStartStop}')
              }, [
                h('i.far.fa-dot-circle')
              ]),
              h('.scale-screw-scale', {}, cementScrewScaleValueStr),
              cementScrewFlowStr != null
                ? h('.scale-screw-scale', {}, [
                  h('.conveyer-delta', {}, DELTA),
                  h('', {}, cementScrewFlowStr)
                ])
                : null
            ])
          ])
          : null
      ]),
      binControlContent && type !== 'SBMBin'
        ? h('.bin-control-content', { style: { marginLeft: binMarginLeft, width: binSizeWidth } }, [
          binCanVibration === 1 && type !== 'Lift'
            ? h('.buttonFontSize', {
              className: binVibrationButton,
              onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
              title: props.fmt('{siloVibration}')
            }, h('i.fa.fa-signal-stream'))
            : null,
          binOutputPump === 1 && type !== 'Lift'
            ? h('', {
              className: binOutputPumpButton,
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_pump_open', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_pump_open']),
              title: props.fmt('{binOutputPump}')
            }, h('i.fab.fa-rev'))
            : null
        ])
        : null
    ])
  ])
}

const renderNodeFlowmeters = props => {
  gloDebugReact && incDebugRenderCount('NodeFlowmeters', props)
  const dfs = props.data
  const id = dfs.id
  const materialType = dfs._material_type
  const countOfSilos = props.precalc.binsAndFlowmeters[materialType][id].silos.length
  const number = dfs._index
  const sizeCoeficientWidth = 5 * props.xxxWideModeCoef
  const sizeCoeficientHeight = 8
  const spaceBetween = number > 1 ? 0.3 : 0.09
  const sizeWidth = (sizeCoeficientWidth * countOfSilos + ((sizeCoeficientWidth * 0.05) * countOfSilos)) - spaceBetween + 'vw'
  const flowSizeWidth = (sizeCoeficientWidth * 0.8) + (sizeCoeficientWidth * 0.05) + 'vw'
  const sizeHeight = sizeCoeficientHeight + 'vh'
  // const flowSizeHeight = (sizeCoeficientHeight * 0.84) + 'vh'
  const displayContentHeight = (sizeCoeficientHeight * 0.35) + 'vh'

  const valueStr = dfs.val_str
  const flowmeterTop = 'flowmeter-top ' + materialType
  const flowmeterSizeHeightTop = (sizeCoeficientHeight * 0.11) + 'vh'
  const flowmeterSizeHeightTopIcon = (sizeCoeficientHeight * 0.39) + 'vh'
  const rangePercent = dfs.range_percent
  const flowmeterTopLevel = 'linear-gradient(to right, var(--colorWeightProgress' + materialType + ') 0% ' + rangePercent + '%, var(--colorWeight' + materialType + ') ' + rangePercent + '% 100%)'

  const isFlow = dfs._is_flow
  const isWarning = dfs._is_warning
  const flowmeterDisplayContentClass = 'flowmeter-display-content' + (isWarning ? ' red' : '')

  return h('.flowmeterContent', { style: { marginLeft: spaceBetween + 'vw', width: sizeWidth } },
    h('.float-left', { style: { width: sizeWidth, height: sizeHeight } },
      h('.flowmeter-content', { style: { width: sizeWidth, height: sizeHeight } },
        h('.flowmeter', { style: { width: flowSizeWidth, height: sizeHeight } }, [
          h('.flowmeter-bottom-spacer', {}, []),
          h('.flowmeter-bottom-content', { style: { width: flowSizeWidth, height: flowmeterSizeHeightTop } }, [
            h('.flowmeter-bottom-left', { style: { height: flowmeterSizeHeightTop } }),
            h('.flowmeter-bottom-right', { style: { height: flowmeterSizeHeightTop } })
          ]),
          h('', {
            className: flowmeterDisplayContentClass,
            style: { width: flowSizeWidth, height: displayContentHeight }
          }, [
            isFlow ? h('.flowmeter-delta', {}, DELTA) : null,
            h('.flowmeter-display', {}, valueStr)
          ]),
          h('', {
            className: flowmeterTop,
            style: { width: flowSizeWidth, height: flowmeterSizeHeightTop, background: flowmeterTopLevel }
          }),
          h('.flowmeter-top-displays', { style: { width: flowSizeWidth, height: flowmeterSizeHeightTopIcon } }, [
            // h('.flowmeter-countdown', {}, dfs.countdown_str), TODO: not implemented, yet
            h('.flowmeter-batches', {}, dfs.seq_tot)
          ])
        ])
      )
    )
  )
}

const renderNodeMixerBay = props => {
  gloDebugReact && incDebugRenderCount('NodeMixerBay', props)
  const id = props.id
  const bayData = props.bayData
  const directionCloseReasonClass = (bayData.set_direction_manual === -1 ? ' reasonManual' : (bayData.set_direction === -1 ? ' reasonAutomat' : ''))
  const directionOpenReasonClass = (bayData.set_direction_manual === 1 ? ' reasonManual' : (bayData.set_direction === 1 ? ' reasonAutomat' : ''))
  const closingClass = 'mixerClosingOpening isOnDark isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (bayData.sig_close_state === 1 ? ' signalActive' : '') +
    directionCloseReasonClass
  const openingClass = 'mixerClosingOpening isOnDark isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (bayData.sig_open_state === 1 ? ' signalActive' : '') +
    directionOpenReasonClass
  const emptyingSemiDurationLeft = bayData.countdown_semi
  const emptyingFullDurationLeft = bayData.countdown_full
  // const mixerLockIcon = 'fa' + (bayData.is_locked ? ' fa-hand-paper faa-flash animated' : ' fa-lock-open')
  const mixerLockIcon = 'fa' + (bayData.is_locked ? ' fa-hand-paper' : ' fa-lock-open')
  const mixerLockClass = 'mixerBlockLock isOnDark isButton isControl' +
    (bayData.is_locked ? ' mixerBlockHand reasonManual' : ' mixerBlockLock green') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
  const bayPercent = bayData.open_percent
  const bayWidth = (100 - Math.min(bayPercent, 90)) + '%'
  // TODO: 'var(--colorVisualButtonTrue)' -> just use some color for "not open and not closed"
  // TODO: 'var(--colorRed)' -> TODO: define a special color for this
  const bayColor = bayPercent === 0 ? 'var(--colorGreen)' : (bayPercent < 90 ? 'var(--colorBaySemi)' : 'var(--colorRed)')

  const baySensors = bayData.sensors.map((x, idx, arr) => {
    const reasonClass = reasonToClassDot(x.reason)
    const title = (idx === 0 ? props.fmt('{mixerBayToClose}') : (idx === (arr.length - 1) ? props.fmt('{mixerBayToOpen}') : (props.fmt('{mixerBayTo}') + ' ' + x.position)))
    const icon = x.is_virtual ? 'fa-dot-circle' : 'fa-circle'
    return h('.mixerSensor.isOnDark.isButton.isControl' + (props.xxxKeys.CTRL ? '.hasCTRL' : '') + (x.state === 1 ? '.signalActive' : '') + reasonClass, {
      key: 'sensor-' + x.position,
      onMouseDown: _e => callApi([id, 'go_to_open' + bayData.cont_or_not + '_manual', x.position]),
      title
    }, h('i.fa.' + icon))
  })

  const sizeCoeficientHeight = 21
  const mixerBayContentHeight = (sizeCoeficientHeight * 0.04) + 'vh'

  return h('.mixerBay', {}, [
    h('.mixerBayControl', {}, [
      h('', {
        className: closingClass,
        onMouseDown: _e => callApi([id, 'set_direction' + bayData.cont_or_not + '_manual', -1]),
        onMouseUp: _e => callApi([id, 'unset_direction' + bayData.cont_or_not + '_manual']),
        title: props.fmt('{mixerBayClose}')
      }, semiNodeButtonClose),
      h('.mixerEmptyingDurationLeft', {}, emptyingSemiDurationLeft),
      h('', {
        className: mixerLockClass,
        onMouseDown: _e => callApi([id, 'lock_bay_toggle']),
        title: props.fmt('{mixerBayBlock}')
      }, h('i', { className: mixerLockIcon })),
      h('.mixerEmptyingDurationLeft', {}, emptyingFullDurationLeft),
      // h(".mixerEmptyingDurationLeft", {}, emptyingCountdown),
      h('', {
        className: openingClass,
        onMouseDown: _e => callApi([id, 'set_direction' + bayData.cont_or_not + '_manual', 1]),
        onMouseUp: _e => callApi([id, 'unset_direction' + bayData.cont_or_not + '_manual']),
        title: props.fmt('{mixerBayOpen}')
      }, semiNodeButtonOpen)
    ]),
    h('.mixer-bay', {}, [
      h('.mixer-bay-side'),
      h('.mixer-bay-middle', {}, [
        h('.mixerSensors', {}, baySensors),
        h('.mixer-bay-content', { style: { height: mixerBayContentHeight } }, [
          bayPercent !== null
            ? h('.mixer-bay-opening', { style: { width: bayWidth, backgroundColor: bayColor } })
            : null
        ])
      ]),
      h('.mixer-bay-side')
    ])
  ])
}

const renderNodeMixers = props => {
  gloDebugReact && incDebugRenderCount('NodeMixers', props)
  const dfs = props.data
  const id = dfs.id
  const consistencyPercent = dfs.consistency_percent
  const consistencyStr = dfs.consistency_str
  // const consistencyHyperStr = dfs.consistency_hyper_str;
  const isCoverOpen = dfs.is_cover_open
  const coverClass = 'mixer-cover' + (isCoverOpen ? ' open' : ' close')
  const batchSize = dfs._vol_str
  const batchState = dfs.seq_tot
  const mixingDurationLeft = dfs.countdown
  // const emptyingSemiDurationLeft = dfs.countdown_semi;
  // const emptyingFullDurationLeft = dfs.countdown_full;
  // const emptyingCountdown = dfs.emptying_countdown;

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  const spz = dfs._veh_id
  const exhaustAvailable = dfs.can_exhaust
  const exhaustClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL' +
    (dfs._is_exhausting === 1 ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.set_exhaust_reason)
  const filterCleanStates = dfs.sigs_filter_clean_states || []
  const filterClean = [] // TODO: use map
  for (let i = 0; i < filterCleanStates.length; i++) {
    const valveState = filterCleanStates[i]
    const valveTitle = props.fmt('{filterClean}') + ' ' + (i + 1)
    const filterCleanClass = 'mixer-exhaust-button isOnDark isButton isControl needsCTRL' + // TODO: exhaust? really?
      (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
      (props.xxxStateFrozen ? ' isFrozen' : '') +
      (valveState ? ' signalActive' : '') +
      reasonToClassSpace(dfs.filter_clean_reasons[i])
    filterClean.push(h('', {
      className: filterCleanClass,
      title: valveTitle,
      onMouseDown: _e => canCallApi && callApi([id, 'set_filter_clean_manual', i]),
      onMouseUp: _e => callApi([id, 'unset_filter_clean_manual', i])
    }, h('i.fa.fa-signal-stream')))
  }
  const canLubricate = dfs.can_lubricate
  // const mixerLubricationClass = ".mixerButton.isOnDark.isButton.isControl"
  const mixerLubricationClass = '.mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL' + // TODO: exhaust? really?
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '') +
    (dfs.is_lubricating === 1 ? '.signalActive' : '') +
    reasonToClassDot(dfs.set_lubricate_reason)
  const hasWashing = dfs.has_washing
  const hasWashingContinuous = dfs.has_washing_continuous
  const hasWashingShort = dfs.has_washing_short
  const hasWashingLong = dfs.has_washing_long
  const mixerWashingContinuousClass = '.mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL' + // TODO: exhaust? really?
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '') +
    (dfs.is_washing ? ' signalActive' : (dfs.is_washing_ready ? ' signalActiveAnother' : '')) +
    reasonToClassDot(dfs.set_washing_reason)
  const mixerWashingShortClass = '.mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL' + // TODO: exhaust? really?
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '') +
    (dfs.is_washing_running_short ? ' signalActive' : '')
  const mixerWashingLongClass = '.mixer-exhaust-button.isOnDark.isButton.isControl.needsCTRL' + // TODO: exhaust? really?
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '') +
    (dfs.is_washing_running_long ? ' signalActive' : '')
  const hasInspectionFlap = dfs.has_inspection_flap
  const mixerInspectionFlapClass = '.mixer-exhaust-button.isOnDark.isButton.isControl' + // TODO: exhaust? really?
    (props.xxxKeys.CTRL ? '.hasCTRL' : '') +
    (props.xxxStateFrozen ? '.isFrozen' : '') +
    (dfs.sig_inspection_flap_open_state ? ' signalActive' : '') +
    reasonToClassDot(dfs.set_inspection_flap_open_reason)
  const inspectionFlapIcon = 'i.fa' + (dfs.inspection_flap_open ? '.fa-video' : '.fa-video-slash')

  const hasMoistureProbe = dfs.moisture_str != null
  const moistureStr = hasMoistureProbe ? dfs.moisture_str : ''

  const totalRunDurationStr = dfs.total_run_duration_str
  const sinceLastMaintenanceStr = dfs.since_last_maintenance_str
  const totalRunDurationsStr = props.fmt('{TotalRunDuration}: ' + totalRunDurationStr + '\n{SinceLastMaintenance}: ' + sinceLastMaintenanceStr)

  const sizeCoeficientWidth = 20 * props.xxxWideModeCoef
  const sizeCoeficientHeight = 21

  const mixerWidth = sizeCoeficientWidth + 'vw'
  const mixerHasTop2 = canLubricate || hasWashing || hasInspectionFlap || exhaustAvailable || filterClean.length > 0
  const mixerHeight = mixerHasTop2 ? (sizeCoeficientHeight * 1) + 'vh' : (sizeCoeficientHeight * 0.9) + 'vh'
  const mixerTopHeight = (sizeCoeficientHeight * 0.05) + 'vh'
  const mixerTop2Height = (sizeCoeficientHeight * 0.11) + 'vh'
  const mixerBodyHeight = (sizeCoeficientHeight * 0.41) + 'vh'
  const mixerBodyToolsHeight = (sizeCoeficientHeight * 0.10) + 'vh'
  const rotation = dfs.is_running
  const isStartingOrStopping = dfs.is_starting_or_stopping
  const mixerTurnOnOff = 'mixerButton isOnDark isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (rotation ? ' red' : ' green')
  const mixerTurnOnOffButtonEvent = 'manual_' + (rotation ? 'stop' : 'start')
  const mixerRotation = (rotation ? 'mixer-run' : 'mixer-idle') + (isStartingOrStopping ? ' reasonAutomat' : '') // TODO: the reasonAutomat is here to somehow indicate motor start phase - maybe there's a cleaner way
  const autostopDurationLeft = dfs.autostop_timeout_to_go
  const mixerTemperature = false // TODO
  // const mixerConsistency = 'mixerConsistencyDisplay' + (props.xxxKeys.CTRL ? ' hasCTRL' : '')

  const consistencyPrintsStr = dfs.consistency_prints_str
  // const consistencyGraphW = 35
  // const consistencyGraphH = 300
  // const consistencyGraphPointsStr = dfs._consistency_graph ? dfs._consistency_graph.map(x => x[0].toFixed(2) + "," + (consistencyGraphH - x[1]).toFixed(2)).join(" ") : ""
  // const mixerConsistencyGraphWidth = (sizeCoeficientWidth / 2.25) + 'vw'
  // const mixerConsistencyWidth = (sizeCoeficientWidth / 1.81) - (sizeCoeficientWidth / 2.25) + 'vw'
  // const mixerDisplaysContentWidth = sizeCoeficientWidth - (sizeCoeficientWidth / 4) - ((sizeCoeficientWidth / 1.81) - (sizeCoeficientWidth / 4)) + 'vw'

  // const mixerConsistencyTimeValues = '10s/5050 20s/3440 28s/3040'
  const bays = dfs.bays.map((x, _idx) => rce(NodeMixerBay, { id, bayData: x, xxxKeys: props.xxxKeys, fmt: props.fmt }))
  // interleave bays with spacers - for zero-item case insert just spacer, for single-item case insert spacer before, for multiple-item case insert between element
  const baysAndSpacers = bays.reduce((accum, x, _idx, arr) => {
    if ((arr.length === 1) || (accum.length > 0)) {
      accum.push(h('.mixer-bay-spacer')) // TODO: in-place mutability!
    }
    accum.push(x) // TODO: in-place mutability!
    return accum
  }, dfs.bays.length === 0 ? [h('.mixer-bay-spacer', { style: { height: '4vh' } })] : []) // TODO: the hard-coded style is an ugly hack

  const gaugeValue = consistencyStr
  const gaugeMin = dfs.consistency_gauge_min
  const gaugeMax = dfs.consistency_gauge_max
  const gaugeTarget = dfs.consistency_gauge_target
  const gaugePercent = consistencyPercent
  const gaugeDegrees = gaugePercent * 1.8 // magic constant stepan found that fixes things
  const gaugeDeg = 'rotate(' + gaugeDegrees + 'deg)'

  return h('.content-mixer', {}, [
    h('.mixer-batch-content', { style: { width: mixerWidth } }, [
      h('.mixerBatch', { title: props.fmt('{sequence}') }, batchState)
    ]),
    h('.mixer-content', { style: { width: mixerWidth, height: mixerHeight } }, [
      h('.mixer-top', { style: { height: mixerTopHeight } }, [
        isCoverOpen != null ? h('', { className: coverClass }) : null
      ]),
      mixerHasTop2
        ? h('.mixer-top2', { style: { height: mixerTop2Height } }, [
          canLubricate
            ? h(mixerLubricationClass, {
              title: props.fmt('{MixerLubrication}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_lubricate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_lubricate'])
            }, h('i.fa.fa-oil-can'))
            : null,
          hasWashingContinuous
            ? h(mixerWashingContinuousClass, {
              title: props.fmt('{MixerWashing}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_washing', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_washing'])
            }, h('i.fa.fa-shower'))
            : null,
          hasWashingShort
            ? h(mixerWashingShortClass, {
              title: props.fmt('{MixerWashingShort}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_washing_start_short', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_washing_start_short'])
            }, h('i.fa.fa-shower'))
            : null,
          hasWashingLong
            ? h(mixerWashingLongClass, {
              title: props.fmt('{MixerWashingLong}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_washing_start_long', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_washing_start_long'])
            }, h('i.fa.fa-shower'))
            : null,
          h('.grow'), // spacer
          hasInspectionFlap
            ? h(mixerInspectionFlapClass, {
              title: props.fmt('{MixerInspectionFlap}'),
              onMouseDown: _e => callApi([id, 'set_override', 'set_inspection_flap_open', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_inspection_flap_open'])
            }, h(inspectionFlapIcon))
            : null,
          h('.grow'), // spacer
          filterClean.length > 0 ? filterClean : null,
          exhaustAvailable
            ? h('', {
              className: exhaustClass,
              title: props.fmt('{mixerExhaust}'),
              onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_exhaust', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_exhaust'])
            },
            h('i.fa.fa-fan'))
            : null
        ])
        : null,
      h('.mixer-body', {
        style: { height: mixerBodyHeight },
        title: totalRunDurationsStr,
        onClick: _e => callApi([id, 'do_maintenance'])
      }, [
        h('.mixerConsistencyGraph-content', {}, [
          h('.mixerConsistencyPrints', {}, consistencyPrintsStr) // h("canvas.mixerConsistencyGraph", { width: "100px" }),
          /* h("svg", {
            viewBox: "0 0 " + consistencyGraphW + " " + consistencyGraphH,
            preserveAspectRatio: "none",
            style: { width: "100%", height: "70px", border: "solid 1px black" },
          }, [
            h("polyline", { stroke: "green", fill: "none", points: consistencyGraphPointsStr }),
          ]) */
        ]),
        h('.mixerConsistencyDisplay-content', {}, [
          h('.gaugeContent', {
            onClick: e => { callApi([id, 'cons_tare']); e.stopPropagation() },
            title: props.fmt('{mixerConsistency}')
          }, [
            h('.gauge', {}, [
              h('.gauge-percentage', { style: { transform: gaugeDeg } }, []),
              h('.gauge-mask', {}, []),
              h('.gauge-value', {}, [gaugeValue]),
              gaugeTarget
                ? h('.gauge_target', {}, [
                  h('i.fa.fa-sort-up')
                ])
                : null
            ]),
            h('.gauge_hint', {}, [
              h('.gauge_hint_item1', {}, gaugeMin),
              h('.gauge_hint_item2', {}, gaugeTarget),
              h('.gauge_hint_item3', {}, gaugeMax)
            ])
          ]),
          // original plain-number consistency
          /* h('.mixerConsistency', {}, [
            h('', {
              className: mixerConsistency,
              onClick: e => { callApi([id, 'cons_tare']); e.stopPropagation() },
              title: props.fmt('{mixerConsistency}')
            }, consistencyStr)
          ]), */
          // h(".mixerConsistencyHyperDisplay", {title: props.fmt("{mixerConsistencyHyper}")}, consistencyHyperStr),
          hasMoistureProbe ? h('.mixerMoistureProbeDisplay', {}, moistureStr) : null
        ]),
        h('.mixerDisplay-content', {}, [
          h('.mixerBatchSize', { title: props.fmt('{batchVolume}') }, batchSize),
          h('.mixerMixingDurationLeft', { title: props.fmt('{mixingDuration}') }, mixingDurationLeft),
          h('.mixerSpz', { title: props.fmt('{vehicleIdentificationNumber}') }, props.fmt(spz)) // we have to translate the value because of mixer washing - this field is being used to tell the user the mixer is being washed
        ])
      ]),
      h('.mixer-body-tools', { style: { height: mixerBodyToolsHeight } }, [
        h('.mixerToolsControl-content', {}, [
          h('.mixerControlButtons-content', {}, [
            h('.mixerRotationHole', { style: { height: mixerBodyToolsHeight } }, [
              h('.mixerRotationBackground', {}, [
                h('', { className: mixerRotation + ' reverse' })
              ])
            ]),
            h('.mixerAutostopDurationLeft', {}, autostopDurationLeft),
            h('', {
              className: mixerTurnOnOff,
              style: { height: mixerBodyToolsHeight },
              onClick: _e => callApi([id, mixerTurnOnOffButtonEvent]),
              title: props.fmt('{mixerOnOff}')
            }, h('i.fa.fa-power-off')),
            h('.mixerAutostopDurationLeft', {}, ''), // just for symetry
            h('.mixerRotationHole', { style: { height: mixerBodyToolsHeight } }, [
              h('.mixerRotationBackground', {}, [
                h('', { className: mixerRotation })
              ])
            ])
          ])
        ]),
        mixerTemperature
          // ? h('.mixerIcons.red', {}, h('i.fa.fa-thermometer-half.faa-flash.animated'))
          ? h('.mixerIcons.red', {}, h('i.fa.fa-thermometer-half'))
          : null
      ]),
      h('.mixer-body2', {}, baysAndSpacers)
    ])
  ])
}

const renderNodeMixerLift = props => {
  gloDebugReact && incDebugRenderCount('NodeMixerLift', props)
  const dfs = props.data
  const id = dfs.id
  // const sizeCoeficientWidth = 7.3;
  const sizeCoeficientHeight = 21

  const mixerLiftContentWidth = '6.5vw'
  const mixerLiftControlContentHeight = sizeCoeficientHeight + 'vh'
  const isTop = dfs.is_in_empty
  const isSemiTop = dfs.is_in_empty_semi
  const isWait = dfs.is_in_waiting
  const isDown = dfs.is_in_down
  const position = dfs._position
  const sigDirectionUp = dfs.sig_go_up_state
  const sigDirectionDown = dfs.sig_go_down_state
  const mixerCanVibration = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const emptyingSemiDurationLeft = dfs.countdown_semi
  const emptyingFullDurationLeft = dfs.countdown_full

  const strokeCount = dfs.stroke_count
  const strokeCountSinceLastMaintenance = dfs.stroke_count_since_last_maintenance
  const strokeCountStr = props.fmt('{StrokeCount}: ' + strokeCount + '\n{SinceLastMaintenance}: ' + strokeCountSinceLastMaintenance)

  const mixerLiftLockDown = dfs.is_locked_lift_down
  // const mixerLiftLockDownIcon = 'fa' + (mixerLiftLockDown ? ' fa-lock faa-flash animated' : ' fa-lock-open')
  const mixerLiftLockDownIcon = 'fa' + (mixerLiftLockDown ? ' fa-lock signalActive' : ' fa-lock-open')
  const mixerLiftLockDownClass = 'mixerLiftLock isButton isControl' +
    (mixerLiftLockDown ? ' reasonManual' : ' green') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
  const mixerLiftLockEmpty = dfs.is_locked_lift_empty
  // const mixerLiftLockEmptyIcon = 'fa' + (mixerLiftLockEmpty ? ' fa-lock faa-flash animated' : ' fa-lock-open')
  const mixerLiftLockEmptyIcon = 'fa' + (mixerLiftLockEmpty ? ' fa-lock signalActive' : ' fa-lock-open')
  const mixerLiftLockEmptyClass = 'mixerLiftControl isButton isControl' +
    (mixerLiftLockEmpty ? ' reasonManual' : ' green') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')

  const mixerLiftFail = (position == null) // TODO: what is this?
  const mixerMiddle = 'mixerMiddle' + (position == null ? ' mixerCurrent mixerLiftFailState' : '') + (position > POS_DOWN && position < POS_WAITING ? ' mixerCurrent' : '')

  const mixerTop = 'mixerTop' + (position === POS_EMPTY ? ' mixerCurrent' : '')
  const mixerSemiTop = 'mixerSemiTop' + (position > POS_WAITING && position < POS_EMPTY ? ' mixerCurrentSemiTop' : '')
  const mixerWait = 'mixerWait' + (position === POS_WAITING ? ' mixerCurrent' : '')
  // const mixerMiddle = "mixerMiddle" + (position > 0 && position < 1 ? " mixerCurrent" : "");
  const mixerDown = 'mixerDown' + (position === POS_DOWN ? ' mixerCurrent' : '')

  const cancontrol = props.xxxKeys.CTRL ? ' hasCTRL' : ''
  const mixerTopSensor = 'mixerLiftControl isButton isControl needsCTRL' +
    (isTop ? ' mixerLiftSensorTop' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.lift_sensors.empty.reason)
  const mixerSemiTopSensor = 'mixerLiftControl isButton isControl needsCTRL' +
    (isSemiTop ? ' mixerLiftSensorSemiTop' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.lift_sensors.empty_semi.reason)
  const mixerWaitSensor = 'mixerLiftControl isButton isControl needsCTRL' +
    (isWait ? ' mixerLiftSensorWait' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.lift_sensors.waiting.reason)
  const mixerDownSensor = 'mixerLiftControl isButton isControl needsCTRL' +
    (isDown ? ' mixerLiftSensorDown' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    reasonToClassSpace(dfs.lift_sensors.down.reason)
  const directionUpReasonClass = (dfs.lift_set_direction_manual === 1 ? ' reasonManual' : (dfs.lift_set_direction === 1 ? ' reasonAutomat' : ''))
  const directionUp = 'mixerLiftControl isButton isControl needsCTRL' +
    (sigDirectionUp ? ' signalActive' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    directionUpReasonClass
  const directionDownReasonClass = (dfs.lift_set_direction_manual === -1 ? ' reasonManual' : (dfs.lift_set_direction === -1 ? ' reasonAutomat' : ''))
  const directionDown = 'mixerLiftControl isButton isControl needsCTRL' +
    (sigDirectionDown ? ' signalActive' : '') +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '') +
    directionDownReasonClass
  const mixerStop = 'mixerLiftControl mixerLiftControlStop isButton isControl needsCTRL' +
    cancontrol +
    (props.xxxStateFrozen ? ' isFrozen' : '')
  const mixerLiftVibrationButton = 'mixerLiftControl isButton isControl' +
    (isVibrating ? ' signalActive' : '') +
    cancontrol +
    reasonToClassSpace(dfs.set_vibrate_reason)

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  return h('.mixer-lift-content', { style: { width: mixerLiftContentWidth, height: mixerLiftControlContentHeight } }, [
    h('.mixer-positions-content', {}, [
      h('', { className: mixerTop }),
      h('', { className: mixerSemiTop }),
      h('', { className: mixerWait }),
      h('', { className: mixerMiddle }, [
        mixerLiftFail
          // ? h('i.fa.fa-exclamation.faa-flash.animated.buttonFontSize13vw')
          ? h('i.fa.fa-exclamation.buttonFontSize13vw')
          : null
      ]),
      h('', {
        className: mixerDown,
        title: strokeCountStr,
        onClick: _e => callApi([id, 'reset_stroke_count'])
      })
    ]),
    h('.mixer-rail-content', {}, [
      h('.mixerRail')
    ]),
    h('.mixerLiftControl-content', { style: { height: mixerLiftControlContentHeight } }, [
      mixerCanVibration === 1
        ? h('.buttonFontSize', {
          className: mixerLiftVibrationButton,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{mixerLiftVibration}')
        }, h('i.fa.fa-signal-stream'))
        : null,
      h('', {
        className: mixerLiftLockEmptyClass,
        onClick: _e => callApi([id, 'lock_lift_empty_toggle']),
        title: props.fmt('{mixerLiftLockEmpty}')
      }, h('i', { className: mixerLiftLockEmptyIcon })),
      h('', {
        className: mixerTopSensor,
        onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY]),
        title: props.fmt('{mixerLiftToEmpty}')
      }, h('i.fa.fa-circle')),
      isSemiTop == null
        ? null
        : h('', {
          className: mixerSemiTopSensor,
          onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY_SEMI])
        }, h('i.fa.fa-circle')),
      isWait == null
        ? null
        : h('', {
          className: mixerWaitSensor,
          onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_WAITING]),
          title: props.fmt('{mixerLiftToWaiting}')
        }, h('i.fa.fa-circle')),
      h('.buttonFontSize1vw', {
        className: directionUp,
        onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', 1]),
        onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
        title: props.fmt('{mixerLiftUp}')
      }, h('i.fa.fa-angle-double-up')),
      h('.buttonFontSizeSmaller', {
        className: mixerStop,
        onClick: _e => canCallApi && callApi([id, 'stop_lift']),
        title: props.fmt('{mixerLiftStop}')
      }, h('i.far.fa-dot-circle')),
      h('.buttonFontSize1vw', {
        className: directionDown,
        onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', -1]),
        onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
        title: props.fmt('{mixerLiftDown}')
      }, h('i.fa.fa-angle-double-down')),
      h('', {
        className: mixerDownSensor,
        onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_DOWN]),
        title: props.fmt('{mixerLiftToDown}')
      }, h('i.fa.fa-circle'))
    ]),
    h('.mixerLiftDurationLeft-content', { style: { height: mixerLiftControlContentHeight } }, [
      h('.mixerLiftDurationLeft', {}, emptyingFullDurationLeft),
      h('.mixerLiftDurationLeft', {}, emptyingSemiDurationLeft),
      h('.mixerLiftLock-content', {}, [
        h('', {
          className: mixerLiftLockDownClass,
          onClick: _e => callApi([id, 'lock_lift_down_toggle']),
          title: props.fmt('{mixerLiftLockDown}')
        }, h('i', { className: mixerLiftLockDownIcon }))
      ])
    ])
  ])
}

const renderNodeLifts = props => {
  gloDebugReact && incDebugRenderCount('NodeLifts', props)
  const dfs = props.data
  const id = dfs.id
  const isTop = dfs.is_in_empty
  const isSemiTop = dfs.is_in_empty_semi
  const isWait = dfs.is_in_waiting
  const isDown = dfs.is_in_down
  const position = dfs._position
  const sigDirectionUp = dfs.sig_go_up_state
  const sigDirectionDown = dfs.sig_go_down_state
  const liftCanVibration = dfs.can_vibrate
  const isVibrating = dfs._is_vibrating
  const topDuration = dfs.countdown_top_str
  const sequence = dfs.seq_tot
  // const val = dfs.val_str;
  const scaleValueStr = dfs.scale_val_str
  const liftIsNonEmpty = dfs._is_nonempty ? ' liftNonEmpty' : ''
  const bottomDuration = dfs.countdown_bottom_str

  const strokeCount = dfs.stroke_count
  const strokeCountSinceLastMaintenance = dfs.stroke_count_since_last_maintenance
  const strokeCountStr = props.fmt('{StrokeCount}: ' + strokeCount + '\n{SinceLastMaintenance}: ' + strokeCountSinceLastMaintenance)

  const sizeCoeficientWidth = 18 * props.xxxWideModeCoef
  const sizeCoeficientHeight = 34
  const liftWidthWithSpace = sizeCoeficientWidth + 1 + 'vw'
  const liftWidth = sizeCoeficientWidth + 'vw'
  const liftHeight = sizeCoeficientHeight + 'vh'
  const liftBottomDurationMarginTop = '30vh'
  const liftDownDisplayMarginTop = '24vh'
  const liftDownDisplayMarginLeft = '0.1vw'

  const liftTopSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.empty.reason)
  const liftTopSensor = 'liftControl isButton isControl needsCTRL' + (isTop ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + liftTopSensorReasonClass
  const liftSemiTopSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.empty_semi.reason)
  const liftSemiTopSensor = 'liftControl isButton isControl needsCTRL' + (isSemiTop ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + liftSemiTopSensorReasonClass
  const liftWaitSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.waiting.reason)
  const liftWaitSensor = 'liftControl isButton isControl needsCTRL' + (isWait ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + liftWaitSensorReasonClass
  const liftDownSensorReasonClass = reasonToClassSpace(dfs.lift_sensors.down.reason)
  const liftDownSensor = 'liftControl isButton isControl needsCTRL' + (isDown ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + liftDownSensorReasonClass
  const liftVibrationReasonClass = reasonToClassSpace(dfs.set_vibrate_reason)
  const liftVibrationButton = 'liftControl isButton isControl' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (isVibrating ? ' signalActive' : '') + liftVibrationReasonClass
  const directionUpReasonClass = (dfs.lift_set_direction_manual === 1 ? ' reasonManual' : (dfs.lift_set_direction === 1 ? ' reasonAutomat' : ''))
  const directionUp = 'liftControl isButton isControl needsCTRL' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + (sigDirectionUp ? ' signalActive' : '') + directionUpReasonClass
  const directionDownReasonClass = (dfs.lift_set_direction_manual === -1 ? ' reasonManual' : (dfs.lift_set_direction === -1 ? ' reasonAutomat' : ''))
  const directionDown = 'liftControl isButton isControl needsCTRL' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + (sigDirectionDown ? ' signalActive' : '') + directionDownReasonClass
  const liftStop = 'liftControl liftControlStop isButton isControl needsCTRL' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '')

  const liftTop = 'liftTop' + (position >= POS_EMPTY ? (' liftCurrentTop' + liftIsNonEmpty) : '')
  const liftSemiTop = 'liftSemiTop' + (position > POS_WAITING && position < POS_EMPTY ? (' liftCurrentSemiTop' + liftIsNonEmpty) : '')
  const liftWait = 'liftWait' + (position === POS_WAITING ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftMiddle = 'liftMiddle' + (position == null || position === POS_TOTAL ? ' liftCurrent liftFailState' : '') + (position > POS_DOWN && position < POS_WAITING ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftDown = 'liftDown' + (position === POS_DOWN ? (' liftCurrent' + liftIsNonEmpty) : '')
  const liftDownDisplayVisibility = (position === POS_DOWN ? 'visible' : 'hidden')
  const liftFail = (position == null || position === POS_TOTAL)

  const liftLockDown = dfs.is_locked_lift_down
  const liftLockDownIcon = 'fa' + (liftLockDown ? ' fa-lock signalActive' : ' fa-lock-open') + ' liftLockRotated'
  const liftLockDownClass = 'liftControl isButton isControl' +
    (liftLockDown ? ' reasonManual' : ' green') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
  const liftLockEmpty = dfs.is_locked_lift_empty
  const liftLockEmptyIcon = 'fa' + (liftLockEmpty ? ' fa-lock signalActive' : ' fa-lock-open') + ' liftLockRotated'
  const liftLockEmptyClass = 'liftControl isButton isControl' +
    (liftLockEmpty ? ' reasonManual' : ' green') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  return h('.content-lift', { style: { width: liftWidthWithSpace, height: liftHeight, marginLeft: '1vw' } }, [
    h('.lift-content', { style: { width: liftWidth, height: liftHeight } }, [
      h('.liftDisplay', { style: { marginTop: liftDownDisplayMarginTop, marginLeft: liftDownDisplayMarginLeft, visibility: liftDownDisplayVisibility } }, scaleValueStr),
      h('.liftTopDuration', { style: { marginLeft: liftWidth } }, topDuration),
      h('.liftBottomContent', { style: { marginTop: liftBottomDurationMarginTop } }, [
        h('.liftBottomSequence', {}, sequence),
        h('.liftBottomDuration', {}, bottomDuration)
      ]),
      h('', {
        className: liftDown,
        title: strokeCountStr,
        onClick: _e => callApi([id, 'reset_stroke_count'])
      }),
      h('', { className: liftMiddle }, liftFail
        // ? h('i.fa.fa-question.faa-flash.animated.buttonFontSize13vw')
        ? h('i.fa.fa-question.buttonFontSize13vw')
        : null),
      h('', { className: liftWait }),
      h('', { className: liftSemiTop }),
      h('', { className: liftTop }),
      h('.liftRail-content', {}, [
        h('.liftRail'),
        h('.liftControl-content', {}, [
          liftCanVibration === 1
            ? h('.buttonFontSize', {
              className: liftVibrationButton,
              onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
              onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
              title: props.fmt('{liftVibration}')
            }, h('i.fa.fa-signal-stream'))
            : null,
          h('', {
            className: liftLockEmptyClass,
            onClick: _e => callApi([id, 'lock_lift_empty_toggle']),
            title: props.fmt('{liftLockEmpty}')
          }, h('i', { className: liftLockEmptyIcon })),
          h('', {
            className: liftTopSensor,
            onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY]),
            title: props.fmt('{liftToEmpty}')
          }, h('i.fa.fa-circle')),
          isSemiTop != null
            ? h('', {
              className: liftSemiTopSensor,
              onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_EMPTY_SEMI])
            }, h('i.fa.fa-circle'))
            : null,
          isWait != null
            ? h('', {
              className: liftWaitSensor,
              onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_WAITING]),
              title: props.fmt('{liftToWaiting}')
            }, h('i.fa.fa-circle'))
            : null,
          h('.buttonFontSizeCalc07', {
            className: directionUp,
            onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', 1]),
            onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
            title: props.fmt('{liftUp}')
          }, h('i.fa.fa-angle-double-up')),
          h('.buttonFontSizeCalc06', {
            className: liftStop,
            onClick: _e => canCallApi && callApi([id, 'stop_lift']),
            title: props.fmt('{liftStop}')
          }, h('i.far.fa-dot-circle')),
          h('.buttonFontSizeCalc07', {
            className: directionDown,
            onMouseDown: _e => canCallApi && callApi([id, 'set_lift_direction_manual', -1]),
            onMouseUp: _e => callApi([id, 'unset_lift_direction_manual']),
            title: props.fmt('{liftDown}')
          }, h('i.fa.fa-angle-double-down')),
          h('', {
            className: liftDownSensor,
            onClick: _e => canCallApi && callApi([id, 'go_to_position_manual', POS_DOWN]),
            title: props.fmt('{liftToDown}')
          }, h('i.fa.fa-circle')),
          h('', {
            className: liftLockDownClass,
            onClick: _e => callApi([id, 'lock_lift_down_toggle']),
            title: props.fmt('{liftLockDown}')
          }, h('i', { className: liftLockDownIcon }))
        ])
      ])
    ])
  ])
}

const renderNodeConveyers = props => {
  gloDebugReact && incDebugRenderCount('NodeConveyers', props)
  const dfs = props.data
  const id = dfs.id
  const silosLength = _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'silos']) ? _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'silos']).length : 0 // TODO: better in future
  const flowmetersLength = _.get(props.precalc.binsAndFlowmeters, ['Aggregate', id, 'flowmeters'], []).length
  const silosCount = silosLength > 0 ? silosLength : (flowmetersLength > 0 ? flowmetersLength : 1) // TODO: better in future
  const isRunning = dfs.is_running
  const conveyerType = dfs._is_inclined ? 'inclined' : 'horizontal' // TODO: get rid of this shit
  const conveyerIsFlowmeter = dfs.flow_str != null
  const isScrewConveyer = dfs._type === 'Screw'
  const flow = dfs.flow_str
  const scale = dfs.scale
  const scaleVal = dfs.scale_val_str
  const scaleReqVal = dfs.scale_req_str
  const batchState = dfs.seq_tot
  const fillPercentScale = dfs.scale_fill_within_bin_percent
  const horn = props.xxxHorn ? props.xxxHorn.id : null
  const hornEnable = (props.xxxHorn && props.xxxHorn.is_enabled) ? ' signalActive' : ''
  const conveyerControlMalfunctionHorn = 'conveyerIcon' + hornEnable
  // const conveyerRunState = "conveyerIcon " + (isRunning ? "green" : "");
  // const conveyerRunIcon = "fas fa-cog" + (isRunning ? " fa-spin" : "");
  const flushState = dfs._is_flushing
  const flushAvailable = dfs.can_flush
  const conveyerFlushReasonClass = reasonToClassSpace(dfs.set_flush_reason)
  const conveyerControlFlushButton = 'conveyerControl isButton isControl needsCTRL' + (flushState ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + conveyerFlushReasonClass
  const vibrationState = dfs._is_vibrating
  const vibrationAvailable = dfs.can_vibrate
  const vibrationReasonClass = reasonToClassSpace(dfs.set_vibrate_reason)
  const conveyerControlVibrationButton = 'conveyerControl isButton isControl' + (vibrationState ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + vibrationReasonClass
  const materialOnConveyer = dfs.is_empty_raw != null
  const materialOnConveyerEnable = dfs.is_empty_raw === 0
  const materialOnConveyerIconClass = 'fas fa-ellipsis-h' + (materialOnConveyerEnable ? ' signalActive' : '')

  const requirementState = dfs._set_requirement
  const requirementGranted = dfs.is_granted

  const displayContent = 'conveyerDisplayContent-' + conveyerType
  const hasOutputFlap = dfs.has_output_flap
  const hasOutputFlap2 = dfs.has_output_flap2
  const outputFlapState = dfs.is_output_flap_open
  const outputFlap2State = dfs.is_output_flap2_open
  const outputFlapClass = outputFlapState ? 'conveyerFlapOpened' : 'conveyerFlap'
  const outputFlap2Class = outputFlap2State ? 'conveyerFlapOpened' : 'conveyerFlap'
  const outputFlapButton = 'conveyerFlapButtons isButton isControl needsCTRL' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '')
  const outputFlap2Button = 'conveyerFlapButtons isButton isControl needsCTRL' + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '')

  const conveyerClass = 'conveyer-' + conveyerType
  /* const isSlow = d.is_slow */
  const conveyerRightToLeft = dfs._right_to_left
  const conveyerRunClass = isRunning ? ('conveyer-run' /* + (isSlow ? ' slow' : '') */ + (conveyerRightToLeft ? ' reverse' : '')) : 'conveyer-idle'

  const conveyerRunReasonClass = reasonToClassSpace(dfs.set_open_reason)
  const conveyerControlStartButton = 'conveyerControl isButton isControl needsCTRL' + (isRunning ? ' red' : ' green') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + conveyerRunReasonClass
  const conveyerMoveMaterialReasonClass = reasonToClassSpace(dfs.set_move_material_reason)
  const conveyerControlMoveMaterialButton = 'conveyerControl isButton isControl' + (dfs.sig_start_state ? ' signalActive' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + conveyerMoveMaterialReasonClass
  const conveyerContent = 'conveyer-' + conveyerType + '-content'
  const conveyerControlContent = 'conveyer-' + conveyerType + '-control-content' + (conveyerRightToLeft ? '-righttoleft' : '')

  const sizeCoeficientWidth = dfs._is_inclined
    ? 15
    : (dfs.sink === 'Final'
        ? 20
        : (silosCount * 5) + (silosCount * 0.05) + (silosCount > 1 ? 0.5 : 0)
      ) * props.xxxWideModeCoef
  const sizeCoeficientHeight = dfs._is_inclined ? 34 : 10
  const conveyerWidthWithSpace = sizeCoeficientWidth + 0.2 + 'vw'
  const conveyerWidth = sizeCoeficientWidth + 'vw'
  const conveyerHeight = sizeCoeficientHeight + 'vh'

  const scaleBar = 'conveyer-scale-bar Aggregate'
  const levelScale = 'linear-gradient(to right, var(--colorWeightProgressAggregate) 0% ' + fillPercentScale + '%, var(--colorWeightAggregate) ' + fillPercentScale + '% 100%)'

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  return h('.contentConveyer', { style: { width: conveyerWidthWithSpace, height: conveyerHeight, marginLeft: dfs._is_inclined ? '1vw' : 0 } }, [
    h('.content-conveyer', { style: { width: conveyerWidth, height: conveyerHeight } }, [
      dfs._is_inclined // TODO: we should check for flap presence, not "is inclined"
        ? h('.conveyerFlaps', {}, [
          hasOutputFlap2
            ? h('.conveyerFlapL', {}, [
              h('', {
                className: outputFlap2Button,
                onMouseDown: _e => canCallApi && callApi([id, 'output_flap2_toggle'])
              }, [
                outputFlap2State ? h('i.fa.fa-arrow-up') : h('i.fa.fa-arrow-down')
              ]),
              h('', { className: outputFlap2Class })
            ])
            : null,
          hasOutputFlap
            ? h('.conveyerFlapR', {}, [
              h('', { className: outputFlapClass }),
              h('', {
                className: outputFlapButton,
                onMouseDown: _e => canCallApi && callApi([id, 'output_flap_toggle'])
              }, [
                outputFlapState ? h('i.fa.fa-arrow-up') : h('i.fa.fa-arrow-down')
              ])
            ])
            : null
        ])
        : null,
      h('.conveyer-content', { style: { width: conveyerWidth, height: conveyerHeight } }, [
        h('', { className: conveyerContent }, [
          h('', { className: displayContent }, [
            materialOnConveyer
              ? h('i', {
                className: materialOnConveyerIconClass,
                title: props.fmt('{conveyerHasMaterial}')
              })
              : null,
            requirementState != null
              ? h('.bin-top-request', {}, requirementState ? props.fmt('{requested}') : '')
              : h(''),
            requirementGranted != null
              ? h('.bin-top-request-accepted', {}, requirementGranted ? props.fmt('{switched}') : '')
              : h(''),
            scale ? h('.conveyerBatchStateDisplay', {}, batchState) : null
          ]),
          scale || conveyerIsFlowmeter
            ? h('', { className: scaleBar, style: { background: levelScale } })
            : null,
          h('', { className: conveyerClass }, [
            h('.conveyerMaterial', {
              style: {
                width: dfs._material_width_percent + '%',
                marginLeft: dfs._material_left_percent + '%'
              }
            }),
            h('.conveyerRotationBackground', {}, [
              h('', { className: conveyerRunClass })
            ])
          ]),
          h('', { className: conveyerControlContent }, [
            h('.buttonFontSizeCalc06', {
              className: conveyerControlStartButton,
              title: props.fmt('{conveyerOnOff}'),
              onMouseDown: _e => canCallApi && callApi([id, 'toggle_open_invert'])
            }, h('i.far.fa-dot-circle')),
            (!conveyerIsFlowmeter && !isScrewConveyer)
              ? h('.buttonFontSizeCalc08', {
                className: conveyerControlMoveMaterialButton,
                title: props.fmt('{conveyerMoveMaterial}'),
                onMouseDown: _e => callApi([id, 'set_override', 'set_move_material', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_move_material'])
              }, h('i.fa.fa-angle-double-' + (conveyerRightToLeft ? 'left' : 'right')))
              : null,
            flushAvailable
              ? h('.buttonFontSizeSmaller', {
                className: conveyerControlFlushButton,
                title: props.fmt('{flushConveyer}'),
                onMouseDown: _e => canCallApi && callApi([id, 'set_override', 'set_flush', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_flush'])
              }, h('i.fas.fa-tint'))
              : null,
            vibrationAvailable
              ? h('.buttonFontSize', {
                className: conveyerControlVibrationButton,
                title: props.fmt('{vibrationConveyer}'),
                onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
                onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate'])
              }, h('i.fa.fa-signal-stream'))
              : null,
            horn
              ? h('.buttonFontSizeSmaller', {
                className: conveyerControlMalfunctionHorn,
                title: props.fmt('{' + horn + '}')
              }, h('i.fa.fa-bullhorn'))
              : null,
            scale
              ? h('.conveyerDisplaysContent', {}, [
                h('.conveyerRequestDisplay', {}, scaleReqVal),
                h('.conveyerDisplay', {}, scaleVal)
              ])
              : null,
            conveyerIsFlowmeter
              ? h('.conveyerFlowDisplaysContent', {}, [
                h('.conveyer-delta', {}, DELTA),
                h('.conveyerFlowDisplay', {}, flow)
              ])
              : null
          ])
        ])
      ])
    ])
  ])
}

const renderNodeFreePaths = props => {
  gloDebugReact && incDebugRenderCount('NodeFreePaths', props)
  const d = props.data
  const sizeCoeficientWidth = 20 * props.xxxWideModeCoef
  const contentWidth = sizeCoeficientWidth + 'vw'
  const freePath = d._freepath
  const freePathClass = '.content-free-path.' + (freePath ? '.greenDark' : '.redDark')
  return h('freePaths-content', { style: { width: contentWidth } }, [
    h(freePathClass, {}, [
      h('i.fas.fa-shopping-cart')
    ])
  ])
}

// TODO: unfinished
// TODO: make this a component
/* const sensorHelper = props => {
  const reasonClass = reasonToClassDot(props.reason)
  //const title = (idx === 0 ? props.fmt('{mixerBayToClose}') : (idx === (arr.length - 1) ? props.fmt('{mixerBayToOpen}') : (props.fmt('{mixerBayTo}') + ' ' + x.position)))
  const iconClass = '.fa' + (x.is_virtual ? '.fa-dot-circle' : '.fa-circle')
  const activeClass = (x.state === 1 ? '.signalActive' : '')
  return h('.sensorButton.isButton.isControl.needsCTRL' + (props.xxxKeys.CTRL ? '.hasCTRL' : '') + reasonClass, {
    onMouseDown: props.onMouseDown,
    title: props.title,
  }, h('i.sensorLed' + activeClass + iconClass))
} */

const renderNodeCart = props => {
  gloDebugReact && incDebugRenderCount('NodeCart', props)
  const d = props.data
  const id = d.id
  const seqTot = d.seq_to

  const canCallApi = (CTRL_ENABLED && props.xxxKeys.CTRL) || props.xxxStateFrozen

  const openSensorClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
    // + directionOpenReasonClass
  const openSensorIconClass = d.sig_is_open_state === 1 ? ' signalActive' : ''
  // + directionOpenReasonClass
  const openingClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
    // + directionOpenReasonClass
  const closedSensorClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
    // + directionOpenReasonClass
  const closedSensorIconClass = d.sig_is_closed_state === 1 ? ' signalActive' : ''
  // + directionOpenReasonClass
  const closingClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '')
    // + directionCloseReasonClass

  const directionUpReasonClass = (d.set_direction_manual === 1 ? ' reasonManual' : (d.set_direction === 1 ? ' reasonAutomat' : ''))
  const directionDownReasonClass = (d.set_direction_manual === -1 ? ' reasonManual' : (d.set_direction === -1 ? ' reasonAutomat' : ''))
  const upClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    directionUpReasonClass
  const upIconClass = d.sig_is_going_up_state === 1 ? ' signalActive' : ''
  const downClass = 'sensorButton isButton isControl' +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    directionDownReasonClass
  const downIconClass = d.sig_is_going_down_state === 1 ? ' signalActive' : ''

  const isNonEmptyClass = d.is_empty ? '' : ' liftNonEmpty'

  const sensors = d.sensors.map((x, _idx, _arr) => {
    const reasonClass = reasonToClassDot(x.reason)
    const title = props.fmt('{cartToPosition} ') + (x.position + 1) // TODO: use node's human_name? also, i don't like the +1 (it would be better if positions had names such as "hala 1", "pod michackou", "myti" and show these)
    const iconClass = '.fa' + (x.is_virtual ? '.fa-dot-circle' : '.fa-circle')
    const activeClass = (x.state === 1 ? '.signalActive' : '')
    return h('.sensorButton.isButton.isControl.needsCTRL' + (props.xxxKeys.CTRL ? '.hasCTRL' : '') + reasonClass, {
      key: 'sensor-' + x.position,
      onMouseDown: _e => canCallApi && callApi([id, 'go_to_position_manual', x.position]),
      title
    }, h('i.sensorLed' + activeClass + iconClass))
    // TODO: unfinished
    /* return sensorHelper({
      ...x,
      key: 'sensor-' + x.position,
      onMouseDown: _e => canCallApi && callApi([id, 'go_to_position_manual', x.position]),
      title,
    }) */
  })

  return h('.cart.m01vh', {}, [
    // h("", {}, "seq_tot: " + seqTot),
    h('.bin-top-batches', {}, seqTot),
    h('.hflex.justify-between.m01vh', {}, [
      h('', {
        className: downClass,
        // onMouseDown: _e => callApi([id, 'set_direction_manual', -1]),
        // onMouseUp: _e => callApi([id, 'unset_direction_manual']),
        title: props.fmt('{cartMotionLeft}')
      }, h('i.sensorAngle.fa.fa-angle-double-left', { className: downIconClass })),
      h('.hflex.fontColorButtonText', {}, [
        h('', {
          className: openSensorClass,
          title: props.fmt('{cartToOpen}')
        }, h('i.sensorLed.fa.fa-circle', { className: openSensorIconClass })),
        h('', {
          className: openingClass,
          // onMouseDown: _e => callApi([id, 'set_direction_manual', 1]),
          // onMouseUp: _e => callApi([id, 'unset_direction_manual']),
          title: props.fmt('{cartOpen}')
        }, semiNodeButtonOpen)
      ]),
      h('.hflex.fontColorButtonText', {}, [
        h('', {
          className: closedSensorClass,
          title: props.fmt('{cartToClose}')
        }, h('i.sensorLed.fa.fa-circle', { className: closedSensorIconClass })),
        h('', {
          className: closingClass,
          // onMouseDown: _e => callApi([id, 'set_direction_manual', -1]),
          // onMouseUp: _e => callApi([id, 'unset_direction_manual']),
          title: props.fmt('{cartClose}')
        }, semiNodeButtonClose)
      ]),
      // h('.buttonFontSizeCalc06', {
      // className: cartStop,
      // onClick: _e => canCallApi && callApi([id, 'stop_lift']),
      // title: props.fmt('{cartStop}')
      // }, h('i.far.fa-dot-circle')),
      h('', {
        className: upClass,
        // onMouseDown: _e => callApi([id, 'set_direction_manual', 1]),
        // onMouseUp: _e => callApi([id, 'unset_direction_manual']),
        title: props.fmt('{cartMotionRight}')
      }, h('i.sensorAngle.fa.fa-angle-double-right', { className: upIconClass }))
    ]),
    // h("", {}, "is_empty: " + d.is_empty),
    // h("", {}, "pos: " + d._position),
    // h("", {style: {width: "50%"}},
    // TODO: use node's human name for title?
    // TODO: liftCurrent class?
    h('.liftCurrent.m01vh', {
      className: isNonEmptyClass,
      title: props.fmt('{cart}'),
      style: {
        height: '30px',
        width: '40px',
        '--tran': d._position / d.pos_max,
        '--rot': d.open,
        // transform: "translateX(calc(200px * var(--tran))) translateY(calc(-10px * var(--tran))) rotate(calc(180deg * var(--rot)))",
        // transform: "translateX(calc(340px * var(--tran))) rotate(calc(180deg * var(--rot)))",
        // transformOrigin: "bottom right",
        position: 'relative',
        // "--perc": d._position / d.pos_max * 100,
        '--ratio': d._position / d.pos_max,
        '--perc': (d._position / d.pos_max * 100) + '%',
        left: 'calc(var(--perc) - (var(--ratio) * 40px))',
        transform: 'rotate(calc(180deg * var(--rot)))'
      }
    }),
    // ),
    h('.cartRail.m01vh'),
    h('.hflex.justify-around.m01vh', {}, sensors)
  ])
}

const renderNodeFunnels = props => {
  gloDebugReact && incDebugRenderCount('NodeFunnels', props)
  const dfs = props.data
  const id = dfs.id
  const sizeCoeficientWidth = 20 * props.xxxWideModeCoef
  // const sizeCoeficientHeight = 21;
  const funnelWidth = sizeCoeficientWidth + 'vw'
  const funnelOpened = dfs.open
  const truckCounterStr = dfs._truck_counter_str
  const truckFillStr = dfs._truck_fill_str
  const truckChangeCountdown = dfs._truck_change_countdown
  const sizeOfOpen = funnelOpened === 1 ? '35%' : '99%'
  const flushReasonClass = reasonToClassSpace(dfs.set_flush_reason)
  const flush = 'funnelButtons isButton isControl needsCTRL' + (dfs._is_flushing === 1 ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') + (props.xxxStateFrozen ? ' isFrozen' : '') + flushReasonClass
  const openStateClass = 'funnelButtons isButton isControl' + (dfs.sig_open_state ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') // TODO add reason
  const closeStateClass = 'funnelButtons isButton isControl' + (dfs.sig_close_state ? ' signalActive' : '') + (props.xxxKeys.CTRL ? ' hasCTRL' : '') // TODO add reason
  const isLocked = dfs.is_locked_bay
  const lockStateClass = 'funnelButtons isButton isControl' + (isLocked ? ' signalActive reasonManual' : ' green') + (props.xxxKeys.CTRL ? ' hasCTRL' : '')
  // const lockStateButtonIco = 'fa' + (isLocked ? ' fa-hand-paper faa-flash animated' : ' fa-lock-open')
  const lockStateButtonIco = 'fa' + (isLocked ? ' fa-hand-paper' : ' fa-lock-open')

  return h('.content-funnel', { style: { width: funnelWidth } }, [
    h('.funnel-flaps', {}, [
      h('', {
        className: lockStateClass,
        onMouseDown: _e => callApi([id, 'lock_bay_toggle']),
        title: props.fmt('{funnelBlock}')
      }, [
        h('i', { className: lockStateButtonIco })
      ]),
      h('', {
        className: closeStateClass,
        style: { marginLeft: '0vw' },
        onMouseDown: _e => callApi([id, 'go_to_open_manual', 0]),
        title: props.fmt('{funnelClose}')
      }, semiNodeButtonClose),
      h('.funnel-flap-left', {}, [
        h('.funnel-flapL', { style: { width: sizeOfOpen } })
      ]),
      h('.funnel-flap-right', {}, [
        h('.funnel-flapR', { style: { width: sizeOfOpen } })
      ]),
      h('', {
        className: openStateClass,
        style: { marginRight: '0vw' },
        onMouseDown: _e => callApi([id, 'go_to_open_manual', 1]),
        title: props.fmt('{funnelOpen}')
      }, semiNodeButtonOpen),
      h('', {
        className: flush,
        onMouseDown: _e => callApi([id, 'set_override', 'set_flush', 1]),
        onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
        title: props.fmt('{funnelFlush}')
      }, [
        h('i.fas.fa-tint')
      ])
    ]),
    h('.funnel-displays', {}, [
      h('.funnel-display-truck-fill', { title: props.fmt('{truckFillTooltip}') }, truckFillStr),
      h('.funnel-display-countdown', {}, truckChangeCountdown),
      h('.funnel-display-truck-counter', { title: props.fmt('{truckCounterTooltip}') }, truckCounterStr)
    ])
  ])
}

const renderNodeChutes = props => {
  gloDebugReact && incDebugRenderCount('NodeChutes', props)
  const dfs = props.data
  const id = dfs.id
  const canVibrate = dfs.can_vibrate
  const vibrateClass = 'chuteButtons isButton isControl' +
    (dfs._is_vibrating === 1 ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.set_vibrate_reason)
  const canFlush = dfs.can_flush
  const flushClass = 'chuteButtons isButton isControl' +
    (dfs._is_flushing === 1 ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.set_flush_reason)
  const canExtend = dfs.can_extend
  const position = dfs.extend_percent
  const classPos0 = '.content-chute-extend.chute.isControl' + (position === 0 ? '' : '.opacity-40')
  const classPos05 = '.content-chute-extend.chute' + ((position > 0 && position < 100) ? '' : '.invisible')
  const classPos1 = '.content-chute-extend.chute.isControl' + (position === 100 ? '' : '.opacity-40')
  // TODO: reasons - we should probably just set reason for either extend or unextend, not both?
  const unextendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_unextend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (dfs.sig_unextend_state ? reasonToClassSpace(dfs.set_extend_reason) : '')
  const extendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_extend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    (dfs.sig_extend_state ? reasonToClassSpace(dfs.set_extend_reason) : '')

  const theChute = [
    h('.chute-top'),
    h('.chute-bottom-content', {}, [
      h('.chute-bottom.chute-bottom-left'),
      h('.chute-bottom.chute-bottom-right')
    ])
  ]

  return h('.vflex', {}, [
    h('.hflex', {}, [
      canVibrate
        ? h('', {
          className: vibrateClass,
          onMouseDown: _e => callApi([id, 'set_override', 'set_vibrate', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_vibrate']),
          title: props.fmt('{mixerBayVibration}')
        }, [
          h('i.fa.fa-signal-stream')
        ])
        : null,
      canFlush
        ? h('', {
          className: flushClass,
          onMouseDown: _e => callApi([id, 'set_override', 'set_flush', 1]),
          onMouseUp: _e => callApi([id, 'set_override', 'set_flush']),
          title: props.fmt('{mixerBayFlush}')
        }, [
          h('i.fas.fa-tint')
        ])
        : null
    ]),
    canExtend
      ? h('.content-chute-extend', {}, [
        h(classPos0, {
          onMouseDown: _e => callApi([id, 'go_to_extend', 0]),
          title: props.fmt('{chuteUnextend}')
        }, theChute),
        dfs.sig_unextend_state !== null
          ? h('', {
            className: unextendStateClass,
            onMouseDown: _e => callApi([id, 'set_direction_manual', -1]),
            onMouseUp: _e => callApi([id, 'unset_direction_manual']),
            title: props.fmt('{chuteUnextend}')
          }, [
            h('i.fa.fa-arrow-left')
          ])
          : h('.chuteButtons.invisible'),
        h(classPos05, {}, theChute),
        dfs.sig_extend_state !== null
          ? h('', {
            className: extendStateClass,
            onMouseDown: _e => callApi([id, 'set_direction_manual', 1]),
            onMouseUp: _e => callApi([id, 'unset_direction_manual']),
            title: props.fmt('{chuteExtend}')
          }, [
            h('i.fa.fa-arrow-right')
          ])
          : h('.chuteButtons.invisible'),
        h(classPos1, {
          onMouseDown: _e => callApi([id, 'go_to_extend', 1]),
          title: props.fmt('{chuteExtend}')
        }, theChute)
      ])
      : null
  ])
}

const renderNodeSBMFunnel = props => {
  gloDebugReact && incDebugRenderCount('NodeSBMFunnel', props)
  const dfs = props.data
  const id = dfs.id

  const leftPosition = dfs.left_extend_percent
  // TODO: get rid of "chute" references
  const leftPos0Class = '.content-chute-extend.chute.isControl' + (leftPosition === 0 ? '' : '.opacity-20')
  const leftPos05Class = '.content-chute-extend.chute' + ((leftPosition > 0 && leftPosition < 100) ? '' : '.invisible')
  const leftPos1Class = '.content-chute-extend.chute.isControl' + (leftPosition === 100 ? '' : '.opacity-20')

  const theConeOutline = h('img', { src: './static/tabler--cone-2.svg' })
  const theConeFill = h('img', { src: './static/tabler--cone-2-filled.svg' })

  const leftPos0Img = leftPosition === 0 ? theConeFill : theConeOutline
  const leftPos05Img = (leftPosition > 0 && leftPosition < 100) ? theConeFill : ''
  const leftPos1Img = leftPosition === 100 ? theConeFill : theConeOutline

  // TODO: reasons - we should probably just set reason for either extend or unextend, not both?
  const leftUnextendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_left_unextend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.left_set_extend_reason)
  const leftExtendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_left_extend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.left_set_extend_reason)

  const rightPosition = dfs.right_extend_percent
  // TODO: get rid of "chute" references
  const rightPos0Class = '.content-chute-extend.chute.isControl' + (rightPosition === 0 ? '' : '.opacity-20')
  const rightPos05Class = '.content-chute-extend.chute' + ((rightPosition > 0 && rightPosition < 100) ? '' : '.invisible')
  const rightPos1Class = '.content-chute-extend.chute.isControl' + (rightPosition === 100 ? '' : '.opacity-20')

  const theFunnelOutline = h('img', { src: './static/ph--funnel-light.svg' })
  const theFunnelFill = h('img', { src: './static/ph--funnel-fill.svg' })

  const rightPos0Img = rightPosition === 0 ? theFunnelFill : theFunnelOutline
  const rightPos05Img = (rightPosition > 0 && rightPosition < 100) ? theFunnelFill : ''
  const rightPos1Img = rightPosition === 100 ? theFunnelFill : theFunnelOutline

  // TODO: reasons - we should probably just set reason for either extend or unextend, not both?
  const rightUnextendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_right_unextend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.right_set_extend_reason)
  const rightExtendStateClass = 'chuteButtons isButton isControl' +
    (dfs.sig_right_extend_state ? ' signalActive' : '') +
    (props.xxxKeys.CTRL ? ' hasCTRL' : '') +
    reasonToClassSpace(dfs.right_set_extend_reason)

  // TODO: get rid of "chute" references
  return h('.vflex', {}, [
    h('.content-chute-extend', {}, [
      h(leftPos0Class, {
        onMouseDown: _e => callApi([id, 'left_go_to_extend', 0]),
        title: props.fmt('{chuteUnextend}')
      }, leftPos0Img),
      h('', {
        className: leftUnextendStateClass,
        onMouseDown: _e => callApi([id, 'left_set_direction_manual', -1]),
        onMouseUp: _e => callApi([id, 'left_unset_direction_manual']),
        title: props.fmt('{chuteUnextend}')
      }, [
        h('i.fa.fa-arrow-left')
      ]),
      h(leftPos05Class, {}, leftPos05Img),
      h('', {
        className: leftExtendStateClass,
        onMouseDown: _e => callApi([id, 'left_set_direction_manual', 1]),
        onMouseUp: _e => callApi([id, 'left_unset_direction_manual']),
        title: props.fmt('{chuteExtend}')
      }, [
        h('i.fa.fa-arrow-right')
      ]),
      h(leftPos1Class, {
        onMouseDown: _e => callApi([id, 'left_go_to_extend', 1]),
        title: props.fmt('{chuteExtend}')
      }, leftPos1Img),
      h(rightPos1Class, {
        onMouseDown: _e => callApi([id, 'right_go_to_extend', 1]),
        title: props.fmt('{chuteExtend}')
      }, rightPos1Img),
      h('', {
        className: rightExtendStateClass,
        onMouseDown: _e => callApi([id, 'right_set_direction_manual', 1]),
        onMouseUp: _e => callApi([id, 'right_unset_direction_manual']),
        title: props.fmt('{chuteExtend}')
      }, [
        h('i.fa.fa-arrow-left')
      ]),
      h(rightPos05Class, {}, rightPos05Img),
      h('', {
        className: rightUnextendStateClass,
        onMouseDown: _e => callApi([id, 'right_set_direction_manual', -1]),
        onMouseUp: _e => callApi([id, 'right_unset_direction_manual']),
        title: props.fmt('{chuteUnextend}')
      }, [
        h('i.fa.fa-arrow-right')
      ]),
      h(rightPos0Class, {
        onMouseDown: _e => callApi([id, 'right_go_to_extend', 0]),
        title: props.fmt('{chuteUnextend}')
      }, rightPos0Img)
    ])
  ])
}

const renderNodeContinuousConcreteSettings = props => {
  gloDebugReact && incDebugRenderCount('NodeContinuousConcreteSettings', props)
  const hourOutput = props.hour_output
  const truckCapacity = props.truck_capacity

  return h('.topContinuousSettingsContent', {}, [
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentText', {}, props.fmt('{HourOutput}')),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'hour_output_dec'])
      }, [
        h('i.fa.fa-minus')
      ]),
      h('.input.topContinuousSettingsContentInput', {
        title: props.fmt('{HourOutput}')
      }, hourOutput),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'hour_output_inc'])
      }, [
        h('i.fa.fa-plus')
      ])
    ]),
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentText', {}, props.fmt('{TruckCapacity}')),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'truck_capacity_dec'])
      }, [
        h('i.fa.fa-minus')
      ]),
      h('.input.topContinuousSettingsContentInput', {
        title: props.fmt('{TruckCapacity}')
      }, truckCapacity),
      h('.topContinuousSettingsContentButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'truck_capacity_inc'])
      }, [
        h('i.fa.fa-plus')
      ])
    ]),
    h('.topContinuousSettingsContentLine', {}, [
      h('.topContinuousSettingsContentFlags', {}, [
        h('.topContinuousSettingsContentButton.isButton.isOnDark', {
          onClick: _e => callApi(['control', 'finish']),
          title: props.fmt('{finish}')
        }, [
          h('i.fa.fa-flag-checkered.red')
        ]),
        h('.topContinuousSettingsContentFlagsButton.isButton.isOnDark', {
          onClick: _e => callApi(['control', 'finish_truck']),
          title: props.fmt('{finishTruck}')
        }, [
          h('i.fa.fa-truck'),
          h('i.fa.fa-arrow-right'),
          h('i.fa.fa-flag-checkered')
        ])
      ]),
      h('.topContinuousSettingsContentFlagsButton.isButton.isOnDark', {
        onClick: _e => callApi(['control', 'add_truck']),
        title: props.fmt('{truckAdd}')
      }, [
        h('i.fa.fa-truck.green'),
        h('i.fa.fa-plus.green')
      ])
    ])
  ])
}

const renderNodeOrderOnHold = props => {
  gloDebugReact && incDebugRenderCount('NodeOrderOnHold', props)
  const orderOnHold = props.order_on_hold ? props.fmt('{order_on_hold}') : null
  const stopAfterBatch = props.stop_after_batch ? props.fmt('{stop_after_batch}') : null
  const plusStr = orderOnHold && stopAfterBatch ? ' + ' : null
  return h('.orderOnHold', {}, [orderOnHold, plusStr, stopAfterBatch])
}

const renderNodeFunctionsKey = props => {
  gloDebugReact && incDebugRenderCount('NodeFunctionsKey', props)
  // yellow icon (signalActive) = backend state only, never faked by the locally-held key.
  // black "pressed" background = local key/mouse press state (keyActive class + CSS :active),
  // kept independent so: black = press recorded, yellow = backend engaged (and black without
  // yellow means the press was recorded but the backend did not act on it).
  const activeKeys = {
    F1: props.xxxIsHelp,
    F2: props.xxxMixerIsLocked,
    F4: props.xxxMixerDirectionClose,
    F5: props.xxxMixerDirectionOpen,
    F6: props.xxxCloseAggregateSilosOnOpen,
    F7: props.xxxVibrateSilosOnOpen,
    F8: props.xxxAerateSilosOnOpen,
    F10: props.xxxStateFrozen,
    F12: props.xxxAdditionalWaterSiloOpen,
    '*': props.xxxHornIsEnabled
  }
  const keys = props.xxxKeys

  const ctrlPressed = props.xxxKeys.CTRL ? ' hasCTRL' : ''

  // const sizeCoeficientWidth = 44
  // const contentWidth = sizeCoeficientWidth + 'vw'

  return h('.functionKeys-content', {}, [
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F1 ? ' signalActive' : '') + (keys.F1 ? ' keyActive' : '')),
      title: props.fmt('{F1}'),
      onMouseDown: _e => props.openHelpWindow()
    }, [
      h('i.fas.fa-question'),
      h('.functionKey-hint', {}, 'F1')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F2 ? ' signalActive' : '') + (keys.F2 ? ' keyActive' : '')),
      title: props.fmt('{F2}'),
      onMouseDown: _e => actionF2(),
      onMouseUp: _e => actionF2up(),
      onMouseLeave: _e => actionF2up()
    }, [
      h('i.fa.fa-lock-open'),
      h('.functionKey-hint', {}, 'F2')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F3 ? ' signalActive' : '') + (keys.F3 ? ' keyActive' : '')),
      title: props.fmt('{F3}'),
      onMouseDown: _e => actionF3(),
      onMouseUp: _e => actionF3up(),
      onMouseLeave: _e => actionF3up()
    }, [
      h('i.fa.fa-history'),
      h('.functionKey-hint', {}, 'F3')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F4 ? ' signalActive' : '') + (keys.F4 ? ' keyActive' : '')),
      title: props.fmt('{F4}'),
      onMouseDown: _e => actionF4(),
      onMouseUp: _e => actionF4up(),
      onMouseLeave: _e => actionF4up()
    }, [
      h('', {}, semiNodeButtonClose),
      h('.functionKey-hint', {}, 'F4')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F5 ? ' signalActive' : '') + (keys.F5 ? ' keyActive' : '')),
      title: props.fmt('{F5}'),
      onMouseDown: _e => actionF5(),
      onMouseUp: _e => actionF5up(),
      onMouseLeave: _e => actionF5up()
    }, [
      h('', {}, semiNodeButtonOpen),
      h('.functionKey-hint', {}, 'F5')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F6 ? ' signalActive' : '') + (keys.F6 ? ' keyActive' : '')),
      title: props.fmt('{F6}'),
      onMouseDown: _e => actionF6(),
      onMouseUp: _e => actionF6up(),
      onMouseLeave: _e => actionF6up()
    }, [
      h('i.fa.fa-level-up-alt'),
      h('.functionKey-hint', {}, 'F6')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F7 ? ' signalActive' : '') + (keys.F7 ? ' keyActive' : '')),
      title: props.fmt('{F7}'),
      onMouseDown: _e => actionF7(),
      onMouseUp: _e => actionF7up(),
      onMouseLeave: _e => actionF7up()
    }, [
      h('i.fa.fa-signal-stream'),
      h('.functionKey-hint', {}, 'F7')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F8 ? ' signalActive' : '') + (keys.F8 ? ' keyActive' : '')),
      title: props.fmt('{F8}'),
      onMouseDown: _e => actionF8(),
      onMouseUp: _e => actionF8up(),
      onMouseLeave: _e => actionF8up()
    }, [
      h('i.fa.fa-wind'),
      h('.functionKey-hint', {}, 'F8')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F9 ? ' signalActive' : '') + (keys.F9 ? ' keyActive' : '')),
      title: props.fmt('{F9}'),
      onMouseDown: _e => actionF9(),
      onMouseUp: _e => actionF9up(),
      onMouseLeave: _e => actionF9up()
    }, [
      h('i.fas.fa-ban'),
      h('.functionKey-hint', {}, 'F9')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F10 ? ' signalActive' : '') + (keys.F10 ? ' keyActive' : '')),
      title: props.fmt('{F10}'),
      onMouseDown: _e => actionF10(),
      onMouseUp: _e => actionF10up(),
      onMouseLeave: _e => actionF10up()
    }, [
      h('i.fa.fa-pause'),
      h('.functionKey-hint', {}, 'F10')
    ]),
    h('.functionsKey-button.isButton.isControl', {
      className: ctrlPressed + ((activeKeys.F11 ? ' signalActive' : '') + (keys.F11 ? ' keyActive' : '')),
      title: props.fmt('{F11}'),
      onMouseDown: _e => actionF11(),
      onMouseUp: _e => actionF11up(),
      onMouseLeave: _e => actionF11up()
    }, [
      h('i.fa.fa-tint-slash'),
      h('.functionKey-hint', {}, 'F11')
    ]),
    props.xxxAdditionalWaterIsPresent
      ? h('.functionsKey-button.wider.isButton.isControl.hflex.justify-around', {
        className: ctrlPressed + ((activeKeys.F12 ? ' signalActive' : '') + (keys.F12 ? ' keyActive' : '')),
        onMouseDown: _e => actionF12(),
        onMouseUp: _e => actionF12up(),
        onMouseLeave: _e => actionF12up(),
        title: props.fmt('{additionalWater}')
      }, [
        h('.additionalWaterIconArea', {}, [
          h('i.fas.fa-tint'),
          h('.functionKey-hint', {}, 'F12')
        ]),
        h('.additionalWaterDisplay', {}, props.xxxAdditionalWaterValueStr)
      ])
      : null,
    props.xxxSemaphoreId
      ? h('.functionsKey-button.wider.isButton.isControl.hflex.justify-around', { className: ctrlPressed }, [
        h('', {
          onMouseDown: _e => callApi([props.xxxSemaphoreId, 'set_red']),
          title: props.fmt('{semaphoreRed}')
        }, [
          h('i.fa.fa-circle.semaphore-font', {
            className: 'semaphore-red' + (props.xxxSemaphoreColor === 'red' ? '-active' : '')
          })
        ]),
        h('', {
          className: ctrlPressed + '.semaphore-green' + (props.xxxSemaphoreColor === 'green' ? '-active' : ''),
          onMouseDown: _e => callApi([props.xxxSemaphoreId, 'set_green']),
          title: props.fmt('{semaphoreGreen}')
        }, [
          h('i.fa.fa-circle.semaphore-font', {
            className: 'semaphore-green' + (props.xxxSemaphoreColor === 'green' ? '-active' : '')
          })
        ])
      ])
      : null,
    props.xxxHornId
      ? h('.functionsKey-button.isButton.isControl', {
        className: ctrlPressed + (activeKeys['*'] ? ' signalActive' : '') + (keys['*'] ? ' keyActive' : ''),
        onMouseDown: _e => actionStar(),
        onMouseUp: _e => actionStarUp(),
        title: props.fmt('{horn}')
      }, [
        h('i.fa.fa-bullhorn'),
        h('.horn-hint', {}, h('i.fas.fa-star-of-life'))
      ])
      : null
  ])
}

const renderNodeMessage = props => {
  gloDebugReact && incDebugRenderCount('NodeMessage', props)
  const d = props.data
  const options = []
  for (const idx in d.options) {
    const option = d.options[idx]
    const optionText = props.fmt(_.get(option, ['text'], option.id))
    let className = ''
    if (!props.xxxKeyboardFocus) {
      className = ''
    } else if (option.id === d.option_enter) {
      className = 'ready-for-enter'
    } else if (option.id === d.option_esc) {
      className = 'ready-for-esc'
    // TODO: find more elegant way to not offer keyboard shortcut for two-digit options
    } else if (option.id.indexOf('num_hack_') === 0 && option.text < 10) {
      className = 'ready-for-num'
    }
    if (option.id === d.answer) {
      className += ' selectedAnswer'
    }
    options.push(h('.message-button', {
      key: idx,
      className,
      onClick: _e => callApi([d.node_id, 'set_message_answer', d.k, option.id])
    }, optionText))
  }
  const type = 'message ' + d.level + (d.active === 0 ? ' inactive' : '')
  const textVals = d.text_vals || {}
  const text = props.fmt(d.text, textVals)
  if (props.xxxKeyboardFocus && !props.xxxEnterOrEscOrNumUsed) {
    if (d.option_enter && props.xxxKeys.ENTER) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, d.option_enter])
    } else if (d.option_esc && props.xxxKeys.ESC) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, d.option_esc])
    } else if (props.xxxKeys.ONE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_1'])
    } else if (props.xxxKeys.TWO) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_2'])
    } else if (props.xxxKeys.THREE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_3'])
    } else if (props.xxxKeys.FOUR) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_4'])
    } else if (props.xxxKeys.FIVE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_5'])
    } else if (props.xxxKeys.SIX) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_6'])
    } else if (props.xxxKeys.SEVEN) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_7'])
    } else if (props.xxxKeys.EIGHT) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_8'])
    } else if (props.xxxKeys.NINE) {
      props.setEnterOrEscUsed()
      callApi([d.node_id, 'set_message_answer', d.k, 'num_hack_9'])
    }
  }

  // the "key" stuff below is there only to silence react warning - investigate how to solve this better
  return h('.messageContent', {},
    h('', { className: type }, [
      h('.message-text', {}, text),
      h('.message-buttons', {}, options)
    ])
  )
}

const renderNodeLeftMenuCompressor = props => {
  gloDebugReact && incDebugRenderCount('NodeCompressor', props)
  const dfs = props.data
  const id = dfs.id
  const setRun = dfs.set_run
  const startStopAction = setRun ? 'stop' : 'start'
  const startStopLabel = props.fmt(setRun ? '{Stop}' : '{Start}')
  const startStopColor = setRun ? ' red' : ' green'
  return h('.leftMenuItem', {}, [
    h('.left-menu-title', {}, props.fmt('{' + id + '}')),
    h('.left-menu-button' + startStopColor, {
      onMouseDown: _e => callApi([id, startStopAction])
    }, startStopLabel)
  ])
}

const renderNodeLeftMenuScalesTare = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuScalesTare', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi([id, 'tare'])
    }, props.fmt('{' + id + '}')))
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{ScalesTare}')) : null,
    buttons
  ])
}

const renderNodeLeftMenuFlowmetersReset = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuFlowmetersReset', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    buttons.push(h('.left-menu-button', {
      key: id,
      onMouseDown: _e => callApi([id, 'reset'])
    }, props.fmt('{' + id + '}')))
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{FlowmetersReset}')) : null,
    buttons
  ])
}

// TODO: this handles multiple mixers but is actually kind of visually hard-coded for single mixer - find some elegant way to distinguish multiple mixers but make sure it still looks good for single mixer scenarios (which is far more common)
const renderNodeLeftMenuMixersWashing = props => {
  gloDebugReact && incDebugRenderCount('NodeLeftMenuMixersWashing', props)
  const d = props.data
  const buttons = []
  for (const k in d) {
    const id = d[k]
    const node = props.nodes[id]
    if (node && node.has_washing_continuous) {
      buttons.push(h('.left-menu-button', {
        key: id,
        onMouseDown: _e => callApi(['control', 'mixer_washing_start', id])
      }, props.fmt('{Start}')))
    }
    if (node && node.has_washing_short) {
      buttons.push(h('.left-menu-button', {
        key: id + '_short',
        onMouseDown: _e => callApi(['control', 'mixer_washing_start_short', id])
      }, props.fmt('{MixerWashingShort}')))
    }
    if (node && node.has_washing_long) {
      buttons.push(h('.left-menu-button', {
        key: id + '_long',
        onMouseDown: _e => callApi(['control', 'mixer_washing_start_long', id])
      }, props.fmt('{MixerWashingLong}')))
    }
  }
  return h('.leftMenuItem', {}, [
    buttons.length > 0 ? h('.left-menu-title', {}, props.fmt('{MixerWashing}')) : null,
    buttons
  ])
}

/* const renderNodeModal = props => {
  gloDebugReact && incDebugRenderCount('NodeModal', props)
  const orderId = props.xxxOrderId;
  return h(".modal-content", {}, [
    h(".modal", {}, [
      h("iframe.iframeOrder#iframeOrder", {src: "../manager/order_dialog/"+orderId, frameborder: 0, width: "100%", height: "100%"}),
    ]),
  ])
} */

// TODO: currently unused - decouple ctrl/f10 logic from function below
/* function wrap_need_ctrl(func) {
  if (!keys['CTRL'] && !gloStateFrozen) {
    return () => {};
  }
  return func;
} */

// TODO: what an ugly name!
function sendTitle (msg) {
  document.title = msg
  document.title = 'control'
}

// vh is capped by .modalBody's max-height (85vh in style.css); a larger value
// just leaves the body scrolling inside a taller frame.
const MODAL_SIZES = {
  dispatch: { vw: 90, vh: 85 },
  param: { vw: 40, vh: 55 },
  paramQuick: { vw: 20, vh: 55 },
  place: { vw: 35, vh: 55 },
  auth: { vw: 80, vh: 85 },
  order: { vw: 86, vh: 90 }
}

function loadWindowPositions () {
  try {
    return JSON.parse(window.localStorage.getItem('windowPositions')) || {}
  } catch (_e) {
    return {}
  }
}

function saveWindowPosition (name, x, y, w, h) {
  const positions = loadWindowPositions()
  positions[name] = { x, y, w, h }
  window.localStorage.setItem('windowPositions', JSON.stringify(positions))
}

function getSavedWindowPosition (name) {
  return loadWindowPositions()[name] || null
}

// keyed by browser window name, not by position key: order popups share one
// position key but are separate windows, and one must not evict the other
const _openPopups = {}

function _trackPopupPosition (winName, posKey, win) {
  if (_openPopups[winName]) {
    clearInterval(_openPopups[winName].intervalId)
  }

  let lastX, lastY, lastW, lastH
  let lastSkip = null

  console.log(`trackPopup: "${winName}" starting position tracking`)
  const intervalId = setInterval(() => {
    try {
      if (win.closed) {
        console.log(`trackPopup: "${winName}" window closed, stopping tracking`)
        clearInterval(intervalId)
        delete _openPopups[winName]
        return
      }
      const x = win.screenX; const y = win.screenY; const w = win.outerWidth; const h = win.outerHeight
      // skip minimized (zero/tiny dims or offscreen coords) and maximized (fills screen)
      let skip = null
      if (w < 100 || h < 100 || x < -9999 || y < -9999) {
        skip = `skipping (minimized?) left=${x} top=${y} ${w}x${h}`
      } else if (w >= window.screen.availWidth - 20 && h >= window.screen.availHeight - 20) {
        skip = `skipping (maximized?) ${w}x${h}`
      }
      if (skip) {
        // log on state change only, otherwise a minimized popup spams once a second
        if (skip !== lastSkip) {
          console.log(`trackPopup: "${winName}" ${skip}`)
        }
      } else if (x !== lastX || y !== lastY || w !== lastW || h !== lastH) {
        saveWindowPosition(posKey, x, y, w, h)
        console.log(`trackPopup: "${winName}" saved left=${x} top=${y} ${w}x${h}`)
        lastX = x; lastY = y; lastW = w; lastH = h
      }
      lastSkip = skip
    } catch (_e) {
      console.log(`trackPopup: "${winName}" error, stopping tracking`)
      clearInterval(intervalId)
      delete _openPopups[winName]
    }
  }, 1000)

  _openPopups[winName] = { win, intervalId }
}

// Open a popup window at the given viewport-proportional size.
// vw/vh are percentages of the current viewport width/height (same units as MODAL_SIZES).
// posKey overrides the localStorage key (defaults to title).
function openModalWindow (url, title, vw, vh, posKey, closeOnEscape = true) {
  const name = posKey || title

  // Already open: raise it and stop. Calling window.open with a url would
  // renavigate the existing window and throw away whatever the operator had
  // typed or scrolled to inside it.
  // ponytail: the registry lives only as long as this page, so reloading the
  // parent loses track of still-open popups and the next open renavigates one
  // of them once. Persist window names if that turns out to bite.
  const existing = _openPopups[title]
  if (existing && !existing.win.closed) {
    console.log(`openModalWindow: "${title}" already open, trying to raise`)
    // TODO: neither of the next two lines raises anything under control_gui4
    // and this needs fixing there. Tested and all no-ops: focus() from the
    // opener, focus() called by the popup on itself, and reopening by name.
    // moveBy() on the same handle does work, so the window reference is live -
    // Electron simply does not wire renderer focus requests through to child
    // BrowserWindows. The fix belongs in control_gui4's main process, which
    // has a real BrowserWindow.focus() and can restore() a minimized window
    // too: track children by frameName in did-create-window, then raise on an
    // ipcMain message. That needs a renderer-to-main channel, so either a
    // preload with contextBridge or contextIsolation: false.
    // Kept because it does work in a plain browser, and because the branch
    // still earns its place regardless: returning early is what stops
    // window.open from renavigating the popup and discarding its inner state.
    // An empty url looks the window up by name without navigating it; features
    // are ignored for an existing window, hence the two-argument call.
    existing.win.focus()
    window.open('', title)
    return existing.win
  }

  const saved = getSavedWindowPosition(name)

  const width = saved ? saved.w : Math.round(window.innerWidth * vw / 100)
  const height = saved ? saved.h : Math.round(window.innerHeight * vh / 100)

  let features = `width=${width},height=${height},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`
  if (saved) {
    features += `,left=${saved.x},top=${saved.y}`
  }

  if (saved) {
    console.log(`openModalWindow: "${name}" restored position left=${saved.x} top=${saved.y} ${width}x${height}`)
  } else {
    console.log(`openModalWindow: "${name}" default size ${width}x${height}`)
  }

  const win = window.open(url, title, features)
  if (win) {
    // register before load so the reuse check above works even if load never fires
    if (_openPopups[title]) {
      clearInterval(_openPopups[title].intervalId)
    }
    _openPopups[title] = { win, intervalId: null }
    win.focus()
    // Forward key events from popup to main window (same as iframe behavior)
    win.addEventListener('load', () => {
      try {
        win.document.body.addEventListener('keydown', e => {
          // Escape closes the popup and must not be forwarded: the parent's
          // modal guards don't apply in popup mode, so it would fall through to
          // keys.ESC = true, and the matching keyup dies with the popup - leaving
          // ESC stuck down and auto-answering any message with an esc option.
          if (closeOnEscape && (e.key === 'Escape' || e.keyCode === 27)) {
            win.close()
            return
          }
          cloneAndDispatchEvent(e)
        })
        win.document.body.addEventListener('keyup', cloneAndDispatchEvent)
        console.log(`openModalWindow: "${name}" key event forwarding attached`)
      } catch (e) {
        console.log(`openModalWindow: "${name}" failed to attach key forwarding`, e)
      }

      _trackPopupPosition(title, name, win)
    })
  }
  return win
}

// Disable right click on page
document.addEventListener('contextmenu', event => event.preventDefault())

const cloneAndDispatchEvent = e => {
  const x = new e.constructor(e.type, e)
  document.dispatchEvent(x)
}

const actionF2 = () => {
  callApi(['Mixer1', 'lock_toggle']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F2', 1])
}

const actionF2up = () => {
  callApi(['control', 'function_key', 'F2', 0])
}

const actionF3 = () => {
  callApi(['control', 'finish_countdown'])
  callApi(['control', 'function_key', 'F3', 1])
}

const actionF3up = () => {
  callApi(['control', 'function_key', 'F3', 0])
}

const actionF4 = () => {
  callApi(['Mixer1', 'set_direction_manual_smart', -1]) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F4', 1])
}

const actionF4up = () => {
  callApi(['Mixer1', 'unset_direction_manual_smart']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F4', 0])
}

const actionF5 = () => {
  callApi(['Mixer1', 'set_direction_manual_smart', 1]) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F5', 1])
}

const actionF5up = () => {
  callApi(['Mixer1', 'unset_direction_manual_smart']) // TODO: hard-coded shit
  callApi(['control', 'function_key', 'F5', 0])
}

const actionF6 = () => {
  callApi(['control', 'set_override', '_close_aggregate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F6', 1])
}

const actionF6up = () => {
  callApi(['control', 'set_override', '_close_aggregate_silos_on_open'])
  callApi(['control', 'function_key', 'F6', 0])
}

const actionF7 = () => {
  callApi(['control', 'set_override', '_vibrate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F7', 1])
}

const actionF7up = () => {
  callApi(['control', 'set_override', '_vibrate_silos_on_open'])
  callApi(['control', 'function_key', 'F7', 0])
}

const actionF8 = () => {
  callApi(['control', 'set_override', '_aerate_silos_on_open', 1])
  callApi(['control', 'function_key', 'F8', 1])
}

const actionF8up = () => {
  callApi(['control', 'set_override', '_aerate_silos_on_open'])
  callApi(['control', 'function_key', 'F8', 0])
}

const actionF9 = () => {
  callApi(['control', 'function_key', 'F9', 1])
}

const actionF9up = () => {
  callApi(['control', 'function_key', 'F9', 0])
}

const actionF10 = () => {
  callApi(['control', 'freeze_toggle'])
  callApi(['control', 'function_key', 'F10', 1])
}

const actionF10up = () => {
  callApi(['control', 'function_key', 'F10', 0])
}

const actionF11 = () => {
  callApi(['control', 'water_stop'])
  callApi(['control', 'function_key', 'F11', 1])
}

const actionF11up = () => {
  callApi(['control', 'function_key', 'F11', 0])
}

const actionF12 = () => {
  callApi(['WaterAdditionalSilo', 'set_open_invert'])
  callApi(['control', 'function_key', 'F12', 1])
}

const actionF12up = () => {
  callApi(['WaterAdditionalSilo', 'unset_open_invert'])
  callApi(['control', 'function_key', 'F12', 0])
}

const actionStar = () => {
  callApi(['TruckChangeHorn', 'set_override', 'set_enable', 1]) // TODO: hard-coded shit
}

const actionStarUp = () => {
  callApi(['TruckChangeHorn', 'set_override', 'set_enable']) // TODO: hard-coded shit
}

const actionSemaphoreToggle = () => {
  callApi(['Semaphore1', 'toggle']) // TODO: hard-coded shit
}

const actionM1 = () => {
  callApi(['Mixer1', 'manual_start']) // TODO: hard-coded shit
}

const actionM0 = () => {
  callApi(['Mixer1', 'manual_stop']) // TODO: hard-coded shit
}

const actionVB = () => {
  callApi(['Chute1', 'set_override', 'set_vibrate', 1]) // TODO: hard-coded shit
}

const actionVBup = () => {
  callApi(['Chute1', 'set_override', 'set_vibrate']) // TODO: hard-coded shit
}

const actionStopAfterBatch = () => {
  callApi(['control', 'stop_after_batch_toggle'])
}

const actionRecycledWaterDisabled = () => {
  callApi(['control', 'recycled_water_disabled_toggle'])
}

const actionOrderOnHold = () => {
  callApi(['control', 'order_on_hold_toggle'])
}

// TODO: make this a full-fledged component?
const semiNodeButtonClose = [h('i.fa.fa-arrow-right'), 'I', h('i.fa.fa-arrow-left')]
// const semiNodeButtonClose = h('.sensorLed', {}, [h('i.fa.fa-arrow-right'), 'I', h('i.fa.fa-arrow-left')])

// TODO: make this a full-fledged component?
const semiNodeButtonOpen = [h('i.fa.fa-arrow-left'), 'I', h('i.fa.fa-arrow-right')]
// const semiNodeButtonOpen = h('.sensorLed', {}, [h('i.fa.fa-arrow-left'), 'I', h('i.fa.fa-arrow-right')])

class NodeTopComponent extends React.Component {
  state = {}

  componentDidMount () {
    this.setState({
      viewportWidth: window.innerWidth,
      viewportHeight: window.innerHeight,
      theme: window.localStorage.getItem('theme')
    })
    window.addEventListener('resize', () => {
      console.log('resize')
      this.setState({
        viewportWidth: window.innerWidth,
        viewportHeight: window.innerHeight
      })
    })
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeTopComponent', props)
    const wideMode = (this.state.viewportWidth / this.state.viewportHeight) > (16 / 8) // this should actually be 16/9 but we need to add some reserve to account for possible window "widening" by taskbar, missing bottom pixel, etc.
    const aggregatesToNextLineCutoff = 15

    return rce(NodeLanguage, {
      xxxMenuIsOpened: this.state.menuIsOpened,
      xxxModalOrderId: this.state.modalOrderId,
      xxxIsHelp: this.state.isHelp,
      xxxIsExit: this.state.isExit,
      xxxIsDispatch: this.state.isDispatch,
      xxxIsParam: this.state.isParam,
      xxxIsParamQuick: this.state.isParamQuick,
      xxxIsPlace: this.state.isPlace,
      xxxIsAuth: this.state.isAuth,
      xxxIsLanguage: this.state.isLanguage,
      xxxWideMode: wideMode,
      xxxAggregatesToNextLineCutoff: aggregatesToNextLineCutoff,
      xxxTheme: this.state.theme,
      openExitDialog: () => this.setState({ isExit: true }),
      closeExitDialog: () => this.setState({ isExit: false }),
      openHelpWindow: () => this.setState({ isHelp: true }),
      closeHelpWindow: () => this.setState({ isHelp: false }),
      toggleHelpWindow: () => this.setState({ isHelp: !this.state.isHelp }),
      menuToggle: () => this.setState({ menuIsOpened: !this.state.menuIsOpened }),
      openLanguageDialog: () => this.setState({ isLanguage: true }),
      closeLanguageDialog: () => this.setState({ isLanguage: false }),
      dispatchOpenDialog: (lang) => {
        if (USE_IFRAME) {
          this.setState({ isDispatch: true })
        } else {
          openModalWindow('../dispatcher/?lang=' + lang, 'dispatcher', MODAL_SIZES.dispatch.vw, MODAL_SIZES.dispatch.vh, undefined, false)
        }
      },
      dispatchCloseDialog: () => this.setState({ isDispatch: false }),
      paramOpenDialog: (lang) => {
        if (USE_IFRAME) {
          this.setState({ isParam: true })
        } else {
          openModalWindow('../parameters/?lang=' + lang, 'parameters', MODAL_SIZES.param.vw, MODAL_SIZES.param.vh)
        }
      },
      paramCloseDialog: () => this.setState({ isParam: false }),
      paramQuickOpenDialog: (lang) => {
        if (USE_IFRAME) {
          this.setState({ isParamQuick: true })
        } else {
          openModalWindow('../parameters/?quick=1&lang=' + lang, 'parameters-quick', MODAL_SIZES.paramQuick.vw, MODAL_SIZES.paramQuick.vh)
        }
      },
      paramQuickCloseDialog: () => this.setState({ isParamQuick: false }),
      placeOpenDialog: (lang) => {
        if (USE_IFRAME) {
          this.setState({ isPlace: true })
        } else {
          openModalWindow('../place/?lang=' + lang, 'placement', MODAL_SIZES.place.vw, MODAL_SIZES.place.vh)
        }
      },
      placeCloseDialog: () => this.setState({ isPlace: false }),
      authOpenDialog: (lang) => {
        if (USE_IFRAME) {
          this.setState({ isAuth: true })
        } else {
          openModalWindow('../auth/list?lang=' + lang, 'authorization', MODAL_SIZES.auth.vw, MODAL_SIZES.auth.vh)
        }
      },
      authCloseDialog: () => this.setState({ isAuth: false }),
      managerOpenOrderDialog: (orderId, lang) => {
        if (USE_IFRAME) {
          this.setState({ modalOrderId: orderId })
        } else {
          openModalWindow('../manager/order_dialog/' + orderId + '?lang=' + lang, 'order-' + orderId, MODAL_SIZES.order.vw, MODAL_SIZES.order.vh, 'order')
        }
      },
      managerCloseOrderDialog: () => this.setState({ modalOrderId: null }),
      themeToggle: () => {
        const newTheme = this.state.theme === 'day' ? 'night' : 'day'
        window.localStorage.setItem('theme', newTheme)
        this.setState({ theme: newTheme })
      }
    })
  }
}

class NodeKeyDetector extends React.PureComponent {
  state = {}

  componentDidMount () {
    // console.log('NodeKeyDetector componentDidMount');
    window.onkeydown = this.onkeydown.bind(this)
    window.onkeyup = this.onkeyup.bind(this)
  }

  onkeydown (e) {
    const props = this.props
    let keys = this.state
    let prevent = true
    if (e.repeat) {
      // nothing
    } else if (e.ctrlKey && e.keyCode === 'D'.charCodeAt(0)) {
      gloDebugReact = !gloDebugReact
    } else if (e.ctrlKey && e.keyCode === 'F'.charCodeAt(0)) {
      if (document.fullscreenElement) {
        document.exitFullscreen()
      } else {
        document.documentElement.requestFullscreen()
      }
    } else if (e.ctrlKey && e.keyCode === 'R'.charCodeAt(0)) {
      sendTitle('control:reload')
    } else if (e.keyCode === 13 || e.key === 'Enter') {
      // TODO: ugly
      if (props.xxxIsHelp) {
        // return;
      } else if (props.xxxIsExit) {
        // return;
      } else if (props.xxxIsParam) {
        // return;
      } else if (props.xxxIsParamQuick) {
        // return;
      } else if (props.xxxIsPlace) {
        // return;
      } else if (props.xxxIsAuth) {
        // return;
      } else if (props.xxxModalOrderId) {
        // return;
      } else if (props.xxxIsLanguage) {
        // return;
      } else {
        keys = { ...keys, ENTER: true }
      }
    } else if (e.keyCode === 27 || e.key === 'Escape') {
      if (props.xxxIsHelp) {
        props.closeHelpWindow()
        return
      } else if (props.xxxIsExit) {
        props.closeExitDialog()
        return
      } else if (props.xxxIsParam) {
        props.paramCloseDialog()
        return
      } else if (props.xxxIsParamQuick) {
        props.paramQuickCloseDialog()
        return
      } else if (props.xxxIsPlace) {
        props.placeCloseDialog()
        return
      } else if (props.xxxIsAuth) {
        props.authCloseDialog()
        return
      } else if (props.xxxModalOrderId) {
        props.managerCloseOrderDialog()
        return
      } else if (props.xxxIsLanguage) {
        props.closeLanguageDialog()
        return
      }
      keys = { ...keys, ESC: true }
    } else if (e.keyCode === 17 || e.key === 'Control') {
      keys = { ...keys, CTRL: true }
    } else if (e.keyCode === 18 || e.key === 'Alt') {
      keys = { ...keys, ALT: true }
    } else if (e.keyCode === 112 || e.key === 'F1') {
      keys = { ...keys, F1: true }
      props.toggleHelpWindow()
    } else if (e.keyCode === 113 || e.key === 'F2') {
      keys = { ...keys, F2: true }
      actionF2()
    } else if (e.keyCode === 114 || e.key === 'F3') {
      keys = { ...keys, F3: true }
      actionF3()
    } else if (e.keyCode === 115 || e.key === 'F4') {
      keys = { ...keys, F4: true }
      actionF4()
    } else if (e.keyCode === 116 || e.key === 'F5') {
      keys = { ...keys, F5: true }
      actionF5()
    } else if (e.keyCode === 117 || e.key === 'F6') {
      keys = { ...keys, F6: true }
      actionF6()
    } else if (e.keyCode === 118 || e.key === 'F7') {
      keys = { ...keys, F7: true }
      actionF7()
    } else if (e.keyCode === 119 || e.key === 'F8') {
      keys = { ...keys, F8: true }
      actionF8()
    } else if (e.keyCode === 120 || e.key === 'F9') {
      keys = { ...keys, F9: true }
      actionF9()
    } else if (e.keyCode === 121 || e.key === 'F10') {
      keys = { ...keys, F10: true }
      actionF10()
    } else if (e.keyCode === 122 || e.key === 'F11') {
      keys = { ...keys, F11: true }
      actionF11()
    } else if (e.keyCode === 123 || e.key === 'F12') {
      keys = { ...keys, F12: true }
      actionF12()
    } else if (e.keyCode === 106 || e.key === '*') {
      keys = { ...keys, '*': true }
      actionStar()
    } else if (e.keyCode === 77 || e.key === 'm') {
      keys = { ...keys, M: true }
    } else if ((e.keyCode === 96 || e.key === 'Numpad0' || e.keyCode === 48 || e.key === 'Digit0') && keys.M) {
      actionM0()
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1') && keys.M) {
      actionM1()
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1')) {
      keys = { ...keys, ONE: true }
    } else if ((e.keyCode === 98 || e.key === 'Numpad2' || e.keyCode === 50 || e.key === 'Digit2')) {
      keys = { ...keys, TWO: true }
    } else if ((e.keyCode === 99 || e.key === 'Numpad3' || e.keyCode === 51 || e.key === 'Digit3')) {
      keys = { ...keys, THREE: true }
    } else if ((e.keyCode === 100 || e.key === 'Numpad4' || e.keyCode === 52 || e.key === 'Digit4')) {
      keys = { ...keys, FOUR: true }
    } else if ((e.keyCode === 101 || e.key === 'Numpad5' || e.keyCode === 53 || e.key === 'Digit5')) {
      keys = { ...keys, FIVE: true }
    } else if ((e.keyCode === 102 || e.key === 'Numpad6' || e.keyCode === 54 || e.key === 'Digit6')) {
      keys = { ...keys, SIX: true }
    } else if ((e.keyCode === 103 || e.key === 'Numpad7' || e.keyCode === 55 || e.key === 'Digit7')) {
      keys = { ...keys, SEVEN: true }
    } else if ((e.keyCode === 104 || e.key === 'Numpad8' || e.keyCode === 56 || e.key === 'Digit8')) {
      keys = { ...keys, EIGHT: true }
    } else if ((e.keyCode === 105 || e.key === 'Numpad9' || e.keyCode === 57 || e.key === 'Digit9')) {
      keys = { ...keys, NINE: true }
    } else if ((e.keyCode === 37 || e.key === 'ArrowLeft') && keys.M) {
      keys = { ...keys, ArrowLeft: true }
      actionF4()
    } else if ((e.keyCode === 39 || e.key === 'ArrowRight') && keys.M) {
      keys = { ...keys, ArrowRight: true }
      actionF5()
    } else if ((e.keyCode === 86 || e.key === 'v') && keys.B) {
      keys = { ...keys, V: true }
      actionVB()
    } else if (e.keyCode === 86 || e.key === 'v') {
      keys = { ...keys, V: true }
    } else if ((e.keyCode === 66 || e.key === 'b') && keys.V) {
      keys = { ...keys, B: true }
      actionVB()
    } else if (e.keyCode === 66 || e.key === 'b') {
      keys = { ...keys, B: true }
      // stop after batch
    } else if ((e.keyCode === 83 || e.key === 's') && keys.Z) {
      keys = { ...keys, S: true }
      actionStopAfterBatch()
    } else if (e.keyCode === 83 || e.key === 's') {
      keys = { ...keys, S: true }
    } else if ((e.keyCode === 90 || e.key === 'z') && keys.S) {
      keys = { ...keys, Z: true }
      actionStopAfterBatch()
    } else if (e.keyCode === 90 || e.key === 'z') {
      keys = { ...keys, Z: true }
      // recycled water disabled
    } else if (e.keyCode === 111 || e.key === '/') {
      keys = { ...keys, '/': true }
      actionRecycledWaterDisabled()
      // OP objednavka prerusena "order-on-hold"
    } else if ((e.keyCode === 79 || e.key === 'o') && keys.P) {
      keys = { ...keys, O: true }
      actionOrderOnHold()
    } else if ((e.keyCode === 80 || e.key === 'p') && keys.O) {
      keys = { ...keys, P: true }
      actionOrderOnHold()
    } else if ((e.keyCode === 79 || e.key === 'o') && keys.K) {
      keys = { ...keys, O: true }
      actionSemaphoreToggle()
    } else if ((e.keyCode === 75 || e.key === 'k') && keys.O) {
      keys = { ...keys, K: true }
      actionSemaphoreToggle()
    } else if (e.keyCode === 79 || e.key === 'o') {
      keys = { ...keys, O: true }
    } else if (e.keyCode === 80 || e.key === 'p') {
      keys = { ...keys, P: true }
    } else if (e.keyCode === 75 || e.key === 'k') {
      keys = { ...keys, K: true }
    } else {
      prevent = false
    }
    if (prevent) {
      e.preventDefault()
    }
    this.setState(keys)
  }

  onkeyup (e) {
    let keys = this.state
    let prevent = true
    if (e.repeat) {
      // nothing
    } else if (e.keyCode === 13 || e.key === 'Enter') {
      // TODO: ignore when any modal is open?
      keys = { ...keys, ENTER: false, enterOrEscOrNumUsed: false }
    } else if (e.keyCode === 27 || e.key === 'Escape') {
      // TODO: ignore when any modal is open?
      keys = { ...keys, ESC: false, enterOrEscOrNumUsed: false }
    } else if (e.keyCode === 17 || e.key === 'Control') {
      keys = { ...keys, CTRL: false }
    } else if (e.keyCode === 18 || e.key === 'Alt') {
      keys = { ...keys, ALT: false }
    } else if (e.keyCode === 112 || e.key === 'F1') {
      keys = { ...keys, F1: false }
    } else if (e.keyCode === 113 || e.key === 'F2') {
      keys = { ...keys, F2: false }
      actionF2up()
    } else if (e.keyCode === 114 || e.key === 'F3') {
      keys = { ...keys, F3: false }
      actionF3up()
    } else if (e.keyCode === 115 || e.key === 'F4') {
      keys = { ...keys, F4: false }
      actionF4up()
    } else if (e.keyCode === 116 || e.key === 'F5') {
      keys = { ...keys, F5: false }
      actionF5up()
    } else if (e.keyCode === 117 || e.key === 'F6') {
      keys = { ...keys, F6: false }
      actionF6up()
    } else if (e.keyCode === 118 || e.key === 'F7') {
      keys = { ...keys, F7: false }
      actionF7up()
    } else if (e.keyCode === 119 || e.key === 'F8') {
      keys = { ...keys, F8: false }
      actionF8up()
    } else if (e.keyCode === 120 || e.key === 'F9') {
      keys = { ...keys, F9: false }
      actionF9up()
    } else if (e.keyCode === 121 || e.key === 'F10') {
      keys = { ...keys, F10: false }
      actionF10up()
    } else if (e.keyCode === 122 || e.key === 'F11') {
      keys = { ...keys, F11: false }
      actionF11up()
    } else if (e.keyCode === 123 || e.key === 'F12') {
      keys = { ...keys, F12: false }
      actionF12up()
    } else if (e.keyCode === 106 || e.key === '*') {
      keys = { ...keys, '*': false }
      actionStarUp()
    } else if ((e.keyCode === 37 || e.key === 'ArrowLeft') && keys.M) {
      keys = { ...keys, ArrowLeft: false }
      actionF4up()
    } else if ((e.keyCode === 77 || e.key === 'm') && keys.ArrowLeft) {
      keys = { ...keys, M: false }
      actionF4up()
    } else if ((e.keyCode === 39 || e.key === 'ArrowRight') && keys.M) {
      keys = { ...keys, ArrowRight: false }
      actionF5up()
    } else if ((e.keyCode === 77 || e.key === 'm') && keys.ArrowRight) {
      keys = { ...keys, M: false }
      actionF5up()
    } else if (e.keyCode === 77 || e.key === 'm') {
      keys = { ...keys, M: false }
    } else if (e.keyCode === 39 || e.key === 'ArrowRight') {
      keys = { ...keys, ArrowRight: false }
    } else if ((e.keyCode === 86 || e.key === 'v') && keys.B) {
      keys = { ...keys, V: false }
      actionVBup()
    } else if ((e.keyCode === 66 || e.key === 'b') && keys.V) {
      keys = { ...keys, B: false }
      actionVBup()
    } else if (e.keyCode === 86 || e.key === 'v') {
      keys = { ...keys, V: false }
    } else if (e.keyCode === 66 || e.key === 'b') {
      keys = { ...keys, B: false }
      // stop after batch
    } else if (e.keyCode === 83 || e.key === 's') {
      keys = { ...keys, S: false }
    } else if (e.keyCode === 90 || e.key === 'z') {
      keys = { ...keys, Z: false }
    } else if (e.keyCode === 75 || e.key === 'k') {
      keys = { ...keys, K: false }
      // OP objednavka prerusena "order-on-hold"
    } else if (e.keyCode === 79 || e.key === 'o') {
      keys = { ...keys, O: false }
    } else if (e.keyCode === 80 || e.key === 'p') {
      keys = { ...keys, P: false }
    } else if ((e.keyCode === 97 || e.key === 'Numpad1' || e.keyCode === 49 || e.key === 'Digit1')) {
      keys = { ...keys, ONE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 98 || e.key === 'Numpad2' || e.keyCode === 50 || e.key === 'Digit2')) {
      keys = { ...keys, TWO: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 99 || e.key === 'Numpad3' || e.keyCode === 51 || e.key === 'Digit3')) {
      keys = { ...keys, THREE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 100 || e.key === 'Numpad4' || e.keyCode === 52 || e.key === 'Digit4')) {
      keys = { ...keys, FOUR: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 101 || e.key === 'Numpad5' || e.keyCode === 53 || e.key === 'Digit5')) {
      keys = { ...keys, FIVE: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 102 || e.key === 'Numpad6' || e.keyCode === 54 || e.key === 'Digit6')) {
      keys = { ...keys, SIX: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 103 || e.key === 'Numpad7' || e.keyCode === 55 || e.key === 'Digit7')) {
      keys = { ...keys, SEVEN: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 104 || e.key === 'Numpad8' || e.keyCode === 56 || e.key === 'Digit8')) {
      keys = { ...keys, EIGHT: false, enterOrEscOrNumUsed: false }
    } else if ((e.keyCode === 105 || e.key === 'Numpad9' || e.keyCode === 57 || e.key === 'Digit9')) {
      keys = { ...keys, NINE: false, enterOrEscOrNumUsed: false }
    } else {
      prevent = false
    }
    if (prevent) {
      e.preventDefault()
    }
    this.setState(keys)
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeKeyDetector', props)
    return rce(NodeEventSource, {
      ...props,
      xxxKeys: this.state,
      xxxEnterOrEscOrNumUsed: this.state.enterOrEscOrNumUsed,
      setEnterOrEscUsed: () => this.setState({ enterOrEscOrNumUsed: true })
    })
  }
}

class NodeLanguage extends React.Component {
  state = { d: {}, dCur: {}, cur: 'cs', avail: ['cs', 'en', 'de'] } // TODO: hard-coded shit

  init () {
    this.setState({ cur: window.localStorage.getItem('lang') || this.state.cur })
    fetch('./captions').then(res => res.json()).then(data => {
      console.log('lang:', data)
      data = byKeyToByLang(data)
      console.log('lang by lang:', data)
      // this.setState({ d: data })
      // this.setLang(this.state.cur)
      // above is the original code. i had to change to the code below after upgrading to react 18
      // there's supposedly some shit with caching setState calls within promises or something...
      this.setState({ d: data, dCur: data[this.state.cur] || {} })
    })
  }

  setLang (x) {
    this.setState({ cur: x, dCur: this.state.d[x] || {} })
    window.localStorage.setItem('lang', x)
  }

  componentDidMount () {
    this.init()
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeLanguage', props)
    return rce(NodeKeyDetector, {
      ...props,
      fmt: (x, moreVals = {}) => _fmt(x, { ...this.state.dCur, ...moreVals }),
      lang_set: this.setLang.bind(this),
      lang_avail: this.state.avail,
      lang_cur: this.state.cur
    })
  }
}

class NodeEventSource extends React.Component {
  state = { isConnected: false, doCrash: false }
  dateLastData = 0 // watchdog only, nothing renders it - keep it out of state so frames don't re-render
  // TODO: evtSource isn't rendered either, so it could live on `this` the same way - that would also
  // drop the two setState calls in init()/reconnect() (isConnected still belongs in state, render uses it)

  init () {
    const evtSource = new window.EventSource('stream')
    evtSource.onmessage = this.onmessage.bind(this)
    evtSource.onerror = this.onerror.bind(this)
    this.dateLastData = Date.now()
    this.setState({ evtSource })
  }

  onmessage (e) {
    this.dateLastData = Date.now()
    const data = JSON.parse(e.data)
    if (_.isEmpty(data) && this.state.isConnected) return // server keepalive - nothing to merge, nothing to re-render
    // const data_new = data
    const newData = this.state.data ? { ...this.state.data, ...data } : data
    // const newData = this.state.data ? Object.assign(this.state.data, data) : data  // this is supposedly faster but it changes data in place which seems to fuck up react's change detection
    // const newData = this.state.data ? _.merge({}, this.state.data, data) : data;
    const precalc = this.state.precalc || precalcStuff(newData, this.props.xxxAggregatesToNextLineCutoff)
    this.setState({
      data: newData,
      precalc,
      isConnected: true
    })
  }

  onerror (_e) {
    console.error('sse error')
    _.updateWith(gloDebugStuff, ['sse_errors'], x => x ? x + 1 : 1)
    this.reconnect()
  }

  reconnect () {
    this.dateLastData = Date.now() // don't let the watchdog fire again while we're reconnecting
    this.state.evtSource && this.state.evtSource.close()
    this.setState({ evtSource: null, isConnected: false })
    setTimeout(this.init.bind(this), 1000)
  }

  componentDidMount () {
    this.init()
    // the stream carries deltas only, so a stalled-but-open socket looks exactly like an idle one -
    // without this watchdog everything missed while it was stalled stays wrong until it changes again
    this.watchdog = setInterval(() => {
      if (Date.now() - this.dateLastData > SSE_STALE_MS) {
        console.error('sse stale, reconnecting')
        _.updateWith(gloDebugStuff, ['sse_stalls'], x => x ? x + 1 : 1)
        this.reconnect()
      }
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.watchdog)
  }

  render () {
    if (this.state.doCrash) {
      console.error('CRASH')
      null.fuck()
    }
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeEventSource', props)
    const isConnected = this.state.isConnected
    const stateFrozen = _.get(this.state, ['data', 'control', '_is_frozen'])
    const stateIsPc = _.get(this.state, ['data', 'control', '_is_pc'])
    return rce(Main, {
      ...props,
      nodes: this.state.data,
      precalc: this.state.precalc,
      xxxStateFrozen: stateFrozen,
      xxxConnected: isConnected,
      xxxStateIsPc: stateIsPc,
      doCrash: () => { this.setState({ doCrash: true }) }
    })
  }
}

class Clock extends React.PureComponent {
  constructor () {
    super()
    this.state = { clockStr: '' }
  }

  componentDidMount () {
    this.interval = setInterval(() => {
      this.setState({ clockStr: dateTimeString() })
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.interval)
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Clock', props)
    return h('.clock', {}, this.state.clockStr)
  }
}

// TODO: unify with other modals (below)
class ExitDialog extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('ExitDialog', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{exit}')),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeExitDialog() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('', {}, props.fmt('{really_exit_control_program}'))
        ]),
        h('.modal.modalFooter', {}, [
          h('.modal.modalButton', { onClick: _e => { props.closeExitDialog() } }, props.fmt('{NO}')),
          // h('.modal.modalButton', { onClick: _e => { fetch('/loader/quit') } }, props.fmt('{YES}'))
          h('.modal.modalButton', { onClick: _e => { callApi(['control', 'loader_quit']) } }, props.fmt('{YES}'))
        ])
      ])
    ])
  }
}

// TODO: unify with other modals (below)
class LanguageDialog extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('LanguageDialog', props)
    const availableLanguages = props.lang_avail
    const currentLanguage = props.lang_cur
    const languages = availableLanguages.map(lang =>
      h('tr', { key: lang, onClick: _e => { props.lang_set(lang); props.closeLanguageDialog() } }, [
        h('td', {}, [
          h('i.far' + (lang === currentLanguage ? '.fa-dot-circle' : '.fa-circle'), { style: { paddingRight: '0.5vw' } }, [])
        ]),
        h('td', {}, [
          h('i.lang-lg.lang-lbl', { lang }, [])
        ])
      ])
    )
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, ''),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeLanguageDialog() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('table.info', {}, languages)
        ])
      ])
    ])
  }
}

// TOOD: unify with other modals (see below)
class NodeHelp extends React.Component {
  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('NodeHelp', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {}, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{F1}')),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.closeHelpWindow() } })
          ])
        ]),
        h('.modal.modalBodyMain', {}, [
          h('table.info', {}, [
            _.range(1, 13).map(i => h('tr', {}, [
              h('td', {}, '( F' + i + ' )'),
              h('td', {}, ' - ' + props.fmt('{F' + i + '}'))
            ])),
            h('tr', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr', {}, [
              h('td', {}, 'Servisní číslo:\u00a0\u00a0'),
              h('td', {}, '+420 910 125 024')
            ]),
            h('tr', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr.small', {}, h('td', { colspan: 2 }, 'Záložní kontakty')),
            h('tr.small', {}, h('td', { colspan: 2 }, '\u00a0')),
            h('tr.small', {}, [
              h('td', {}, 'Tomáš Malík:'),
              h('td', {}, '+420 770 102 184')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Vladimír Kantor:'),
              h('td', {}, '+420 724 160 710')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Štěpán Donáth:'),
              h('td', {}, '+420 724 290 240')
            ]),
            h('tr.small', {}, [
              h('td', {}, 'Pavel Podgorný:'),
              h('td', {}, '+420 606 370 344')
            ])
          ])
        ])
      ])
    ])
  }
}

class Modal extends React.Component {
  componentDidMount () {
    const el = window.document.getElementById('iframeOrder')
    el.onload = () => {
      el.contentDocument.body.addEventListener('keydown', cloneAndDispatchEvent)
      el.contentDocument.body.addEventListener('keyup', cloneAndDispatchEvent)
      el.contentDocument.getElementById('orderDialogEventContent').addEventListener('input', this.props.managerCloseOrderDialog)
    }
  }

  componentWillUnmount () {
    console.log('Modal componentWillUnmount')
    const el = window.document.getElementById('iframeOrder')
    el.contentDocument.getElementById('orderDialogEventContent').removeEventListener('input', this.props.managerCloseOrderDialog)
    el.contentDocument.dispatchEvent(new Event('dialog_closing'))
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('Modal', props)
    const orderId = props.xxxOrderId
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {
        style: { width: MODAL_SIZES.order.vw + 'vw', height: MODAL_SIZES.order.vh + 'vh' }
      }, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.fmt('{order}: ') + orderId),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: _e => { props.managerCloseOrderDialog() } })
          ])
        ]),
        h('.modal.modalBodyOrderDialog.modalBodyParam', {}, [ // TODO: param? what?
          h('iframe.iframeOrder', {
            id: 'iframeOrder',
            src: '../manager/order_dialog/' + orderId + '?lang=' + props.lang_cur,
            frameborder: 0,
            width: '100%',
            height: '100%'
          })
        ])
      ])
    ])
  }
}

// TODO: unify with Modal above
class IframeModal extends React.Component {
  constructor (props) {
    super(props)
    this.state = { innerWidth: 0, innerHeight: 0 }
  }

  componentDidMount () {
    if (this.props.noResize) {
      return
    }
    const el = window.document.getElementById('theIframe')
    el.onload = () => {
      console.log('iframe loaded')
      el.contentDocument.body.addEventListener('keydown', cloneAndDispatchEvent)
      el.contentDocument.body.addEventListener('keyup', cloneAndDispatchEvent)
      const ro = new window.ResizeObserver((_entries) => {
        const w = el.contentDocument.body.clientWidth
        const h = el.contentDocument.body.clientHeight
        console.log('iframe inner resize', w, h)
        this.setState({ innerWidth: w, innerHeight: h })
      })
      ro.observe(el.contentDocument.body)
      this.setState({ observer: ro })
    }
  }

  componentWillUnmount () {
    console.log('IframeModal componentWillUnmount')
    if (this.props.noResize) {
      return
    }
    // const el = window.document.getElementById("theIframe");
    if (this.state.observer) {
      this.state.observer.disconnect()
      this.setState({ observer: null })
    }
  }

  render () {
    const props = this.props
    gloDebugReact && incDebugRenderCount('IframeModal', props)
    return h('.modal-content', {}, [
      h('.modal.modalBasic', {
        style: {
          width: props.modalWidth + 'vw',
          ...(props.modalHeight ? { height: props.modalHeight + 'vh' } : {})
        }
      }, [
        h('.modal.modalHeader', {}, [
          h('.modal.modalTitle', {}, props.title),
          h('.modal.modalClose', {}, [
            h('i.fa.fa-window-close', { onClick: props.onClose })
          ])
        ]),
        h('.modal.modalBody', { style: { height: (this.state.innerHeight - 1) + 'px' } }, [ // -1 is there to force scrollbar
          h('iframe', {
            id: 'theIframe',
            src: props.url,
            frameborder: 0,
            width: '100%',
            height: '100%'
          })
        ])
      ])
    ])
  }
}

// start watching for crashes after 5 seconds
setTimeout(() => {
  setInterval(() => {
    // console.debug("tick")
    const el = document.getElementById('main')
    if (!el) {
      console.log('crash detected, reloading')
      window.location.reload()
    }
  }, 1000)
}, 5000)

const root = ReactDOM.createRoot(document.getElementById('content'))
root.render(rce(NodeTopComponent))
