var Debug = false;
class UrlsBase {
constructor(root) {
this.root = root;
const paths = ["ws", "upgrade", "config", "auth"];
paths.forEach((path) => {
this[path] = new URL(path, root);
this[path].protocol = root.protocol;
});
if (this.root.protocol === "https:") {
this.ws.protocol = "wss:";
} else {
this.ws.protocol = "ws:";
}
}
}
var Urls = null;
var WebsockPingPong = null;
var Websock = {
send: function() {
},
close: function() {
}
};
class SettingsBase {
constructor() {
this.counters = {};
this.resetCounters();
this.saved = false;
}
resetCounters() {
this.counters.changed = 0;
this.counters.reboot = 0;
this.counters.reconnect = 0;
this.counters.reload = 0;
}
}
var Settings = new SettingsBase();
var Enumerable = {};
var FreeSize = 0;
var Now = 0;
var Ago = 0;
class CmdOutputBase {
constructor(elem) {
this.elem = elem;
this.lastScrollHeight = elem.scrollHeight;
this.lastScrollTop = elem.scrollTop;
this.followScroll = true;
elem.addEventListener("scroll", () => {
// in case we adjust the scroll manually
const current = this.elem.scrollHeight - this.elem.scrollTop;
const last = this.lastScrollHeight - this.lastScrollTop;
if ((current - last) > 16) {
this.followScroll = false;
}
// ...and, in case we return to the bottom row
const offset = current - this.elem.offsetHeight;
if (offset < 16) {
this.followScroll = true;
}
this.lastScrollHeight = this.elem.scrollHeight;
this.lastScrollTop = this.elem.scrollTop;
});
}
follow() {
if (this.followScroll) {
this.elem.scrollTop = this.elem.scrollHeight;
this.lastScrollHeight = this.elem.scrollHeight;
this.lastScrollTop = this.elem.scrollTop;
}
}
clear() {
this.elem.textContent = "";
this.followScroll = true;
}
push(line) {
this.elem.appendChild(new Text(line));
}
pushAndFollow(line) {
this.elem.appendChild(new Text(`${line}\n`));
this.followScroll = true
}
}
var CmdOutput = null;
//removeIf(!light)
var ColorPicker;
//endRemoveIf(!light)
//removeIf(!rfm69)
var Rfm69 = {
filters: {}
};
//endRemoveIf(!rfm69)
//removeIf(!sensor)
var Magnitudes = {
properties: {},
errors: {},
types: {},
units: {
names: {},
supported: {}
},
typePrefix: {},
prefixType: {}
};
function magnitudeTypedKey(magnitude, name) {
const prefix = Magnitudes.typePrefix[magnitude.type];
const index = magnitude.index_global;
return `${prefix}${name}${index}`;
}
//endRemoveIf(!sensor)
// -----------------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------------
function showErrorNotification(message) {
let container = document.getElementById("error-notification");
if (container.childElementCount > 0) {
return false;
}
container.style.display = "inherit";
container.style.whiteSpace = "pre-wrap";
let notification = document.createElement("div");
notification.classList.add("pure-u-1");
notification.classList.add("pure-u-lg-1");
notification.textContent = message;
container.appendChild(notification);
return false;
}
function notifyError(message, source, lineno, colno, error) {
let text = "";
if (error) {
text = error.stack;
} else {
text = message;
}
text += "\n\nFor more info see the Debug Log and / or Developer Tools console.";
return showErrorNotification(text);
}
window.onerror = notifyError;
// TODO: per https://www.chromestatus.com/feature/6140064063029248, using should be enough with recent browsers
// but, side menu needs to be reworked for it to correctly handle panel switching, since it uses
// TODO: also could be done in htmlparser2 + gulp (even, preferably)
function initExternalLinks() {
for (let elem of document.getElementsByClassName("external")) {
if (elem.tagName === "A") {
elem.setAttribute("target", "_blank");
elem.setAttribute("rel", "noopener");
elem.setAttribute("tabindex", "-1");
}
}
}
// TODO: note that we also include kv schema as 'data-settings-schema' on the container.
// produce a 'set' and compare instead of just matching length?
function fromSchema(source, schema) {
if (schema.length !== source.length) {
throw `Schema mismatch! Expected length ${schema.length} vs. ${source.length}`;
}
var target = {};
schema.forEach((key, index) => {
target[key] = source[index];
});
return target;
}
function keepTime() {
document.querySelector("span[data-key='ago']").textContent = Ago;
++Ago;
if (0 === Now) {
return;
}
let text = (new Date(Now * 1000))
.toISOString().substring(0, 19)
.replace("T", " ");
document.querySelector("span[data-key='now']").textContent = text;
++Now;
}
function setUptime(value) {
let uptime = parseInt(value, 10);
let seconds = uptime % 60;
uptime = parseInt(uptime / 60, 10);
let minutes = uptime % 60;
uptime = parseInt(uptime / 60, 10);
let hours = uptime % 24;
uptime = parseInt(uptime / 24, 10);
let days = uptime;
let container = document.querySelector("span[data-key='uptime']");
container.textContent = days + "d " + zeroPad(hours, 2) + "h " + zeroPad(minutes, 2) + "m " + zeroPad(seconds, 2) + "s";
Ago = 0;
}
function zeroPad(number, positions) {
return number.toString().padStart(positions, "0");
}
function validatePassword(password) {
// http://www.the-art-of-web.com/javascript/validate-password/
// at least one lowercase and one uppercase letter or number
// at least eight characters (letters, numbers or special characters)
// MUST be 8..63 printable ASCII characters. See:
// https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution)
// https://github.com/xoseperez/espurna/issues/1151
const Pattern = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*()<>,.?;:{}[\]\\|]{8,63}/;
return (
(password !== undefined)
&& (typeof password === "string")
&& (password.length > 0)
&& Pattern.test(password));
}
// Try to validate 'adminPass{0,1}', searching the first form containing both.
// In case it's default webMode, avoid checking things when both fields are empty (`required === false`)
function validateFormsPasswords(forms, required) {
let [passwords] = Array.from(forms).filter(
form => form.elements.adminPass0 && form.elements.adminPass1);
if (passwords) {
let first = passwords.elements.adminPass0;
let second = passwords.elements.adminPass1;
if (!required && !first.value.length && !second.value.length) {
return true;
}
let firstValid = first.checkValidity() && validatePassword(first.value);
let secondValid = second.checkValidity() && validatePassword(second.value);
if (firstValid && secondValid) {
if (first.value === second.value) {
return true;
}
alert("Passwords are different!");
return false;
}
alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!");
}
return false;
}
// Same as above, but only applies to the general settings page.
// Find the first available form that contains 'hostname' input
function validateFormsHostname(forms) {
// per. [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035)
// Hostname may contain:
// - the ASCII letters 'a' through 'z' (case-insensitive),
// - the digits '0' through '9', and the hyphen.
// Hostname labels cannot begin or end with a hyphen.
// No other symbols, punctuation characters, or blank spaces are permitted.
let [hostname] = Array.from(forms).filter(form => form.elements.hostname);
if (!hostname) {
return true;
}
// Validation pattern is attached to the element itself, so just check that.
// (and, we also re-use the hostname for fallback SSID, thus limited to 1...32 chars instead of 1...63)
hostname = hostname.elements.hostname;
let result = hostname.value.length
&& (!isChangedElement(hostname) || hostname.checkValidity());
if (!result) {
alert("Hostname cannot be empty and may only contain the ASCII letters ('A' through 'Z' and 'a' through 'z'), the digits '0' through '9', and the hyphen ('-')! They can neither start or end with an hyphen.");
}
return result;
}
function validateForms(forms) {
return validateFormsPasswords(forms) && validateFormsHostname(forms);
}
// Right now, group additions happen from:
// - WebSocket, likely to happen exactly once per connection through processData handler(s). Specific keys trigger functions that append into the container element.
// - User input. Same functions are triggered, but with an additional event for the container element that causes most recent element to be marked as changed.
// Removal only happens from user input. MutationObserver will refresh checkboxes and cause everything to be marked as changed.
//
// TODO: distinguish 'current' state to avoid sending keys when adding and immediatly removing the latest node?
// TODO: previous implementation relied on defaultValue and / or jquery $(...).val(), but this does not really work where 'line' only has