Open joepavitt opened 6 months ago
When we lost connection between the client and server, we don't make any attempts to re-connect, nor do we inform the user of a lost connection.
Adding a scenario - when the user deploys or restarts the flows on the editor, the dashboard clients realize it and try to re-render, but the 'onSocket' listeners on the server node stop receiving custom client messages. reloading the client browser (F5) fixes the issue. The temp work around I currently implement, is upon startup, the server node waits 200 ms and then sends a special message to all clients which tells them to reload themselves.
I'm assuming this is the same issue, but thought you may wish to consider PWA when fixing.
EDIT 27/4 This appears to be covered by https://github.com/FlowFuse/node-red-dashboard/issues/747 as after further checks it only affects template nodes, and not the default palette nodes.
Saved as an App (PWA) on a android phone, the data stream is updated every 5 seconds. At 7 seconds in the video, I restart node-RED which produces a 'connection lost' message as expected. When it 'reconnects' at 12 seconds, all of the values then show zero, and remain at zero, despite node-RED updating normally (I'm also watching the dashboard on a laptop)
Dashboard v1.8.1
https://github.com/FlowFuse/node-red-dashboard/assets/973580/22ae5cad-54bc-47c0-8913-55b7c4378103
@joepavitt do you want ongoing issues over connections in here, or in individual issues? I would vote for putting everything related here as several of the issues are related. Except maybe the ui-template issue which it appears is not directly related.
I find that if, on a PC, I open the dashboard and disconnect the network and leave it for 30 mins (not sure what the min to show the problem is) then it never reconnects when I connect the network again, it just sits there with Connection Lost popup. I see the same on Android sometimes when I re-open the dashboard window after it has been in the background for some time. I suspect it is the same issue.
Each in a new issue, them link them here so I can add to the list please.
I need to double check, but I believe that's expected. @Steve-Mcl recommended I add a "give up trying to connect" timeout, and I believe I set it to 30 minutes
Actually, I chose 5 minutes:
https://github.com/FlowFuse/node-red-dashboard/blob/main/ui%2Fsrc%2Fmain.mjs#L97
See issue #810
Also #811
When it 'reconnects' at 12 seconds, all of the values then show zero, and remain at zero, despite node-RED updating normally
@Paul-Reed I am not seeing that running on Android with it saved as an app. Does it happen with text nodes? Can you build a simple flow that shows the problem so I can test it.
@colinl In fact, this only occurs when using template nodes. I'm only using template nodes for gauges etc, but I've just run a test by adding the default palette gauge, chart & text nodes instead, and they do re-connect OK.
Template Example;
[{"id":"d9f948314558a48c","type":"ui-template","z":"be4568a984cb4685","group":"48fc19d1161f11e6","page":"","ui":"","name":"Diverted","order":3,"width":"0","height":"0","head":"","format":"// See https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955\n<template>\n <div>\n <template v-if=\"type === 'round'\">\n <div ref=\"hng\" class=\"round-led-level\" :style=\"`--size:${size}; --shadow:${shadow}; --ledsize:${ledSize};`\">\n <header>\n <div class=\"round-led-level-text\">\n <span class=\"round-led-level-label\">{{label}}</span>\n </div>\n </header>\n <div class=\"round-led-level-graph\">\n <div class=\"round-led-level-stripe\" :class=\"{'led-level-flat': flat }\">\n <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"round-led-level-led\"\n :ref=\"'dot-' + index\">\n </div>\n </div>\n <div class=\"round-led-level-centered-text\">\n <span class=\"round-led-level-value\">{{formattedValue}}</span>\n <span class=\"round-led-level-unit\">{{unit}}</span>\n </div>\n <div class=\"round-led-level-limits\">\n <span>{{min}}</span>\n <span>{{max}}</span>\n </div>\n </div>\n <div>\n </template>\n <template v-if=\"type === 'linear'\">\n <div ref=\"hng\" class=\"led-level\" :style=\"`--shadow:${shadow};`\">\n <div class=\"led-level-text\">\n <span class=\"led-level-label\">{{label}}</span>\n <span class=\"led-level-value\">{{formattedValue}}<span class=\"led-level-unit\">{{unit}}</span></span>\n </div>\n <div class=\"led-level-stripe\" :class=\"{'led-level-flat': flat }\">\n <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"led-level-led\" :ref=\"'dot-' + index\">\n </div>\n </div>\n <div class=\"led-level-limits\">\n <span>{{min}}</span>\n <span>{{max}}</span>\n </div>\n <div>\n </template>\n <template v-if=\"type === 'vertical'\">\n <div ref=\"hng\" class=\"led-level-vertical\" :style=\"`--shadow:${shadow}; --size:${size};`\">\n <header>\n <div class=\"round-led-level-text\">\n <span class=\"round-led-level-label\">{{label}}</span>\n </div>\n </header>\n <div class=\"led-level-vertical-content\">\n <div class=\"led-level-stripe\" :class=\"{'led-level-flat': flat }\">\n <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"led-level-led\" :ref=\"'dot-' + index\">\n </div>\n </div>\n <div class=\"led-level-limits\">\n <span>{{max}}</span>\n <span>{{min}}</span>\n </div>\n <div class=\"round-led-level-centered-text\">\n <span class=\"round-led-level-value\">{{formattedValue}}</span>\n <span class=\"round-led-level-unit\">{{unit}}</span>\n </div>\n </div>\n </div>\n\n </template>\n <template v-if=\"type === 'artless'\">\n <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n <div v-if=\"icon\" class=\"ag-icon\">\n <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n </div>\n <div class=\"ag-content\">\n <div class=\"ag-text\">\n <span class=\"ag-label\">{{label}}</span>\n <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n </div>\n <div class=\"ag-track\" ref=\"agLine\">\n <div class=\"ag-track-background\"></div>\n <div class=\"ag-track-foreground\" :style=\"{'width': percentage +'%'}\"></div>\n </div>\n <div class=\"ag-limits\">\n <span class=\"ag-min\">{{min}}</span>\n <span class=\"ag-max\">{{max}}</span>\n </div>\n </div>\n </div>\n </template>\n </div>\n</template>\n\n\n\n<script>\n export default {\n data(){\n return {\n //Define me here\n type:\"artless\", // Gauge type. \"artless\", \"linear\", \"vertical\" or \"round\" \n label:\"Diverted\", // The label\n icon:\"mdi-electric-switch\", // (type: artless) (optional) the icon\n min:0, // Smallest expected value\n max:100, // Highest expected value\n unit:\"W\",// The unit of the measurement\n dim:0.3, //(type: round, linear, vertical) How dim is led when not glowing\n shadow:0, //(type: round, linear, vertical) Led shadow intensity (too much makes graphics muddy, 0 removes shadows)\n filterFunction:\"brightness\", // (type: round, linear, vertical) \"brightness\" for dark themes, \"opacity\" for light themes \n animate:true, // Animating led's is not most performant thing in the world. \n \n // Define colors\n\n // For type \"round\", \"vertical\" and \"linear\" the count of colors equals count of led's.\n // For type \"artless\" the line changes color based on percentage of value turned index of colors array. \n // For type \"round\" the led size depends on how many colors is defined. About 20 is optimal.\n // Color can be defined as:\n // HEX - \"#FF00FF\" \n // RGB - rgb(0,65,88)\n // named color - \"red\"\n // or depend on some defined CSS variable \n colors:[\"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n \"#0fb60f\",\n ], \n \n //no need to change those\n value:0,\n previousValue:0,\n size:100, \n inited:false\n }\n },\n\n\n \n methods: { \n getElement: function(name,base){ \n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate(data){\n let ret\n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n return null\n } \n }\n else{\n ret = data\n } \n return ret\n },\n\n full: function(){\n return Math.floor(this.colors.length*this.percentage/100)\n },\n half: function (){\n let p = this.colors.length*this.percentage/100;\n p -= this.full()\n p *= .5\n p += this.dim;\n return p;\n },\n filter: function(amount){\n let f\n switch(amount){\n case \"full\":{\n f = this.filterFunction == \"brightness\" ? \"brightness(1.1)\" : \"opacity(1)\";\n break\n }\n case \"half\":{\n f = this.filterFunction == \"brightness\" ? \"brightness(\" +this.half()+\")\" : \"opacity(\" +this.half()+\")\";\n break\n }\n default:{\n f = this.filterFunction == \"brightness\" ? \"brightness(\" +this.dim+\")\" : \"opacity(\" +this.dim+\")\";\n break\n }\n }\n return f\n },\n\n lit: function(){\n if(this.inited == false){\n return\n }\n const down = this.previousValue > this.value\n\n let time = .01 \n this.colors.forEach((color,i) => {\n let dot = this.getElement(\"dot-\"+i);\n if(!dot){\n console.log(\"lit() no dots found\")\n return\n } \n if(i<this.full()){\n dot.style.filter=this.filter(\"full\"); \n }\n else if(i==this.full()){\n dot.style.filter= this.filter(\"half\"); \n }\n else{\n dot.style.filter= this.filter(\"dim\"); \n }\n if(down){\n time = (this.colors.length - i) * .12 \n }else{\n time = i * 0.08\n }\n dot.style.transition = this.animate ? \"filter \"+time+\"s\" : \"unset\";\n })\n this.previousValue = this.value\n },\n changeLine:function(){\n const line = this.getElement(\"agLine\",true);\n if(!line){\n console.log(\"no line found\")\n return \n }\n let c = Math.floor(this.colors.length * this.percentage / 100)\n if(c >= this.colors.length){\n c = this.colors.length - 1\n }\n line.style.setProperty('--line-color',this.colors[c])\n\n },\n onResize:function(){ \n let g = this.getElement(\"hng\",true)\n if(!g){\n return\n } \n this.$nextTick(() => {\n let last = this.size \n let changed = this.type == \"vertical\" ? g.clientHeight : g.clientWidth; \n if(Math.abs(last - changed) < 3){\n return\n }\n this.size = changed\n g.style.setProperty('--size',this.size); \n if(this.type == \"round\"){\n this.updateLayout()\n }\n })\n \n },\n updateLayout:function(){\n let angle;\n const step = 270 / this.colors.length;\n const radius = (this.size - (this.size*0.1))/2\n const s = this.ledSize / -2;\n const outline = this.filterFunction == \"opacity\" ? \"black\" : \"white\"; \n this.colors.forEach((c,i) => {\n let dot = this.getElement(\"dot-\"+i);\n if(!dot){\n console.log(\"round init() no dots found\")\n return\n }\n dot.style.backgroundColor = c\n dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\n dot.style.transition = \"filter 0.1s\";\n dot.style.setProperty('--dot',i);\n angle = ((i+1)*step) * Math.PI / 180;\n dot.style.left = s + radius * Math.cos(angle) + 'px';\n dot.style.top = s + radius * Math.sin(angle) + 'px';\n dot.style.transform = 'translate('+s+'px, '+s+'px)'; \n dot.style.rotate = (angle - 0.08)+\"rad\" \n }\n )\n }\n },\n watch: {\n msg: function(){ \n if(this.msg.payload !== undefined){ \n const v = this.validate(this.msg.payload) \n if(v === null){\n return\n } \n this.value = v\n if(this.type != \"artless\"){\n this.lit()\n }\n else{\n this.changeLine()\n } \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(2)\n },\n percentage: function(){\n return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n }, \n ledSize:function(){\n const s = 4.71239 * ((this.size - (this.size*0.3))/2) \n return s / this.colors.length\n },\n flat:function(){\n return this.shadow == 0\n }\n\n },\n mounted(){\n const outline = this.filterFunction == \"opacity\" ? \"black\" : \"white\";\n if(this.type == \"round\"){\n let g = this.getElement(\"hng\",true)\n if(!g){\n return\n } \n this.resizeObserver = new ResizeObserver((entries) => {\n this.onResize()\n });\n this.resizeObserver.observe(g); \n \n setTimeout(()=>{\n this.onResize()\n },20)\n }\n else if(this.type == \"linear\"){\n \n this.colors.forEach((c,i) => {\n let dot = this.getElement(\"dot-\"+i);\n if(!dot){\n console.log(\"linear init() no dots found\")\n return\n }\n dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\n dot.style.backgroundColor = c \n dot.style.transition = \"filter 0.1s\";\n }\n )\n }\n else if(this.type == \"vertical\"){\n let g = this.getElement(\"hng\",true)\n if(!g){\n return\n }\n this.resizeObserver = new ResizeObserver((entries) => {\n this.onResize()\n });\n this.resizeObserver.observe(g); \n setTimeout(()=>{\n this.onResize()\n },20)\n this.colors.forEach((c,i) => {\n let dot = this.getElement(\"dot-\"+i);\n if(!dot){\n console.log(\"linear init() no dots found\")\n return\n }\n dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\n dot.style.backgroundColor = c\n dot.style.transition = \"filter 0.1s\";\n })\n }\n else if(this.type == \"artless\"){\n const line = this.getElement(\"agLine\",true);\n line.style.setProperty('--line-color',this.colors[0])\n if(this.animate == true){ \n if(!line){\n console.log(\"artless init() no line found\")\n return\n }\n line.style.transition = \"width 0.5s\";\n }\n } \n \n this.inited = true;\n },\n unmounted () {\n if(this.resizeObserver){\n this.resizeObserver.disconnect();\n this.resizeObserver = null; \n }\n }\n}\n</script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":660,"y":1620,"wires":[[]]},{"id":"f0c817fab51fe05a","type":"inject","z":"be4568a984cb4685","name":"","props":[{"p":"payload"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$floor($random() * 100) + 1","payloadType":"jsonata","x":430,"y":1620,"wires":[["d9f948314558a48c"]]},{"id":"48fc19d1161f11e6","type":"ui-group","name":"Test","page":"19eb6d108e9275e2","width":"20","height":"13","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"19eb6d108e9275e2","type":"ui-page","name":"Examples","ui":"ae3d4aeb3f977a90","path":"/examples","icon":"","layout":"notebook","theme":"a965ccfef139317a","order":-1,"className":"","visible":"true","disabled":"false"},{"id":"ae3d4aeb3f977a90","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"temporary"},{"id":"a965ccfef139317a","type":"ui-theme","name":"Default","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]
I'm only using template nodes
That is covered, at least partly, by #747.
I'm only using template nodes for gauges etc, but I've just run a test by adding the default palette gauge, chart & text nodes instead, and they do re-connect OK.
Our challenge here is that when we re-connect, the client requests the latest ui-config
, which details the widget types, etc. to render. With ui-template
, it includes Vue Component to render. With. the received ui-config
, we then re-render the ui-template
with whatever is received. It's difficult to do a diff/comparison here, and then only assign widget.component
if something has changed, but it's the best bet I have I think to fixing this problem.
On my laptop, if I suspend the laptop for some time, then when I resume I see the connection lost message on permanent display. The connection is not restored.
It says it is attempting to reconnect but appears not to be. Refreshing the page recovers the situation.
This leaves just ui-template issues #619 and #747 which appear to be duplicates, even though I submitted them both myself.
I have closed #619 as a duplicate. This leaves only #747. It would be good to get that fixed as then, as far as I know, all the reconnection issues will be sorted.
Thanks Colin, #747 is going to be a beast, as it's more linked to Vue internals than the socket/connection pieces. I have had a look in the not-so-distant past, but didn't make much headway. May get @cstns to put some eyes on it and see if he can view things differently to myself
Does the fact that it recovers if one switches to a different page and back again help? Perhaps there is at least a simple workaround there. On reconnection do whatever happens when one switches to a page.
On reconnection do whatever happens when one switches to a page.
The reason this works is that it re-renders the entire template, which we can't trigger without a page refresh
Understood.
Description
Using this as a centralised store for websocket connection-related problems.
Have you provided an initial effort estimate for this issue?
I have provided an initial effort estimate