Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui-template nodes not receiving messages after network disconnection/recovery. #747

Closed
colinl opened this issue Apr 5, 2024 · 27 comments · Fixed by #1097 or #1301
Closed

ui-template nodes not receiving messages after network disconnection/recovery. #747

colinl opened this issue Apr 5, 2024 · 27 comments · Fixed by #1097 or #1301
Assignees
Labels
bug Something isn't working size:M - 3 Sizing estimation point

Comments

@colinl
Copy link
Contributor

colinl commented Apr 5, 2024

Current Behavior

If the network connection between the browser and the server is broken for even a few seconds and then restored, ui-template nodes reset to their initial state and stop receiving messages.
The same symptom can be seen on Android if the page is put in the background for a while, or even left in the foreground with an extended period of non-interaction with the user. I suspect Android is putting the page to sleep which effectively means that it is disconnected from the network.

Expected Behavior

The dashboard should recover after re-connection.

Steps To Reproduce

2024-04-25 Edited below to reflect slightly different symptoms with dashboard 1.8.0

With the dashboard browser on a different machine to the server, import the flow below (which is slightly extended version of the default ui-template) and deploy.
Refresh the dashboard browser. Initially the counter shows a count of 0.
With the developer console open on the dashboard page, click the Inject button in the editor.
The received message is shown in the developer console and the counter is incremented by 5 as expected.
Disconnect the network from the machine showing the dashboard for a few seconds. A connection error is shown in the developer console and the Connection Lost popup is shown.
Reconnect the network. The connection restored popup appears.
Click the Inject button. No message is shown in the console.
Click the Increment button on the dashboard and the counter does increment.
Refreshing the browser restores a working system.

[{"id":"9ea21274b5061628","type":"inject","z":"eff7ca5fd9394ba5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":2720,"wires":[["dec16dcbe7aa673d"]]},{"id":"dec16dcbe7aa673d","type":"ui-template","z":"eff7ca5fd9394ba5","group":"9339a318b3f1cfb9","name":"#747 test","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <div>\n        <h2>Counter</h2>\n        <p>Current Count: {{ count }}</p>\n        <p class=\"my-class\">Formatted Count: {{ formattedCount }}</p>\n        <v-btn @click=\"increase()\">Increment</v-btn>\n    </div>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                count: 0\n            }\n        },\n        watch: {\n            msg: function() {\n                console.log(`msg: ${JSON.stringify(this.msg)}`)\n                this.count += 5\n            },\n            // watch for any changes of \"count\"\n            count: function () {\n                if (this.count % 5 === 0) {\n                    this.send({payload: 'Multiple of 5'})\n                }\n            }\n        },\n        computed: {\n            // automatically compute this variable\n            // whenever VueJS deems appropriate\n            formattedCount: function () {\n                return this.count + ' Apples'\n            }\n        },\n        methods: {\n            // expose a method to our <template> and Vue Application\n            increase: function () {\n                this.count++\n            }\n        },\n        mounted() {\n            // code here when the component is first loaded\n        },\n        unmounted() {\n            // code here when the component is removed from the Dashboard\n            // i.e. when the user navigates away from the page\n        }\n    }\n</script>\n<style>\n    /* define any styles here - supports raw CSS */\n    .my-class {\n        color: red;\n    }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":370,"y":2720,"wires":[[]]},{"id":"9339a318b3f1cfb9","type":"ui-group","name":"Test","page":"1d9a70bb7c3ff3f5","width":"6","height":"1","order":-1,"showTitle":false,"className":"","visible":"true","disabled":"false"},{"id":"1d9a70bb7c3ff3f5","type":"ui-page","name":"Test","ui":"04ee189a49c54f22","path":"/test","icon":"home","layout":"grid","theme":"23aa79f4e489b044","order":12,"className":"","visible":"true","disabled":"false"},{"id":"04ee189a49c54f22","type":"ui-base","name":"Dashboard","path":"/dashboard","showPathInSidebar":false,"navigationStyle":"temporary"},{"id":"23aa79f4e489b044","type":"ui-theme","name":"Theme 1","colors":{"surface":"#ffffff","primary":"#514fba","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"1px","groupGap":"1px","groupBorderRadius":"4px","widgetGap":"4px"}}]

Environment

  • Dashboard version: 1.8.0
  • Node-RED version: 4.0.0-Beta.2
  • Node.js version: 20
  • npm version:
  • Platform/OS: Pi OS
  • Browser: Edge

Have you provided an initial effort estimate for this issue?

I am not a FlowFuse employee

@colinl colinl added bug Something isn't working needs-triage Needs looking at to decide what to do labels Apr 5, 2024
@colinl
Copy link
Contributor Author

colinl commented Apr 5, 2024

Also see possibly related issues #620 and #444 and forum discussion https://discourse.nodered.org/t/ui-template-fully-kiosk-browser-update-problem-dashboard-2/87003

@joepavitt joepavitt moved this from Backlog to Up Next in Dashboard Backlog Apr 18, 2024
@joepavitt joepavitt added size:M - 3 Sizing estimation point and removed needs-triage Needs looking at to decide what to do labels Apr 18, 2024
@colinl
Copy link
Contributor Author

colinl commented Apr 25, 2024

I had hoped that this would be fixed by the comms enhancements in 1.8.0 but it seems not. I have updated the description to reflect the slightly changed symptoms with 1.8.0

@joepavitt joepavitt moved this from Up Next to In Progress in Dashboard Backlog Apr 26, 2024
@joepavitt joepavitt moved this from In Progress to Under Review in Dashboard Backlog May 14, 2024
@joepavitt joepavitt moved this from Under Review to In Progress in Dashboard Backlog May 16, 2024
@colinl
Copy link
Contributor Author

colinl commented Jun 12, 2024

Note that just switching pages and back again is enough restore normal operation.

@cstns
Copy link
Contributor

cstns commented Jun 12, 2024

I'll give this a go in the coming days

@Paul-Reed
Copy link
Contributor

Any further forward with this issue pls, it's proving to be a pain!

@cstns
Copy link
Contributor

cstns commented Jun 25, 2024

I apologize, I lost track of this issue. Will try an make some headway this week!

@cstns
Copy link
Contributor

cstns commented Jun 27, 2024

Follow up: Haven't lost track of this issue but I'll have to postpone the investigation to next week

@cstns
Copy link
Contributor

cstns commented Jul 8, 2024

After a lot of poking/prodding, wild goose chases and theories, I may have found the culprit for this specific scenario

The UITemplate's anonymous component is unmounted and re-mounted on socket reconnect. The unmounted method calls this.$socket.off('msg-input:${this.id}') from line ui/src/widgets/ui-template/UITemplate.vue:184 right before calling any custom unmounted functionality that it may have been passed in.

this.$socket.off('msg-input:${this.id}') methods cuts all component reactivity from then onward.

As to why anonymous components are getting unmounted/remounted on socket disconnect is still unclear to me and would need more time to investigate while the socket.off() seems to be a good practice, and a common one across the repo.

I'll try and pick up where I left off in a couple of days time.

@cstns
Copy link
Contributor

cstns commented Jul 15, 2024

Fighting anonymous components reactivity on socket disconnect was a dead end, as there are a lot of factors would have side effects on the socket - anon component relationship.

Embracing side effects is the way to go. Not 100% sure if the dataTracker was intended to be bound to the parent component, but binding it to the anonymous component seems to have no side effects plus the added benefit of re connecting on component lifecycle refresh caused by the socket disconnect.

Creating a PR to address the problem

@joepavitt joepavitt moved this from In Progress to Under Review in Dashboard Backlog Jul 30, 2024
@github-project-automation github-project-automation bot moved this from Under Review to Done in Dashboard Backlog Jul 30, 2024
@Paul-Reed
Copy link
Contributor

Paul-Reed commented Aug 8, 2024

Using DB v1.14.0 this is still not working for me when viewing on a android phone with the dashboard installed as a PWA app.
The below video shows a dashboard when I've allowed the screen to sleep for just 15 seconds, upon wakeup the data has reverted to zero, and can only be resumed by refreshing the screen. The data update period in 3 seconds.

db2.mp4

Example flow -

[{"id":"baeb76b9d06f520c","type":"ui-template","z":"1326aadbacf36704","group":"25f065c072b233c0","page":"","ui":"","name":"Grid (Zero Cross ver)","order":2,"width":"7","height":"1","head":"","format":"// See https://discourse.nodered.org/t/gauges-for-dashboard-2-0-made-with-ui-template/85955/59\n<template>\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': linesize +'%'}\"></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\n<script>\n    export default {\n    data(){\n        return {\n            //Define me here\n                                             \n            label:\"Grid\", // The label\n            icon:\"mdi-power-socket\", // (type: artless) (optional) the icon\n            zeroCross:true,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined)\n            min:0, // Smallest expected value\n            max:5000, // Highest expected value\n            unit:\"W\",// The unit of the measurement           \n            animate:true, // Animating led's is not most performant thing in the world.                          \n            \n            // Define colors           \n            colors:[\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"orange\",\n                    \"orange\",                    \n                    \"red\"\n                   ],            \n            \n            //no need to change those\n            value:0,          \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        changeLine:function(){\n            const line = this.getElement(\"agLine\",true);\n            if(!line){\n                console.log(\"no line found\")\n                return            \n            }\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            if(c < 0){\n                c = 0\n            }\n            if(this.zeroCross){\n                c = this.value > 0 ? (this.colors.length - 1) : 0\n            }\n            line.style.setProperty('--line-color',this.colors[c])\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                this.changeLine()              \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        linesize:function(){\n            if(this.zeroCross){\n                return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100);           \n            }\n            else{\n                return Math.max(0,this.percentage)\n            }\n        }\n    },\n    mounted(){\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        this.inited = true;\n    }\n}\n</script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":900,"y":140,"wires":[[]]},{"id":"cf31acb3987e6ed8","type":"inject","z":"1326aadbacf36704","name":"","props":[{"p":"payload"}],"repeat":"3","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"$round($random()*19)+200","payloadType":"jsonata","x":710,"y":140,"wires":[["baeb76b9d06f520c"]]},{"id":"25f065c072b233c0","type":"ui-group","name":"gauges","page":"d6557c66b0d2c002","width":"7","height":"6","order":1,"showTitle":false,"className":"","visible":true,"disabled":"false"},{"id":"d6557c66b0d2c002","type":"ui-page","name":"Energy","ui":"ae3d4aeb3f977a90","path":"/energy","icon":"home","layout":"flex","theme":"a965ccfef139317a","order":2,"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"}}]

Note: I have tried deleting the app from my phone, and re-installing it, and same result.

@joepavitt joepavitt reopened this Aug 8, 2024
@joepavitt
Copy link
Collaborator

Thanks Paul - have re-opened the issue and will investigate - maybe @cstns can take another look?

@joepavitt joepavitt moved this from Done to Up Next in Dashboard Backlog Aug 8, 2024
@cstns
Copy link
Contributor

cstns commented Aug 9, 2024

Is this happening anywhere else other than mobile/pwa?

@colinl
Copy link
Contributor Author

colinl commented Aug 9, 2024

I have managed to replicate the issue on Android, but not otherwise. It does not always fail, I am trying to work out what the trigger is.
@Paul-Reed do you see a Disconnected, then Reconnected message when you re-activate?
If you refresh the page and then let it sleep again does it still fail?

@colinl
Copy link
Contributor Author

colinl commented Aug 9, 2024

Running in Android with current main from github, using remote debugging, if I break the network connection to the server then I see

Baseline.vue:191 
 Uncaught 
TypeError: Cannot read properties of null (reading 'close')
    at Array.<anonymous> (Baseline.vue:191:30)
    at Object.emit (alerts.js:17:30)
    at Socket.<anonymous> (main.mjs:134:20)
    at Emitter.emit (index.mjs:136:20)
    at Socket.onclose (socket.js:432:14)
    at Emitter.emit (index.mjs:136:20)
    at Manager.onclose (manager.js:299:14)
    at Emitter.emit (index.mjs:136:20)
    at lt.onClose (socket.js:570:18)
    at WS.<anonymous> (socket.js:189:43)

Where Baseline.vue has

 mounted () {
        this.updateTheme()
        Alerts.subscribe((title, description, color, alertOptions) => {
            this.$refs.alert.close()                                      <-- Line 191 **********
            this.alert = alertOptions
            this.$nextTick(() => {
                this.$refs.alert.onMsgInput({
                    payload: `<h3>${title}</h3><p>${description}</p>`,
                    color
                })
            })
        })
    },

After that all is gone to pot.

@joepavitt
Copy link
Collaborator

Ooh - good find @colinl - that's odd that the alert isn't mounted though 🤔

@Paul-Reed
Copy link
Contributor

@cstns - I've only noticed it on mobile/PWA.

@colinl - I always get the reconnecting alert, so it is reconnecting (my screen recorder didn't capture that as it's set to record the app and not the screen).

If I refresh the page, the data again flows, but yes, if I let it sleep, it almost always fails again.

@colinl
Copy link
Contributor Author

colinl commented Aug 10, 2024

I can make it fail with that error when installed as an app in Edge on a PC with the flow below. It is running the prototype of my classic gauge widget when it was implemented in a ui-template. It does not fail when not running as an app.

  • Import the flow, install the dashboard as an app on a PC which is not the one running node-red and which can be disconnected from the network.
  • Go to the page showing the gauge and open the developer tools console
  • Disconnect the PC from the network and, for me, the error immediately appears.

In fact I am not convinced that this is the same problem that @Paul-Reed is seeing as the symptom is different, but I think that if there is a repeatable error then then it is worth fixing that issue, even if one is not convinced that it is the same problem.

[{"id":"eb81f7b4e75d3b1e","type":"inject","z":"6e4dd70a4b804955","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":190,"y":1660,"wires":[["6c5fe238f2739cdf"]]},{"id":"12b5d3fefe6faf5b","type":"inject","z":"6e4dd70a4b804955","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.6","payloadType":"num","x":190,"y":1700,"wires":[["6c5fe238f2739cdf"]]},{"id":"bb5ff38c6ca6062c","type":"inject","z":"6e4dd70a4b804955","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1.4","payloadType":"num","x":190,"y":1740,"wires":[["6c5fe238f2739cdf"]]},{"id":"6c5fe238f2739cdf","type":"ui-template","z":"6e4dd70a4b804955","group":"31f2da0d04a5d5e2","page":"","ui":"","name":"Gauge template master","order":1,"width":"3","height":"3","head":"","format":"<!-- Gauge Template CDL v1.4.0\n  Based on original work by @HotNipi \n-->\n<script>\n    export default{\n        data(){\n            let data = {\n                //define settings here\n                // Min and max scale values.  Max may be less that min.\n                //min: -50,\n                //max: 50,\n                min:0,\n                max:1.4,\n                //min:0,\n                //max:-1.4,\n                majorDivision: 0.2,       // number of input units for each (numbered) major division\n                minorDivision: 0.05,       // number of input units for each minor division\n                unit:\"°C\",\n                label:\"Hot Water\",\n                measurement:\"Temperature\",\n                valueDecimalPlaces: 2,    // number of decimal places to show in the value display\n                majorDecimalPlaces: 1,    // number of decimal places to show on the scale\n                // Coloured sectors around the scale.  Sectors can be in any order and it makes no difference if \n                // start and end are reversed.\n                //  Any gaps are left at background colour\n                sectors:[{start:0,end:0.4,color:\"skyblue\"},{start:0.4,end:0.75,color:\"green\"},{start:0.75,end:1.4,color:\"red\"}],\n                //sectors:[{start:0,end:-0.4,color:\"skyblue\"},{start:-0.4,end:-0.75,color:\"green\"},{start:-0.75,end:-1.4,color:\"red\"}],\n\n                // The position and alignment of the gauge inside the 100x100 svg box for the widget can be changed by modifying the settings below\n                // The origin of the svg box is the top left hand corner. The bottom right hand corner is 100,100\n                // Obviously, if you move the gauge you may have to move the text fields also.\n                // Take care with these settings, if you put silly values in the browser showing the dashboard may lock up. If this happens,\n                // close the dashboard browser tab (which may take some time as it is locked up).\n                arc: {\n                    cx:50,              // the x and y coordinates of the centre of the gauge arc\n                    cy: 64, \n                    radius: 47.5,       // the radius of the arc\n                    startDegrees: -123, // the angle of the start and end points of the arc.  Zero is vertically up from the centre\n                    endDegrees: 123,    // +ve values are clockwise\n                },\n                //arc: {cx:50, cy: 64.383, radius: 47.5, startDegrees: -120, endDegrees:45}  //??\n                //arc: {cx:50, cy: 64.383, radius: 47.5, startDegrees: 60, endDegrees:45}  //??\n\n                //don't change this\n                value: null,\n            }\n            // calculate derived values\n            // make sure startDegrees < endDegrees, but the difference is <= 360\n            while (data.arc.startDegrees >= data.arc.endDegrees) {\n                data.arc.startDegrees -= 360\n            }\n            while (data.arc.endDegrees - data.arc.startDegrees > 360) {\n                data.arc.startDegrees += 360\n            }\n            const startRadians = data.arc.startDegrees * Math.PI/180\n            const endRadians = data.arc.endDegrees * Math.PI/180\n            data.arc.startx = data.arc.cx - data.arc.radius * Math.sin(startRadians-Math.PI)\n            data.arc.starty = data.arc.cy + data.arc.radius * Math.cos(startRadians-Math.PI)\n            data.arc.endx = data.arc.cx + data.arc.radius * Math.sin(Math.PI-endRadians)\n            data.arc.endy = data.arc.cy + data.arc.radius * Math.cos(Math.PI-endRadians)\n            data.arc.arcLength = 2 * Math.PI * data.arc.radius * (data.arc.endDegrees - data.arc.startDegrees)/360\n\n            // sanity checks - probably there should be more of these\n            this.majorDivision = this.majorDivision <= 0  ?  1  : this.majorDivision\n            this.minorDivision = this.minorDivision <= 0  ?  1  : this.minorDivision\n            //console.log({arc: data.arc})\n            return data\n        }\n    }\n</script>\n\n<template>\n    <div class=\"hn-sng\">\n        <div class=\"label\">{{label}}</div>\n        <svg ref=\"hn-gauge\" width=\"100%\" height=\"100%\" viewBox=\"0 0 100 100\">\n            <g>\n                <path v-for=\"(item, index) in sectors\" :key=\"index\" :ref=\"'sector-' + index\" class=\"sector\" stroke-width=\"5\" :d=\"arcspec\" ></path>                \n            </g>\n            <g>\n                <path class=\"tick-minor\" stroke-width=\"5\" :d=\"arcspec\" :style=\"tickStyle(this.minorDivision, 0.5)\"></path>\n                <path ref=\"arc\" class=\"tick-major\" stroke-width=\"5\" :d=\"arcspec\" :style=\"tickStyle(this.majorDivision, 1)\"></path>\n                \n            </g>         \n            <g>\n                <text v-for=\"(item, index) in numbers\" :key=\"index\" class=\"num\" text-anchor=\"middle\" :y=\"`${10.5-this.arc.radius}`\" \n                  :style=\"`rotate: ${item.r}deg; transform-origin: ${this.arc.cx}% ${this.arc.cy}%; transform: translate(${this.arc.cx}%, ${this.arc.cy}%)`\">\n                  {{item.n}}</text>\n            </g>\n            <g>\n                <text class=\"measurement\" y=\"48\" x=\"50%\" text-anchor=\"middle\">{{measurement}}</text>\n                <text class=\"unit\" y=\"75\" x=\"50%\" text-anchor=\"middle\">{{unit}}</text>\n                <text class=\"value\" y=\"90\" x=\"50%\" text-anchor=\"middle\">{{formattedValue}}</text>\n            </g>\n            <g ref=\"o-needle\" class=\"o-needle\" v-html=\"needle\">\n            </g>\n        </svg>\n    </div>\n</template>\n\n<script>\n    export default{\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: function(data){\n                let ret                \n                if(typeof data !== \"number\"){\n                    ret = parseFloat(data)\n                    if(isNaN(ret)){\n                        console.log(\"BAD DATA! gauge id:\",this.id,\"data:\",data)\n                        ret = null\n                    }\n                }                    \n                else{\n                    ret = data\n                }                \n                return ret\n            },\n            range:function (n, p, r) {\n                // clamp n to be within input range\n                if (p.maxIn > p.minIn) {\n                    n = Math.min(n, p.maxIn)\n                    n = Math.max(n, p.minIn)\n                } else {\n                    n = Math.min(n, p.minIn)\n                    n = Math.max(n, p.maxIn)\n                }\n                if(r){\n                    return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);\n                }\n                return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;\n            },\n            generateNumbers:function(min,max,majorDivision){    \n                let minDegrees, maxDegrees, startValue\n                if (max > min) {\n                    minDegrees = this.arc.startDegrees\n                    maxDegrees = this.arc.endDegrees\n                    startValue = min    \n                } else {\n                    minDegrees = this.arc.endDegrees\n                    maxDegrees = this.arc.startDegrees\n                    startValue = max              \n                }\n                // Calculate number of major divisions, adding on a bit and rounding down in case last one is just off the end\n                const numDivs = Math.floor(Math.abs(max-min) / majorDivision + 0.1)\n                const degRange = maxDegrees-minDegrees\n                const degPerDiv = degRange * majorDivision/Math.abs(max-min)\n                let nums = []\n                for (let div=0; div<=numDivs; div++) {\n                    let degrees = div*degPerDiv + minDegrees\n                    const n = (startValue + div * majorDivision).toFixed(this.majorDecimalPlaces)\n                    nums.push({r: degrees, n: n})\n                }\n                return nums \n            },\n            sectorData:function(full){               \n                let ret = []\n                this.sectors.forEach((sector,idx) => {\n                    let sec = {name:'sector-'+idx,color:sector.color}\n                    const params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full}\n                    const start = this.range(sector.start,params,false)\n                    const end = this.range(sector.end,params,false)\n                    const pos = Math.min(start, end)\n                    const span = Math.max(start, end) - pos\n                    sec.css = `0 ${pos} ${span} var(--dash)`\n                    ret.push(sec)\n                })\n                return ret\n            },\n            rotation:function(v){\n                // allow pointer to go 10% off ends of scale, but not more than half way to the other end of the scale\n                const deltaDeg = this.arc.endDegrees - this.arc.startDegrees\n                const gapDeg = 360 - deltaDeg\n                const overflowFactor = Math.min(0.1, gapDeg/2/deltaDeg)\n                const overflow = (this.max-this.min)*overflowFactor\n                const angleOverflow = (deltaDeg)*overflowFactor \n                const min = this.min - overflow\n                const max = this.max + overflow\n                const minAngle = this.arc.startDegrees - angleOverflow\n                const maxAngle = this.arc.endDegrees + angleOverflow\n                const params = {minIn:min, maxIn:max, minOut:minAngle, maxOut:maxAngle};\n                if (v === null) {\n                    v = Math.min(min, max)\n                }\n                return `${this.range(v,params,false)}deg`\n            },\n            tickStyle: function(division, width) {\n                // division is the number of input units per tick\n                // width is the width (length?) of the tick in svg units\n\n                // total arc length in svg units\n                const arcLength = this.arc.arcLength\n                // length in user units\n                const range = Math.abs(this.max - this.min)\n                const tickPeriod = division/range * arcLength\n                // marker is width wide, so gap is tickPeriod-width\n                // stroke-dashoffset sets the first tick to half width\n                return `stroke-dasharray: ${width} ${tickPeriod-width}; stroke-dashoffset: ${width/2};`\n            },\n        },\n        watch: {\n            msg: function(){\n                // allow undefined payload through as it will show the invalid data state\n                const v = this.validate(this.msg.payload)                   \n                // v is null if payload is invalid, this is coped with then it is displayed\n                this.value = v\n                this.getElement('o-needle',true).style.rotate = this.rotation(this.value)\n            }\n        },\n        computed: {\n            needle: function() {\n                const cx = this.arc.cx\n                const cy = this.arc.cy\n                const length = this.arc.radius - 4.5\n                return `<path d=\"M ${cx},${cy} ${cx-1.5},${cy} ${cx-0.15},${cy-length} ${cx+0.15},${cy-length} ${cx+1.5},${cy} z\"></path> \n                  <circle cx=\"${this.arc.cx}\" cy=\"${this.arc.cy}\" r=\"3\"></circle>`\n            },\n            arcspec: function() {\n                const delta = this.arc.endDegrees - this.arc.startDegrees\n                // if more than 180 deg sweep then large-arg-flag should be 1\n                const largeArcFlag = delta > 180  ?  1  :  0\n\n                return `M ${this.arc.startx} ${this.arc.starty} A ${this.arc.radius} ${this.arc.radius} 0 ${largeArcFlag} 1 ${this.arc.endx} ${this.arc.endy}`\n            },\n            formattedValue: function () {\n                // Show --- for the value until a valid value is recevied\n                return this.value !== null  ?  this.value.toFixed(this.valueDecimalPlaces)  :  \"---\"\n            },\n            numbers:function(){\n                return this.generateNumbers(this.min,this.max,this.majorDivision)\n            },\n        },\n        mounted(){\n           \n            const dal = this.getElement('arc',true).getTotalLength()\n            const sec = this.sectorData(dal)              \n            const gauge = this.getElement('hn-gauge',true)\n            gauge.style.setProperty('--dash',dal)\n            sec.forEach(s =>{\n                const sector = this.getElement(s.name,false)\n                sector.style.setProperty(\"stroke-dasharray\",s.css)\n                sector.style.setProperty(\"stroke\",s.color)\n            })\n            // set the needle centre of rotation\n            this.getElement('o-needle',true).style[\"transform-origin\"] = `${this.arc.cx}% ${this.arc.cy}%`\n            // initialise the needle off the bottom\n            this.getElement('o-needle',true).style.rotate = this.rotation(null) \n        }\n\n    }\n</script>\n\n<style>\n    .hn-sng{\n        position:relative;\n    }\n    .hn-sng .label{\n        position:absolute;\n        font-size:1rem;\n        color:currentColor;\n        text-align:center;\n        width:100%;\n        overflow: hidden;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n    }\n    .hn-sng .value {\n        fill:currentColor;\n    }\n    .hn-sng .unit {\n        fill:currentColor;\n        font-size:0.4rem;\n    }\n    .hn-sng .measurement {\n        fill:currentColor;\n        font-size:0.5rem;\n    }\n    .hn-sng .num{\n        fill:currentColor;\n        fill-opacity:0.6;\n        font-size:.35rem;\n    }\n    .hn-sng .tick-minor{\n        fill:none;\n        stroke:currentColor;\n        stroke-opacity:0.6;\n    }\n    .hn-sng .tick-major{\n        fill:none;\n        stroke:currentColor;\n    }\n    .hn-sng .sector{        \n        fill:none;\n        stroke:transparent;      \n    } \n    .hn-sng .o-needle{        \n        transition:.5s;\n    }\n    .hn-sng .o-needle path, .hn-sng .o-needle circle{\n        fill:black;\n    }\n    \n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"Gauge template master","x":510,"y":1700,"wires":[[]]},{"id":"31f2da0d04a5d5e2","type":"ui-group","name":"Group 2","page":"2290baf8322d936b","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"2290baf8322d936b","type":"ui-page","name":"d2 P2","ui":"04ee189a49c54f22","path":"/d2p2","icon":"home","layout":"grid","theme":"f9b6670b127dc219","order":6,"className":"","visible":"true","disabled":"false"},{"id":"04ee189a49c54f22","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-control","ui-notification"],"showPathInSidebar":false,"navigationStyle":"temporary"},{"id":"f9b6670b127dc219","type":"ui-theme","name":"FlowForge Theme","colors":{"surface":"#152a47","primary":"#005aff","bgPage":"#ffffff","groupBg":"#ffffff","groupOutline":"#cc3e3e"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

@colinl
Copy link
Contributor Author

colinl commented Aug 12, 2024

I think it must be something to do with the template contents as I have other templates that do not give the problem. Or perhaps it is some sort of race condition so it varies unpredictably.
Are others able to see the problem with the flow I posted?

@joepavitt
Copy link
Collaborator

FF Customer hitting this problem - @cstns can you prioritise sooner rather than later please?

@cstns
Copy link
Contributor

cstns commented Sep 15, 2024

Yes, It managed to slip through the cracks.

@cstns cstns moved this from Up Next to In Progress in Dashboard Backlog Sep 16, 2024
@cstns
Copy link
Contributor

cstns commented Sep 16, 2024

Tried my best to replicate the issue but with no success and I used both @colinl & @Paul-Reed's flows.

My personal phone is a Pixel 7 Pro running android 14. For some reason I'm not able to connect in debug mode to it and get access to chrome's dev tools so i don't really know if there are any errors in the console.

In order to install the pwa app on my device I needed valid tls certificates so what I ended up doing is spinning up a docker container with a node-red and dashboard combo and using ngrok to tunnel the app towards a public address with valid tls certs.

When developing the initial fix, I managed to fake a network disconnect locally by removing the docker container with node-red from the virtual network and reconnecting it shortly after, succesfully and predictably reproducing the bug.

No matter how long I left my phone in a sleeping state, connecting or disconnecting the phones wifi / mobile network using, disconnecting/connecting the docker container to the vlan and any combo of these procedures did not reproduce the bug.

In order to get access to the mobile console and fidgeting with my device for too long, I installed Android Studio locally and emulated my phone (Pixel 7 Pro running android 14).

With the pwa installed and access to the console I did notice something odd in that once the app disconnects, the xhr poll frequency decreases from once every three seconds to once every 30 seconds giving the impression that the the connection is not re-established for 30 or so seconds.

Nonetheless, I was not able to replicate the described results in the emulator either. With Android Studio installed I could emulate any version of android, I haven't tried 13 yet.

@cstns
Copy link
Contributor

cstns commented Sep 16, 2024

I was worried that my setup was somehow interfeering but after some more fidgeting I was able to replicate colinl's error predictably, which prevent's ws reconnect on network reconnect.

Baseline.vue:191 
 Uncaught 
TypeError: Cannot read properties of null (reading 'close')
    at Array.<anonymous> (Baseline.vue:191:30)
    at Object.emit (alerts.js:17:30)
    at Socket.<anonymous> (main.mjs:134:20)
    at Emitter.emit (index.mjs:136:20)
    at Socket.onclose (socket.js:432:14)
    at Emitter.emit (index.mjs:136:20)
    at Manager.onclose (manager.js:299:14)
    at Emitter.emit (index.mjs:136:20)
    at lt.onClose (socket.js:570:18)
    at WS.<anonymous> (socket.js:189:43)

Where Baseline.vue has

 mounted () {
        this.updateTheme()
        Alerts.subscribe((title, description, color, alertOptions) => {
            this.$refs.alert.close()                                      <-- Line 191 **********
            this.alert = alertOptions
            this.$nextTick(() => {
                this.$refs.alert.onMsgInput({
                    payload: `<h3>${title}</h3><p>${description}</p>`,
                    color
                })
            })
        })
    },

Tricky to replicate consistently as it only occurs when you first open the app or changing pages. It does not occur however if the page was refreshed..

I'll see what's mishbehaving with the alert component first thing tomorrow.

Still not able to replicate it on pc

@colinl
Copy link
Contributor Author

colinl commented Sep 16, 2024

I have deleted my earlier post from a few hours ago, it seems I was not paying attention when I posted it.

@cstns
Copy link
Contributor

cstns commented Sep 17, 2024

You were right though, I found a way to replicate the bug consistently on desktop as well.

There's something strange happening to the alert component reference on route changes between different layouts in the Baseline.vue component. It was a bit tricky to figure out but I got there

@cstns
Copy link
Contributor

cstns commented Sep 17, 2024

The conclusion is that WebSocket connectivity was lost because the entire Vue application failed silently due to lifecycle inconsistencies between the parent Baseline.vue and child UiNotification.vue components.

I've tested all mentioned scenarios successfully but would appreciate if anyone with a bit of free time on their hand could confirm on their end that the fix addressed the problem as well.

@colinl
Copy link
Contributor Author

colinl commented Sep 17, 2024

Building as I type.

@github-project-automation github-project-automation bot moved this from In Progress to Done in Dashboard Backlog Sep 17, 2024
@colinl
Copy link
Contributor Author

colinl commented Sep 17, 2024

Looks good to me. I had hoped this might be related to #1251, but apparently not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working size:M - 3 Sizing estimation point
Projects
Status: Done
4 participants