/** This file is part of Reactor.
 *  Copyright 2020-2026 Kedron Holdings LLC, All Rights Reserved.
 *  Reactor is not public domain or open source. Distribution or derivative works are expressly prohibited.
 */

export const version = 26171;

const required_api_version = 26012;   /* Min version of client/ClientAPI.js */

const BUILD_VERSION = "latest-26177-bd65ecf5"; /* automatically set -- do not modify */

const LICENSE_VERSION = 23054;

import api from '/client/ClientAPI.js';

import * as Common from './reactor-ui-common.js';
const idSelector = Common.idSelector; /* freq use */

import alertWatcher from './alert-watcher.js';

import { _T, getLocale, _LD } from './i18n.js';

import Tab from "./tab.js";

const SMARTHOME_LANDING_URL = "https://smarthome.community/topic/768/category-topic-guide-read-before-posting";

export default (function($) {

    var current_tab = false;

    var MAP = {'doAboutTab':doAboutTab,'doToolsTab':doToolsTab};

    function TBD() {  // eslint-disable-line no-unused-vars
        Common.showSysModal({ title: "Coming Soon!", body: "This function has not yet been implemented"});
    }

    function getErrorTab( id, error ) {
        console.log("Creating error tab for",id,"error",error);
        let $tab = $( `div.re-tab-container#tab-${idSelector(id)}` );
        if ( 0 === $tab.length ) {
            const tab = new Tab( `tab-${id}`, $( 'div.re-ui-content' ) );
            $tab = tab.getTabElement();
        }
        $tab.empty().attr( "errortab", true );
        $( '<h1></h1>' )
            .text( _T(['#ui-tab-not-loaded','Tab Not Loaded']) )
            .appendTo( $tab );
        $( '<p></p>' )
            .text( _T(["#ui-tab-not-loaded-text", "The requested tab could not be loaded. Please reload the UI and try again."]) )
            .appendTo( $tab );
        $( '<pre></pre>' )
            .text( String( error ) )
            .appendTo( $tab );
        $( '<pre></pre>' ).addClass( "user-select-all" )
            .text( ( error instanceof Error ) ? error.stack : "" )
            .appendTo( $tab );
        return $tab.data( 'tabobject' );
    }

    function getMainNavItem( id, title, href, icon ) {
        var $el = $( `<li class="nav-item text-nowrap"> \
  <a class="nav-link tab-link px-2 px-md-3" href="#"> \
    <svg class="bi pe-none" width="18" height="18" fill="currentColor" role="img" aria-label="${icon}"><use xlink:href="/node_modules/bootstrap-icons/bootstrap-icons.svg#${icon}"></use></svg> \
    <span class="nav-item-title d-none d-lg-inline"></span> \
  </a> \
</li>` );
        $( 'span.nav-item-title', $el ).text( title );
        if ( "function" === typeof href ) {
            $( 'a.nav-link', $el )
                .attr( 'title', title )
                .on( 'click', href );
        } else {
            $( 'a.nav-link', $el )
                .attr( 'title', title )
                .attr( 'href', href || '#' );
        }
        $el.attr( 'id', 'nav-item-' + id );

        return $el;
    }

    function setActiveNav( id ) {
        $( '#nav-primary a.tab-link > span.visually-hidden' ).remove();
        $( '#nav-primary > li.nav-item a.tab-link' ).removeClass( 'active' );
        $( '#nav-primary > li#' + idSelector( 'nav-item-' + id ) + '.nav-item a.tab-link' )
            .addClass( 'active' );
        $( '#nav-primary a.tab-link.active' )
            .append( '<span class="visually-hidden"> (current)</span>' );
    }

    function tabExists( tabId ) {
        const $navitem = $( `ul#nav-primary li.nav-item#nav-item-${idSelector(tabId)}` );
        return $navitem.length !== 0;
    }

    async function switchTab( new_tab, args, query ) {
        if ( current_tab ) {
            let $tab = $( `div.re-tab-container#tab-${idSelector(current_tab)}` );
            if ( false === $tab.triggerHandler( 'suspend' ) ) {
                Common.showSysModal( { title: _T('Operation in Progress'),
                    body: _T('Please complete the current operation before switching views')
                } );
                console.log( `switchTab() tab ${current_tab} rejected request to switch` );
                return;
            }
            $tab.hide();
        } else {
            /* We're lost, hide everything */
            $( 'div.re-tab-container' ).hide();
        }

        if ( new_tab ) {
            const $navitem = $( `ul#nav-primary li.nav-item#nav-item-${idSelector(new_tab)}` );
            let $tab = $( `div.re-tab-container#tab-${idSelector(new_tab)}` );
            console.log("Tab", $tab, "errortab", $tab.attr("errortab"));
            if ( $navitem.attr( "method" ) ) {
                // Method/function-based tab display.
                if ( 0 === $tab.length || $tab.attr( "errortab" ) ) {
                    $tab.empty().removeAttr( "errortab" );
                    const tab = new Tab( `tab-${new_tab}`, $( 'main > div.re-ui-content' ) );
                    $tab = tab.getTabElement();
                    $tab.on( 'activate', MAP[$navitem.attr( "method" )] );
                }
            } else {
                if ( 0 === $tab.length || $tab.attr( "errortab" ) ) {
                    console.log(`Loading reactor-ui-${new_tab}.js`);
                    try {
                        const module = await import( `./reactor-ui-${new_tab}.js?t=${Date.now()}` );
                        module.default.init( $( 'main > div.re-ui-content' ) );
                    } catch ( err ) {
                        console.error(`Failed to load reactor-ui-${new_tab}.js`);
                        console.error(err);
                        getErrorTab( new_tab, err );
                    }
                    $tab = $( `div.re-tab-container#tab-${idSelector(new_tab)}` );
                }
            }

            $tab.show();
            current_tab = new_tab;
            setActiveNav( new_tab );

            $tab.triggerHandler( 'activate', args, query );
        } else {
            current_tab = false;
        }

        /* Check core version */
        console.log( "Tab switch, checking api have", api.version, "need", required_api_version );
        if ( api.version < required_api_version ) {
            alert( "The cached UI in your browser needs to be updated. Please HARD REFRESH your browser or flush your browser cache. If this message persists, try restarting the Reactor core and then hard-refreshing." );
        }

        return false;
    }

    function handleHashKey() {
        let [rest,query] = document.location.hash.split( '?' );
        let m = rest.replace( /^#\/*/, "" ).split( '/' ).map( el => decodeURIComponent(el) );
        let tabId = m.shift();
        api.echo({ comment: "UI activity" }).catch( err => {
            console.log("handleTabClick() echo miss (benign)", err.message);
        });  // validates auth if applicable, bumps timer if rolling
        if ( tabExists( tabId ) ) {
            let qp = new URLSearchParams(query);
            console.log("core.handleHashKey dispatching", document.location.href,"to", tabId, "with path", m, "query", qp);
            return switchTab( tabId, { path: m, query: qp } );
        }
    }

    /**
     *  handleTabClick - respond to a click in navigation that causes a tab to be displayed.
     *  There are two types of tabs: loadable and static. A loadable tab has a "method" attribute on the nav link
     *  that says what function in this module to run in response to the click. Otherwise, a loadable module is
     *  assumed to be available (named reactor-ui-tabid.js) that can be loaded and run to manage the tab.
     */
    function handleTabClick( event ) {
        const $target = $( event.target );
        const $navitem = $target.closest( 'li.nav-item' );
        const new_tab = ($navitem.attr( 'id' ) || 'status').replace( /nav-item-/, "" );
        document.location.hash = "";
        switchTab( new_tab );
        api.echo({ comment: "UI activity" }).catch( err => {
            console.log("handleTabClick() echo miss (benign)", err.message);
        });  // validates auth if applicable, bumps timer if rolling
        return false;
    }

    function doAboutTab( event ) {
        console.log("ReactorUICore.doAboutTab()");
        const $content = $( event.target );
        $content.empty();

        $( `<h2>${_T('#nav_about')}</h2>` ).appendTo( $content );
        $content.html(`<h2>About Reactor</h2><p class="about-content">Reactor (Multi-hub) <span id="reactor-version"></span> &#169; 2020-${(new Date).getFullYear()} Kedron Holdings LLC, All Rights Reserved; Patrick Rigney (toggledbits), Principal Engineer.</p> \
<p style="font-size: 1.1rem; background-color: var(--bs-secondary-bg); border: 4px dashed orange;" class="my-2 px-2 py-2 text-center">Reactor is currently a community-supported project, but it can only remain so if users make donations. If you have not yet made a donation (at all or recently), please consider doing so.<br>\
<a href="https://buymeacoffee.com/toggledbits/" target="_blank"><img src="/img/buymeadram-209x48.png" width="209" height="48" alt="Buy me a dram!"></a> \
</p> \
<h4 id="license-header">License</h4>\
<div id="re-license"> \
<p>This license ("License") constitues the sole and entire agreement with respect to Reactor (Multi-hub) ("Reactor" or "Multi-System Reactor" or "MSR") between its author, Kedron Holdings, LLC ("KHLLC") as licensor and you ("You") as licensee. <b>YOUR POSSESSION AND/OR USE OF REACTOR CONSTITUTES AFFIRMATIVE AGREEMENT BY YOU TO ALL OF THE TERMS AND CONDITIONS OF THIS LICENSE AS FURTHER SPECIFIED BELOW.</b> If you do not agree to ALL of the terms and conditions herein, your sole remedy is to not use Reactor and destroy any and all copies you may have in your possession.</p> \
<p class="about-content"><b>RIGHTS AND RESTRICTIONS.</b> You are hereby granted a limited, non-exclusive, non-transferable license to use Reactor for non-commercial use while you remain in full compliance with all of terms and conditions herein. You may install and use Reactor only on compatible (as determined by KHLLC in its sole discretion) systems that you own and operate for your own private use. Except as otherwise expressly provided herein, You acquire no other rights or licenses. Reactor is and at all times remains the exclusive property of KHLLC. You shall not install, store, or use Reactor on a system owned by or for the benefit of any third-party, for any reason or purpose, without the prior express written permission of KHLLC. You may not use or distribute Reactor for any commercial purpose without the prior express written permission of KHLLC. You acknowledge and agree that Reactor is not "open source" software, even though versions of source code may be made available to You for its operation. You further agree that you shall not disseminate, disclose, disassemble, reverse engineer, or produce derivative works of Reactor, in whole or part. The Privacy Notice appearing later on this page is hereby incorporated herein and a part of this License.\
<p><b>DISCLAIMER OF WARRANTY.</b> <b>THIS SOFTWARE IS OFFERED AS-IS, TOGETHER WITH ANY AND ALL DEFECTS. KHLLC MAKES NO EXPRESS, IMPLIED, OR STATUTORY WARRANTY WITH RESPECT TO REACTOR INCLUDING (BUT NOT LIMITED TO) ANY WARRANTY WITH RESPECT TO MERCHANTABILITY, FITNESS, SECURITY, PRIVACY OR PERFORMANCE.</b> In the event of any failure of Reactor in any respect, your sole remedy is to notify KHLLC and/or discontinue use of Reactor. KHLLC may, in its sole discretion, fix any defect reported in any way it deems appropriate; however, nothing in this License creates any obligation or duty upon KHLLC to fix any defect, or update Reactor in any way, even if it is alleged or determined to be not conforming to its documentation, specifications, applicable standards, or best practices.\
<p><b>LIMITATION OF LIABILITY.</b> Under no circumstances will KHLLC or any of its agents, assigns, heirs, or successors be liable to you or any third party for any direct, indirect, special, indicental, or consequential damages whatsoever (including without limitation any loss of information or any other pecuniary loss) however caused and under any legal or equitable theory of liability, and whether or not for breach of contract, negligence or otherwise. You agree to indemnify, defend, and hold harmless KHLLC, its agents, assigns, heirs, and successors, from any of the foregoing arising in connection with your use of the Software. You bear solely the entire risk of the use of Reactor. Notwithstanding the foregoing, KHLLC's maximum liability under this agreement shall be the amount paid to KHLLC for Reactor. If the laws of your jurisdiction limit or prohibit any of the foregoing exclusions or limitations (including the preceding Disclaimer of Warranty), you may not use Reactor and your rights under this License terminate immediately without further notice.\
<p><b>GOVERNING LAW.</b> This License shall be governed by, and construed in accordance with, the laws of the State of Georgia, United States of America. Any legal action or proceeding arising instituted by You out of or relating to Reactor or this License shall be instituted in the courts of the State of Georgia and the parties hereto irrevocably submit to the jurisdiction of each such court in any action or proceeding. You hereby irrevocably waive and agree not to assert, by way of motion, as a defense, or otherwise, in every suit, action or other proceeding arising out of or based on Reactor or this License and brought in any such court, any claim that You are not subject personally to the jurisdiction of the above-named courts, that You are exempt or immune from attachment or execution, or that the venue of the suit, action, or proceeding is improper or inconvenient.\
<p><b>NOTICES.</b> Any notice to KHLLC with respect to Reactor or this License shall be made in writing delivered by United States Postal Service certified mail or commercial overnight carrier with signature confirmation of delivery to Kedron Holdings, LLC, Attn: Reactor License, 1029 North Peachtree Parkway #200, Peachtree City, GA 30269-6635. Any notice by KHLLC to You may be made by any means available to KHLLC (including but not limited to electronic mail or private messaging on forum web sites) and shall be deemed delivered when given.\
<p><b>MODIFICATION.</b> KHLLC reserves the right to modify the terms of this license at any time. Any such modifications may be made without prior announcement, and will be published within the software (as You see here). Your continued use of Reactor constitutes your acceptance of, and agreement to be bound by, the modifications. If you do not agree, your sole remedy is to discontinue use of Reactor. \
<p><b>TERMINATION.</b> This License terminates immediately upon any breach by You of any term or condition, without notice or prior warning required. This License also terminates when you cease any and all use of Reactor and have destroyed all copies in your possession (including backups), and may be terminated by KHLLC for any reason at any time without notice. Upon any termination of this License, all rights granted to You hereunder terminate immediately, and You must immediately destroy any and all copies, including backups, of Reactor in your possession. Except for the rights granted to You in "Rights and Restrictions" above, all other restrictions and provisions of this License shall survive any termination. \
<hr>\
<h5>Privacy Notice</h5>\
<p>The Reactor application and this web-based user interface collect information about the home automation gateways/hubs/controllers, devices and services that you configure, so the collection of this data is entirely under your control. The web interface uses cookies and local storage, but this data is not used elsewhere. This locally-stored data is thus also under your control to review or delete at any time you wish. The user interface loads some external resources/libraries from third-party sites, and you may be subject to data collection and tracking by these sites outside the control of KHLLC. Reactor gathers and reports general statistical, usage, and environment information to KHLLC, including information about the Reactor version in use, the hardware and software environment in which it is running, which hubs are used with it, configured language, region of the world, etc., but *does not* include personal identifying information such as email addresses, usernames, access tokens, passwords, etc. You explicitly grant permission for this data collection. You may at any time request a copy of this data, or that it be deleted, by written notice to KHLLC as provided in NOTICES above.</p> \
<p>On Reactor's bug tracking site and the KHLLC web site, IP addresses, cookies and local storage are used to track your session and preferences, and these sites load third-party libraries that may subject you to data collection and tracking by those third parties. You may request deletion of personal identifying information from KHLLC's web sites at any time. \
<p>Tracking information collected by and held with third parties may be reviewed or deleted by contacting the third party directly; KHLLC cannot and will not make these requests for you.</p> \
<p>We will use your email address only to communicate with you about the project, support, bugs, updates, and questions.  KHLLC does not and will not sell or intentionally disclose your email address or other personal identifying information to any third party without your express prior consent.</p> \
</div><!-- #re-license --> \
<div id="reactor-about-agreemnt" class="mb-3"></div> \
<h4>Host Public Key</h4>
<div class="d-none d-md-block p-0 m-0"> \
<p>The key below is unique to your Reactor host system. Please provide it in its entirety (without editing and including the <tt>BEGIN</tt> and <tt>END</tt> lines) when requested for support.</p> \
<pre id="host-public-key" class="user-select-all"></pre> \
</div> \
<p class="d-block d-md-none">The host public key is only displayed on the desktop site.</p> \
<hr> \
<h4>Support and Updates</h4>\
<p>Discussions and announcements around Reactor (Multi-Hub) are conducted at the <a href="${SMARTHOME_LANDING_URL}" target="_blank">SmartHome Community</a>. Please <strong>do not</strong> use the Vera/eZLO Community forums for Reactor (Multi-Hub) discussions (they will likely ban you, and they've gone largely dormant anyway). If you have not yet signed up for an account at the Smart Home Community, please do. Please <em><a href="${SMARTHOME_LANDING_URL}" target="_blank">read the posting guidelines</a></em> before asking for help from the community. Please <strong>do not</strong> email or DM the author directly for support &mdash; the author will only respond to community public posts and Mantis bug reports. Recognized annual/monthly donors receive priority support.<sup>1</sup></p> \
<hr> \
<h4>Open Source Licenses</h4> \
<p class="d-block d-md-none">Please see the desktop site for a list of open source products/libraries used by Reactor, and their license and source information.</p> \
<div class="d-none d-md-block p-0 m-0"> \
<p>Reactor uses and, in its distribution, may include all or part of the following open source products/libraries:</p> \
<ul id="opensourceproducts"> \
<li><a href="https://nodejs.org/" target="_blank">node.js&reg;</a> - &copy; Node.js contributors; <a href="https://raw.githubusercontent.com/nodejs/node/refs/heads/main/LICENSE" target="_blank">full notice and license</a>.</li> \
<li><a href="https://github.com/googlefonts/opensans" target="_blank">@fontsource/open-sans</a> - &copy; 2020 The Open Sans Project Authors; <a href="http://scripts.sil.org/OFL" target="_blank">SIL Open Font License</a></li>
<li><a href="https://github.com/influxdata/influxdb-client-js" target="_blank">@influxdata/influxdb-client</a> - &copy; 2019 InfluxData; <a class="mit-license-link"></a></li> \
<li><a class="npmjs-package" data-package="@popperjs/core"></a> - &copy; 2019 Federico Zivolo; <a class="mit-license-link"></a></li> \
<li><a href="https://github.com/expressjs/body-parser" target="_blank">body-parser</a> - &copy; 2014 Jonathan Ong &lt;me@jongleberry.com>; &copy; 2014-2015 Douglas Christopher Wilson &lt;doug@somethingdoug.com>; <a class="mit-license-link"></a></li> \
<li><a href="https://getbootstrap.com/" target="_blank">Bootstrap</a> - &copy; 2011-2024 The Bootstrap Authors; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/twbs/icons" target="_blank">Bootstrap Icons</a> - &copy; 2019-2024 The Bootstrap Authors; <a class="mit-license-link"></a>.</li> \
<li><a class="npmjs-package" data-package="digest-fetch"></a> - &copy; 2018 Stefan Liu; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/iximiuz/node-diskusage-ng" target="_blank">diskusage-ng</a> - &copy; 2015 Ivan Velichko &lt;iximiuz@gmail.com>; <a class="mit-license-link"></a>.</li> \
<li><a href="https://expressjs.com/" target="_blank">express</a> - &copy; 2009-2014 TJ Holowaychuk &lt;tj@vision-media.ca>; &copy; 2013-2014 Roman Shtylman &lt;shtylman+expressjs@gmail.com>; &copy; 2014-2015 Douglas Christopher Wilson &lt;doug@somethingdoug.com>; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/gridstack/gridstack.js" target="_blank">gridstack.js</a> - &copy; 2019-2023 Alain Dumesny; <a class="mit-license-link"></a>.</li> \
<li><a href="https://jquery-ui.com" target="_blank">jQuery-UI</a> - &copy; OpenJS Foundation and jQuery contributors; <a class="mit-license-link"></a>.</li> \
<li><a href="https://jquery.com" target="_blank">jQuery</a> - &copy; OpenJS Foundation and jQuery contributors; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/nodeca/js-yaml" target="_blank">js-yaml</a> - &copy; 2011-2015 by Vitaly Puzrin; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/toggledbits/lexpjs" target="_blank">lexpjs</a> - &copy; 2020-2025 Patrick H. Rigney (toggledbits); <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/node-fetch/node-fetch" target="_blank">node-fetch</a> - &copy; 2016 - 2020 Node Fetch Team; <a class="mit-license-link"></a>.</li> \
<li><a class="npmjs-package" data-package="node-stream-zip"></a> - &copy; 2021 <a href="https://github.com/antelle" target="_blank">Antelle</a>; <a href="https://raw.githubusercontent.com/antelle/node-stream-zip/refs/heads/master/LICENSE" target="_blank">license</a>.</li> \
<li><a href="https://nodemailer.com/" target="_blank">nodemailer</a> - &copy; 2011-2023 Andris Reinman; <a href="https://raw.githubusercontent.com/nodemailer/nodemailer/refs/heads/master/LICENSE" target="_blank">license</a>.</li> \
<li><a href="https://github.com/expressjs/serve-static#readme" target="_blank">serve-static</a> - &copy; 2010 Sencha Inc.; &copy; 2011 LearnBoost; &copy; 2011 TJ Holowaychuk; &copy; 2014-2016 Douglas Christopher Wilson; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/uuidjs/uuid" target="_blank">uuid</a> - &copy; 2010-2020 Robert Kieffer and other contributors; <a class="mit-license-link"></a>.</li> \
<li><a href="https://github.com/websockets/ws" target="_blank">ws</a> - &copy; 2011 Einar Otto Stangvik &lt;einaros@gmail.com>,
&copy; 2013 Arnout Kazemier and contributors,
&copy; 2016 Luigi Pinca and contributors; <a class="mit-license-link"></a>.</li> \
</ul> \
<p><small>Persons and/or business entities are named in the foregoing list for reference and compliance only and are not endorsements by or of Reactor or KHLLC. All trademarks mentioned in this software and its accompanying documentation, scripts, and other files are the property of their respective holders.</small></p> \
<p><small><sup>1</sup> All support is offered on an as-available basis. There is no guaranteed response time for support, but I will prioritize requests from users who have donated US$25 or more in the last 12 months.</small></p> \
</div> \
`);
        $( "a.mit-license-link", $content ).attr( { href: "https://tldrlegal.com/license/mit-license", target: "_blank" } )
            .text( "MIT License" );
        $( "a.npmjs-package", $content ).each( (ix,el) => {
            const pkg = $(el).attr('data-package') || "#";
            $(el).attr( 'href', `https://www.npmjs.com/${pkg}` ).attr( 'target', "_blank" )
            if ( "" === $(el).text() ) {
                $(el).text( pkg );
            }
        });

        let cookie = ( document.cookie || "").split( /;\s*/ ).find( el => el.match( /^reactor-about=/ ) );
        let $ag = $( '#reactor-about-agreemnt', $content );
        if ( cookie && cookie.substring( 14 ) == LICENSE_VERSION ) {
            console.log( "cookie", cookie.substring( 14 ), "expecting", LICENSE_VERSION );
            $ag.html( '<div class="re-license-agreed">Thank you for using Reactor! To review the software license, please click the right-pointing arrow above.</div>' );
            let $lel = $( 'div#re-license' );
            $lel.hide();
            let $leh = $( 'h4#license-header' );
            $leh.append( '<span>&nbsp;<i class="bi bi-chevron-right ms-1"></i></span>' );
            $( 'span', $leh ).on( 'click', ev => {  // eslint-disable-line no-unused-vars
                if ( $( 'span > i.bi', $leh ).hasClass( 'bi-chevron-right' ) ) {
                    $( 'span > i.bi', $leh ).removeClass( 'bi-chevron-right' ).addClass( 'bi-chevron-down' );
                    $lel.show();
                    $( 'div.re-license-agreed' ).text( 'Thank you for using Reactor!' );
                } else {
                    $( 'span > i.bi', $leh ).removeClass( 'bi-chevron-down' ).addClass( 'bi-chevron-right' );
                    $lel.hide();
                }
            });
        } else {
            $( '<div class="re-license-pending">To stop this About page from displaying first, please enter the last four characters of your <em>Host Public Key</em> (shown below). \
  <div class="row gy-2 gx-3 align-items-center"> \
    <div class="col-auto"> \
      <label class="form-label" for="hpk4">Last four characters of Host Public Key:</label> \
    </div> \
    <div class="col-auto"> \
      <input id="hpk4" class="form-control text-center ms-2" value="" size="6"> \
    </div> \
    <div class="col-auto"> \
      <button id="re-agree" class="btn btn-primary ms-2">Verify</button> \
    </div> \
  </div> \
</div>' )
                .appendTo( $ag );
            $( 'button#re-agree', $content ).on( 'click', async () => {
                let hk = await api.getHostPublicKey();
                if ( "string" === typeof hk ) {
                    hk = hk.replace( /[-]+END PUBLIC KEY.*/, "" ).replace( /[\r\n]/g, "" );
                    if ( $( 'input#hpk4', $content ).val().trim() === hk.substring( hk.length - 4 ) ) {
                        const d = 90 * 86400;
                        document.cookie = `reactor-about=${LICENSE_VERSION}; max-age=${d}; path=/reactor`;
                        switchTab( 'status' );
                    } else {
                        document.cookie = `reactor-about=; max-age=0; path=/reactor`;
                        Common.showSysModal({
                            title:'Invalid Entry',
                            body:'Your entry did not match the last four characters of the Host Public Key. Please try again.'
                        });
                    }
                } else {
                    document.cookie = `reactor-about=; max-age=0; path=/reactor`;
                    Common.showSysModal({
                        title:'Sorry!',
                        body:'I was not able to retrieve the Host Public Key. Please make sure the Reactor service is up and running.'
                    });
                }
            });
        }

        $( 'span#reactor-version', $content ).text( String(BUILD_VERSION) );
        api.getHostPublicKey().then( key => {
            $( '#host-public-key', $content ).text( key );
        }).catch( err => {
            $( '#host-public-key', $content ).html( '<em>Not available! Please make sure the Reactor service is running!</em>' );
            console.error( err );
        });

        /* Attribution/version info for controllers */
        const controllers = api.getControllers();
        let $ct = $( 'div#about-controllers ul' );
        for ( let id of controllers ) {
            let ctrl = api.getController( id );
            if ( ctrl.metadata?.pkginfo ) {
                if ( 0 === $ct.length ) {
                    $ct = $( '<div id="about-controllers" class="d-none d-md-block"></div>' )
                        .insertAfter( $( '#host-public-key' ) );
                    $( '<hr/>' ).insertBefore( $ct );
                    $ct.append( '<h4>Controllers and Plugins</h4>' );
                    $ct = $ct.append( '<ul></ul>' );
                }
                let $li = $( '<li></li>' ).appendTo( $( 'ul', $ct ) );
                let m = ctrl.metadata;
                $li.text( `${m.implementationName} [${m.pkginfo.version}] ${m.pkginfo.author}` );
                if ( m.pkginfo.license ) {
                    $li.append( `<span>; ${m.pkginfo.license}</span>` );
                }
                if ( m.pkginfo.homepage ) {
                    let $cp = $( '<span>; </span>' ).appendTo( $li );
                    $( '<a></a>' )
                        .attr( 'href', m.pkginfo.homepage )
                        .attr( 'target', '_blank' )
                        .text( m.pkginfo.homepage )
                        .appendTo( $cp );
                }
            }
        }

        /* Attribution for language file */
        if ( "string" === typeof _LD.attribution ) {
            let $l = $( '<li></li>' ).prependTo( 'ul#opensourceproducts', $content );
            if ( _LD.github_repository ) {
                $l = $( '<a></a>').attr( {
                    href: 'https://github.com/' + _LD.github_repository,
                    target: '_blank'
                }).appendTo( $l );
            }
            $l.text( _LD.attribution );
        }
    };

    function doToolsTab( event ) {
        const $content = $( event.target );
        $content.empty();

        $( `<h2>${_T('#nav_tools')}</h2>`).appendTo( $content );
        let $ct = $( '<div></div>' ).appendTo( $content );
        $( `<button class="btn btn-sm btn-danger"><i class="bi bi-bootstrap-reboot pe-1"></i>${_T('Restart Reactor')}</button>` )
            .on( 'click', ( ev ) => {
                const $el = $( ev.currentTarget );
                Common.busyAction( _T( "Restarting Reactor..." ), new Promise( (resolve, reject) => {
                    $el.prop( 'disabled', true );
                    console.log("Requesting restart");
                    api.restartReactor().then( () => {
                        console.log("Restart request succeeded, waiting for new connection");
                        api.on( 'connected.tools', () => {
                            api.off( 'connected.tools' );
                            resolve();
                        });
                    }).catch( err => {
                        reject( err );
                    }).finally( () => {
                        $el.prop( 'disabled', false );
                    });
                }));
                return false;
            })
            .appendTo( $ct );

/*
        $ct = $( '<div></div>' ).appendTo( $content );
        $( '<button class="btn btn-sm btn-primary">Test</button>' )
            .on( 'click', async function() {
                try {
                    let mod = await import( './entity-picker.js' );
                    let picker = mod.default;
                    picker.pick().then( (thing) => { alert("picked "+thing); } );
                } catch ( err ) {
                    alert( err );
                    console.log( err );
                }
            })
            .appendTo( $ct );
*/

        $ct = $( '<footer></footer>' ).appendTo( $content );
        let $dv = $( '<div></div>' ).addClass("user-select-all").appendTo( $ct );
        $( '<p class="my-0"></p>' )
            .text( `${_LD.locale} (${_LD.description}) ${_LD.version}; browser ${navigator.userAgent}; lang ${navigator.languages.join(', ')}` )
            .appendTo( $dv );

        let $rr = $('<p class="my-0"></p>').appendTo( $dv );
        $( '<span></span>' ).text( `jQuery version ${jQuery?.fn?.jquery}` ).appendTo( $rr );
        $( '<span class="ps-2"></span>' ).text( `jQuery-UI version ${jQuery?.ui?.version}` ).appendTo( $rr );
        $( '<span class="ps-2"></span>' ).text( `Bootstrap version ${bootstrap?.Tooltip?.VERSION}` ).appendTo( $rr );
        $( '<span class="ps-2">break&nbsp;is&nbsp;\
<span class="d-sm-none">xs</span>\
<span class="d-none d-sm-inline d-md-none">sm</span>\
<span class="d-none d-md-inline d-lg-none">md</span>\
<span class="d-none d-lg-inline d-xl-none">lg</span>\
<span class="d-none d-xl-inline d-xxl-none">xl</span>\
<span class="d-none d-xxl-inline">xxl</span>\
</span>' ).appendTo( $rr );
        $( `<span class="ps-2"><a href="javascript:if (confirm('Really?')) localStorage.clear();">Clear Local Storage</a></span>` )
            .appendTo( $ct );
    };

    function showErrorDialog( error ) {
        const $body = $( '<div></div>' );
        $( '<pre></pre>' )
            .addClass( 'user-select-all' )
            .text( error instanceof Error ? error.stack : String( error ) )
            .appendTo( $body );
        $( '<div></div>' )
            .html( `You may report this error, but <b>do not</b> screen shot it. Copy-paste the complete text. Remember to include a description of the operation you were performing in as much detail as possible. Report at the <a href="${SMARTHOME_LANDING_URL}" target="_blank">SmartHome Community</a>.` )
            .appendTo( $body );

        Common.showSysModal({
            title: "Runtime Error",
            extraClasses: "modal-xl re-dialog-error",
            body: $body
        });
    }

    function start() {

        /* Error handlers */
        window.onerror = (error, url, line) => {
            console.error("Error from", url, "line", line);
            console.error( error );
            debugger;
            showErrorDialog( error );
        };
        addEventListener( 'unhandledrejection', (event) => {
            console.error("Trapped unhandled Promise rejection:", event);
            // event.reason is the error object
            debugger;
            try {
                showErrorDialog( event.reason );
            } catch ( err ) {  // eslint-disable-line no-unused-vars
                // nada
            }
            //event.preventDefault();
        });

        /* Special handlers to assist tabs, since <div> elements do not
           normally get show/hide events in jQuery */
        $.each(['show', 'hide'], function (i, ev) {
            const el = $.fn[ev];
            $.fn[ev] = function () {
                this.trigger(ev);
                return el.apply(this, arguments);
            };
        });

        let $main = $( 'main[role="main"]' ).first().empty();
        $( '<div class="re-ui-alerts collapse"></div>' ).appendTo( $main );
        $main = $( '<div class="re-ui-content"></div>' ).appendTo( $main );
        let $ct = $( 'ul#nav-primary' ).empty();

        /* Set up sub-modules and navigation */
        getMainNavItem( 'status', _T('#nav_status'), "#status", 'activity' )
            .appendTo( $ct );

        let $el = getMainNavItem( 'rules', _T('#nav_rulesets'), handleTabClick, 'cpu' )
            .appendTo( $ct );
        // <a class="nav-link" href="#offcanvasRulesets" role="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRulesets" aria-controls="#offcanvasRulesets">
        $( 'a.nav-link', $el ).attr({
            'role': 'button',
            'data-bs-toggle': 'offcanvas',
            'data-bs-target': '#offcanvasRulesets',
            'aria-controls': '#offcanvasRulesets'
        });

        getMainNavItem( 'reactions', _T('#nav_reactions'), "#reactions", 'lightning' )
            .appendTo( $ct );

        getMainNavItem( 'expressions', _T('#nav_expressions'), "#expressions", 'calculator' )
            .appendTo( $ct );

        getMainNavItem( 'entities', _T('#nav_entities'), "#entities", 'list' )
            .appendTo( $ct );

        getMainNavItem( 'scope', _T('#nav_scope'), "#scope", 'eye' )
            .addClass( "d-none d-sm-block" )
            .appendTo( $ct );

        getMainNavItem( 'tools', _T('#nav_tools'), "#tools", 'tools' )
            .attr( "method", "doToolsTab" )
            .appendTo( $ct );

        getMainNavItem( 'dashboard', _T('#nav_dashboard'), '/dashboard/', 'tablet' )
            .appendTo( $ct )
            .find( 'a.nav-link' ).attr( 'target', '_blank' );

        getMainNavItem( 'docs', _T('#nav_docs'), '/docs/', 'book' )
            .appendTo( $ct )
            .find( 'a.nav-link' ).attr( 'target', '_blank' );

        getMainNavItem( 'community', _T(['#nav_community', 'Community']),
                'https://smarthome.community/topic/768/category-topic-guide-read-before-posting', 'chat-left-dots' )
            .addClass( "d-none d-sm-block" )
            .appendTo( $ct )
            .find( 'a.nav-link' ).attr( 'target', '_blank' );

        getMainNavItem( 'donate', _T('#nav_donate'), 'https://buymeacoffee.com/toggledbits',
                'star' )
            .appendTo( $ct )
            .find( 'a.nav-link' ).attr( 'target', '_blank' );

        getMainNavItem( 'about', _T('#nav_about'), handleTabClick, 'info-circle' )
            .attr( "method", "doAboutTab" )
            .appendTo( $ct );

        try {
            const auth = Common.getAuthInfo( 'reactor' );
            if ( auth?.token ) {
                getMainNavItem( 'logout', _T(['#nav_logout','Log Out']), '/reactor/login.html', 'door-closed' )
                    .appendTo( $ct );
            }
        } catch ( err ) {
            console.error( err );
        }

        $( 'a#re-api-connstate' ).text("Starting...");
        api.on( 'connected.core', function() {
            $( 'a#re-api-connstate' ).html( `<span class="text-success">${_T('Connected')}</span>` );
        });
        api.on( 'disconnected.core', function() {
            $( 'a#re-api-connstate' ).html( `<span class="text-danger"><b>${_T('DISCONNECTED!')}</b></span>` );
        });

        /* Version string in greeting/profile spot for now */
        $( '#site-greeting' ).text( BUILD_VERSION ).on( 'click.reactor', ( event ) => {
            if ( event.shiftKey && event.ctrlKey ) {
                api.recycle();
            } else {
                switchTab( 'about' );
            }
            return false;
        });

        try {
            const d = new Date();
            if ( d.getMonth() === 11 || ( d.getMonth() === 0 && d.getDate() < 3 ) ) {
                $( 'div#re-nav-banner' )
                    .addClass( 'text-center' )
                    .css( 'color', '#0c0 !important' )
                    .text( "Happy Holidays and Best Wishes for a Peaceful and Prosperous New Year! -- @toggledbits" );
            }
        } catch ( err ) {
            console.error( err );
        }

        $( 'a#re-ui-alarms' ).on( 'click.reactor', () => {
            switchTab( 'status' );
        });

        api.off( 'auth_fail.core' ).on( 'auth_fail.core', () => {
            window.location.href = '/reactor/login.html';
        });
        api.start().then( function() { // no options passed; reusing prior authorization from reactor-ui.js
            console.log("UI core starting (API ready)");
            /* API started before us, so before on handler applied */
            $( 'a#re-api-connstate' ).html( `<span class="text-success">${_T('Connected')}</span>` );

            /* Link up the alerts */
            alertWatcher.start();

            /* Display/update host time */
            /* Note: browser time is updated by reactor-ui.js */
            (function (){
                const $el = $( 'a#re-hostclock' );
                let host_last = 0;
                let host_offset = null;
                let host_tz = "unknown";
                let request_pending = false;
                let hostticktimer = false;
                const update_host_time = function() {
                    if ( hostticktimer ) {
                        clearTimeout( hostticktimer );
                        hostticktimer = false;
                    }
                    let now = new Date();
                    let d = now - host_last;
                    if ( d >= 900000 && api.isConnected() && !request_pending ) {
                        // Figure out the current RTT time for a query
                        console.log( "UI core: updating host time sync" );
                        request_pending = true;
                        api.getSysTime().then( (data) => {
                            /** NTP-like synchronization. The ClientAPI supplies t0, the moment the packet
                             *  is sent. The host supplies t1 (time received) and t2 (time reply sent),
                             *  and pnow here is t3. So we have four moments: t0 -> t1 -> t2 -> t3 (pnow).
                             *  The RTT is simply t3 - t0; the clock offset is ((t1-t0)+(t2-t3))/2.
                             *  We don't use t1 and t2, however, because these are real time on the host,
                             *  and what we want is to show engine time (which may be in test time).
                             */
                            const pnow = performance.timeOrigin + performance.now();
                            now = new Date(); // freshen
                            host_last = now.getTime();
                            const rtt = pnow - data.t0;  // t0 is supplied by ClientAPI
                            /* Compute offset with skew for timezone difference between host (engine) and browser */
                            host_offset = data.engine_time - now.getTime() + rtt / 2;
                            host_offset += 60000 * ( now.getTimezoneOffset() + data.tzoffset );
                            host_tz = data.timezone;
                            console.log("UI core systimes: host", data.engine_time, ", me", host_last, "; host_offset=",
                                host_offset, "; rtt=", rtt, "; full response", data);
                            //console.log(">>> theta=", ((data.t1-data.t0)+(data.t2-pnow))/2, "server_delay=", data.t2-data.t1);
                            $el.closest( 'span' ).attr( 'title', _T( 'Host time' ) + ` (${host_tz})` );
                            request_pending = false;
                        }).catch( (e) => {
                            console.error("UI Core: host time query failed: ", e);
                            host_last = 0;
                            host_offset = null;
                            request_pending = false;
                        });
                    }

                    /* Fixed format time display */
                    if ( null !== host_offset && api.isConnected() ) {
                        const host_time = now.getTime() + host_offset;
                        $el.text( new Date( host_time ).toLocaleTimeString( getLocale(), { hour12: false } ) );
                        /* Update timing based on host offset */
                        hostticktimer = setTimeout( update_host_time, 1000 - (host_time % 1000 ) );
                    } else {
                        $el.text( "--:--:--" );
                        hostticktimer = setTimeout( update_host_time, 1000 );
                    }
                };
                const resync_host_time = function() {
                    host_last = 0;
                    host_offset = null;
                    update_host_time();
                };
                resync_host_time();
                $el.on( 'click.reactor', resync_host_time );
                api.off( "structure_update.hostclock" ).on( "structure_update.hostclock", resync_host_time );
            })();
            $( 'a#re-hostclock' ).closest( 'span' ).attr( 'title', _T( 'Host time' ) );
            $( 'a#re-sysclock' ).closest( 'span' ).attr( 'title', _T( 'Local (browser) time' ) +
                ` (${String(Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone)})` );

            /* Launch the status page to start once the API is ready. */
            $( 'main > h1' ).remove();
            console.log("Core hash:",document.location.hash);
            /* New way (Apr 2025): http://ip:port/reactor/en-US/#status/more/stuff/that/follows?queryparams */
            window.onhashchange = () => handleHashKey();
            if ( ( document.location?.hash || "#" ) !== "#" ) {
                handleHashKey();
            } else {
                /* Old way; deprecated. */
                let tab;
                if ( String( document.location?.search ).startsWith( '?' ) ) {
                    debugger;
                    const params = new URLSearchParams( document.location.search );
                    tab = params.get( "tab" );
                }
                if ( !tab ) {
                    const cookie = ( document.cookie || "" ).split( /;\s*/ ).find( el => el.match( /^reactor-about=/ ) );
                    if ( cookie && cookie.substring( 14 ) == LICENSE_VERSION ) {
                        switchTab( 'status' );
                    } else {
                        switchTab( 'about' );
                    }
                } else {
                    console.log("Switch to",tab);
                    switchTab( tab );
                }
            }

        }).catch( err => {
            if ( err.status === 401 ) {
                window.location.href = "/reactor/login.html";
            } else {
                console.error( err );
            }
        }).finally( () => {
            Common.hideGlobalLoading();
        });
    };

    return {
        "start": start
    };
})(jQuery);
