I wanted a clean bookmark page for my internal webpages that also verifies if the pages are reachable (ie. servers are running).
This flow will produce a responsive endpoint on http://<node-red-host:port>/bookmarks
that looks good on any device (or at least during my testing). Indicators: green reachable, red (subtle blinking) unreachable, transparent is not checked. Links open in new tabs.
The flow
Edit the "bookmarks" node to setup the links (in yaml format):
- title: Proxmox
address: 10.0.0.99
verify: ping
- title: unifi
address: https://10.0.0.180:8443
verify: http
If there is 'http(s)' in the address, it will end up in the bookmarks section, if not, it is considered a host.
Verify is optional, can be either ping or http
The page uses 2 external libraries (tailwind.js and alpinejs) which are loaded via CDN.
They can be loaded locally as well, but I couldn't include them in this flow as it would become too large.
[{"id":"54225ebd4d56d002","type":"inject","z":"51924a530596d728","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":95,"y":880,"wires":[["3326ec7c0a4cdb04"]],"l":false},{"id":"3326ec7c0a4cdb04","type":"template","z":"51924a530596d728","name":"Bookmarks","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"- title: Proxmox\n address: 10.0.0.99\n verify: ping\n\n- title: router\n address: https://10.0.0.1\n\n- title: Node-RED\n address: http://10.0.0.170:1880\n \n- title: Proxmox\n address: https://10.0.0.99:8006\n verify: http\n \n- title: unifi\n address: https://10.0.0.180:8443\n verify: http\n \n- title: Zigbee2mqtt\n address: http://10.0.0.170:8080/\n verify: http\n\n- title: Grafana\n address: http://10.0.0.175:3000/\n verify: http\n\n- title: Spotweb\n address: http://10.0.0.171:81/\n verify: http\n\n- title: Homebridge\n address: http://10.0.0.173:8581/\n verify: http\n\n- title: NZBGET\n address: http://10.0.0.171:6789/\n verify: http\n\n- title: Internet\n address: google.com\n verify: ping\n \n- title: MQTT\n address: 10.0.0.172\n verify: ping\n","output":"yaml","x":210,"y":880,"wires":[["6a3ab8216f56decb"]]},{"id":"6a3ab8216f56decb","type":"link out","z":"51924a530596d728","name":"link out 15","mode":"link","links":["8f86eaee6e9aa277"],"x":325,"y":880,"wires":[]},{"id":"5dd132f5a560e63e","type":"group","z":"51924a530596d728","style":{"fill":"#e3f3d3","fill-opacity":"0.31","label":true,"color":"#777777"},"nodes":["5a3e7b8657c32c45","e3d3cc939bec6251","f13a0d70525cde3e","ebf736b2f460bd97","78ce0ef772483a26"],"x":48,"y":919,"w":824,"h":508},{"id":"5a3e7b8657c32c45","type":"comment","z":"51924a530596d728","g":"5dd132f5a560e63e","name":"http://<node-red-ip>:1880/bookmarks","info":"","x":230,"y":960,"wires":[]},{"id":"e3d3cc939bec6251","type":"group","z":"51924a530596d728","g":"5dd132f5a560e63e","name":"websocket","style":{"label":true},"nodes":["f848bb13e7ad35ed","17fc32a711331bd6","c3190323aab037d3","7bfe44e023d995a6","494b536c518dc7b4","56a1f57531ef5996","866dbf109c98f918"],"x":74,"y":999,"w":422,"h":82},{"id":"f848bb13e7ad35ed","type":"websocket in","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":115,"y":1040,"wires":[["c3190323aab037d3"]],"l":false},{"id":"17fc32a711331bd6","type":"websocket out","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":455,"y":1040,"wires":[],"l":false},{"id":"c3190323aab037d3","type":"json","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","property":"payload","action":"","pretty":false,"x":165,"y":1040,"wires":[["7bfe44e023d995a6"]],"l":false},{"id":"7bfe44e023d995a6","type":"switch","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","property":"payload.topic","propertyType":"msg","rules":[{"t":"eq","v":"init","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":215,"y":1040,"wires":[["494b536c518dc7b4"]],"l":false},{"id":"494b536c518dc7b4","type":"change","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"bookmarks1234","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":265,"y":1040,"wires":[["866dbf109c98f918"]],"l":false},{"id":"56a1f57531ef5996","type":"link in","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"link in 4","links":["c2780ff5240ee441","866dbf109c98f918"],"x":405,"y":1040,"wires":[["17fc32a711331bd6"]]},{"id":"866dbf109c98f918","type":"link out","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"link out 7","mode":"link","links":["8f86eaee6e9aa277","56a1f57531ef5996"],"x":315,"y":1040,"wires":[]},{"id":"70969700cfe06176","type":"websocket-listener","path":"/ws/bookmarks1234","wholemsg":"false"},{"id":"f13a0d70525cde3e","type":"group","z":"51924a530596d728","g":"5dd132f5a560e63e","name":"Endpoint","style":{"label":true},"nodes":["721be53de5e89b6d","6a1b42ca5d5f046b","71b1978f3f988c47"],"x":74,"y":1099,"w":352,"h":82},{"id":"721be53de5e89b6d","type":"http in","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","url":"/bookmarks","method":"get","upload":false,"swaggerDoc":"","x":180,"y":1140,"wires":[["71b1978f3f988c47"]]},{"id":"6a1b42ca5d5f046b","type":"http response","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","statusCode":"","headers":{},"x":385,"y":1140,"wires":[],"l":false},{"id":"71b1978f3f988c47","type":"template","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"index","field":"payload","fieldType":"msg","format":"handlebars","syntax":"plain","template":"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Bookmarks</title>\n <link\n href=\"https://fonts.googleapis.com/css2?family=Barlow:wght@300;400;500;600;700&display=swap\"\n rel=\"stylesheet\"\n />\n <script src=\"https://cdn.tailwindcss.com\"></script>\n\n <script src=\"//unpkg.com/alpinejs\" defer></script>\n <style>\n body {\n font-family: 'Barlow', sans-serif;\n }\n [x-cloak] { display: none !important; }\n </style>\n </head>\n <body class=\"bg-gray-700\" >\n \n <div x-data=\"data()\" x-cloak class=\"max-w-5xl mx-auto px-2\">\n <div class=\"uppercase text-2xl md:text-4xl text-gray-200 py-2 md:p-12\">Bookmarks</div>\n <div class=\"md:px-12 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 lg:grid-cols-5\">\n <template x-for=\"bookmark in bookmarks\">\n <div\n @click=\"if(bookmark.address.includes('http')) window.open(bookmark.address)\"\n class=\"flex gap-2 bg-gray-500/40 p-2 text-gray-200 rounded leading-tight\"\n :class=\"bookmark.address.includes('http') ? 'hover:bg-gray-400/80 cursor-pointer hover:text-gray-50' : ''\"\n >\n <div\n class=\"w-1 h-full rounded-full bg-gray-700\"\n :class=\"if(bookmark.reachable !== undefined) { if(bookmark.reachable){ return 'bg-green-400'} else { return 'bg-red-400 animate-pulse'}}\"\n ></div>\n <div>\n <div x-text=\"bookmark.title\" class=\"uppercase\"></div>\n <div x-text=\"getHostname(bookmark.address)\" class=\"text-gray-300/70 text-xs uppercase\"></div>\n </div>\n </div>\n </template>\n </div>\n <div class=\"uppercase text-xl text-gray-200 py-2 md:px-12 md:pt-8 md:pb-4\">Hosts</div>\n <div class=\"md:px-12 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 lg:grid-cols-5\">\n <template x-for=\"host in hosts\">\n <div class=\"flex gap-2 bg-gray-500/40 p-2 text-gray-200 rounded leading-tight\">\n <div\n class=\"w-1 h-full rounded-full bg-gray-700\"\n :class=\"if(host.reachable !== undefined) { if(host.reachable){ return 'bg-green-400'} else { return 'bg-red-400 animate-pulse'}}\"\n ></div>\n <div>\n <div x-text=\"host.title\" class=\"uppercase\"></div>\n <div x-text=\"host.address\" class=\"text-gray-300/70 text-xs uppercase\"></div>\n\n </div>\n </div>\n </template>\n </div>\n </div>\n\n <script>\n let bookmarks = []\n let hosts = []\n function data() {\n return {\n bookmarks,\n hosts,\n init() {\n this.ws = new WebSocket(location.origin.replace(/^http/, 'ws') + \"/ws/bookmarks1234\")\n\n this.ws.onopen = event => {\n this.ws.send(JSON.stringify({ topic: 'init' }))\n }\n this.ws.onmessage = event => {\n const input = JSON.parse(event.data)\n const bookmarks = input.filter(item => item.address.includes(\"http\"))\n const hosts = input.filter(item => !item.address.includes(\"http\"))\n this.hosts = hosts\n this.bookmarks = bookmarks\n //console.log(input)\n }\n },\n getHostname(u){\n const url = new URL(u);\n return url.hostname\n },\n }\n }\n </script>\n </body>\n</html>\n","output":"str","x":315,"y":1140,"wires":[["6a1b42ca5d5f046b"]],"l":false},{"id":"ebf736b2f460bd97","type":"group","z":"51924a530596d728","g":"5dd132f5a560e63e","name":"check reachability via http or ping ","style":{"label":true},"nodes":["df79cf7140767475","0bd822509eb5127e","27f7ab7c2e53fd39","47bf0e8227c41ed8","d2e445421c0fe2d9","d3de8f0a3b023f09","317b61d1b74a0d90","bfc62fc443ed015d","62c8dae740091d3b","c2780ff5240ee441","b82e8ee6b22e0226","10054e58f29347a9","efb093423c3951e9","e1f438683f28f6b4","50bebb0f45d5dffb","0a49e8367d868137","5700a6f2142e71d5","96fb6cf6d91f6436","8f86eaee6e9aa277"],"x":74,"y":1199,"w":772,"h":202},{"id":"df79cf7140767475","type":"switch","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","property":"payload.verify","propertyType":"msg","rules":[{"t":"eq","v":"ping","vt":"str"},{"t":"eq","v":"http","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":315,"y":1320,"wires":[["e1f438683f28f6b4"],["317b61d1b74a0d90"],["0a49e8367d868137"]],"l":false},{"id":"0bd822509eb5127e","type":"split","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":265,"y":1320,"wires":[["df79cf7140767475"]],"l":false},{"id":"27f7ab7c2e53fd39","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"get hostname","rules":[{"t":"set","p":"hostname","pt":"msg","to":"$match(payload.address, /^(?:((?:https?|s?ftp):)\\/\\/)([^:\\/\\s]+)(?::(\\d*))?(?:\\/([^\\s?#]+)?([?][^?#]*)?(#.*)?)?/).groups[1]","tot":"jsonata"},{"t":"set","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":405,"y":1240,"wires":[["47bf0e8227c41ed8"]],"l":false},{"id":"47bf0e8227c41ed8","type":"exec","z":"51924a530596d728","g":"ebf736b2f460bd97","command":"ping -c 2 -w1","addpay":"hostname","append":"| grep -E -o '[0-9]+ received' | cut -f1 -d ' '","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":495,"y":1260,"wires":[["d2e445421c0fe2d9"],[],[]],"l":false},{"id":"d2e445421c0fe2d9","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"set reachable","rules":[{"t":"set","p":"tmp.reachable","pt":"msg","to":"payload.$trim().$number()>0 ? true : false","tot":"jsonata"},{"t":"move","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":565,"y":1260,"wires":[["bfc62fc443ed015d"]],"l":false},{"id":"d3de8f0a3b023f09","type":"http request","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":true,"authType":"","senderr":false,"headers":[],"x":465,"y":1320,"wires":[["10054e58f29347a9"]],"l":false},{"id":"317b61d1b74a0d90","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","rules":[{"t":"set","p":"tmp","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"url","pt":"msg","to":"payload.address","tot":"msg"},{"t":"set","p":"rejectUnauthorized","pt":"msg","to":"false","tot":"bool"},{"t":"set","p":"requestTimeout","pt":"msg","to":"500","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":405,"y":1320,"wires":[["d3de8f0a3b023f09"]],"l":false},{"id":"bfc62fc443ed015d","type":"join","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":685,"y":1360,"wires":[["62c8dae740091d3b"]],"l":false},{"id":"62c8dae740091d3b","type":"function","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"clean payload and set context bookmarks1234","func":"flow.set(\"bookmarks1234\",msg.payload)\nnode.send({payload:msg.payload})\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":745,"y":1360,"wires":[["c2780ff5240ee441"]],"l":false},{"id":"c2780ff5240ee441","type":"link out","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"link out 8","mode":"link","links":["56a1f57531ef5996"],"x":805,"y":1360,"wires":[]},{"id":"b82e8ee6b22e0226","type":"catch","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","scope":["d3de8f0a3b023f09"],"uncaught":false,"x":165,"y":1260,"wires":[[]],"l":false},{"id":"10054e58f29347a9","type":"function","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"function 24","func":"\nlet reachable = false\nif (!isNaN(msg.statusCode)) reachable = true\n\nmsg.tmp.reachable = reachable\n\nreturn msg","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":515,"y":1320,"wires":[["efb093423c3951e9"]],"l":false},{"id":"efb093423c3951e9","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"set reachable","rules":[{"t":"move","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":565,"y":1320,"wires":[["bfc62fc443ed015d"]],"l":false},{"id":"e1f438683f28f6b4","type":"switch","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","property":"payload.address","propertyType":"msg","rules":[{"t":"cont","v":"http","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":335,"y":1260,"wires":[["27f7ab7c2e53fd39"],["50bebb0f45d5dffb"]],"l":false},{"id":"50bebb0f45d5dffb","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"get hostname","rules":[{"t":"set","p":"hostname","pt":"msg","to":"payload.address","tot":"msg"},{"t":"set","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":405,"y":1280,"wires":[["47bf0e8227c41ed8"]],"l":false},{"id":"0a49e8367d868137","type":"junction","z":"51924a530596d728","g":"ebf736b2f460bd97","x":380,"y":1360,"wires":[["bfc62fc443ed015d"]]},{"id":"5700a6f2142e71d5","type":"switch","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"websocket connected ?","property":"bookmarks_socket_connected","propertyType":"flow","rules":[{"t":"eq","v":"connect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":215,"y":1320,"wires":[["0bd822509eb5127e"]],"l":false},{"id":"96fb6cf6d91f6436","type":"change","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"","rules":[{"t":"set","p":"bookmarks1234","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":165,"y":1320,"wires":[["5700a6f2142e71d5"]],"l":false},{"id":"8f86eaee6e9aa277","type":"link in","z":"51924a530596d728","g":"ebf736b2f460bd97","name":"link in 9","links":["6a3ab8216f56decb","866dbf109c98f918"],"x":115,"y":1320,"wires":[["96fb6cf6d91f6436"]]},{"id":"78ce0ef772483a26","type":"group","z":"51924a530596d728","g":"5dd132f5a560e63e","name":"socket status","style":{"label":true},"nodes":["23e1cf1b916c387d","cfe354a9c610ac7b","dc0a7d4f5672088e"],"x":514,"y":999,"w":182,"h":82},{"id":"23e1cf1b916c387d","type":"status","z":"51924a530596d728","g":"78ce0ef772483a26","name":"","scope":["f848bb13e7ad35ed"],"x":555,"y":1040,"wires":[["cfe354a9c610ac7b"]],"l":false},{"id":"cfe354a9c610ac7b","type":"rbe","z":"51924a530596d728","g":"78ce0ef772483a26","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"status.event","topi":"topic","x":605,"y":1040,"wires":[["dc0a7d4f5672088e"]],"l":false},{"id":"dc0a7d4f5672088e","type":"change","z":"51924a530596d728","g":"78ce0ef772483a26","name":"socket connected ?","rules":[{"t":"set","p":"bookmarks_socket_connected","pt":"flow","to":"status.event","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":655,"y":1040,"wires":[[]],"l":false}]
Enjoy