New info page for my home dashboard

OK, so I've had some time off this week. Time to work on both uibuilder and my web components. :smiley:

But also time to do a bit of well overdue work on my home dashboard.

A new summary page is in the works:



Still more to do, but starting to take shape. Probably needs a bit of colour.

System status page is now a bit more immediate since I've moved from a single cache to multiples (one of the advantages of UIBUILDER is that you have that option).


For both routes, monitoring starts automatically and updates the respective uib-cache. Changing to the route triggers a cache replay so that you always get the latest data from Node-RED.

For the system status page, the group titled 1) ... can be triggered manually if the list of services to monitor is updated. It rebuilds the base page file. This means that even if monitoring a large number of services, the page loads almost instantly.

The walks route I've shared before:



The flow is simpler because it simply dumps the full data each time. Even with over 100 walks at the moment in the table, it still only takes a fraction of a second to rebuild.

Similarly for the npm stats page:


The source data for that is massive but currently I still just dump the whole thing to the front-end and process it there.

One day I'll make those last 2 a bit more efficient but they work just fine for now. :slight_smile:

4 Likes

I like the small 'network' panels, with a visual indicator to the left of the text.
Very smart!

What are you using, or is it html?

UIBUILDER, of course! :smiling_face_with_sunglasses:

The collapsible part is simple HTML:

    <details>
        <summary> role="heading" aria-level="2"><h2>Services</h2></summary>
        <div class="status-grid">
            <a href="https://xxxxxxxxxxxxxx:5001/" target="_blank" class="status-link">
            <div id="st-NAS" class="box flex surface4" data-topic="telegraf/ping/192.168.1.161" title="MQTT Topic: telegraf/ping/192.168.1.161">
                <div class="status-side-panel"></div>
                <div>
                    <div>NAS</div>
                    <div class="text-smaller">nas.knightnet.co.uk:5001</div>
                </div>
            </div>
            </a>
            <a href="https://xxxxxxx:1880/red/" target="_blank" class="status-link">
            <div id="st-nrmain" class="box flex surface4" data-topic="telegraf/http_response/https:__localhost:1880_test1" title="MQTT Topic: telegraf/http_response/https:__localhost:1880_test1">
                <div class="status-side-panel"></div>
                <div>
                    <div>Node-RED (Live)</div>
                    <div class="text-smaller">xxxxxxxxxxxxx:1880</div>
                </div>
            </div>
            </a>
            ....
        </div>
    </details>
    ....

Styling is mostly already included in uibuilder's default uib-brand.css file with just a couple of simple tweaks:

    .status summary {
        cursor: pointer;
    }
    .status summary > h2 {
        display:inline-block;
    }

The classes to get the fancy boxes are:

  • status-grid just sets up a grid that expands automatically based on a min individual content width of 14em with a .5rem gap.
  • box - gives a style very similar to how I've got the <article> tags styled.
  • flex - uses css flex for the content of the box.
  • surface4 is the background colour.
  • status-side-panel is the coloured vertical bar. The output from the flow includes a uib-update node that adds a msg._uib object to the sent msg. It uses JSONata to set both the CSS selector and the _uib object:
    "[data-topic=\"" & 	topic & 	"\"] .status-side-panel"
    
    {
     "class": payload in ["active", "Online", "online", true, 1, "1"] ? "success status-side-panel" : "error animate-pulse status-side-panel",
     "title": "Last update: " & $now(),
     "data-updated": $now(),
     "data-updatedBy": "monitor"
    }   
    
    Which sets the bar colour for each entry as well as the additional data.

Of course, that could have been done with a function node but I originally created it as a demo of just using node-red core nodes without JavaScript.

In fact, I originally created that layout style for a senior exec demo at work showing some very different data that supposedly some actual developers should have done but signally failed.

Incidentally, I stole borrowed-with-pride the box with the coloured status sidebar from someone else. :smiley:

I've been playing with it this afternoon, and so far have this...
boxxy
...same as you, just hmtl, css and a bit of javascript.

I'm starting to get the hang of it now :winking_face_with_tongue:

2 Likes

Great! Don't forget to also enjoy all the future-proofing you are adding to your UI's as well. :man_mage:

Oh, I should have said that there was some extra secret sauce in there that makes the red bars "breath" in and out - see if you can find it in the uib-brand.css file. :slight_smile:

1 Like

I'm using;
transition: background-color 0.3s; in my CSS.

...but I must try to start using the css files which you've already written...

.status-box {
  display: flex;
  align-items: center;
  background-color: #f8f8f8;
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 6px 10px;
  margin: 5px 0;
  font-family: sans-serif;
  width: fit-content;
}

.indicator {
  width: 20px;
  height: 40px;
  margin-right: 10px;
  border-radius: 4px;
  background-color: green; /* default */
  transition: background-color 0.3s;
}
1 Like

With my AI buddy, we've taken it a little further and converted the text box into a button...

boxxy2

2 Likes

Nice. Actually, I've been looking for some inspiration for buttons and lighting controls. Always looking for things I can turn into useful web components.

Can't resist tweaking my quick info page:

Looking at your nav bar..

Are you using different 'uibuilder nodes' to serve different pages, or different 'uib-save' nodes to load different profiles?
I'm using the latter option so far, which works great, but not sure if it's the favoured route.

I'm using the additional front-end router library that is included in uibuilder.

<script defer src="../uibuilder/utils/uibrouter.iife.min.js"></script>

Each route is loaded from an html partial file that can include styles, html and scripts. Here is the file for the quick info route:

<style>
    .hidden {
        display: none !important;
    }
    .door-open {
        background-color: var(--error);
    }
    .door-closed {
        background-color: var(--success);
        color: var(--text1);
        font-weight: bold;
    }
    #door-open-status {
        text-align: center;
        font-weight: bold;
        font-size: 1.5em;
        padding: 0.2em;
        margin-bottom: 0.2em;;
        border-radius: 0.5em;
    }
    #front-door > div > div {
        text-align: center;
    }
    .list-no-bullets {
        list-style: none;
        padding: 0;
        margin: 0;
    }
    .list-no-bullets > li {
        display: grid;
        grid-template-columns: 1fr 2fr;
        padding: 0;
        margin: 0;
    }
    .list-no-bullets > li > div:first-child {
        width:5.5em;
    }
    .list-no-bullets > li > div:last-child {
        width: 100%;
        text-align: center;
    }
    .status-grid > article{
        max-width: 18em;
    }
</style>

<section class="status-grid">
    <article id="front-door">
        <h2>Front Door</h2>
        <div id="door-open-status" style="width: 100%">Unknown</div>
        <ul class="list-no-bullets">
            <li><div>Bell</div><div id="bell-status">Unknown</div></li>
            <li><div>Opened</div><div id="door-last-opened">Unknown</div></li>
            <li><div>Closed</div><div id="door-last-closed">Unknown</div></li>
        </ul>
    </article>
    <article id="server-updates">
        <h2>Server Updates?</h2>
        <p class="hidden">None.</p>
        <ul id="npm-outdated" class="list-no-bullets">
            <li id="npm-outdated_global"><div>Global</div><div>None</div></li>
            <li id="npm-outdated_master"><div>Master</div><div>None</div></li>
            <li id="npm-outdated_user"><div>User</div><div>None</div></li>
            <li id="npm-outdated_uibuilder"><div>UIBUILDER</div><div>None</div></li>
        </ul>
    </article>
</section>

<script>
    'use strict'

    // Only ever run this once per page load
    if (!window.dashSetup) {
        console.log('Running initial setup for dash route - wont be run again until page reload')
        window.dashSetup = {}

        window.dashSetup.tidyDate = (dateStr) => {
            // Convert a date string to a more readable format
            const splitDate = dateStr.split('T')
            // if splitDate[0] is today, set it to 'Today' & if yesterday, set it to 'Yesterday'
            const today = new Date()
            const yesterday = new Date(today)
            yesterday.setDate(today.getDate() - 1)
            const dateObj = new Date(splitDate[0])
            if (dateObj.toDateString() === today.toDateString()) {
                splitDate[0] = 'Today'
            } else if (dateObj.toDateString() === yesterday.toDateString()) {
                splitDate[0] = 'Yesterday'
            } else {
                // Remove the year from splitDate[0]
                splitDate[0] = splitDate[0].replace(/^\d{4}-/, '')
            }
            // Remove the fractional seconds & trailing Z from splitDate[1]
            splitDate[1] = splitDate[1].replace(/\.\d+Z$/, '')
            return splitDate.join(' ')
        }

        /** Listener function to handle the 'sendStats' message from uibuilder
         * @param {object} msg - The message object containing route data
         */
        window.dashSetup.rcvDash = uibuilder.onTopic('sendDash', (msg) => {
            console.log('Received dash data:', msg)
            const fmt = uibuilder.formatNumber
            if (msg && msg.payload) {
                //
            } else {
                console.error('No data received in message:', msg)
            }
        })

        window.dashSetup.handleMsg = uibuilder.onChange('msg', (msg) => {
            let doorOpen = null
            const topic = msg.topic.replace('/', '_')
            let el = document.querySelector(`#${topic}`)

            switch (msg.topic) {
                case 'npm-outdated/master':
                case 'npm-outdated/global':
                case 'npm-outdated/user':
                case 'npm-outdated/uibuilder': {
                    const el2 = el.querySelector('div:last-child')
                    el2.innerHTML = msg.payload
                    if (msg.payload === 'None') {
                        el.classList.add('hidden')
                    } else {
                        el.classList.remove('hidden')
                    }
                    break
                }

                case 'bell-status': {
                    const el = document.querySelector(`#${msg.topic}`)
                    el.innerHTML = window.dashSetup.tidyDate(msg.payload)
                    break
                }

                case 'door-open-status': {
                    el.innerHTML = msg.payload
                    const pEl = el.parentElement
                    if (msg.payload === 'Open') {
                        doorOpen = true
                        el.classList.add('door-open')
                        el.classList.remove('door-closed')
                    } else if (msg.payload === 'Closed') {
                        el.classList.add('door-closed')
                        el.classList.remove('door-open')
                    } else {
                        el.classList.remove('door-open', 'door-closed')
                    }
                    break
                }

                case 'door-last-opened':
                case 'door-last-closed': {
                    el.innerHTML = window.dashSetup.tidyDate(msg.payload)
                    break
                }

                // Ignore anything else
                case 'sendDash':
                default: {
                    el = null
                    break
                }
            }

            if (el && msg.attributes) {
                // If there are attributes, add them to the element
                for (const [key, value] of Object.entries(msg.attributes)) {
                    el.setAttribute(key, value)
                }
            }
        })
    }
</script>

That looks complex!
Is there an optional no/low code alternative for us lightweights?
I assume that the no code version is using a different uibuilder node for each page, and referencing them in HTML as <li><a href="/charger">Charger</a></li> which make sense and is easy to implement.

Sorry for the delayed response, been out shopping with my daughter today. :smiley:

No more so than any other front-end router. It is similar to what both Dashboards do - its just that they hide things from you.

Well, uibuilder is never totally no-code of course - there is always a static template even if you never have/want to touch it directly.

But in essence, a multi-page app is often going to be simpler to create than a single-page app (using a front-end router). Currently, of course, neither of the Dashboard's can do multi-page (though it is on the backlog for D2 I believe).

The big difference from your perspective between MPA and SPA other than the complexity, is that with MPA, you are loading a new page each time and creating a new connection to Node-RED each time. Meaning that any shared resources must be re-sent from Node-RED on each page change, that is not always necessary for SPA's.

One day, I will get round to adding a web shared worker to the front-end code that will enable you to use a single shared connection while being able to use multi-page's. It has been on my mind for a long time (along with using the same feature to provide true offline use) but my skill levels haven't quite been up to it.

Update to the above statement: On further investigation, I'm reminded that I need a shared worker not a web worker. Web workers create new instances of the worker on each load so don't help share resources. Annoyingly, shared workers were only supported on Safari since 2022 (I try to target compatibility for early 2019 for now so that you aren't locked out of using old phones/tablets with uibuilder). Even more annoyingly, they still are not supported on Android. However, there is a polyfill library that I might look at when time permits.