Bookmark page with reachability verification

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 :slight_smile:

7 Likes

Interesting. I wonder if you considered using uibuilder rather than building the endpoints and websockets yourself? I've not reviewed the flow in detail yet - I will - but I suspect that it would have been a bit slimmer with uibuilder. You could also have chosen to load the front-end libraries locally if you wanted to which would save the page not working if Internet wasn't available.

I wonder if you considered using uibuilder rather than building the endpoints and websockets yourself

I did not. This is a drop-in flow, works without depencies (other than the cdn), nothing to install.

but I suspect that it would have been a bit slimmer with uibuilder.

Maybe from 8 to 4 nodes, I dont see a gain really, probably the opposite: way more things to deal with.

You could also have chosen to load the front-end libraries locally if you wanted to which would save the page not working if Internet wasn't available.

This can be done by including the js in a template node (that is how I have it running here). Note that the uibuilder vue3 example uses a cdn as well.

The way I develop little things like this is purely in vscode and its local live server, data from node-red comes down via a websocket - and yes I know that this can be done with uibuilder as well, but I don't want to deal with all kind of scaffolding setups and working in multiple interfaces.

I have mentioned it before - I don't want to plow through pages and pages of text. When reading your comments about uibuilder, you often use superlatives; easy, quick, fast, simple, but it requires whole documentation studies because there are no simple examples - and what is there talks about version this version that, iife, umd, esm, whatever this all means, it is where I get lost sorry.

If it cannot be explained in a simple straight-forward manner, I (and I suspect many others) will not use it. It might all be "simple" to you, but you breathe uibuilder, I don't.

This approach works great for me, I have many of these setups/endpoints.

2 Likes

A post was split to a new topic: Web endpoint status dashboard - uibuilder zero-code example

Only because more people have Vue v2 installed already. I think it also contains the local version commented out. Again, not a criticism, only an observation.

Ah, now you've also prompted another thought - and potentially a really useful one for other people. Not sure why I didn't think of that approach. I generally use VScode to develop the files for uibuilder endpoints. Time to go see if I can find a way to make that work dynamically with uibuilder.

As it happens, I already get live stylesheet updates because I've told Edge where the local folders that match the uibuilder ones exist. But I haven't managed to get the main html/js pages to do the same.

If you ever happen to use Svelte, its dev server does work brilliantly with uibuilder with no scaffolding at all. You just leave it running in the background but load the normal node-red/uibuilder page (not the dev server url).

Are you using the Microsoft "Live Preview" extension to get your live server?

Why does my topic need to be hijacked by another uibuilder marketing campaign ?
I am really not interested.

1 Like

Indeed. This is the “share you projects” category. It is not intended to be a discussion.

1 Like

Fine. Thought it might be interesting to see contrasting ideas but clearly not. Moved to another thread.

Excellent! This is now [one of] my browser's home page. Thanks for sharing it. :grinning:

A question:

Is it possible to force a new row in the output to group related bookmarks together? Like this:

1 Like

Thanks :slight_smile:

Yes this should be possible, I will try to keep it in a way where you can specify the group name in the yaml like;

- title: Proxmox
  address: 10.0.0.99
  verify: ping
  group: connectivity

and then it will render the title (because if you have more than 5 links you will get a new row anyway).

Let me investigate.

2 Likes

I have simplified the flow and the setup of the yaml is a bit different, but hopefully a bit more flexible:

The yaml setup:

- group: connectivity
  links:
  - title: router
    address: https://10.0.0.1

  - title: unifi
    address: https://10.0.0.180:8443
    verify: http

- group: home automation
  links:
  - title: Node-RED
    address: http://10.0.0.170:1880
    verify: http

etc...

Flow:

[{"id":"54225ebd4d56d002","type":"inject","z":"51924a530596d728","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":105,"y":860,"wires":[["ab4e6eab906a19c4"]],"l":false},{"id":"ab4e6eab906a19c4","type":"template","z":"51924a530596d728","name":"Bookmarks","field":"bookmarks1234","fieldType":"flow","format":"handlebars","syntax":"mustache","template":"- group: connectivity\n  links:\n  - title: router\n    address: https://10.0.0.1\n\n  - title: unifi\n    address: https://10.0.0.180:8443\n    verify: http\n\n- group: home automation\n  links:\n  - title: Node-RED\n    address: http://10.0.0.170:1880\n    verify: http\n\n  - title: Zigbee2mqtt\n    address: http://10.0.0.170:8080/\n    verify: http\n\n  - title: Homebridge\n    address: http://10.0.0.173:8581/\n    verify: http\n\n- group: websites\n  links:\n  - title: Spotweb\n    address: http://10.0.0.171:81\n    verify: http\n\n  - title: Proxmox\n    address: https://10.0.0.99:8006\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: NZBGET\n    address: http://10.0.0.171:6789/\n    verify: http\n\n- group: hosts\n  links:\n  - title: Internet\n    address: google.com\n    verify: ping\n    \n  - title: MQTT\n    address: 10.0.0.172\n    verify: ping\n\n  - title: Proxmox\n    address: 10.0.0.99\n    verify: ping","output":"yaml","x":220,"y":860,"wires":[[]]},{"id":"c6abd655fc5fee6a","type":"group","z":"51924a530596d728","name":"","style":{"fill":"#e3f3d3","fill-opacity":"0.22","label":true},"nodes":["5a3e7b8657c32c45","e3d3cc939bec6251","f13a0d70525cde3e","78ce0ef772483a26","615bccfd4d9e541f"],"x":48,"y":899,"w":654,"h":508},{"id":"5a3e7b8657c32c45","type":"comment","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"http://<node-red-ip>:1880/bookmarks","info":"","x":230,"y":940,"wires":[]},{"id":"e3d3cc939bec6251","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"websocket","style":{"label":true},"nodes":["f848bb13e7ad35ed","17fc32a711331bd6","494b536c518dc7b4","0925a188e187b056"],"x":74,"y":979,"w":202,"h":122},{"id":"f848bb13e7ad35ed","type":"websocket in","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":115,"y":1060,"wires":[["494b536c518dc7b4"]],"l":false},{"id":"17fc32a711331bd6","type":"websocket out","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":235,"y":1040,"wires":[],"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":165,"y":1060,"wires":[["17fc32a711331bd6"]],"l":false},{"id":"0925a188e187b056","type":"inject","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"bookmarks1234","payloadType":"flow","x":165,"y":1020,"wires":[["17fc32a711331bd6"]],"l":false},{"id":"70969700cfe06176","type":"websocket-listener","path":"/ws/bookmarks1234","wholemsg":"false"},{"id":"f13a0d70525cde3e","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"Endpoint","style":{"label":true},"nodes":["721be53de5e89b6d","6a1b42ca5d5f046b","71b1978f3f988c47"],"x":74,"y":1119,"w":332,"h":82},{"id":"721be53de5e89b6d","type":"http in","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","url":"/bookmarks","method":"get","upload":false,"swaggerDoc":"","x":180,"y":1160,"wires":[["71b1978f3f988c47"]]},{"id":"6a1b42ca5d5f046b","type":"http response","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","statusCode":"","headers":{},"x":365,"y":1160,"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    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\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] {\n        display: none !important;\n      }\n    </style>\n  </head>\n  <body class=\"bg-gray-700\">\n    <div x-data=\"data()\" x-cloak class=\"max-w-5xl mx-auto px-2 md:p-12\">\n      <div class=\"uppercase text-2xl md:text-4xl text-gray-200 md:mb-8\">\n        Bookmarks\n      </div>\n      <template x-for=\"group in bookmarks\">\n        <div>\n          <div\n            x-text=\"group.group\"\n            class=\"uppercase w-full text-gray-200 mt-4 mb-2\"\n          ></div>\n          <div\n            class=\"mb-2 md:mb-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 lg:grid-cols-5\"\n          >\n            <template x-for=\"bookmark in group.links\">\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\n                    x-text=\"getHostname(bookmark.address)\"\n                    class=\"text-gray-300/70 text-xs uppercase\"\n                  ></div>\n                </div>\n              </div>\n            </template>\n          </div>\n        </div>\n      </template>\n    </div>\n\n    <script>\n      let bookmarks = []\n\n      function data() {\n        return {\n          bookmarks,\n\n          init() {\n            this.ws = new WebSocket(\n               location.origin.replace(/^http/, 'ws') + '/ws/bookmarks1234'\n            )\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              this.bookmarks = input\n            }\n          },\n          getHostname(u) {\n            if (u.includes('http')) {\n              const url = new URL(u)\n              return url.hostname\n            } else {\n              return u\n            }\n          },\n        }\n      }\n    </script>\n  </body>\n</html>\n","output":"str","x":305,"y":1160,"wires":[["6a1b42ca5d5f046b"]],"l":false},{"id":"78ce0ef772483a26","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"socket status","style":{"label":true},"nodes":["23e1cf1b916c387d","cfe354a9c610ac7b","dc0a7d4f5672088e"],"x":304,"y":979,"w":182,"h":82},{"id":"23e1cf1b916c387d","type":"status","z":"51924a530596d728","g":"78ce0ef772483a26","name":"","scope":["f848bb13e7ad35ed"],"x":345,"y":1020,"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":395,"y":1020,"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":445,"y":1020,"wires":[[]],"l":false},{"id":"615bccfd4d9e541f","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["5917c6bef7a0e367","e6d9e6dfa78ec798","cd83f280a3d9a03a","cb37d04ec1131097","b8c50e3ed1d5ed70","67f645a4257dc589","5b711266e895b0c1","edfea405b3957ab8","2c56c0f90409cf67","08af360f3852eb2f","83966505ef1db220","8cc88c963105dafe","061733d53aca65c4","0bfa5136523abd79"],"x":74,"y":1219,"w":602,"h":162},{"id":"5917c6bef7a0e367","type":"inject","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":135,"y":1280,"wires":[["0bfa5136523abd79"]],"l":false},{"id":"e6d9e6dfa78ec798","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"bookmarks1234","tot":"flow"},{"t":"set","p":"payload","pt":"msg","to":"payload.links[verify!= ''].{     \"address\": address,     \"verify\": verify }","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":235,"y":1280,"wires":[["cd83f280a3d9a03a"]],"l":false},{"id":"cd83f280a3d9a03a","type":"split","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":295,"y":1280,"wires":[["67f645a4257dc589"]],"l":false},{"id":"cb37d04ec1131097","type":"switch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"verify type","property":"payload.verify","propertyType":"msg","rules":[{"t":"eq","v":"http","vt":"str"},{"t":"eq","v":"ping","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":405,"y":1280,"wires":[["2c56c0f90409cf67"],["b8c50e3ed1d5ed70"]],"l":false},{"id":"b8c50e3ed1d5ed70","type":"exec","z":"51924a530596d728","g":"615bccfd4d9e541f","command":"ping -c 2 -w1","addpay":"payload.address","append":"| grep -E -o '[0-9]+ received' | cut -f1 -d ' '","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":455,"y":1320,"wires":[["5b711266e895b0c1"],[],[]],"l":false},{"id":"67f645a4257dc589","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"tmp","pt":"msg","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":345,"y":1280,"wires":[["cb37d04ec1131097"]],"l":false},{"id":"5b711266e895b0c1","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","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":515,"y":1300,"wires":[["061733d53aca65c4"]],"l":false},{"id":"edfea405b3957ab8","type":"http request","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","persist":false,"insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":515,"y":1260,"wires":[["08af360f3852eb2f"]],"l":false},{"id":"2c56c0f90409cf67","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"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":455,"y":1260,"wires":[["edfea405b3957ab8"]],"l":false},{"id":"08af360f3852eb2f","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"tmp.reachable","pt":"msg","to":"$type(statusCode)=\"number\" ? true:false ","tot":"jsonata"},{"t":"move","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":575,"y":1260,"wires":[["061733d53aca65c4"]],"l":false},{"id":"83966505ef1db220","type":"status","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","scope":["edfea405b3957ab8"],"x":135,"y":1340,"wires":[[]],"l":false},{"id":"8cc88c963105dafe","type":"catch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","scope":["edfea405b3957ab8"],"uncaught":false,"x":185,"y":1340,"wires":[[]],"l":false},{"id":"061733d53aca65c4","type":"function","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"recreate flow.bookmarks1234","func":"const input = msg.payload\n\nconst groups = flow.get(\"bookmarks1234\")\n\ngroups.forEach(group =>{\n        group.links.forEach(link=>{\n                if(link.address == input.address){\n                        link.reachable = input.reachable\n                }\n        })\n})\nflow.set(\"bookmarks1234\",groups)\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":635,"y":1300,"wires":[[]],"l":false},{"id":"0bfa5136523abd79","type":"switch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","property":"bookmarks_socket_connected","propertyType":"flow","rules":[{"t":"eq","v":"connect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":185,"y":1280,"wires":[["e6d9e6dfa78ec798"]],"l":false}]

Note that I have remove the distinction between (un)reachable hosts and websites, you can make a separate category for them, if there http in the address it will be a link else no action.

4 Likes

Brilliant - thanks for sharing your flow.

1 Like

Love it! Thanks.

Some targets can be valid targets of either ping or html validation. For example my router is 192.168.1.1 and it's admin UI is at http://192.168.1.1.
If I happen to set the target as http://192.168.1.1 and verification ping, I get an error

Invalid JSONata expression: Unable to cast value to a number: ""

I have inserted a change node to remove any leading http[s]:// before the ping validation to avoid this error. (Of course I know ping http://192.168.1.1 is an invalid command and I shouldn't have specified it!)

1 Like

Very. very nice!! Now I can have links to all my devices in one place!

1 Like

Ah indeed, I was a bit too rigorous in my cleanup, you can add a function node between the switch/exec, contents:

const input = msg.payload
if(input.address.startsWith("http")){
    const address = input.address.match(/(?:\w+\.)+\w+/)[0]
    msg.payload.address = address 
}
return msg;

image

I added the ping vs http, because not all my devices have web interfaces or they do and I want to check them both (some flexibility).

Also note; if you add to your homescreen on your phone (iphone only i think) it will act like an app.

1 Like

Some additional features; customization of colors, border, gaps, hiding of the title and address.

This is done using an additional "theme" template node with yaml formatted contents:

title_text: 'Servers'
title: 'show'
background: 'bg-zinc-800'
item: 'bg-[#9ba6a5]/50'
gap: 'gap-[1px]'
hover: 'bg-[#aeccc6]/60'
edge: ''
address: 'show'

The color/gap/edge names come from tailwind, docs and can also be arbitrary with the bracket notation [value]. (the bg-[value]/<number> number means opacity)

I have added 3 examples in the flow, just connect (only 1) and see the difference.
Note that all defined elements are required.

image

updated flow:

[{"id":"54225ebd4d56d002","type":"inject","z":"51924a530596d728","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":105,"y":880,"wires":[["ab4e6eab906a19c4","e91af064993c144f"]],"l":false},{"id":"ab4e6eab906a19c4","type":"template","z":"51924a530596d728","name":"Bookmarks","field":"bookmarks1234","fieldType":"flow","format":"handlebars","syntax":"mustache","template":"- group: connectivity\n  links:\n  - title: router\n    address: http://10.0.0.1\n    verify: ping\n\n  - title: unifi\n    address: https://10.0.0.180:8443\n    verify: http\n\n- group: servers\n  links:\n  - title: Proxmox\n    address: https://10.0.0.99:8006\n    verify: http\n\n- group: home automation\n  links:\n  - title: Node-RED\n    address: http://10.0.0.170:1880\n    verify: http\n\n  - title: Zigbee2mqtt\n    address: http://10.0.0.170:8080/\n    verify: http\n\n  - title: Homebridge\n    address: http://10.0.0.173:8581/\n    verify: http\n    actions:\n      - restart\n\n- group: websites\n  links:\n  - title: Spotweb\n    address: http://10.0.0.171:81\n    verify: http\n    \n  - title: Grafana\n    address: http://10.0.0.175:3000/\n    verify: http\n\n  - title: NZBGET\n    address: http://10.0.0.171:6789/\n    verify: http\n\n- group: hosts\n  links:\n  - title: Internet\n    address: google.com\n    verify: ping\n    \n  - title: MQTT\n    address: 10.0.0.172\n    verify: ping\n\n  - title: Proxmox\n    address: 10.0.0.99\n    verify: ping","output":"yaml","x":230,"y":940,"wires":[[]]},{"id":"e91af064993c144f","type":"template","z":"51924a530596d728","name":"theme: grayish","field":"theme1234","fieldType":"flow","format":"handlebars","syntax":"mustache","template":"# for color/gap/rounded reference see https://tailwindcss.com/docs/\n# title_text: 'Bookmarks' \n# title: 'show'             option: hide\n# background: 'bg-gray-700' option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# item: 'bg-gray-500/40'    option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# gap: \"gap-2\"              option: gap-[1px] arbitrary or gap-3 etc\n# hover: 'bg-gray-500/80'   option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# edge: 'rounded'           option: '' for no rounded edges or, rounded-sm/md/lg/xl\n# address: 'show'           option: hide\n\ntitle_text: 'Bookmarks'\ntitle: 'show'\nbackground: 'bg-gray-700'\nitem: 'bg-gray-500/40'\ngap: 'gap-2'\nhover: 'bg-gray-500/80'\nedge: 'rounded'\naddress: 'show'\n\n\n\n","output":"yaml","x":240,"y":900,"wires":[[]]},{"id":"cb0c3e4291add6f8","type":"template","z":"51924a530596d728","name":"theme: blueish","field":"theme1234","fieldType":"flow","format":"handlebars","syntax":"mustache","template":"# for color/gap/rounded reference see https://tailwindcss.com/docs/\n# title_text: 'Bookmarks' \n# title: 'show'             option: hide\n# background: 'bg-gray-700' option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000] include /<number> for opacity\n# item: 'bg-gray-500/40'    option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# gap: \"gap-2\"              option: gap-[1px] arbitrary or gap-3 etc\n# hover: 'bg-gray-500/80'   option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# edge: 'rounded'           option: '' for no rounded edges or, rounded-sm/md/lg/xl\n# address: 'show'           option: hide\n\ntitle_text: 'Bookmarks'\ntitle: 'hide'\nbackground: 'bg-[#142d4c]'\nitem: 'bg-[#385170]'\ngap: \"gap-[1px]\"\nhover: 'bg-[#9fd3c7]/60'\nedge: ''\naddress: 'hide'\n\n\n\n","output":"yaml","x":240,"y":860,"wires":[[]]},{"id":"c9ad46093bde0287","type":"template","z":"51924a530596d728","name":"theme: greenish","field":"theme1234","fieldType":"flow","format":"handlebars","syntax":"mustache","template":"# for color/gap/rounded reference see https://tailwindcss.com/docs/\n# title_text: 'Bookmarks' \n# title: 'show'             option: hide\n# background: 'bg-gray-700' option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000] include /<number> for opacity\n# item: 'bg-gray-500/40'    option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# gap: \"gap-2\"              option: gap-[1px] arbitrary or gap-3 etc\n# hover: 'bg-gray-500/80'   option: bg-<color name>-<variant> or arbitrary: bg-[#ff0000]\n# edge: 'rounded'           option: '' for no rounded edges or, rounded-sm/md/lg/xl\n# address: 'show'           option: hide\n\ntitle_text: 'Servers'\ntitle: 'show'\nbackground: 'bg-zinc-800'\nitem: 'bg-[#9ba6a5]/50'\ngap: \"gap-[1px]\"\nhover: 'bg-[#aeccc6]/60'\nedge: ''\naddress: 'show'\n\n\n\n","output":"yaml","x":240,"y":820,"wires":[[]]},{"id":"c6abd655fc5fee6a","type":"group","z":"51924a530596d728","name":"","style":{"fill":"#e3f3d3","fill-opacity":"0.22","label":true},"nodes":["5a3e7b8657c32c45","e3d3cc939bec6251","f13a0d70525cde3e","78ce0ef772483a26","615bccfd4d9e541f"],"x":48,"y":979,"w":654,"h":515.5},{"id":"5a3e7b8657c32c45","type":"comment","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"http://<node-red-ip>:1880/bookmarks","info":"","x":230,"y":1020,"wires":[]},{"id":"e3d3cc939bec6251","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"websocket","style":{"label":true},"nodes":["f848bb13e7ad35ed","17fc32a711331bd6","494b536c518dc7b4","0925a188e187b056"],"x":74,"y":1059,"w":252,"h":122},{"id":"f848bb13e7ad35ed","type":"websocket in","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":115,"y":1140,"wires":[["494b536c518dc7b4"]],"l":false},{"id":"17fc32a711331bd6","type":"websocket out","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","server":"70969700cfe06176","client":"","x":285,"y":1120,"wires":[],"l":false},{"id":"494b536c518dc7b4","type":"change","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"payload.bookmarks","pt":"msg","to":"bookmarks1234","tot":"flow"},{"t":"set","p":"payload.theme","pt":"msg","to":"theme1234","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":185,"y":1140,"wires":[["17fc32a711331bd6"]],"l":false},{"id":"0925a188e187b056","type":"inject","z":"51924a530596d728","g":"e3d3cc939bec6251","name":"","props":[{"p":"payload.bookmarks","v":"bookmarks1234","vt":"flow"},{"p":"payload.theme","v":"theme1234","vt":"flow"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":185,"y":1100,"wires":[["17fc32a711331bd6"]],"l":false},{"id":"70969700cfe06176","type":"websocket-listener","path":"/ws/bookmarks1234","wholemsg":"false"},{"id":"f13a0d70525cde3e","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"Endpoint","style":{"label":true},"nodes":["721be53de5e89b6d","6a1b42ca5d5f046b","71b1978f3f988c47"],"x":74,"y":1199,"w":332,"h":82},{"id":"721be53de5e89b6d","type":"http in","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","url":"/bookmarks","method":"get","upload":false,"swaggerDoc":"","x":180,"y":1240,"wires":[["71b1978f3f988c47"]]},{"id":"6a1b42ca5d5f046b","type":"http response","z":"51924a530596d728","g":"f13a0d70525cde3e","name":"","statusCode":"","headers":{},"x":365,"y":1240,"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] {\n        display: none !important;\n      }\n    </style>\n  </head>\n  <body :class=\"theme.background\" x-data=\"data()\">\n    <div x-cloak class=\"max-w-5xl mx-auto px-2 md:p-12\">\n      <div\n        class=\"uppercase text-2xl md:text-4xl text-gray-200 md:mb-8\"\n        x-text=\"theme.title_text\"\n        x-show=\"theme?.title !== 'hide'\"\n      ></div>\n      <template x-for=\"group in bookmarks\">\n        <div>\n          <div\n            x-text=\"group.group\"\n            class=\"uppercase w-full text-gray-200 mt-4 mb-2\"\n          ></div>\n          <div\n            class=\"mb-2 md:mb-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5\"\n            :class=\"theme.gap\"\n          >\n            <template x-for=\"bookmark in group.links\">\n              <div\n                class=\"group flex gap-2 p-2 text-gray-200 leading-tight\"\n                :class=\"bookmark.address.includes('http') ? `hover:${theme.hover} ${theme.edge} ${theme.item} cursor-pointer hover:text-gray-50` : `${theme.item}  ${theme.edge}`\"\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 class=\"w-full\">\n                  <div\n                    x-text=\"bookmark.title\"\n                    class=\"uppercase\"\n                    @click=\"if(bookmark.address.includes('http')) window.open(bookmark.address)\"\n                  ></div>\n\n                  <div\n                    x-text=\"getHostname(bookmark.address)\"\n                    class=\"text-gray-300/70 text-xs uppercase\"\n                    :class=\"theme.address == 'show' ? 'block': 'hidden'\"\n                  ></div>\n                </div>\n              </div>\n            </template>\n          </div>\n        </div>\n      </template>\n    </div>\n\n    <script>\n      let bookmarks = []\n      let theme = {}\n\n      function data() {\n        return {\n          theme,\n          bookmarks,\n\n          init() {\n          \n            this.ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '/ws/bookmarks1234')\n  \n            this.ws.onopen = event => {\n              this.ws.send(JSON.stringify('init'))\n            }\n            this.ws.onmessage = event => {\n              const input = JSON.parse(event.data)\n              this.bookmarks = input.bookmarks\n              if (input.hasOwnProperty('theme')) {\n                this.theme = input.theme\n              }\n              //  console.log(input)\n            }\n          },\n          getHostname(u) {\n            if (u.includes('http')) {\n              const url = new URL(u)\n              return url.hostname\n            } else {\n              return u\n            }\n          },\n          sendAction(bookmark, action) {\n            const msg = JSON.stringify({ action: action, bookmark })\n            this.ws.send(msg)\n          },\n        }\n      }\n    </script>\n  </body>\n</html>\n","output":"str","x":305,"y":1240,"wires":[["6a1b42ca5d5f046b"]],"l":false},{"id":"78ce0ef772483a26","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","name":"socket status","style":{"label":true},"nodes":["23e1cf1b916c387d","cfe354a9c610ac7b","dc0a7d4f5672088e"],"x":364,"y":1059,"w":182,"h":82},{"id":"23e1cf1b916c387d","type":"status","z":"51924a530596d728","g":"78ce0ef772483a26","name":"","scope":["f848bb13e7ad35ed"],"x":405,"y":1100,"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":455,"y":1100,"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":505,"y":1100,"wires":[[]],"l":false},{"id":"615bccfd4d9e541f","type":"group","z":"51924a530596d728","g":"c6abd655fc5fee6a","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["5917c6bef7a0e367","e6d9e6dfa78ec798","cd83f280a3d9a03a","cb37d04ec1131097","b8c50e3ed1d5ed70","67f645a4257dc589","5b711266e895b0c1","edfea405b3957ab8","2c56c0f90409cf67","08af360f3852eb2f","83966505ef1db220","8cc88c963105dafe","061733d53aca65c4","0bfa5136523abd79","d63f339ad0f3a804"],"x":74,"y":1299,"w":602,"h":169.5},{"id":"5917c6bef7a0e367","type":"inject","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":135,"y":1360,"wires":[["0bfa5136523abd79"]],"l":false},{"id":"e6d9e6dfa78ec798","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"bookmarks1234","tot":"flow"},{"t":"set","p":"payload","pt":"msg","to":"payload.links[verify!= ''].{     \"address\": address,     \"verify\": verify }","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":235,"y":1360,"wires":[["cd83f280a3d9a03a"]],"l":false},{"id":"cd83f280a3d9a03a","type":"split","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":295,"y":1360,"wires":[["67f645a4257dc589"]],"l":false},{"id":"cb37d04ec1131097","type":"switch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"verify type","property":"payload.verify","propertyType":"msg","rules":[{"t":"eq","v":"http","vt":"str"},{"t":"eq","v":"ping","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":395,"y":1360,"wires":[["2c56c0f90409cf67"],["d63f339ad0f3a804"]],"l":false},{"id":"b8c50e3ed1d5ed70","type":"exec","z":"51924a530596d728","g":"615bccfd4d9e541f","command":"ping -c 2 -w1","addpay":"payload.address","append":"| grep -E -o '[0-9]+ received' | cut -f1 -d ' '","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"","x":465,"y":1420,"wires":[["5b711266e895b0c1"],[],[]],"l":false},{"id":"67f645a4257dc589","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"tmp","pt":"msg","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":345,"y":1360,"wires":[["cb37d04ec1131097"]],"l":false},{"id":"5b711266e895b0c1","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","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":515,"y":1380,"wires":[["061733d53aca65c4"]],"l":false},{"id":"edfea405b3957ab8","type":"http request","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","method":"GET","ret":"txt","paytoqs":false,"url":"","persist":false,"insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":515,"y":1340,"wires":[["08af360f3852eb2f"]],"l":false},{"id":"2c56c0f90409cf67","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"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":455,"y":1340,"wires":[["edfea405b3957ab8"]],"l":false},{"id":"08af360f3852eb2f","type":"change","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","rules":[{"t":"set","p":"tmp.reachable","pt":"msg","to":"$type(statusCode)=\"number\" ? true:false ","tot":"jsonata"},{"t":"move","p":"tmp","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":575,"y":1340,"wires":[["061733d53aca65c4"]],"l":false},{"id":"83966505ef1db220","type":"status","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","scope":["edfea405b3957ab8"],"x":135,"y":1420,"wires":[[]],"l":false},{"id":"8cc88c963105dafe","type":"catch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","scope":["edfea405b3957ab8"],"uncaught":false,"x":185,"y":1420,"wires":[[]],"l":false},{"id":"061733d53aca65c4","type":"function","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"recreate flow.bookmarks1234","func":"const input = msg.payload\n\nconst groups = flow.get(\"bookmarks1234\")\n\ngroups.forEach(group =>{\n        group.links.forEach(link=>{\n                if(link.address == input.address){\n                        link.reachable = input.reachable\n                }\n        })\n})\nflow.set(\"bookmarks1234\",groups)\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":635,"y":1380,"wires":[[]],"l":false},{"id":"0bfa5136523abd79","type":"switch","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"","property":"bookmarks_socket_connected","propertyType":"flow","rules":[{"t":"eq","v":"connect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":185,"y":1360,"wires":[["e6d9e6dfa78ec798"]],"l":false},{"id":"d63f339ad0f3a804","type":"function","z":"51924a530596d728","g":"615bccfd4d9e541f","name":"function 27","func":"const input = msg.payload\nif(input.address.startsWith(\"http\")){\n    const address = input.address.match(/(?:\\w+\\.)+\\w+/)[0]\n    msg.payload.address = address \n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":405,"y":1420,"wires":[["b8c50e3ed1d5ed70"]],"l":false}]
2 Likes