Long overdue home dashboard revamp using UIBUILDER (Work in progress)

Been meaning to get round to this for a looooooong time. Christmas break seems like a good time to make a start.

Only started today so not a lot to show. But this new version uses UIBUILDER v6.7 (current live version) to make sure I'm finding any issues that others might also hit. Importantly, it uses the new front-end router library which really does make front-end development a lot easier I've discovered - who knew! :sunglasses:

Will try to share progress as I go along.

Right now, not much to see:
Animation1

Only 1 sensor platform currently showing (and the light sensor seems to have stopped working! That is a cached value). You can hover over the numbers to see the last update timestamp and you can collapse/expand each floor. The menu is rubbish of course, not yet formatted.

The flow to manage everything is this:

This is the function node:

function dp(inp, dp, int) {
    if (!int) int = 'en-GB'
    if (!dp) dp = 1
    return inp.toLocaleString(int, { 'minimumFractionDigits': dp, 'maximumFractionDigits': dp })
}

// What output to change in the web page
switch (msg.topic) {
    case 'ESP/m5basic01/BH1750/Lux': {
        msg.outputId = '#office-lux'
        break
    }
    case 'ESP/m5basic01/SHT30/Temperature': {
        msg.outputId = '#office-temp'
        break
    }
    case 'ESP/m5basic01/SHT30/Humidity': {
        msg.outputId = '#office-hum'
        break
    }
    default: {
        return // no output
    }
}

// Format the output
msg.payload = dp(msg.payload)
// Last update date/time
msg.update = new Date()

return msg

And this is the configuration of the uib-update node:

So nice and efficient.

Minimal front-end code though currently the page is manually coded, there is automation that could be done but not much incentive right now as I've really only a few things to show. The router config, menu, floor and room layouts could all be data-driven from Node-RED for sure but I want to spend more time creating the display than messing with over-optimising this time. :slight_smile:

Couple of hours work so far and nearly all of that was messing with the HTML and CSS to get the nested, collapsible grid working properly.

Doubtless much more to come


index.html

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" href="../uibuilder/images/node-blue.ico">
    
    <title>Home Panel</title>
    <meta name="description" content="Home Panel">
    
    <!-- Your own CSS (defaults to loading uibuilders css)-->
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">
    
    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->
    <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
    <script defer src="../uibuilder/utils/uibrouter.iife.min.js"></script>
    <script defer src="./index.js">
        /* OPTIONAL: Put your custom code in that */
    </script>
    <!-- #endregion -->

</head><body class="uib">

    <h1 class="with-subtitle">Home Panel</h1>
    <div role="doc-subtitle">Using the UIBUILDER IIFE &amp; router libraries</div>
    
    <nav id="menu" aria-labelledby="primary-navigation">
        <h2>Route Menu</h2>
        <ul id="routemenu" aria-describedby="menu">
            <li><a href="#route01">Home</a></li>
            <li><a href="#wanted">Wanted</a></li>
        </ul>
    </nav>

    <div id="more">
        <!-- '#more' is used as a parent for dynamic HTML content in examples -->
    </div>

</body></html>

index.js

'use strict'

const routerConfig = {
    defaultRoute: 'route01',
    // Stops routes with scripts having them re-run but may leave the page bigger in memory
    hide: true,
    routes: [
        { id: 'route01', src: './fe-routes/route01.html', type: 'url', description: 'Boom' },
        { id: 'wanted', src: './fe-routes/wanted.html', type: 'url', description: 'Bam' },
    ],
}

const router = new UibRouter(routerConfig)

fe-routes/route01.html

<style>
    .house h3, .house h2 {
        margin: 0;
    }
    .house summary {
        /* list-style: none; */ /* OR display: block; */
        grid-column: 1 / -1;
        cursor: pointer;
    }
    /* .house summary::-webkit-details-marker { display: none } */
    summary > h2 {
        display:inline-block
    }
    .floor {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        gap: 0;
        border: 1px solid hsl(60, 100%, 18%);
    }
    .floor > div {
        border: 1px solid var(--text3);
        border-radius: var(--border-radius);
        margin: 0.5rem;
        padding: 0.5rem;
        
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 0;
        align-content: start;
    }
    .floor > div :first-child {
        grid-column: 1 / -1;
        border-bottom: 1px solid var(--text4);
    }
    .floor > div > div:nth-child(odd) {
        text-align: right;
    }
</style>

<div id="rooms" class="house">

    <details id="floor0" open>
        <summary role="heading" aria-level="2"><h2>Ground Floor</h2></summary>
        <div id="floor0" class="floor">
            <div id="dining">
                <h3>Dining Room</h3>
                <div>℃</div><div id="dining-temp">--</div>
                <div>%H</div><div id="dining-hum">--</div>
            </div>
            <div id="living">
                <h3>Living Room</h3>
                <div>℃</div><div id="living-temp">--</div>
                <div>%H</div><div id="living-hum">--</div>
            </div>
            <div id="kitchen">
                <h3>Kitchen</h3>
                <div>℃</div><div id="kitchen-temp">--</div>
                <div>%H</div><div id="kitchen-hum">--</div>
            </div>
            <div id="fronthall">
                <h3>Front Hall</h3>
                <div>℃</div><div id="fronthall-temp">--</div>
                <div>%H</div><div id="fronthall-hum">--</div>
            </div>
            <div id="rearhall">
                <h3>Rear Hall</h3>
                <div>℃</div><div id="rearhall-temp">--</div>
                <div>%H</div><div id="rearhall-hum">--</div>
                <div>Lux</div><div id="rearhall-lux">--</div>
            </div>
        </div>
    </details>
    
    <details id="floor1" open>
        <summary role="heading" aria-level="2"><h2>First Floor</h2></summary>
        <div class="floor">
            <div id="masterbed">
                <h3>Master Bedroom</h3>
                <div>℃</div><div id="masterbed-temp">--</div>
                <div>%H</div><div id="masterbed-hum">--</div>
            </div>
            <div id="bed2">
                <h3>Bedroom 2</h3>
                <div>℃</div><div id="bed2-temp">--</div>
                <div>%H</div><div id="bed2-hum">--</div>
            </div>
            <div id="office">
                <h3>Office</h3>
                <div>℃</div><div id="office-temp">--</div>
                <div>%H</div><div id="office-hum">--</div>
                <div>Lux</div><div id="office-lux">--</div>
            </div>
            <div id="bath1">
                <h3>Bathroom</h3>
                <div>℃</div><div id="bath1-temp">--</div>
                <div>%H</div><div id="bath1-hum">--</div>
            </div>
        </div>
    </details>
</div>
3 Likes

Not had much time to work on this but I've improved the look of the default page slightly - specially for mobile use. More rooms with the ones without current data collapsed & the card format aligned to the system status route.

Some may recognise this second route from a previous post a while back. Each card shows the status (the red pulses slightly for emphasis) &, where relevant, the cards are clickable which takes you to the specific web page for the service or device.

The flow isn't that much more complex:

Note that the structural part for the system status route is generated from a saved flow variable via a template node, a uib-html and a uib-save node to update the physical route file. The same flow var is also used to subscribe to the appropriate MQTT topics which are fed to the uib-update node and out to uibuilder. So the structural part only needs running when you want to change the structure or what is being monitored. The actual data is updated as MQTT gets its updates dynamically.

Next up will be the lighting controls.

2 Likes

Fantastic work @TotallyInformation - really nice :clap:

But I must say, there is a hint of outlook in this design :wink:

Ouch! Low blow! :rofl:

Actually, that wasn't the inspiration - of course, there was an inspiration, I forget where now but I've reused this type of display a few times & there is CSS for it in the uib-brand.css file.

At some point, I need to do an advanced version where the side-bar is actually a gauge instead of just green/red. Probably want a RAG (Red/Amber/Green) version too since that would be pretty handy for some project reporting.

2 Likes

Actually, I'm trying to think If I have it mixed up with PRTG Network Monitor
(which is great BTW - we use it A LOT of critical service monitoring for various sites across the UK)

Still - your design looks super clean :sunglasses:

2 Likes

I need to get to grips with some simple charts and sparklines. I keep putting it off but I'll certainly need it soon.

1 Like

IMO (and looking at your design) adding a border-radius is like a get-out-of-jail card :laughing:

Adding one always makes things better.

One of my products (hid the name for sake of not flaunting)
when I try to come up with a design , a border radius always adds that slight edge

2 Likes

Yes, it usually makes things look just that little more professional.

Your page looks good. Like the icons.

1 Like

Some updates - and I've had to move over to my dev machine and the dev version of UIBUILDER as I realised some shortcomings in the initial release of the router library. So working on both in parallel.

Animation1

Note that the page title updates as you change routes. That uses the <uib-var> custom web component that is built-in to uibuilder. Route changes now set some uibuilder managed variables automatically.

Also note the changes to the route menu. Not particularly happy with the styling yet but the base CSS is good and another update to the router library automatically adds class="currentRoute" and aria-current="page" to the current route menu item for ALL menu's on page (as long as you use the recommended menu structure - something that I will doubtless add a fn to the router for).

And finally note that the To Do list has a handler that rotates the list bullets between :x:, :heavy_check_mark:, and :white_check_mark:. The code for that is dead simple and not reliant on uibuilder at all. I've added a debounce to help ensure double-clicks don't also trigger single-click processing. I'm probably going to use this list to experiment with some more advanced event handling and it is reasonably likely that such handling will eventually make it into the uibuilder front-end library.

Here is the full code for the To Do list route:

    function checklistEventHander(event) {
        const target = event.target
        if (target.tagName === 'LI') {
            switch (target.className) {
                case 'started': {
                    target.classList.remove('started')
                    target.classList.add('completed')                    
                    break
                }
            
                case 'unstarted': {
                    target.classList.remove('unstarted')
                    target.classList.add('started')
                    break
                }
            
                case 'completed': {
                    target.classList.remove('completed')
                    target.classList.add('unstarted')
                    break
                }
            
                default: {
                    break
                }
            }
        }
    }
    // debounce single-clicks so that double-clicks don't also trigger singles
    const maxMsBetweenClicks = 250
    let clickTimeoutId
    function handleSingleClick(e) { 
        clearTimeout(clickTimeoutId)
        clickTimeoutId = setTimeout( function() { checklistEventHander(e);}, maxMsBetweenClicks)
    }    
    function handleDoubleClick(e) { 
        clearTimeout(clickTimeoutId)
        checklistEventHander(e)
    }
    function checklistHandler(cssSelector) {
        const li = document.querySelector(cssSelector)
        li.addEventListener('click', handleSingleClick)
        li.addEventListener('dblclick', handleDoubleClick)
        li.addEventListener('contextmenu', checklistEventHander)
        li.addEventListener('wheel', checklistEventHander)
    }
    checklistHandler('#wishlist')
</script>

<h2>Things I want in this app</h2>
<ul id="wishlist" class="checklist">
    <li class="started"><a href="#lights">Lighting</a></li>
    <li class="started"><a href="#status01">Network device list</a></li>
    <li class="started"><a href="#status01">Links to device & zigbee2mqtt pages</a></li>
    <li class="started"><a href="#route01">Heating</a></li>
    <li class="unstarted">Battery state</li>
    <li class="unstarted">npm outdated</li>
    <li class="unstarted">docker-compose</li>
    <li class="unstarted">zigbee2mqtt config</li>
    <li class="unstarted">MQTT Explorer</li>
    <li class="unstarted">Package stats</li>
    <li class="unstarted">Links to my web pages & config pages</li>
    <li class="unstarted">Links to my GitHub repos and pages</li>
</ul>