luci-app-bmx7: convert to JS
authorPaul Donald <redacted>
Mon, 23 Feb 2026 21:03:39 +0000 (22:03 +0100)
committerPaul Donald <redacted>
Tue, 24 Feb 2026 00:23:31 +0000 (01:23 +0100)
migrate away from the old luci-app lua control system.

See: https://github.com/openwrt/luci/issues/7310

The lua app was relatively broken prior to conversion.

Strangely, bmx7-uci-config package is what installs the
init.d service and uci config.

Signed-off-by: Paul Donald <redacted>
24 files changed:
applications/luci-app-bmx7/Makefile
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/bmx7logo.png [moved from applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/bmx7logo.png with 100% similarity]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph-theme.css [new file with mode: 0644]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph.css [new file with mode: 0644]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/js/netjsongraph.js [new file with mode: 0644]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/js/polling.js [moved from applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/polling.js with 52% similarity]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/world.png [moved from applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/world.png with 100% similarity]
applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/world_small.png [moved from applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/world_small.png with 100% similarity]
applications/luci-app-bmx7/htdocs/luci-static/resources/view/bmx7/config.js [new file with mode: 0644]
applications/luci-app-bmx7/root/etc/config/luci-bmx7 [deleted file]
applications/luci-app-bmx7/root/usr/lib/lua/luci/controller/bmx7.lua [deleted file]
applications/luci-app-bmx7/root/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm [deleted file]
applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/status_j.htm [deleted file]
applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/topology.htm [deleted file]
applications/luci-app-bmx7/root/usr/share/luci/menu.d/luci-app-bmx7.json [new file with mode: 0644]
applications/luci-app-bmx7/root/usr/share/rpcd/acl.d/luci-app-bmx7.json [new file with mode: 0644]
applications/luci-app-bmx7/root/www/cgi-bin/bmx7-info
applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph-theme.css [deleted file]
applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph.css [deleted file]
applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/netjsongraph.js [deleted file]
applications/luci-app-bmx7/ucode/template/bmx7/bmxnodes.ut [moved from applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/nodes_j.htm with 64% similarity]
applications/luci-app-bmx7/ucode/template/bmx7/bmxstatus.ut [new file with mode: 0644]
applications/luci-app-bmx7/ucode/template/bmx7/bmxtopology.ut [new file with mode: 0644]
applications/luci-app-bmx7/ucode/template/bmx7/bmxtunnels.ut [moved from applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/tunnels_j.htm with 52% similarity]

index 48e6fc023ac6c266dcd88ab89d64e63990d22240..242f6a1dfae9cf7ffe81463a08529b0a01082525 100644 (file)
@@ -4,7 +4,7 @@
 include $(TOPDIR)/rules.mk
 
 LUCI_TITLE:=LuCI support for BMX7
-LUCI_DEPENDS:=+luci-compat +luci-lib-json +luci-base +bmx7 +bmx7-json
+LUCI_DEPENDS:=+luci-base +bmx7 +bmx7-uci-config +bmx7-json +bmx7-tun +bmx7-iwinfo
 PKG_MAINTAINER:= Roger Pueyo <roger.pueyo@guifi.net> \
        Pau Escrich <p4u@dabax.net>
 PKG_LICENSE:=GPL-2.0-or-later
diff --git a/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph-theme.css b/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph-theme.css
new file mode 100644 (file)
index 0000000..39b77f1
--- /dev/null
@@ -0,0 +1,59 @@
+.njg-overlay{
+       background: #fbfbfb;
+       border-radius: 2px;
+       border: 1px solid #ccc;
+       color: #6d6357;
+       font-family: Arial, sans-serif;
+       font-family: sans-serif;
+       font-size: 14px;
+       line-height: 20px;
+       height: auto;
+       max-width: 400px;
+       min-width: 200px;
+       padding: 0 15px;
+       right: 10px;
+       top: 10px;
+       width: auto;
+}
+
+.njg-metadata{
+       background: #fbfbfb;
+       border-radius: 2px;
+       border: 1px solid #ccc;
+       color: #6d6357;
+       display: none;
+       font-family: Arial, sans-serif;
+       font-family: sans-serif;
+       font-size: 14px;
+       height: auto;
+       left: 10px;
+       max-width: 500px;
+       min-width: 200px;
+       padding: 0 15px;
+       top: 10px;
+       width: auto;
+}
+
+.njg-node{
+       stroke-opacity: 0.5;
+       stroke-width: 7px;
+       stroke: #fff;
+}
+
+.njg-node:hover,
+.njg-node.njg-open {
+       stroke: rgba(0, 0, 0, 0.2);
+}
+
+.njg-link{
+       cursor: pointer;
+       stroke: #999;
+       stroke-width: 2;
+       stroke-opacity: 0.25;
+}
+
+.njg-link:hover,
+.njg-link.njg-open{
+       stroke-width: 4 !important;
+       stroke-opacity: 0.5;
+}
diff --git a/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph.css b/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/css/netjsongraph.css
new file mode 100644 (file)
index 0000000..255ec01
--- /dev/null
@@ -0,0 +1,58 @@
+.njg-hidden {
+       display: none !important;
+       visibility: hidden !important;
+}
+
+.njg-tooltip{
+       font-family: sans-serif;
+       font-size: 10px;
+       fill: #000;
+       opacity: 0.5;
+       text-anchor: middle;
+}
+
+.njg-overlay{
+       z-index: 0;
+}
+
+.njg-close{
+       cursor: pointer;
+       position: absolute;
+       right: 10px;
+       top: 10px;
+}
+.njg-close:before { content: "\2716"; }
+
+.njg-metadata{
+       z-index: 0;
+}
+
+.njg-node{ cursor: pointer }
+.njg-link{ cursor: pointer }
+
+#njg-select-group {
+       text-align: center;
+       box-shadow: 0 0 10px #ccc;
+       position: fixed;
+       left: 50%;
+       top: 50%;
+       width: 50%;
+       margin-top: -7.5em;
+       margin-left: -25%;
+       padding: 5em 2em;
+}
+
+#njg-select-group select {
+       font-size: 2em;
+       padding: 10px 15px;
+       width: 50%;
+       cursor: pointer;
+}
+
+#njg-select-group option {
+       padding: 0.5em;
+}
+
+#njg-select-group option[value=""] {
+       color: #aaa;
+}
diff --git a/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/js/netjsongraph.js b/applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/js/netjsongraph.js
new file mode 100644 (file)
index 0000000..7225d8b
--- /dev/null
@@ -0,0 +1,568 @@
+// version 0.1
+(function () {
+       /**
+        * vanilla JS implementation of jQuery.extend()
+        */
+       d3._extend = function(defaults, options) {
+               let extended = {},
+                       prop;
+               for(prop in defaults) {
+                       if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
+                               extended[prop] = defaults[prop];
+                       }
+               }
+               for(prop in options) {
+                       if(Object.prototype.hasOwnProperty.call(options, prop)) {
+                               extended[prop] = options[prop];
+                       }
+               }
+               return extended;
+       };
+
+       /**
+         * @function
+         *   @name d3._pxToNumber
+         * Convert strings like "10px" to 10
+         *
+         * @param  {string}       val         The value to convert
+         * @return {int}              The converted integer
+         */
+       d3._pxToNumber = function(val) {
+               return parseFloat(val.replace('px'));
+       };
+
+       /**
+         * @function
+         *   @name d3._windowHeight
+         *
+         * Get window height
+         *
+         * @return  {int}            The window innerHeight
+         */
+       d3._windowHeight = function() {
+               return window.innerHeight || document.documentElement.clientHeight || 600;
+       };
+
+       /**
+         * @function
+         *   @name d3._getPosition
+         *
+         * Get the position of `element` relative to `container`
+         *
+         * @param  {object}      element
+         * @param  {object}      container
+         */
+        d3._getPosition = function(element, container) {
+               let n = element.node(),
+                       nPos = n.getBoundingClientRect();
+               let cPos = container.node().getBoundingClientRect();
+               return {
+                       top: nPos.top - cPos.top,
+                       left: nPos.left - cPos.left,
+                       width: nPos.width,
+                       bottom: nPos.bottom - cPos.top,
+                       height: nPos.height,
+                       right: nPos.right - cPos.left
+               };
+        };
+
+       /**
+        * netjsongraph.js main function
+        *
+        * @constructor
+        * @param  {string}      url             The NetJSON file url
+        * @param  {object}      opts            The object with parameters to override {@link d3.netJsonGraph.opts}
+        */
+       d3.netJsonGraph = function(url, opts) {
+               /**
+                * Default options
+                *
+                * @param  {string}     el                  "body"      The container element                                  el: "body" [description]
+                * @param  {bool}       metadata            true        Display NetJSON metadata at startup?
+                * @param  {bool}       defaultStyle        true        Use css style?
+                * @param  {bool}       animationAtStart    false       Animate nodes or not on load
+                * @param  {array}      scaleExtent         [0.25, 5]   The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent}
+                * @param  {int}        charge              -130        The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge}
+                * @param  {int}        linkDistance        50          The target distance between linked nodes to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkDistance}
+                * @param  {float}      linkStrength        0.2         The strength (rigidity) of links to the specified value in the range. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkStrength}
+                * @param  {float}      friction            0.9         The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction}
+                * @param  {string}     chargeDistance      Infinity    The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance}
+                * @param  {float}      theta               0.8         The Barnes–Hut approximation criterion to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#theta}
+                * @param  {float}      gravity             0.1         The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity}
+                * @param  {int}        circleRadius        8           The radius of circles (nodes) in pixel
+                * @param  {string}     labelDx             "0"         SVG dx (distance on x axis) attribute of node labels in graph
+                * @param  {string}     labelDy             "-1.3em"    SVG dy (distance on y axis) attribute of node labels in graph
+                * @param  {function}   onInit                          Callback function executed on initialization
+                * @param  {function}   onLoad                          Callback function executed after data has been loaded
+                * @param  {function}   onEnd                           Callback function executed when initial animation is complete
+                * @param  {function}   linkDistanceFunc                By default high density areas have longer links
+                * @param  {function}   redraw                          Called when panning and zooming
+                * @param  {function}   prepareData                     Used to convert NetJSON NetworkGraph to the javascript data
+                * @param  {function}   onClickNode                     Called when a node is clicked
+                * @param  {function}   onClickLink                     Called when a link is clicked
+                */
+               opts = d3._extend({
+                       el: "body",
+                       metadata: true,
+                       defaultStyle: true,
+                       animationAtStart: true,
+                       scaleExtent: [0.25, 5],
+                       charge: -130,
+                       linkDistance: 50,
+                       linkStrength: 0.2,
+                       friction: 0.9,  // d3 default
+                       chargeDistance: Infinity,  // d3 default
+                       theta: 0.8,  // d3 default
+                       gravity: 0.1,
+                       circleRadius: 8,
+                       labelDx: "0",
+                       labelDy: "-1.3em",
+                       nodeClassProperty: null,
+                       linkClassProperty: null,
+                       /**
+                        * @function
+                        * @name onInit
+                        *
+                        * Callback function executed on initialization
+                        * @param  {string|object}  url     The netJson remote url or object
+                        * @param  {object}         opts    The object of passed arguments
+                        * @return {function}
+                        */
+                       onInit: function(url, opts) {},
+                       /**
+                        * @function
+                        * @name onLoad
+                        *
+                        * Callback function executed after data has been loaded
+                        * @param  {string|object}  url     The netJson remote url or object
+                        * @param  {object}         opts    The object of passed arguments
+                        * @return {function}
+                        */
+                       onLoad: function(url, opts) {},
+                       /**
+                        * @function
+                        * @name onEnd
+                        *
+                        * Callback function executed when initial animation is complete
+                        * @param  {string|object}  url     The netJson remote url or object
+                        * @param  {object}         opts    The object of passed arguments
+                        * @return {function}
+                        */
+                       onEnd: function(url, opts) {},
+                       /**
+                        * @function
+                        * @name linkDistanceFunc
+                        *
+                        * By default, high density areas have longer links
+                        */
+                       linkDistanceFunc: function(d){
+                               let val = opts.linkDistance;
+                               if(d.source.linkCount >= 4 && d.target.linkCount >= 4) {
+                                       return val * 2;
+                               }
+                               return val;
+                       },
+                       /**
+                        * @function
+                        * @name redraw
+                        *
+                        * Called on zoom and pan
+                        */
+                       redraw: function() {
+                               panner.attr("transform",
+                                       "trans"+"late(" + d3.event.translate + ") " +
+                                       "scale(" + d3.event.scale + ")"
+                               );
+                       },
+                       /**
+                        * @function
+                        * @name prepareData
+                        *
+                        * Convert NetJSON NetworkGraph to the data structure consumed by d3
+                        *
+                        * @param graph {object}
+                        */
+                       prepareData: function(graph) {
+                               let nodesMap = {},
+                                       nodes = graph.nodes.slice(), // copy
+                                       links = graph.links.slice(), // copy
+                                       nodes_length = graph.nodes.length,
+                                       links_length = graph.links.length;
+
+                               for(let i = 0; i < nodes_length; i++) {
+                                       // count how many links every node has
+                                       nodes[i].linkCount = 0;
+                                       nodesMap[nodes[i].id] = i;
+                               }
+                               for(let c = 0; c < links_length; c++) {
+                                       let sourceIndex = nodesMap[links[c].source],
+                                       targetIndex = nodesMap[links[c].target];
+                                       // ensure source and target exist
+                                       if(!nodes[sourceIndex]) { throw("source '" + links[c].source + "' not found"); }
+                                       if(!nodes[targetIndex]) { throw("target '" + links[c].target + "' not found"); }
+                                       links[c].source = nodesMap[links[c].source];
+                                       links[c].target = nodesMap[links[c].target];
+                                       // add link count to both ends
+                                       nodes[sourceIndex].linkCount++;
+                                       nodes[targetIndex].linkCount++;
+                               }
+                               return { "nodes": nodes, "links": links };
+                       },
+                       /**
+                        * @function
+                        * @name onClickNode
+                        *
+                        * Called when a node is clicked
+                        */
+                       onClickNode: function(n) {
+                               let overlay = d3.select(".njg-overlay"),
+                                       overlayInner = d3.select(".njg-overlay > .njg-inner"),
+                                       html = "<p><b>id</b>: " + n.id + "</p>";
+                                       if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
+                                       if(n.properties) {
+                                               for(let key in n.properties) {
+                                                       if(!n.properties.hasOwnProperty(key)) { continue; }
+                                                       html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
+                                       }
+                               }
+                               if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
+                               if(n.local_addresses) {
+                                       html += "<p><b>local addresses</b>:<br />" + n.local_addresses.join('<br />') + "</p>";
+                               }
+                               overlayInner.html(html);
+                               overlay.classed("njg-hidden", false);
+                               overlay.style("display", "block");
+                               // set "open" class to current node
+                               removeOpenClass();
+                               d3.select(this).classed("njg-open", true);
+                       },
+                       /**
+                        * @function
+                        * @name onClickLink
+                        *
+                        * Called when a node is clicked
+                        */
+                       onClickLink: function(l) {
+                               let overlay = d3.select(".njg-overlay"),
+                                       overlayInner = d3.select(".njg-overlay > .njg-inner"),
+                                       html = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
+                                       html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
+                                       html += "<p><b>cost</b>: " + l.cost + "</p>";
+                               if(l.properties) {
+                                       for(let key in l.properties) {
+                                               if(!l.properties.hasOwnProperty(key)) { continue; }
+                                               html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
+                                       }
+                               }
+                               overlayInner.html(html);
+                               overlay.classed("njg-hidden", false);
+                               overlay.style("display", "block");
+                               // set "open" class to current link
+                               removeOpenClass();
+                               d3.select(this).classed("njg-open", true);
+                       }
+               }, opts);
+
+               // init callback
+               opts.onInit(url, opts);
+
+               if(!opts.animationAtStart) {
+                       opts.linkStrength = 2;
+                       opts.friction = 0.3;
+                       opts.gravity = 0;
+               }
+               if(opts.el == "body") {
+                       let body = d3.select(opts.el),
+                               rect = body.node().getBoundingClientRect();
+                       if (d3._pxToNumber(d3.select("body").style("height")) < 60) {
+                               body.style("height", d3._windowHeight() - rect.top - rect.bottom + "px");
+                       }
+               }
+               let el = d3.select(opts.el).style("position", "relative"),
+                       width = d3._pxToNumber(el.style('width')),
+                       height = d3._pxToNumber(el.style('height')),
+                       force = d3.layout.force()
+                                         .charge(opts.charge)
+                                         .linkStrength(opts.linkStrength)
+                                         .linkDistance(opts.linkDistanceFunc)
+                                         .friction(opts.friction)
+                                         .chargeDistance(opts.chargeDistance)
+                                         .theta(opts.theta)
+                                         .gravity(opts.gravity)
+                                         // width is easy to get, if height is 0 take the height of the body
+                                         .size([width, height]),
+                       zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent),
+                       // panner is the element that allows zooming and panning
+                       panner = el.append("svg")
+                                          .attr("width", width)
+                                          .attr("height", height)
+                                          .call(zoom.on("zoom", opts.redraw))
+                                          .append("g")
+                                          .style("position", "absolute"),
+                       svg = d3.select(opts.el + " svg"),
+                       drag = force.drag(),
+                       overlay = d3.select(opts.el).append("div").attr("class", "njg-overlay"),
+                       closeOverlay = overlay.append("a").attr("class", "njg-close"),
+                       overlayInner = overlay.append("div").attr("class", "njg-inner"),
+                       metadata = d3.select(opts.el).append("div").attr("class", "njg-metadata"),
+                       metadataInner = metadata.append("div").attr("class", "njg-inner"),
+                       closeMetadata = metadata.append("a").attr("class", "njg-close"),
+                       // container of ungrouped networks
+                       str = [],
+                       selected = [],
+                       /**
+                        * @function
+                        * @name removeOpenClass
+                        *
+                        * Remove open classes from nodes and links
+                        */
+                       removeOpenClass = function () {
+                               d3.selectAll("svg .njg-open").classed("njg-open", false);
+                       };
+                       processJson = function(graph) {
+                               /**
+                                * Init netJsonGraph
+                                */
+                               init = function(url, opts) {
+                                       d3.netJsonGraph(url, opts);
+                               };
+                               /**
+                                * Remove all instances
+                                */
+                               destroy = function() {
+                                       force.stop();
+                                       d3.select("#selectGroup").remove();
+                                       d3.select(".njg-overlay").remove();
+                                       d3.select(".njg-metadata").remove();
+                                       overlay.remove();
+                                       overlayInner.remove();
+                                       metadata.remove();
+                                       svg.remove();
+                                       node.remove();
+                                       link.remove();
+                                       nodes = [];
+                                       links = [];
+                               };
+                               /**
+                                * Destroy and e-init all instances
+                                * @return {[type]} [description]
+                                */
+                               reInit = function() {
+                                       destroy();
+                                       init(url, opts);
+                               };
+
+                               let data = opts.prepareData(graph),
+                                       links = data.links,
+                                       nodes = data.nodes;
+
+                               // disable some transitions while dragging
+                               drag.on('dragstart', function(n){
+                                       d3.event.sourceEvent.stopPropagation();
+                                       zoom.on('zoom', null);
+                               })
+                               // re-enable transitions when dragging stops
+                               .on('dragend', function(n){
+                                       zoom.on('zoom', opts.redraw);
+                               })
+                               .on("drag", function(d) {
+                                       // avoid pan & drag conflict
+                                       d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
+                               });
+
+                               force.nodes(nodes).links(links).start();
+
+                               let link = panner.selectAll(".link")
+                                                                .data(links)
+                                                                .enter().append("line")
+                                                                .attr("class", function (link) {
+                                                                        let baseClass = "njg-link",
+                                                                                addClass = null;
+                                                                                value = link.properties && link.properties[opts.linkClassProperty];
+                                                                        if (opts.linkClassProperty && value) {
+                                                                                // if value is stirng use that as class
+                                                                                if (typeof(value) === "string") {
+                                                                                        addClass = value;
+                                                                                }
+                                                                                else if (typeof(value) === "number") {
+                                                                                        addClass = opts.linkClassProperty + value;
+                                                                                }
+                                                                                else if (value === true) {
+                                                                                        addClass = opts.linkClassProperty;
+                                                                                }
+                                                                                return baseClass + " " + addClass;
+                                                                        }
+                                                                        return baseClass;
+                                                                })
+                                                                .on("click", opts.onClickLink),
+                                       groups = panner.selectAll(".node")
+                                                                  .data(nodes)
+                                                                  .enter()
+                                                                  .append("g");
+                                       node = groups.append("circle")
+                                                                .attr("class", function (node) {
+                                                                        let baseClass = "njg-node",
+                                                                                addClass = null;
+                                                                                value = node.properties && node.properties[opts.nodeClassProperty];
+                                                                        if (opts.nodeClassProperty && value) {
+                                                                                // if value is stirng use that as class
+                                                                                if (typeof(value) === "string") {
+                                                                                        addClass = value;
+                                                                                }
+                                                                                else if (typeof(value) === "number") {
+                                                                                        addClass = opts.nodeClassProperty + value;
+                                                                                }
+                                                                                else if (value === true) {
+                                                                                        addClass = opts.nodeClassProperty;
+                                                                                }
+                                                                                return baseClass + " " + addClass;
+                                                                        }
+                                                                        return baseClass;
+                                                                })
+                                                                .attr("r", opts.circleRadius)
+                                                                .on("click", opts.onClickNode)
+                                                                .call(drag);
+
+                                       let labels = groups.append('text')
+                                                                          .text(function(n){ return n.label || n.id })
+                                                                          .attr('dx', opts.labelDx)
+                                                                          .attr('dy', opts.labelDy)
+                                                                          .attr('class', 'njg-tooltip');
+
+                               // Close overlay
+                               closeOverlay.on("click", function() {
+                                       removeOpenClass();
+                                       overlay.classed("njg-hidden", true);
+                               });
+                               // Close Metadata panel
+                               closeMetadata.on("click", function() {
+                                       // Reinitialize the page
+                                       if(graph.type === "NetworkCollection") {
+                                               reInit();
+                                       }
+                                       else {
+                                               removeOpenClass();
+                                               metadata.classed("njg-hidden", true);
+                                       }
+                               });
+                               // default style
+                               // TODO: probably change defaultStyle
+                               // into something else
+                               if(opts.defaultStyle) {
+                                       let colors = d3.scale.category20c();
+                                       node.style({
+                                               "fill": function(d){ return colors(d.linkCount); },
+                                               "cursor": "pointer"
+                                       });
+                               }
+                               // Metadata style
+                               if(opts.metadata) {
+                                       metadata.attr("class", "njg-metadata").style("display", "block");
+                               }
+
+                               let attrs = ["protocol",
+                                                        "version",
+                                                        "revision",
+                                                        "metric",
+                                                        "router_id",
+                                                        "topology_id"],
+                                       html = "";
+                               if(graph.label) {
+                                       html += "<h3>" + graph.label + "</h3>";
+                               }
+                               for(let i in attrs) {
+                                       let attr = attrs[i];
+                                       if(graph[attr]) {
+                                               html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
+                                       }
+                               }
+                               // Add nodes and links count
+                               html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
+                               html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
+                               metadataInner.html(html);
+                               metadata.classed("njg-hidden", false);
+
+                               // onLoad callback
+                               opts.onLoad(url, opts);
+
+                               force.on("tick", function() {
+                                       link.attr("x1", function(d) {
+                                               return d.source.x;
+                                       })
+                                       .attr("y1", function(d) {
+                                               return d.source.y;
+                                       })
+                                       .attr("x2", function(d) {
+                                               return d.target.x;
+                                       })
+                                       .attr("y2", function(d) {
+                                               return d.target.y;
+                                       });
+
+                                       node.attr("cx", function(d) {
+                                               return d.x;
+                                       })
+                                       .attr("cy", function(d) {
+                                               return d.y;
+                                       });
+
+                                       labels.attr("transform", function(d) {
+                                               return "trans"+"late(" + d.x + "," + d.y + ")";
+                                       });
+                               })
+                               .on("end", function(){
+                                       force.stop();
+                                       // onEnd callback
+                                       opts.onEnd(url, opts);
+                               });
+
+                               return force;
+                       };
+
+               if(typeof(url) === "object") {
+                       processJson(url);
+               }
+               else {
+                       /**
+                       * Parse the provided json file
+                       * and call processJson() function
+                       *
+                       * @param  {string}     url         The provided json file
+                       * @param  {function}   error
+                       */
+                       d3.json(url, function(error, graph) {
+                               if(error) { throw error; }
+                               /**
+                               * Check if the json contains a NetworkCollection
+                               */
+                               if(graph.type === "NetworkCollection") {
+                                       let selectGroup = body.append("div").attr("id", "njg-select-group"),
+                                               select = selectGroup.append("select")
+                                                                                       .attr("id", "select");
+                                               str = graph;
+                                       select.append("option")
+                                                 .attr({
+                                                         "value": "",
+                                                         "selected": "selected",
+                                                         "name": "default",
+                                                         "disabled": "disabled"
+                                                 })
+                                                 .html("Choose the network to display");
+                                       graph.collection.forEach(function(structure) {
+                                               select.append("option").attr("value", structure.type).html(structure.type);
+                                               // Collect each network json structure
+                                               selected[structure.type] = structure;
+                                       });
+                                       select.on("change", function() {
+                                               selectGroup.attr("class", "njg-hidden");
+                                               // Call selected json structure
+                                               processJson(selected[this.options[this.selectedIndex].value]);
+                                       });
+                               }
+                               else {
+                                       processJson(graph);
+                               }
+                       });
+               }
+        };
+})();
similarity index 52%
rename from applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/polling.js
rename to applications/luci-app-bmx7/htdocs/luci-static/resources/bmx7/js/polling.js
index 234391a975f6e84d67cd7d1d780a0a958adb55fb..3518627c4ab03c30430bb20bc2f7829c6aaadf3c 100644 (file)
@@ -1,23 +1,23 @@
 /*
-    Copyright Â© 2011 Pau Escrich <pau@dabax.net>
-    Contributors Lluis Esquerda <eskerda@gmail.com>
+       Copyright Â© 2011 Pau Escrich <pau@dabax.net>
+       Contributors Lluis Esquerda <eskerda@gmail.com>
 
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
+       This program is free software; you can redistribute it and/or modify
+       it under the terms of the GNU General Public License as published by
+       the Free Software Foundation; either version 2 of the License, or
+       (at your option) any later version.
 
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
+       This program is distributed in the hope that it will be useful,
+       but WITHOUT ANY WARRANTY; without even the implied warranty of
+       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+       GNU General Public License for more details.
 
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+       You should have received a copy of the GNU General Public License along
+       with this program; if not, write to the Free Software Foundation, Inc.,
+       51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-    The full GNU General Public License is included in this distribution in
-    the file called "COPYING".
+       The full GNU General Public License is included in this distribution in
+       the file called "COPYING".
 */
 
 
        Table pooler is a function to easy call XHR poller. 
 
        new TablePooler(5,"/cgi-bin/bmx7-info", {'status':''}, "status_table", function(st){
-               var table = Array()
+               let table = Array()
                table.push(st.first,st.second)
                return table
        }
 
        The parameters are: 
-               polling_time: time between pollings
-               json_url: the json url to fetch the data
-               json_call: the json call
-               output_table_id: the table where javascript will put the data
+               polling_time: time between polls
+               json_url: the JSON URL to fetch the data
+               json_call: the JSON call
+               output_table_id: the table where JavaScript will put the data
                callback_function: the function that will be executed each polling_time
        
        The callback_function must return an array of arrays (matrix).
-       In the code st is the data obtained from the json call
+       In the code st is the data obtained from the JSON call
 */
 
 function TablePooler (time, jsonurl, getparams, div_id, callback) {
@@ -51,9 +51,9 @@ function TablePooler (time, jsonurl, getparams, div_id, callback) {
 
        this.start = function(){
                XHR.poll(this.time, this.jsonurl, this.getparams, function(x, st){
-                       var data = this.callback(st);
-                       var content;
-                       for (var i = 0; i < data.length; i++){
+                       let data = this.callback(st);
+                       let content, rowDiv, rowId, cellDiv, cellId;
+                       for (let i = 0; i < data.length; i++){
                                rowId = "trDiv_" + this.div_id + i;
                                rowDiv = document.getElementById(rowId);
                                if (rowDiv === null) {
@@ -62,7 +62,7 @@ function TablePooler (time, jsonurl, getparams, div_id, callback) {
                                        rowDiv.className = "tr";
                                        this.div.appendChild(rowDiv);
                                }
-                               for (var j = 0; j < data[i].length; j++){
+                               for (let j = 0; j < data[i].length; j++){
                                        cellId = "tdDiv_" + this.div_id + i + j;
                                        cellDiv = document.getElementById(cellId);
                                        if (cellDiv === null) {
diff --git a/applications/luci-app-bmx7/htdocs/luci-static/resources/view/bmx7/config.js b/applications/luci-app-bmx7/htdocs/luci-static/resources/view/bmx7/config.js
new file mode 100644 (file)
index 0000000..74fff58
--- /dev/null
@@ -0,0 +1,67 @@
+'use strict';
+'require view';
+'require form';
+'require tools.widgets as widgets';
+
+return view.extend({
+       render() {
+               let m, s, o;
+
+               m = new form.Map('bmx7', _('BMX7'));
+               s = m.section(form.NamedSection, 'general');
+               s.anonymous = true;
+
+               o = s.option(form.Value, 'runtimeDir', _('runtimeDir'));
+               o = s.option(form.Value, 'trustedNodesDir', _('trustedNodesDir'));
+
+               s = m.section(form.TypedSection, 'plugin', _('Plugins'));
+               s.addremove = true;
+               s.anonymous = true;
+
+               o = s.option(form.Value, 'plugin', _('Plugin')); 
+
+               s = m.section(form.TypedSection, 'dev', _('Devices'));
+               s.addremove = true;
+               s.anonymous = false;
+
+               o = s.option(widgets.DeviceSelect, 'dev', _('Dev')); 
+
+               s = m.section(form.TypedSection, 'tunDev', _('Tunnel Devices'));
+               s.addremove = true;
+               s.anonymous = false;
+
+               o = s.option(form.Value, 'tunDev', _('Dev'));
+               o = s.option(form.Value, 'tun6Address', _('tun6Address'));
+               o = s.option(form.Value, 'tun4Address', _('tun4Address'));
+
+               s = m.section(form.TypedSection, 'tunOut', _('Gateway Devices'));
+               s.addremove = true;
+               s.anonymous = true;
+
+               o = s.option(form.Value, 'tunOut', _('tunOut'));
+               o.value('ip4');
+               o.value('ip6');
+
+               o = s.option(form.Value, 'network', _('Network')); 
+               o.datatype = 'ipaddr';
+
+               o = s.option(form.Value, 'exportDistance', _('exportDistance'));
+               o.datatype = 'uinteger';
+               o = s.option(form.Value, 'minPrefixLen', _('minPrefixLen'));
+               o.datatype = 'uinteger';
+
+               s = m.section(form.NamedSection, 'luci', _('luci'));
+               s.uciconfig = 'bmx7-luci';
+               s.anonymous = true;
+
+               o = s.option(form.Flag, 'ignore', _('Ignore'));
+               o.default = '0';
+               o.rmempty = false;
+
+               o = s.option(form.Value, 'json', _('JSON source')); 
+               o.rmempty = false;
+
+               return m.render();
+       },
+});
+
diff --git a/applications/luci-app-bmx7/root/etc/config/luci-bmx7 b/applications/luci-app-bmx7/root/etc/config/luci-bmx7
deleted file mode 100755 (executable)
index 46a7727..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-config 'bmx7' 'luci'
-       option ignore '0'
-       option place 'admin network BMX7'
-       #option place 'qmp Mesh'
-       option position '3'
-       #option json 'http://127.0.0.1/cgi-bin/bmx7-info?'
-       option json 'exec:/www/cgi-bin/bmx7-info -s'
diff --git a/applications/luci-app-bmx7/root/usr/lib/lua/luci/controller/bmx7.lua b/applications/luci-app-bmx7/root/usr/lib/lua/luci/controller/bmx7.lua
deleted file mode 100644 (file)
index 482fb5d..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
---[[
-    Copyright (C) 2011 Pau Escrich <pau@dabax.net>
-    Contributors Jo-Philipp Wich <xm@subsignal.org>
-                 Roger Pueyo Centelles <roger.pueyo@guifi.net>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-    The full GNU General Public License is included in this distribution in
-    the file called "COPYING".
---]]
-
-module("luci.controller.bmx7", package.seeall)
-
-function index()
-       local place = {}
-       local ucim = require "luci.model.uci"
-       local uci = ucim.cursor()
-
-       -- checking if ignore is on
-       if uci:get("luci-bmx7","luci","ignore") == "1" then
-               return nil
-       end
-
-       -- getting value from uci database
-       local uci_place = uci:get("luci-bmx7","luci","place")
-
-       -- default values
-       if uci_place == nil then
-               place = {"bmx7"}
-       else
-               local util = require "luci.util"
-               place = util.split(uci_place," ")
-       end
-
-       -- getting position of menu
-       local uci_position = uci:get("luci-bmx7","luci","position")
-
-
-       ---------------------------
-       -- Placing the pages in the menu
-       ---------------------------
-
-       -- Status (default)
-       entry(place,call("action_status_j"),place[#place],tonumber(uci_position))
-
-       table.insert(place,"Status")
-       entry(place,call("action_status_j"),"Status",0)
-       table.remove(place)
-
-       -- Topology
-       table.insert(place,"Topology")
-       entry(place,call("topology"),"Topology",1)
-       table.remove(place)
-
-       -- Nodes
-       table.insert(place,"Nodes")
-       entry(place,call("action_nodes_j"),"Nodes",2)
-       table.remove(place)
-
-       -- Tunnels
-       table.insert(place,"Gateways")
-       entry(place,call("action_tunnels_j"),"Gateways",3)
-       table.remove(place)
-
-       -- Integrate bmx7-mdns if present
-       if nixio.fs.stat("/usr/lib/lua/luci/model/cbi/bmx7-mdns.lua","type") ~= nil then
-               table.insert(place,"mDNS")
-               entry(place, cbi("bmx7-mdns"), "mesh DNS", 1).dependent=false
-               table.remove(place)
-       end
-
-end
-
-
-function action_status_j()
-       luci.template.render("bmx7/status_j", {})
-end
-
-function action_tunnels_j()
-       luci.template.render("bmx7/tunnels_j", {})
-end
-
-function topology()
-       luci.template.render("bmx7/topology", {})
-end
-
-function action_nodes_j()
-       luci.template.render("bmx7/nodes_j", {})
-end
diff --git a/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm b/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/admin_status/index/bmx7_nodes.htm
deleted file mode 100644 (file)
index c8ddb2d..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<div class="cbi-map">
-<div class="cbi-section">
-       <legend><%:Bmx7 mesh nodes%></legend>
-       <div class="cbi-section-node">
-       <div class="table" id="nodes_div">
-               <div class="tr table-titles">
-                       <div class="th"><%:Name%></div>
-                       <div class="th"><%:Short ID%></div>
-                       <div class="th"><%:S/s/T/t%></div>
-                       <div class="th"><%:Primary IPv6%></div>
-                       <div class="th"><%:Via Neighbour%></div>
-                       <div class="th"><%:Device%></div>
-                       <div class="th"><%:Metric%></div>
-                       <div class="th"><%:Last Ref%></div>
-               </div>
-       </div>
-       </div>
-</div>
-</div>
-
-<script src="<%=resource%>/bmx7/js/polling.js"></script>
-<script>
-               new TablePooler(10,"/cgi-bin/bmx7-info", {'originators':''}, "nodes_div", function(st){
-                       var originators = st.originators;
-                       var res = Array();
-                       originators.forEach(function(originator,i){
-                               var name =  originator.name;
-                               var shortId =  originator.shortId;
-                               var SsTt = originator.S+'/'+originator.s+'/'+originator.T+'/'+originator.t;
-                               var primaryIp = originator.primaryIp;
-                               var nbName = originator.nbName;
-                               var dev = originator.dev;
-                               var metric = originator.metric;
-                               var lastRef = originator.lastRef;
-                       res.push([name, shortId, SsTt, primaryIp,
-                                                               nbName, dev, metric, lastRef]);
-                 });
-                 return res;
-               });
-</script>
diff --git a/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/status_j.htm b/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/status_j.htm
deleted file mode 100644 (file)
index bfabf1e..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-<%+header%>
-<script src="<%=resource%>/cbi.js"></script>
-<script src="<%=resource%>/bmx7/js/polling.js"></script>
-
-<div class="cbi-map">
-       <center>
-               <img src="<%=resource%>/bmx7/bmx7logo.png" />
-               <br />
-               <br />
-               A mesh routing protocol for Linux devices.<br />
-               Visit <a href="http://bmx6.net">bmx6.net</a> for more information.<br />
-               <br />
-       </center>
-
-<div class="cbi-map-descr"></div>
-
-<div class="cbi-section">
-       <legend><%:Node configuration%></legend>
-       <div class="cbi-section-node">
-       <div class="table" id="config_div">
-               <div class="tr table-titles">
-                       <div class="th"><%:Short ID%></div>
-                       <div class="th"><%:Node name%></div>
-                       <div class="th"><%:Primary IPv6 address%></div>
-                       <div class="th"><%:Node key%></div>
-                       <div class="th"><%:Short DHash%></div>
-                       <div class="th"><%:BMX7 revision%></div>
-               </div>
-       </div>
-       </div>
-</div>
-
-
-<div class="cbi-section">
-       <legend><%:Node status%></legend>
-       <div class="cbi-section-node">
-       <div class="table" id="status_div">
-               <div class="tr table-titles">
-                       <div class="th"><%:Nodes seen%></div>
-                       <div class="th"><%:Neighbours%></div>
-                       <div class="th"><%:Tunnelled IPv6 address%></div>
-                       <div class="th"><%:Tunnelled IPv4 address%></div>
-                       <div class="th"><%:Uptime%></div>
-                       <div class="th"><%:CPU usage%></div>
-                       <div class="th"><%:Memory usage%></div>
-                       <div class="th"><%:Tx queue%></div>
-               </div>
-       </div>
-       </div>
-</div>
-
-<div class="cbi-section">
-       <legend><%:Network interfaces%></legend>
-       <div class="cbi-section-node">
-       <div class="table" id="ifaces_div">
-               <div class="tr table-titles">
-                       <div class="th"><%:Interface%></div>
-                       <div class="th"><%:State%></div>
-                       <div class="th"><%:Type%></div>
-                       <div class="th"><%:Max rate%></div>
-                       <div class="th"><%:Link-local IPv6%></div>
-                       <div class="th"><%:RX BpP%></div>
-                       <div class="th"><%:TX BpP%></div>
-               </div>
-       </div>
-       </div>
-</div>
-
-
-<div class="cbi-section">
-       <legend><%:Links%></legend>
-       <div class="cbi-section-node">
-       <div class="table" id="links_div">
-               <div class="tr table-titles">
-                       <div class="th"><%:Short ID%></div>
-                       <div class="th"><%:Name%></div>
-                       <div class="th"><%:Link key%></div>
-                       <div class="th"><%:Remote link-local IPv6%></div>
-                       <div class="th"><%:Device%></div>
-                       <div class="th"><%:RX rate%></div>
-                       <div class="th"><%:TX rate%></div>
-                       <div class="th"><%:Routes%></div>
-               </div>
-       </div>
-       </div>
-</div>
-
-</div>
-
-<script>
-       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "config_div", function(st){
-               var res = Array();
-               var sta = st.info[0].status;
-               res.push([sta.shortId, sta.name, sta.primaryIp, sta.nodeKey, sta.shortDhash, sta.revision]);
-               return res;
-       });
-
-       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "status_div", function(st){
-               var res = Array();
-               var sta = st.info[0].status;
-               var mem = st.info[3].memory.bmx7;
-               var txQ = sta.txQ.split('/');
-               var ptxQ = '<p style="color:rgb('+parseInt(255*txQ[0]/txQ[1])+','+parseInt(128*(txQ[1]-txQ[0])/txQ[1])+',0)")>'+sta.txQ+'</p>';
-               res.push([sta.nodes, sta.nbs, sta.tun6Address, sta.tun4Address, sta.uptime, sta.cpu, mem, ptxQ]);
-               return res;
-       });
-
-       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "ifaces_div", function(st){
-               var res = Array();
-               var ifaces = st.info[1].interfaces;
-
-               ifaces.forEach(function(iface){
-                       res.push([iface.dev, iface.state, iface.type, iface.rateMax, iface.localIp, iface.rxBpP, iface.txBpP]);
-               });
-               return res;
-       });
-
-       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':''}, "links_div", function(st){
-               var res = Array();
-               links = st.info[2].links;
-
-               links.forEach(function(link){
-                       res.push([link.shortId, link.name, link.linkKey, link.nbLocalIp, link.dev, link.rxRate, link.txRate, link.rts]);
-               });
-               return res;
-       });
-
-</script>
-
-<%+footer%>
diff --git a/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/topology.htm b/applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/topology.htm
deleted file mode 100644 (file)
index 1f09cc4..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<%+header%>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js" integrity="sha512-uy3foVtL4u0+5430l7zZt4PHjVtICfrbu3mtzdanR425sKD7kS5264djeZAzNIV0l4vc1QkFpW2+G5i5KoJIFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
-<script src="<%=resource%>/bmx7/js/netjsongraph.js"></script>
-
-<link href="<%=resource%>/bmx7/css/netjsongraph.css" rel="stylesheet">
-<style>
-        body {
-            font-family: Arial, sans-serif;
-            font-size: 13px;
-        }
-
-        .njg-overlay{
-            width: auto;
-            height: auto;
-            min-width: 200px;
-            max-width: 400px;
-            border: 1px solid #000;
-            border-radius: 2px;
-            background: rgba(0, 0, 0, 0.7);
-            top: 10px;
-            right: 10px;
-            padding: 0 15px;
-            font-family: Arial, sans-serif;
-            font-size: 14px;
-            color: #fff
-        }
-
-        .njg-node {
-            fill: #008000;
-            fill-opacity: 0.8;
-            stroke: #008000;
-            stroke-width: 1px;
-            cursor: pointer;
-        }
-        .njg-node:hover,
-        .njg-node.njg-open{
-            fill-opacity: 1;
-        }
-
-        .njg-link {
-            stroke: #00ff00;
-            stroke-width: 2;
-            stroke-opacity: .5;
-            cursor: pointer;
-        }
-        .njg-link:hover,
-        .njg-link.njg-open{
-            stroke-width: 3;
-            stroke-opacity: 1
-        }
-</style>
-<script>d3.netJsonGraph("/cgi-bin/bmx7-info?netjson/network-graph.json", { defaultStyle: false });</script>
-<%+footer%>                                                                        
-
diff --git a/applications/luci-app-bmx7/root/usr/share/luci/menu.d/luci-app-bmx7.json b/applications/luci-app-bmx7/root/usr/share/luci/menu.d/luci-app-bmx7.json
new file mode 100644 (file)
index 0000000..a5f75c6
--- /dev/null
@@ -0,0 +1,73 @@
+{
+       "admin/network/bmx7": {
+               "title": "BMX7",
+               "order": 50,
+               "action": {
+                       "type": "view",
+                       "path": "bmx7/config"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       },
+
+       "admin/network/bmx7/config": {
+               "title": "Config",
+               "order": 1,
+               "action": {
+                       "type": "view",
+                       "path": "bmx7/config"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       },
+
+       "admin/network/bmx7/status": {
+               "title": "Status",
+               "order": 3,
+               "action": {
+                       "type": "template",
+                       "path": "bmx7/bmxstatus"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       },
+
+       "admin/network/bmx7/topology": {
+               "title": "Topology",
+               "order": 4,
+               "action": {
+                       "type": "template",
+                       "path": "bmx7/bmxtopology"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       },
+
+       "admin/network/bmx7/nodes": {
+               "title": "Nodes",
+               "order": 5,
+               "action": {
+                       "type": "template",
+                       "path": "bmx7/bmxnodes"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       },
+
+       "admin/network/bmx7/tunnels": {
+               "title": "Tunnels",
+               "order": 6,
+               "action": {
+                       "type": "template",
+                       "path": "bmx7/bmxtunnels"
+               },
+               "depends": {
+                       "acl": [ "luci-app-bmx7" ]
+               }
+       }
+}
diff --git a/applications/luci-app-bmx7/root/usr/share/rpcd/acl.d/luci-app-bmx7.json b/applications/luci-app-bmx7/root/usr/share/rpcd/acl.d/luci-app-bmx7.json
new file mode 100644 (file)
index 0000000..174b0dd
--- /dev/null
@@ -0,0 +1,18 @@
+{
+       "luci-app-bmx7": {
+               "description": "Grant UCI access for luci-app-bmx7",
+               "read": {
+                       "file": {
+                               "/www/cgi-bin/bmx7-info *": "exec"
+                       },
+                       "uci": [
+                               "bmx7"
+                       ]
+               },
+               "write": {
+                       "uci": [
+                               "bmx7"
+                       ]
+               }
+       }
+}
index 327e3f0f169f4c275f61d0262ac926aff02d026d..c65aae048f0d7d7dda54d9c8a98496a78970f726 100755 (executable)
 #    the file called "COPYING".
 #
 #    This script gives information about bmx7
-#    Can be executed from a linux shell: ./bmx7-info -s links
-#    Or from web interfae (with cgi enabled): http://host/cgi-bin/bmx7-info?links
-#    If you ask for a directory you wil get the directory contents in JSON forman
+#    Can be executed from a Linux shell: ./bmx7-info -s links
+#    Or from web interface (with cgi enabled): http://host/cgi-bin/bmx7-info?links
+#    If you ask for a directory you will get the directory contents in JSON format
 
 BMX7_DIR="$(uci get bmx7.general.runtimeDir 2>/dev/null)" || BMX7_DIR="/var/run/bmx7/json"
 
-#Checking if shell mode or cgi-bin mode
-if [ "$1" == "-s" ]; then
-       QUERY="$2"
-else
-       QUERY="${QUERY_STRING%%=*}"
-       echo "Content-type: application/json"
-       echo ""
-fi
+case "${1:-}" in
+       -s)
+               QUERY="$2"
+               ;;
+       *)
+               QUERY="${QUERY_STRING%%&*}"
+               QUERY="${QUERY%%=*}"
+               printf 'Content-type: application/json\n\n'
+               ;;
+esac
 
 check_path() {
-       [ -d "$1" ] && path=$(cd $1; pwd)
-       [ -f "$1" ] && path=$(cd $1/..; pwd)
-       [ $(echo "$path" | grep -c "^$BMX7_DIR") -ne 1 ] && exit 1
-}
+       target="$1"
+
+       # Resolve real absolute path safely
+       resolved="$(cd "$(dirname -- "$target")" 2>/dev/null && pwd -P)/$(basename -- "$target")"
 
+       [ -e "$resolved" ] || return
+}
 print_mem() {
-       echo -n '{ "memory": { "bmx7": "'
-       cat /proc/$(cat /var/run/bmx7/pid)/status |grep -i VmSize | tr -s " " | cut -d " " -f 2,3 | tr -d "\n"
-       echo '"}}'
+       pid="$(pidof bmx7 2>/dev/null)" || return
+       [ -r "/proc/$pid/status" ] || return
+
+       vm=$(awk '/VmSize:/ {print $2" "$3}' "/proc/$pid/status")
+
+       printf '{ "memory": { "bmx7": "%s" }}' "$vm"
 }
 
 print_query() {
@@ -53,81 +60,75 @@ print_query() {
        [ -d "$BMX7_DIR/$1" ] &&
        {
        # If /all has not been specified
-               [ -z "$QALL" ] &&
-               {
-               total=$(ls $BMX7_DIR/$1 | wc -w)
-               i=1
-               echo -n "{ \"$1\": [ "
-               for f in $(ls $BMX7_DIR/$1); do
-                       echo -n "{ \"name\": \"$f\" }"
-                       [ $i -lt $total ]  && echo -n ','
-                       i=$(( $i + 1 ))
+               if [ -z "$QALL" ]; then
+               first=1
+               printf '{ "%s": [ ' "$1"
+               for f in "$BMX7_DIR"/$1; do
+                       [ -e "$f" ] || continue
+                       printf '{ "name": "%s" }' "$(tr -d '\n' < "$f")"
+                       [ $first -eq 0 ] && printf ','
+                       first=0
                done
-               echo -n " ] }"
+               printf " ] }"
 
-       # If /all has been specified, printing all the files together
-               } || {
-               comma=""
-               echo -n "[ "
-               for entry in "$BMX7_DIR/$1/"*; do
+       # If /all has been specified, print all the files together
+               else
+               first=1
+               printf "[ "
+               for entry in "$BMX7_DIR"/$1; do
                        [ -f "$entry" ] &&
                        {
-                               ${comma:+echo "$comma"}
-                               tr -d '\n' < "$entry"
-                               comma=","
+                               [ $first -eq 0 ] && printf ','
+                               printf "%s" "$(tr -d '\n' < "$entry")"
+                               first=0
                        }
                done
-               echo -n " ]"
-               }
+               printf " ]"
+               fi
        }
 
-       # If the query is a file, just printing the file
+       # If the query is a file, just print the file
        [ -f "$BMX7_DIR/$1" ] && [ -s "$BMX7_DIR/$1" ] && cat "$BMX7_DIR/$1" && return 0 || return 1
 }
 
-if [ "${QUERY##*/}" == "all" ]; then
+if [ "${QUERY##*/}" = "all" ]; then
        QUERY="${QUERY%/all}"
        QALL=1
 fi
 
-if [ "$QUERY" == 'info' ]; then
-       echo '{ "info": [ '
-       print_query status
-       echo -n ","
-       print_query interfaces && echo -n "," || echo -n '{ "interfaces": "" },'
-       print_query links && echo -n "," || echo -n '{ "links": "" },'
-       print_mem
-       echo "] }"
-fi
-
-if [ "$QUERY" == 'neighbours' ]; then
-       QALL=1
-       echo '{ "neighbours": [ '
-       echo '{ "originators": '
-       print_query originators
-       echo '}, '
-       echo '{ "descriptions": '
-       print_query descriptions
-       echo "} ] }"
-       exit 0
-
-else if [ "$QUERY" == 'tunnels' ]; then
-       bmx7 -c --jshow tunnels /r=0
-       exit 0
-
-       else if [ "$QUERY" == 'originators' ]; then
+case "$QUERY" in
+       neighbours)
+               QALL=1
+               printf '{ "neighbours": [ '
+               printf '{ "originators": '
+               print_query originators
+               printf '}, '
+               printf '{ "descriptions": '
+               print_query descriptions
+               printf "} ] }"
+               exit 0
+               ;;
+       tunnels)
+               bmx7 -c --jshow tunnels /r=0
+               exit 0
+               ;;
+       originators)
                bmx7 -c --jshow originators /r=0
                exit 0
-
-               else
+               ;;
+       info)
+               printf '{ "info": [ '
+               print_query status && printf "," || printf '{ "status": "" },'
+               print_query interfaces && printf "," || printf '{ "interfaces": "" },'
+               print_query links && printf "," || printf '{ "links": "" },'
+               print_mem
+               printf "] }"
+               ;;
+       *)
                check_path "$BMX7_DIR/$QUERY"
-               print_query $QUERY
-               exit 0
-               fi
-       fi
-fi
-fi
+               print_query "$QUERY"
+               ;;
+esac
 
-ls -1F "$BMX7_DIR"
 exit 0
 
diff --git a/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph-theme.css b/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph-theme.css
deleted file mode 100644 (file)
index 276d362..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-.njg-overlay{
-    background: #fbfbfb;
-    border-radius: 2px;
-    border: 1px solid #ccc;
-    color: #6d6357;
-    font-family: Arial, sans-serif;
-    font-family: sans-serif;
-    font-size: 14px;
-    line-height: 20px;
-    height: auto;
-    max-width: 400px;
-    min-width: 200px;
-    padding: 0 15px;
-    right: 10px;
-    top: 10px;
-    width: auto;
-}
-
-.njg-metadata{
-    background: #fbfbfb;
-    border-radius: 2px;
-    border: 1px solid #ccc;
-    color: #6d6357;
-    display: none;
-    font-family: Arial, sans-serif;
-    font-family: sans-serif;
-    font-size: 14px;
-    height: auto;
-    left: 10px;
-    max-width: 500px;
-    min-width: 200px;
-    padding: 0 15px;
-    top: 10px;
-    width: auto;
-}
-
-.njg-node{
-    stroke-opacity: 0.5;
-    stroke-width: 7px;
-    stroke: #fff;
-}
-
-.njg-node:hover,
-.njg-node.njg-open {
-    stroke: rgba(0, 0, 0, 0.2);
-}
-
-.njg-link{
-    cursor: pointer;
-    stroke: #999;
-    stroke-width: 2;
-    stroke-opacity: 0.25;
-}
-
-.njg-link:hover,
-.njg-link.njg-open{
-    stroke-width: 4 !important;
-    stroke-opacity: 0.5;
-}
diff --git a/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph.css b/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/css/netjsongraph.css
deleted file mode 100644 (file)
index 556c520..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-.njg-hidden {
-    display: none !important;
-    visibility: hidden !important;
-}
-
-.njg-tooltip{
-    font-family: sans-serif;
-    font-size: 10px;
-    fill: #000;
-    opacity: 0.5;
-    text-anchor: middle;
-}
-
-.njg-overlay{
-    display: none;
-    position: absolute;
-    z-index: 11;
-}
-
-.njg-close{
-    cursor: pointer;
-    position: absolute;
-    right: 10px;
-    top: 10px;
-}
-.njg-close:before { content: "\2716"; }
-
-.njg-metadata{
-    display: none;
-    position: absolute;
-    z-index: 12;
-}
-
-.njg-node{ cursor: pointer }
-.njg-link{ cursor: pointer }
-
-#njg-select-group {
-    text-align: center;
-    box-shadow: 0 0 10px #ccc;
-    position: fixed;
-    left: 50%;
-    top: 50%;
-    width: 50%;
-    margin-top: -7.5em;
-    margin-left: -25%;
-    padding: 5em 2em;
-}
-
-#njg-select-group select {
-    font-size: 2em;
-    padding: 10px 15px;
-    width: 50%;
-    cursor: pointer;
-}
-
-#njg-select-group option {
-    padding: 0.5em;
-}
-
-#njg-select-group option[value=""] {
-    color: #aaa;
-}
diff --git a/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/netjsongraph.js b/applications/luci-app-bmx7/root/www/luci-static/resources/bmx7/js/netjsongraph.js
deleted file mode 100644 (file)
index 1df0db4..0000000
+++ /dev/null
@@ -1,568 +0,0 @@
-// version 0.1
-(function () {
-    /**
-     * vanilla JS implementation of jQuery.extend()
-     */
-    d3._extend = function(defaults, options) {
-        var extended = {},
-            prop;
-        for(prop in defaults) {
-            if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
-                extended[prop] = defaults[prop];
-            }
-        }
-        for(prop in options) {
-            if(Object.prototype.hasOwnProperty.call(options, prop)) {
-                extended[prop] = options[prop];
-            }
-        }
-        return extended;
-    };
-
-    /**
-      * @function
-      *   @name d3._pxToNumber
-      * Convert strings like "10px" to 10
-      *
-      * @param  {string}       val         The value to convert
-      * @return {int}              The converted integer
-      */
-    d3._pxToNumber = function(val) {
-        return parseFloat(val.replace('px'));
-    };
-
-    /**
-      * @function
-      *   @name d3._windowHeight
-      *
-      * Get window height
-      *
-      * @return  {int}            The window innerHeight
-      */
-    d3._windowHeight = function() {
-        return window.innerHeight || document.documentElement.clientHeight || 600;
-    };
-
-    /**
-      * @function
-      *   @name d3._getPosition
-      *
-      * Get the position of `element` relative to `container`
-      *
-      * @param  {object}      element
-      * @param  {object}      container
-      */
-     d3._getPosition = function(element, container) {
-         var n = element.node(),
-             nPos = n.getBoundingClientRect();
-             cPos = container.node().getBoundingClientRect();
-         return {
-            top: nPos.top - cPos.top,
-            left: nPos.left - cPos.left,
-            width: nPos.width,
-            bottom: nPos.bottom - cPos.top,
-            height: nPos.height,
-            right: nPos.right - cPos.left
-        };
-     };
-
-    /**
-     * netjsongraph.js main function
-     *
-     * @constructor
-     * @param  {string}      url             The NetJSON file url
-     * @param  {object}      opts            The object with parameters to override {@link d3.netJsonGraph.opts}
-     */
-    d3.netJsonGraph = function(url, opts) {
-        /**
-         * Default options
-         *
-         * @param  {string}     el                  "body"      The container element                                  el: "body" [description]
-         * @param  {bool}       metadata            true        Display NetJSON metadata at startup?
-         * @param  {bool}       defaultStyle        true        Use css style?
-         * @param  {bool}       animationAtStart    false       Animate nodes or not on load
-         * @param  {array}      scaleExtent         [0.25, 5]   The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent}
-         * @param  {int}        charge              -130        The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge}
-         * @param  {int}        linkDistance        50          The target distance between linked nodes to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkDistance}
-         * @param  {float}      linkStrength        0.2         The strength (rigidity) of links to the specified value in the range. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkStrength}
-         * @param  {float}      friction            0.9         The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction}
-         * @param  {string}     chargeDistance      Infinity    The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance}
-         * @param  {float}      theta               0.8         The Barnes–Hut approximation criterion to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#theta}
-         * @param  {float}      gravity             0.1         The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity}
-         * @param  {int}        circleRadius        8           The radius of circles (nodes) in pixel
-         * @param  {string}     labelDx             "0"         SVG dx (distance on x axis) attribute of node labels in graph
-         * @param  {string}     labelDy             "-1.3em"    SVG dy (distance on y axis) attribute of node labels in graph
-         * @param  {function}   onInit                          Callback function executed on initialization
-         * @param  {function}   onLoad                          Callback function executed after data has been loaded
-         * @param  {function}   onEnd                           Callback function executed when initial animation is complete
-         * @param  {function}   linkDistanceFunc                By default high density areas have longer links
-         * @param  {function}   redraw                          Called when panning and zooming
-         * @param  {function}   prepareData                     Used to convert NetJSON NetworkGraph to the javascript data
-         * @param  {function}   onClickNode                     Called when a node is clicked
-         * @param  {function}   onClickLink                     Called when a link is clicked
-         */
-        opts = d3._extend({
-            el: "body",
-            metadata: true,
-            defaultStyle: true,
-            animationAtStart: true,
-            scaleExtent: [0.25, 5],
-            charge: -130,
-            linkDistance: 50,
-            linkStrength: 0.2,
-            friction: 0.9,  // d3 default
-            chargeDistance: Infinity,  // d3 default
-            theta: 0.8,  // d3 default
-            gravity: 0.1,
-            circleRadius: 8,
-            labelDx: "0",
-            labelDy: "-1.3em",
-            nodeClassProperty: null,
-            linkClassProperty: null,
-            /**
-             * @function
-             * @name onInit
-             *
-             * Callback function executed on initialization
-             * @param  {string|object}  url     The netJson remote url or object
-             * @param  {object}         opts    The object of passed arguments
-             * @return {function}
-             */
-            onInit: function(url, opts) {},
-            /**
-             * @function
-             * @name onLoad
-             *
-             * Callback function executed after data has been loaded
-             * @param  {string|object}  url     The netJson remote url or object
-             * @param  {object}         opts    The object of passed arguments
-             * @return {function}
-             */
-            onLoad: function(url, opts) {},
-            /**
-             * @function
-             * @name onEnd
-             *
-             * Callback function executed when initial animation is complete
-             * @param  {string|object}  url     The netJson remote url or object
-             * @param  {object}         opts    The object of passed arguments
-             * @return {function}
-             */
-            onEnd: function(url, opts) {},
-            /**
-             * @function
-             * @name linkDistanceFunc
-             *
-             * By default, high density areas have longer links
-             */
-            linkDistanceFunc: function(d){
-                var val = opts.linkDistance;
-                if(d.source.linkCount >= 4 && d.target.linkCount >= 4) {
-                    return val * 2;
-                }
-                return val;
-            },
-            /**
-             * @function
-             * @name redraw
-             *
-             * Called on zoom and pan
-             */
-            redraw: function() {
-                panner.attr("transform",
-                    "trans"+"late(" + d3.event.translate + ") " +
-                    "scale(" + d3.event.scale + ")"
-                );
-            },
-            /**
-             * @function
-             * @name prepareData
-             *
-             * Convert NetJSON NetworkGraph to the data structure consumed by d3
-             *
-             * @param graph {object}
-             */
-            prepareData: function(graph) {
-                var nodesMap = {},
-                    nodes = graph.nodes.slice(), // copy
-                    links = graph.links.slice(), // copy
-                    nodes_length = graph.nodes.length,
-                    links_length = graph.links.length;
-
-                for(var i = 0; i < nodes_length; i++) {
-                    // count how many links every node has
-                    nodes[i].linkCount = 0;
-                    nodesMap[nodes[i].id] = i;
-                }
-                for(var c = 0; c < links_length; c++) {
-                    var sourceIndex = nodesMap[links[c].source],
-                    targetIndex = nodesMap[links[c].target];
-                    // ensure source and target exist
-                    if(!nodes[sourceIndex]) { throw("source '" + links[c].source + "' not found"); }
-                    if(!nodes[targetIndex]) { throw("target '" + links[c].target + "' not found"); }
-                    links[c].source = nodesMap[links[c].source];
-                    links[c].target = nodesMap[links[c].target];
-                    // add link count to both ends
-                    nodes[sourceIndex].linkCount++;
-                    nodes[targetIndex].linkCount++;
-                }
-                return { "nodes": nodes, "links": links };
-            },
-            /**
-             * @function
-             * @name onClickNode
-             *
-             * Called when a node is clicked
-             */
-            onClickNode: function(n) {
-                var overlay = d3.select(".njg-overlay"),
-                    overlayInner = d3.select(".njg-overlay > .njg-inner"),
-                    html = "<p><b>id</b>: " + n.id + "</p>";
-                    if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
-                    if(n.properties) {
-                        for(var key in n.properties) {
-                            if(!n.properties.hasOwnProperty(key)) { continue; }
-                            html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
-                    }
-                }
-                if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
-                if(n.local_addresses) {
-                    html += "<p><b>local addresses</b>:<br />" + n.local_addresses.join('<br />') + "</p>";
-                }
-                overlayInner.html(html);
-                overlay.classed("njg-hidden", false);
-                overlay.style("display", "block");
-                // set "open" class to current node
-                removeOpenClass();
-                d3.select(this).classed("njg-open", true);
-            },
-            /**
-             * @function
-             * @name onClickLink
-             *
-             * Called when a node is clicked
-             */
-            onClickLink: function(l) {
-                var overlay = d3.select(".njg-overlay"),
-                    overlayInner = d3.select(".njg-overlay > .njg-inner"),
-                    html = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
-                    html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
-                    html += "<p><b>cost</b>: " + l.cost + "</p>";
-                if(l.properties) {
-                    for(var key in l.properties) {
-                        if(!l.properties.hasOwnProperty(key)) { continue; }
-                        html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
-                    }
-                }
-                overlayInner.html(html);
-                overlay.classed("njg-hidden", false);
-                overlay.style("display", "block");
-                // set "open" class to current link
-                removeOpenClass();
-                d3.select(this).classed("njg-open", true);
-            }
-        }, opts);
-
-        // init callback
-        opts.onInit(url, opts);
-
-        if(!opts.animationAtStart) {
-            opts.linkStrength = 2;
-            opts.friction = 0.3;
-            opts.gravity = 0;
-        }
-        if(opts.el == "body") {
-            var body = d3.select(opts.el),
-                rect = body.node().getBoundingClientRect();
-            if (d3._pxToNumber(d3.select("body").style("height")) < 60) {
-                body.style("height", d3._windowHeight() - rect.top - rect.bottom + "px");
-            }
-        }
-        var el = d3.select(opts.el).style("position", "relative"),
-            width = d3._pxToNumber(el.style('width')),
-            height = d3._pxToNumber(el.style('height')),
-            force = d3.layout.force()
-                      .charge(opts.charge)
-                      .linkStrength(opts.linkStrength)
-                      .linkDistance(opts.linkDistanceFunc)
-                      .friction(opts.friction)
-                      .chargeDistance(opts.chargeDistance)
-                      .theta(opts.theta)
-                      .gravity(opts.gravity)
-                      // width is easy to get, if height is 0 take the height of the body
-                      .size([width, height]),
-            zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent),
-            // panner is the element that allows zooming and panning
-            panner = el.append("svg")
-                       .attr("width", width)
-                       .attr("height", height)
-                       .call(zoom.on("zoom", opts.redraw))
-                       .append("g")
-                       .style("position", "absolute"),
-            svg = d3.select(opts.el + " svg"),
-            drag = force.drag(),
-            overlay = d3.select(opts.el).append("div").attr("class", "njg-overlay"),
-            closeOverlay = overlay.append("a").attr("class", "njg-close"),
-            overlayInner = overlay.append("div").attr("class", "njg-inner"),
-            metadata = d3.select(opts.el).append("div").attr("class", "njg-metadata"),
-            metadataInner = metadata.append("div").attr("class", "njg-inner"),
-            closeMetadata = metadata.append("a").attr("class", "njg-close"),
-            // container of ungrouped networks
-            str = [],
-            selected = [],
-            /**
-             * @function
-             * @name removeOpenClass
-             *
-             * Remove open classes from nodes and links
-             */
-            removeOpenClass = function () {
-                d3.selectAll("svg .njg-open").classed("njg-open", false);
-            };
-            processJson = function(graph) {
-                /**
-                 * Init netJsonGraph
-                 */
-                init = function(url, opts) {
-                    d3.netJsonGraph(url, opts);
-                };
-                /**
-                 * Remove all instances
-                 */
-                destroy = function() {
-                    force.stop();
-                    d3.select("#selectGroup").remove();
-                    d3.select(".njg-overlay").remove();
-                    d3.select(".njg-metadata").remove();
-                    overlay.remove();
-                    overlayInner.remove();
-                    metadata.remove();
-                    svg.remove();
-                    node.remove();
-                    link.remove();
-                    nodes = [];
-                    links = [];
-                };
-                /**
-                 * Destroy and e-init all instances
-                 * @return {[type]} [description]
-                 */
-                reInit = function() {
-                    destroy();
-                    init(url, opts);
-                };
-
-                var data = opts.prepareData(graph),
-                    links = data.links,
-                    nodes = data.nodes;
-
-                // disable some transitions while dragging
-                drag.on('dragstart', function(n){
-                    d3.event.sourceEvent.stopPropagation();
-                    zoom.on('zoom', null);
-                })
-                // re-enable transitions when dragging stops
-                .on('dragend', function(n){
-                    zoom.on('zoom', opts.redraw);
-                })
-                .on("drag", function(d) {
-                    // avoid pan & drag conflict
-                    d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
-                });
-
-                force.nodes(nodes).links(links).start();
-
-                var link = panner.selectAll(".link")
-                                 .data(links)
-                                 .enter().append("line")
-                                 .attr("class", function (link) {
-                                     var baseClass = "njg-link",
-                                         addClass = null;
-                                         value = link.properties && link.properties[opts.linkClassProperty];
-                                     if (opts.linkClassProperty && value) {
-                                         // if value is stirng use that as class
-                                         if (typeof(value) === "string") {
-                                             addClass = value;
-                                         }
-                                         else if (typeof(value) === "number") {
-                                             addClass = opts.linkClassProperty + value;
-                                         }
-                                         else if (value === true) {
-                                             addClass = opts.linkClassProperty;
-                                         }
-                                         return baseClass + " " + addClass;
-                                     }
-                                     return baseClass;
-                                 })
-                                 .on("click", opts.onClickLink),
-                    groups = panner.selectAll(".node")
-                                   .data(nodes)
-                                   .enter()
-                                   .append("g");
-                    node = groups.append("circle")
-                                 .attr("class", function (node) {
-                                     var baseClass = "njg-node",
-                                         addClass = null;
-                                         value = node.properties && node.properties[opts.nodeClassProperty];
-                                     if (opts.nodeClassProperty && value) {
-                                         // if value is stirng use that as class
-                                         if (typeof(value) === "string") {
-                                             addClass = value;
-                                         }
-                                         else if (typeof(value) === "number") {
-                                             addClass = opts.nodeClassProperty + value;
-                                         }
-                                         else if (value === true) {
-                                             addClass = opts.nodeClassProperty;
-                                         }
-                                         return baseClass + " " + addClass;
-                                     }
-                                     return baseClass;
-                                 })
-                                 .attr("r", opts.circleRadius)
-                                 .on("click", opts.onClickNode)
-                                 .call(drag);
-
-                    var labels = groups.append('text')
-                                       .text(function(n){ return n.label || n.id })
-                                       .attr('dx', opts.labelDx)
-                                       .attr('dy', opts.labelDy)
-                                       .attr('class', 'njg-tooltip');
-
-                // Close overlay
-                closeOverlay.on("click", function() {
-                    removeOpenClass();
-                    overlay.classed("njg-hidden", true);
-                });
-                // Close Metadata panel
-                closeMetadata.on("click", function() {
-                    // Reinitialize the page
-                    if(graph.type === "NetworkCollection") {
-                        reInit();
-                    }
-                    else {
-                        removeOpenClass();
-                        metadata.classed("njg-hidden", true);
-                    }
-                });
-                // default style
-                // TODO: probably change defaultStyle
-                // into something else
-                if(opts.defaultStyle) {
-                    var colors = d3.scale.category20c();
-                    node.style({
-                        "fill": function(d){ return colors(d.linkCount); },
-                        "cursor": "pointer"
-                    });
-                }
-                // Metadata style
-                if(opts.metadata) {
-                    metadata.attr("class", "njg-metadata").style("display", "block");
-                }
-
-                var attrs = ["protocol",
-                             "version",
-                             "revision",
-                             "metric",
-                             "router_id",
-                             "topology_id"],
-                    html = "";
-                if(graph.label) {
-                    html += "<h3>" + graph.label + "</h3>";
-                }
-                for(var i in attrs) {
-                    var attr = attrs[i];
-                    if(graph[attr]) {
-                        html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
-                    }
-                }
-                // Add nodes and links count
-                html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
-                html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
-                metadataInner.html(html);
-                metadata.classed("njg-hidden", false);
-
-                // onLoad callback
-                opts.onLoad(url, opts);
-
-                force.on("tick", function() {
-                    link.attr("x1", function(d) {
-                        return d.source.x;
-                    })
-                    .attr("y1", function(d) {
-                        return d.source.y;
-                    })
-                    .attr("x2", function(d) {
-                        return d.target.x;
-                    })
-                    .attr("y2", function(d) {
-                        return d.target.y;
-                    });
-
-                    node.attr("cx", function(d) {
-                        return d.x;
-                    })
-                    .attr("cy", function(d) {
-                        return d.y;
-                    });
-
-                    labels.attr("transform", function(d) {
-                        return "trans"+"late(" + d.x + "," + d.y + ")";
-                    });
-                })
-                .on("end", function(){
-                    force.stop();
-                    // onEnd callback
-                    opts.onEnd(url, opts);
-                });
-
-                return force;
-            };
-
-        if(typeof(url) === "object") {
-            processJson(url);
-        }
-        else {
-            /**
-            * Parse the provided json file
-            * and call processJson() function
-            *
-            * @param  {string}     url         The provided json file
-            * @param  {function}   error
-            */
-            d3.json(url, function(error, graph) {
-                if(error) { throw error; }
-                /**
-                * Check if the json contains a NetworkCollection
-                */
-                if(graph.type === "NetworkCollection") {
-                    var selectGroup = body.append("div").attr("id", "njg-select-group"),
-                        select = selectGroup.append("select")
-                                            .attr("id", "select");
-                        str = graph;
-                    select.append("option")
-                          .attr({
-                              "value": "",
-                              "selected": "selected",
-                              "name": "default",
-                              "disabled": "disabled"
-                          })
-                          .html("Choose the network to display");
-                    graph.collection.forEach(function(structure) {
-                        select.append("option").attr("value", structure.type).html(structure.type);
-                        // Collect each network json structure
-                        selected[structure.type] = structure;
-                    });
-                    select.on("change", function() {
-                        selectGroup.attr("class", "njg-hidden");
-                        // Call selected json structure
-                        processJson(selected[this.options[this.selectedIndex].value]);
-                    });
-                }
-                else {
-                    processJson(graph);
-                }
-            });
-        }
-     };
-})();
similarity index 64%
rename from applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/nodes_j.htm
rename to applications/luci-app-bmx7/ucode/template/bmx7/bmxnodes.ut
index bbbeb52c0e63557476b4a38920290706e139b0bc..6718e33182bc7f8768dd214189866747cf51960f 100644 (file)
@@ -1,4 +1,4 @@
-<%#
+{#
    Copyright Â© 2011 Pau Escrich <pau@dabax.net>
    Contributors Lluis Esquerda <eskerda@gmail.com>
                 Roger Pueyo Centelles <roger.pueyo@guifi.net>
 
    The full GNU General Public License is included in this distribution in
    the file called "COPYING".
--%>
+-#}
 
-<%+header%>
-<script src="<%=resource%>/cbi.js"></script>
-<script src="<%=resource%>/bmx7/js/polling.js"></script>
+{% include('header', { }) %}
+
+<script src="{{ resource }}/bmx7/js/polling.js"></script>
 
 
 <style>
 <div id="extra-info" class="info">
        <br />
        <center>
-       Tip: click the <img src="<%=resource%>/bmx7/world.png" /> icon to see individual node information.
+       Tip: click the <img src="{{ resource }}/bmx7/world.png" /> icon to see individual node information.
        </center>
 </div>
 
 
 <div class="cbi-section">
-       <legend><%:Originators%></legend>
+       <legend>{{_('Originators')}}</legend>
        <div class="cbi-section-node">
        <div class="table" id="nodes_div">
                <div class="tr table-titles">
                        <div class="th"></div>
-                       <div class="th"><%:Name%></div>
-                       <div class="th"><%:Short ID%></div>
-                       <div class="th"><%:S/s/T/t%></div>
-                       <div class="th"><%:Primary IPv6%></div>
-                       <div class="th"><%:Via Neighbour%></div>
-                       <div class="th"><%:Metric%></div>
-                       <div class="th"><%:Last Desc%></div>
-                       <div class="th"><%:Last Ref%></div>
-                       <div class="th"><%: %></div>
+                       <div class="th">{{_('Name')}}</div>
+                       <div class="th">{{_('Short ID')}}</div>
+                       <div class="th">S/s/T/t</div>
+                       <div class="th">{{_('Primary IPv6')}}</div>
+                       <div class="th">{{_('Via Neighbour')}}</div>
+                       <div class="th">{{_('Metric')}}</div>
+                       <div class="th">{{_('Last Desc')}}</div>
+                       <div class="th">{{_('Last Ref')}}</div>
+                       <div class="th"></div>
                </div>
        </div>
        </div>
 </div>
 
 <script>
-               var displayExtraInfo = function ( id ) {
+               let displayExtraInfo = function ( id ) {
                        document.getElementById('extra-info').innerHTML = document.getElementById(id).innerHTML;
                }
-               new TablePooler(5,"/cgi-bin/bmx7-info", {'originators':''}, "nodes_div", function(st){
-                       var infoicon = "<%=resource%>/bmx7/world_small.png";
-                       var originators = st.originators;
-                       var res = Array();
-                       originators.forEach(function(originator,i){
-                               var name =  originator.name;
-                               var shortId =  originator.shortId;
-                               var nodeId = originator.nodeId;
-                               var extensions = originator.name;
-                               var SsTt = originator.S+'/'+originator.s+'/'+originator.T+'/'+originator.t;
-                               var nodeKey = originator.nodeKey;
-                               var descSize = originator.descSize;
-                               var primaryIp = originator.primaryIp;
-                               var nbName = originator.nbName;
-                               var dev = originator.dev;
-                               var nbLocalIp = originator.nbLocalIp;
-                               var metric = originator.metric;
-                               var lastDesc = originator.lastDesc;
-                               var lastRef = originator.lastRef;
-
-                       var extrainfo = '<a onclick="displayExtraInfo(\'ip-' + i + '\')"><img src="' + infoicon + '" / ></a>';
-                       var extrainfo_link = '<a onclick="displayExtraInfo(\'ip-' + i + '\')">' +  '<img src="' + infoicon + '" />' + '</a>';
+               new TablePooler(5,"/cgi-bin/bmx7-info", {'originators':'1'}, "nodes_div", function(st){
+                       let infoicon = "{{ resource }}/bmx7/world_small.png";
+                       let originators = st?.originators;
+                       let res = Array();
+                       originators.forEach(function(originator, i){
+                               let name =  originator?.name;
+                               let shortId =  originator?.shortId;
+                               let nodeId = originator?.nodeId;
+                               let extensions = originator?.name;
+                               let SsTt = originator?.S+'/'+originator?.s+'/'+originator?.T+'/'+originator?.t;
+                               let nodeKey = originator?.nodeKey;
+                               let descSize = originator?.descSize;
+                               let primaryIp = originator?.primaryIp;
+                               let nbName = originator?.nbName;
+                               let dev = originator?.dev;
+                               let nbLocalIp = originator?.nbLocalIp;
+                               let metric = originator?.metric;
+                               let lastDesc = originator?.lastDesc;
+                               let lastRef = originator?.lastRef;
+
+                       let extrainfo = '<a onclick="displayExtraInfo(\'ip-' + i + '\')"><img src="' + infoicon + '" / ></a>';
+                       let extrainfo_link = '<a onclick="displayExtraInfo(\'ip-' + i + '\')">' +  '<img src="' + infoicon + '" />' + '</a>';
 
                        extrainfo = '<div id="ip-'+ i +'" class="hideme">'
                        + "<div class='inforow'>"
                });
 </script>
 
-<%+footer%>
+{% include('footer', { }) %}
diff --git a/applications/luci-app-bmx7/ucode/template/bmx7/bmxstatus.ut b/applications/luci-app-bmx7/ucode/template/bmx7/bmxstatus.ut
new file mode 100644 (file)
index 0000000..8063868
--- /dev/null
@@ -0,0 +1,132 @@
+{% include('header', { }) %}
+
+
+<script src="{{ resource }}/bmx7/js/polling.js"></script>
+
+<div class="cbi-map">
+       <center>
+               <img src="{{ resource }}/bmx7/bmx7logo.png" />
+               <br />
+               <br />
+               A mesh routing protocol for Linux devices.<br />
+               Visit <a href="http://bmx6.net">bmx6.net</a> for more information.<br />
+               <br />
+       </center>
+
+<div class="cbi-map-descr"></div>
+
+<div class="cbi-section">
+       <legend>{{ _('Node configuration') }}</legend>
+       <div class="cbi-section-node">
+       <div class="table" id="config_div">
+               <div class="tr table-titles">
+                       <div class="th">{{ _('Short ID') }}</div>
+                       <div class="th">{{ _('Node name') }}</div>
+                       <div class="th">{{ _('Primary IPv6 address') }}</div>
+                       <div class="th">{{ _('Node key') }}</div>
+                       <div class="th">{{ _('Short DHash') }}</div>
+                       <div class="th">{{ _('BMX7 revision') }}</div>
+               </div>
+       </div>
+       </div>
+</div>
+
+
+<div class="cbi-section">
+       <legend>{{ _('Node status') }}</legend>
+       <div class="cbi-section-node">
+       <div class="table" id="status_div">
+               <div class="tr table-titles">
+                       <div class="th">{{ _('Nodes seen') }}</div>
+                       <div class="th">{{ _('Neighbours') }}</div>
+                       <div class="th">{{ _('Tunnelled IPv6 address') }}</div>
+                       <div class="th">{{ _('Tunnelled IPv4 address') }}</div>
+                       <div class="th">{{ _('Uptime') }}</div>
+                       <div class="th">{{ _('CPU usage') }}</div>
+                       <div class="th">{{ _('Memory usage') }}</div>
+                       <div class="th">{{ _('Tx queue') }}</div>
+               </div>
+       </div>
+       </div>
+</div>
+
+<div class="cbi-section">
+       <legend>{{ _('Network interfaces') }}</legend>
+       <div class="cbi-section-node">
+       <div class="table" id="ifaces_div">
+               <div class="tr table-titles">
+                       <div class="th">{{ _('Interface') }}</div>
+                       <div class="th">{{ _('State') }}</div>
+                       <div class="th">{{ _('Type') }}</div>
+                       <div class="th">{{ _('Max rate') }}</div>
+                       <div class="th">{{ _('Link-local IPv6') }}</div>
+                       <div class="th">{{ _('RX BpP') }}</div>
+                       <div class="th">{{ _('TX BpP') }}</div>
+               </div>
+       </div>
+       </div>
+</div>
+
+
+<div class="cbi-section">
+       <legend>{{ _('Links') }}</legend>
+       <div class="cbi-section-node">
+       <div class="table" id="links_div">
+               <div class="tr table-titles">
+                       <div class="th">{{ _('Short ID') }}</div>
+                       <div class="th">{{ _('Name') }}</div>
+                       <div class="th">{{ _('Link key') }}</div>
+                       <div class="th">{{ _('Remote link-local IPv6') }}</div>
+                       <div class="th">{{ _('Device') }}</div>
+                       <div class="th">{{ _('RX rate') }}</div>
+                       <div class="th">{{ _('TX rate') }}</div>
+                       <div class="th">{{ _('Routes') }}</div>
+               </div>
+       </div>
+       </div>
+</div>
+
+</div>
+
+<script>
+       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "config_div", function(st){
+               let res = Array();
+               let sta = st?.info[0]?.status;
+               res.push([sta?.shortId, sta?.name, sta?.primaryIp, sta?.nodeKey, sta?.shortDhash, sta?.revision]);
+               return res;
+       });
+
+       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "status_div", function(st){
+               let res = Array();
+               let sta = st?.info[0]?.status;
+               let mem = st?.info[3]?.memory?.bmx7;
+               let txQ = sta?.txQ?.split('/');
+               let ptxQ = '<p style="color:rgb('+parseInt(255*txQ?.[0]/txQ?.[1])+','+parseInt(128*(txQ?.[1]-txQ?.[0])/txQ?.[1])+',0)")>'+sta.txQ+'</p>';
+               res.push([sta.nodes, sta.nbs, sta.tun6Address, sta.tun4Address, sta.uptime, sta.cpu, mem, ptxQ]);
+               return res;
+       });
+
+       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "ifaces_div", function(st){
+               let res = Array();
+               let ifaces = st?.info[1]?.interfaces;
+
+               if (ifaces)
+                       ifaces?.forEach(function(iface) {
+                               res.push([iface?.dev, iface?.state, iface?.type, iface?.rateMax, iface?.localIp, iface?.rxBpP, iface?.txBpP]);
+                       });
+               return res;
+       });
+
+       new TablePooler(10,"/cgi-bin/bmx7-info", {'info':'1'}, "links_div", function(st){
+               let res = Array();
+               links = st?.info[2]?.links;
+               if (links)
+                       links.forEach(function(link) {
+                               res.push([link?.shortId, link?.name, link?.linkKey, link?.nbLocalIp, link?.dev, link?.rxRate, link?.txRate, link?.rts]);
+                       });
+               return res;
+       });
+
+</script>
+
+{% include('footer', { }) %}
diff --git a/applications/luci-app-bmx7/ucode/template/bmx7/bmxtopology.ut b/applications/luci-app-bmx7/ucode/template/bmx7/bmxtopology.ut
new file mode 100644 (file)
index 0000000..8af2a88
--- /dev/null
@@ -0,0 +1,53 @@
+{% include('header', { }) %}
+<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min.js" integrity="sha512-uy3foVtL4u0+5430l7zZt4PHjVtICfrbu3mtzdanR425sKD7kS5264djeZAzNIV0l4vc1QkFpW2+G5i5KoJIFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
+<script src="{{ resource }}/bmx7/js/netjsongraph.js"></script>
+
+<link href="{{ resource }}/bmx7/css/netjsongraph.css" rel="stylesheet">
+<style>
+               body {
+                       font-family: Arial, sans-serif;
+                       font-size: 13px;
+               }
+
+               .njg-overlay{
+                       width: auto;
+                       height: auto;
+                       min-width: 200px;
+                       max-width: 400px;
+                       border: 1px solid #000;
+                       border-radius: 2px;
+                       background: rgba(0, 0, 0, 0.7);
+                       top: 10px;
+                       right: 10px;
+                       padding: 0 15px;
+                       font-family: Arial, sans-serif;
+                       font-size: 14px;
+                       color: #fff
+               }
+
+               .njg-node {
+                       fill: #008000;
+                       fill-opacity: 0.8;
+                       stroke: #008000;
+                       stroke-width: 1px;
+                       cursor: pointer;
+               }
+               .njg-node:hover,
+               .njg-node.njg-open{
+                       fill-opacity: 1;
+               }
+
+               .njg-link {
+                       stroke: #00ff00;
+                       stroke-width: 2;
+                       stroke-opacity: .5;
+                       cursor: pointer;
+               }
+               .njg-link:hover,
+               .njg-link.njg-open{
+                       stroke-width: 3;
+                       stroke-opacity: 1
+               }
+</style>
+<script>d3.netJsonGraph("/cgi-bin/bmx7-info?netjson/network-graph.json", { defaultStyle: false });</script>
+{% include('footer', { }) %}
similarity index 52%
rename from applications/luci-app-bmx7/root/usr/lib/lua/luci/view/bmx7/tunnels_j.htm
rename to applications/luci-app-bmx7/ucode/template/bmx7/bmxtunnels.ut
index 1c8d3a9cd8ffacc79eeb270fc5991adeb125976b..9477aa3cab8559d1343030bdc223d9c989bcbab8 100644 (file)
@@ -1,4 +1,4 @@
-<%#
+{#
    Copyright (C) 2011 Pau Escrich <pau@dabax.net>
    Contributors Lluis Esquerda <eskerda@gmail.com>
 
 
    The full GNU General Public License is included in this distribution in
    the file called "COPYING".
--%>
+-#}
 
 
-<%+header%>
-<script src="<%=resource%>/cbi.js"></script>
-<script src="<%=resource%>/bmx7/js/polling.js"></script>
+{% include('header', { }) %}
+
+<script src="{{ resource }}/bmx7/js/polling.js"></script>
 
 <div class="cbi-map">
 <h2>Gateway announcements</h2>
 <div class="cbi-map-descr">Networks announced by mesh nodes</div>
 
 <div class="cbi-section">
-       <legend><%:Announcements%></legend>
+       <legend>{{ _('Announcements') }}</legend>
        <div class="cbi-section-node">
        <div class="table" id="tunnels_div">
                <div class="tr table-titles">
-                       <div class="th"><%:Status%></div>
-                       <div class="th"><%:Name%></div>
-                       <div class="th"><%:Node%></div>
-                       <div class="th"><%:Network%></div>
-                       <div class="th"><%:Bandwidth%></div>
-                       <div class="th"><%:Local net%></div>
-                       <div class="th"><%:Path Metric%></div>
-                       <div class="th"><%:Tun Metric%></div>
-                       <div class="th"><%:Rating%></div>
+                       <div class="th">{{ _('Status') }}</div>
+                       <div class="th">{{ _('Name') }}</div>
+                       <div class="th">{{ _('Node') }}</div>
+                       <div class="th">{{ _('Network') }}</div>
+                       <div class="th">{{ _('Bandwidth') }}</div>
+                       <div class="th">{{ _('Local net') }}</div>
+                       <div class="th">{{ _('Path Metric') }}</div>
+                       <div class="th">{{ _('Tun Metric') }}</div>
+                       <div class="th">{{ _('Rating') }}</div>
                </div>
        </div>
        </div>
 </div>
 
 <script>
-               new TablePooler(5,"/cgi-bin/bmx7-info", {'tunnels':''}, "tunnels_div", function(st){
-        var tunicon = "<%=resource%>/icons/tunnel.svg";
-        var tunicon_dis = "<%=resource%>/icons/tunnel_disabled.svg";
-        var applyicon = "<%=resource%>/cbi/apply.gif";
-                   var res = Array();
-        for ( var k in st.tunnels ) {
-          var tunnel = st.tunnels[k];
-          var nodename = tunnel.remoteName;
-                           var advnet = tunnel.advNet;
-          var status = '<img src="'+tunicon_dis+'"/>';
-          if ( tunnel.tunName != "---" ) status = '<img src="'+tunicon+'"/>';
+               new TablePooler(5,"/cgi-bin/bmx7-info", {'tunnels':'1'}, "tunnels_div", function(st){
+        let tunicon = "{{ resource }}/icons/tunnel.svg";
+        let tunicon_dis = "{{ resource }}/icons/tunnel_disabled.svg";
+        let applyicon = "{{ resource }}/cbi/apply.gif";
+                   let res = Array();
+        for ( let k in st?.tunnels ) {
+          let tunnel = st?.tunnels[k];
+          let nodename = tunnel?.remoteName;
+                           let advnet = tunnel?.advNet;
+          let status = '<img src="'+tunicon_dis+'"/>';
+          if ( tunnel?.tunName != "---" ) status = '<img src="'+tunicon+'"/>';
           if ( advnet == "0.0.0.0/0" ) advnet = "<b>Internet IPv4</b>";
           if ( advnet == "::/0" ) advnet = "<b>Internet IPv6</b>";
           if (nodename != "---") {
-            res.push([status, tunnel.tunName, nodename, advnet, tunnel.advBw, tunnel.net,
-              tunnel.pathMtc, tunnel.tunMtc, tunnel.rating]);
+            res.push([status, tunnel?.tunName, nodename, advnet, tunnel?.advBw, tunnel?.net,
+              tunnel?.pathMtc, tunnel?.tunMtc, tunnel?.rating]);
             }
           }
                  return res;
                });
 </script>
 
-<%+footer%>
+{% include('footer', { }) %}
git clone https://git.99rst.org/PROJECT